feat: update ui

This commit is contained in:
2024-11-08 18:53:30 +00:00
parent 887cb64878
commit 334d90e691
32 changed files with 829 additions and 262 deletions
+69
View File
@@ -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;
+25
View File
@@ -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;
+48
View File
@@ -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;
+93
View File
@@ -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;