mirror of
https://github.com/khairul169/vaulterm.git
synced 2026-09-15 17:03:30 +07:00
feat: update ui
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import Icons from "@/components/ui/icons";
|
||||
import Select, { SelectItem } from "@/components/ui/select";
|
||||
import api from "@/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { Button, Input, Label, ScrollView, Text, View, XStack } from "tamagui";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const typeOptions: SelectItem[] = [
|
||||
{ label: "SSH", value: "ssh" },
|
||||
{ label: "Proxmox VE", value: "pve" },
|
||||
{ label: "Incus", value: "incus" },
|
||||
];
|
||||
|
||||
const HostForm = (props: Props) => {
|
||||
const keys = useQuery({
|
||||
queryKey: ["keychains"],
|
||||
queryFn: () => api("/keychains"),
|
||||
select: (i) => i.rows,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView contentContainerStyle={{ padding: "$4" }}>
|
||||
<Label>Hostname</Label>
|
||||
<Input placeholder="IP or hostname..." />
|
||||
|
||||
<Label>Type</Label>
|
||||
<Select items={typeOptions} />
|
||||
|
||||
<Label>Port</Label>
|
||||
<Input keyboardType="number-pad" placeholder="SSH Port" />
|
||||
|
||||
<Label>Label</Label>
|
||||
<Input placeholder="Label..." />
|
||||
|
||||
<XStack gap="$3" mt="$3" mb="$1">
|
||||
<Label flex={1}>Credentials</Label>
|
||||
<Button size="$3" icon={<Icons size={16} name="plus" />}>
|
||||
Add
|
||||
</Button>
|
||||
</XStack>
|
||||
|
||||
<Select
|
||||
placeholder="Username & Password"
|
||||
items={keys.data?.map((key: any) => ({
|
||||
label: key.label,
|
||||
value: key.id,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
mt="$3"
|
||||
placeholder="Private Key"
|
||||
items={keys.data?.map((key: any) => ({
|
||||
label: key.label,
|
||||
value: key.id,
|
||||
}))}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
<View p="$4">
|
||||
<Button icon={<Icons name="content-save" size={18} />}>Save</Button>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostForm;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { View, Text, Button, ScrollView, Spinner, Card, XStack } from "tamagui";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import api from "@/lib/api";
|
||||
import { useNavigation } from "expo-router";
|
||||
import { MultiTapPressable } from "@/components/ui/pressable";
|
||||
import Icons from "@/components/ui/icons";
|
||||
import SearchInput from "@/components/ui/search-input";
|
||||
import { useTermSession } from "@/stores/terminal-sessions";
|
||||
|
||||
const HostsList = () => {
|
||||
const openSession = useTermSession((i) => i.push);
|
||||
const navigation = useNavigation();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const hosts = useQuery({
|
||||
queryKey: ["hosts"],
|
||||
queryFn: () => api("/hosts"),
|
||||
select: (i) => i.rows,
|
||||
});
|
||||
|
||||
const hostsList = useMemo(() => {
|
||||
let items = hosts.data || [];
|
||||
|
||||
if (search) {
|
||||
items = items.filter((item: any) => {
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
item.label.toLowerCase().includes(q) ||
|
||||
item.host.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [hosts.data, search]);
|
||||
|
||||
const onOpen = (host: any) => {
|
||||
const session: any = {
|
||||
id: host.id,
|
||||
label: host.label,
|
||||
type: host.type,
|
||||
params: {
|
||||
hostId: host.id,
|
||||
},
|
||||
};
|
||||
|
||||
if (host.type === "pve") {
|
||||
session.params.client = host.metadata?.type === "lxc" ? "xtermjs" : "vnc";
|
||||
}
|
||||
|
||||
if (host.type === "incus") {
|
||||
session.params.shell = "bash";
|
||||
}
|
||||
|
||||
openSession(session);
|
||||
navigation.navigate("terminal" as never);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View p="$4" pb="$3">
|
||||
<SearchInput
|
||||
placeholder="Search label, host, or IP..."
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{hosts.isLoading ? (
|
||||
<View alignItems="center" justifyContent="center" flex={1}>
|
||||
<Spinner size="large" />
|
||||
<Text mt="$4">Loading...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
padding: "$3",
|
||||
paddingTop: 0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{hostsList?.map((host: any) => (
|
||||
<MultiTapPressable
|
||||
key={host.id}
|
||||
flexBasis="100%"
|
||||
cursor="pointer"
|
||||
$gtXs={{ flexBasis: "50%" }}
|
||||
$gtSm={{ flexBasis: "33.3%" }}
|
||||
$gtMd={{ flexBasis: "25%" }}
|
||||
$gtLg={{ flexBasis: "20%" }}
|
||||
p="$2"
|
||||
group
|
||||
numberOfTaps={2}
|
||||
onMultiTap={() => onOpen(host)}
|
||||
>
|
||||
<Card bordered p="$4">
|
||||
<XStack>
|
||||
<View flex={1}>
|
||||
<Text>{host.label}</Text>
|
||||
<Text fontSize="$3" mt="$2">
|
||||
{host.host}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
circular
|
||||
display="none"
|
||||
$group-hover={{ display: "block" }}
|
||||
>
|
||||
<Icons name="pencil" size={16} />
|
||||
</Button>
|
||||
</XStack>
|
||||
</Card>
|
||||
</MultiTapPressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostsList;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Button } from "tamagui";
|
||||
import React from "react";
|
||||
import useThemeStore from "@/stores/theme";
|
||||
import Drawer from "expo-router/drawer";
|
||||
import HostsList from "./components/hosts-list";
|
||||
|
||||
export default function HostsPage() {
|
||||
const { toggle } = useThemeStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer.Screen
|
||||
options={{
|
||||
headerRight: () => (
|
||||
<Button onPress={() => toggle()} mr="$2">
|
||||
Toggle Theme
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<HostsList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { useTermSession } from "@/stores/terminal-sessions";
|
||||
import { Button, ScrollView, View } from "tamagui";
|
||||
import Icons from "@/components/ui/icons";
|
||||
|
||||
const SessionTabs = () => {
|
||||
const { sessions, curSession, setSession, remove } = useTermSession();
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
flexGrow={0}
|
||||
bg="$background"
|
||||
contentContainerStyle={{
|
||||
flexDirection: "row",
|
||||
pt: "$2",
|
||||
px: "$2",
|
||||
gap: "$2",
|
||||
}}
|
||||
>
|
||||
{sessions.map((session, idx) => (
|
||||
<View key={session.id} position="relative">
|
||||
<Button
|
||||
size="$3"
|
||||
borderBottomLeftRadius={0}
|
||||
borderBottomRightRadius={0}
|
||||
onPress={() => setSession(idx)}
|
||||
pl="$4"
|
||||
pr="$6"
|
||||
bg={curSession === idx ? "$blue7" : "$blue3"}
|
||||
>
|
||||
{session.label}
|
||||
</Button>
|
||||
<Button
|
||||
circular
|
||||
bg="$colorTransparent"
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
remove(idx);
|
||||
}}
|
||||
icon={<Icons name="close" size={16} />}
|
||||
size="$2"
|
||||
position="absolute"
|
||||
top="$1.5"
|
||||
right="$1"
|
||||
opacity={0.6}
|
||||
hoverStyle={{ opacity: 1 }}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<Button
|
||||
onPress={() => setSession(-1)}
|
||||
size="$2.5"
|
||||
bg="$colorTransparent"
|
||||
circular
|
||||
icon={<Icons name="plus" size={16} />}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionTabs;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import InteractiveSession from "@/components/containers/interactive-session";
|
||||
import PagerView from "@/components/ui/pager-view";
|
||||
import { useTermSession } from "@/stores/terminal-sessions";
|
||||
import { Button, useMedia } from "tamagui";
|
||||
import SessionTabs from "./components/session-tabs";
|
||||
import HostsList from "../hosts/components/hosts-list";
|
||||
import Drawer from "expo-router/drawer";
|
||||
import { router } from "expo-router";
|
||||
import Icons from "@/components/ui/icons";
|
||||
|
||||
const TerminalPage = () => {
|
||||
const { sessions, curSession, setSession } = useTermSession();
|
||||
const session = sessions[curSession];
|
||||
const media = useMedia();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer.Screen
|
||||
options={{
|
||||
headerTitle: session?.label || "Terminal",
|
||||
headerRight: () => (
|
||||
<Button
|
||||
bg="$colorTransparent"
|
||||
icon={<Icons name="view-list" size={24} />}
|
||||
onPress={() => router.push("/terminal/sessions")}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{sessions.length > 0 && media.gtSm ? <SessionTabs /> : null}
|
||||
|
||||
<PagerView
|
||||
style={{ flex: 1 }}
|
||||
page={curSession}
|
||||
onChangePage={setSession}
|
||||
EmptyComponent={HostsList}
|
||||
>
|
||||
{sessions.map((session) => (
|
||||
<InteractiveSession key={session.id} {...session} />
|
||||
))}
|
||||
</PagerView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TerminalPage;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { router, Stack } from "expo-router";
|
||||
import { Button, ScrollView, View } from "tamagui";
|
||||
import { useTermSession } from "@/stores/terminal-sessions";
|
||||
import SearchInput from "@/components/ui/search-input";
|
||||
import Icons from "@/components/ui/icons";
|
||||
|
||||
const SessionsPage = () => {
|
||||
const { sessions, setSession, curSession, remove } = useTermSession();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const sessionList = useMemo(() => {
|
||||
let items = sessions;
|
||||
|
||||
if (search) {
|
||||
items = items.filter((item) =>
|
||||
item.label.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [sessions, search]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Sessions",
|
||||
headerRight: () => (
|
||||
<Button
|
||||
bg="$colorTransparent"
|
||||
icon={<Icons name="plus" size={24} />}
|
||||
onPress={() => {
|
||||
router.back();
|
||||
router.push("/hosts");
|
||||
}}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<View p="$3">
|
||||
<SearchInput
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={{ px: "$3", pt: "$1" }}>
|
||||
{sessionList.map((session, idx) => (
|
||||
<View key={session.id} mb="$3" position="relative">
|
||||
<Button
|
||||
bg={idx !== curSession ? "$colorTransparent" : undefined}
|
||||
borderWidth={1}
|
||||
borderColor="$blue4"
|
||||
justifyContent="flex-start"
|
||||
textAlign="left"
|
||||
size="$5"
|
||||
pl="$4"
|
||||
icon={<Icons name="connection" size={16} />}
|
||||
onPress={() => {
|
||||
router.back();
|
||||
setTimeout(() => setSession(idx), 20);
|
||||
}}
|
||||
>
|
||||
{session.label}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bg="$colorTransparent"
|
||||
circular
|
||||
size="$3"
|
||||
position="absolute"
|
||||
top="$2"
|
||||
right="$2"
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
remove(idx);
|
||||
}}
|
||||
>
|
||||
<Icons name="close" size={16} />
|
||||
</Button>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionsPage;
|
||||
Reference in New Issue
Block a user