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,26 @@
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { Drawer } from "expo-router/drawer";
|
||||
import React from "react";
|
||||
import { useMedia } from "tamagui";
|
||||
|
||||
export default function Layout() {
|
||||
const media = useMedia();
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Drawer
|
||||
screenOptions={{
|
||||
drawerType: media.sm ? "front" : "permanent",
|
||||
drawerStyle: { width: 250 },
|
||||
headerLeft: media.sm ? undefined : () => null,
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen name="hosts" options={{ title: "Hosts" }} />
|
||||
<Drawer.Screen
|
||||
name="terminal"
|
||||
options={{ title: "Terminal", headerShown: true }}
|
||||
/>
|
||||
</Drawer>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import HostsPage from "@/pages/hosts/page";
|
||||
|
||||
export default HostsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import TerminalPage from "@/pages/terminal/page";
|
||||
|
||||
export default TerminalPage;
|
||||
@@ -27,7 +27,12 @@ export default function RootLayout() {
|
||||
return (
|
||||
<Providers>
|
||||
<Stack>
|
||||
<Stack.Screen name="+not-found" />
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
options={{ headerShown: false, title: "Loading..." }}
|
||||
/>
|
||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" options={{ title: "Not Found" }} />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</Providers>
|
||||
|
||||
+35
-10
@@ -1,4 +1,4 @@
|
||||
import React, { PropsWithChildren, useMemo, useState } from "react";
|
||||
import React, { PropsWithChildren, useEffect, useMemo, useState } from "react";
|
||||
import tamaguiConfig from "@/tamagui.config";
|
||||
import {
|
||||
DarkTheme,
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import { TamaguiProvider, Theme } from "@tamagui/core";
|
||||
import useThemeStore from "@/stores/theme";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { router, usePathname, useRootNavigationState } from "expo-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
type Props = PropsWithChildren;
|
||||
|
||||
@@ -33,16 +35,39 @@ const Providers = ({ children }: Props) => {
|
||||
}, [theme, colorScheme]);
|
||||
|
||||
return (
|
||||
<ThemeProvider value={navTheme}>
|
||||
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
||||
<Theme name="blue">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Theme>
|
||||
</TamaguiProvider>
|
||||
</ThemeProvider>
|
||||
<>
|
||||
<AuthProvider />
|
||||
<ThemeProvider value={navTheme}>
|
||||
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
||||
<Theme name="blue">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Theme>
|
||||
</TamaguiProvider>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AuthProvider = () => {
|
||||
const pathname = usePathname();
|
||||
const rootNavigationState = useRootNavigationState();
|
||||
const { isLoggedIn } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootNavigationState?.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pathname.startsWith("/auth") && !isLoggedIn) {
|
||||
router.replace("/auth/login");
|
||||
} else if (pathname.startsWith("/auth") && isLoggedIn) {
|
||||
router.replace("/");
|
||||
}
|
||||
}, [pathname, rootNavigationState, isLoggedIn]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Providers;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { View, Text, Button } from "tamagui";
|
||||
import React from "react";
|
||||
import authStore from "@/stores/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<View>
|
||||
<Text>LoginPage</Text>
|
||||
<Button onPress={() => authStore.setState({ token: "123" })}>
|
||||
Login
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import HostForm from "./_comp/host-form";
|
||||
import HostForm from "@/pages/hosts/components/form";
|
||||
|
||||
export default function CreateHostPage() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import HostForm from "./_comp/host-form";
|
||||
import HostForm from "@/pages/hosts/components/form";
|
||||
|
||||
export default function EditHostPage() {
|
||||
return (
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { View, Text, Button, ScrollView, Spinner, Card, XStack } from "tamagui";
|
||||
import React from "react";
|
||||
import useThemeStore from "@/stores/theme";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import api from "@/lib/api";
|
||||
import { Stack } from "expo-router";
|
||||
import Pressable from "@/components/ui/pressable";
|
||||
import Icons from "@/components/ui/icons";
|
||||
|
||||
export default function Hosts() {
|
||||
const { toggle } = useThemeStore();
|
||||
|
||||
const hosts = useQuery({
|
||||
queryKey: ["hosts"],
|
||||
queryFn: () => api("/hosts"),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Hosts",
|
||||
headerRight: () => (
|
||||
<Button onPress={() => toggle()} mr="$2">
|
||||
Toggle Theme
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{hosts.isLoading ? (
|
||||
<View alignItems="center" justifyContent="center" flex={1}>
|
||||
<Spinner size="large" />
|
||||
<Text mt="$4">Loading...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
padding: "$2",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
// gap: "$4",
|
||||
}}
|
||||
>
|
||||
{hosts.data.rows?.map((host: any) => (
|
||||
<Pressable
|
||||
key={host.id}
|
||||
flexBasis="100%"
|
||||
$gtXs={{ flexBasis: "50%" }}
|
||||
$gtSm={{ flexBasis: "33.3%" }}
|
||||
$gtMd={{ flexBasis: "25%" }}
|
||||
$gtLg={{ flexBasis: "20%" }}
|
||||
p="$2"
|
||||
group
|
||||
>
|
||||
<Card elevate 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>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import React from "react";
|
||||
import { Redirect } from "expo-router";
|
||||
import { useTermSession } from "@/stores/terminal-sessions";
|
||||
|
||||
export default function index() {
|
||||
return <Redirect href="/hosts" />;
|
||||
const { sessions, curSession } = useTermSession();
|
||||
|
||||
return (
|
||||
<Redirect
|
||||
href={sessions.length > 0 && curSession >= 0 ? "/terminal" : "/hosts"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { View, ScrollView, Button } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import InteractiveSession, {
|
||||
InteractiveSessionProps,
|
||||
} from "@/components/containers/interactive-session";
|
||||
import PagerView from "@/components/ui/pager-view";
|
||||
|
||||
type Session = InteractiveSessionProps & { id: string };
|
||||
|
||||
const HomePage = () => {
|
||||
const [sessions, setSessions] = useState<Session[]>([
|
||||
{
|
||||
id: "1",
|
||||
type: "ssh",
|
||||
params: { hostId: "01jc3v9w609f8e2wzw60amv195" },
|
||||
},
|
||||
// {
|
||||
// id: "2",
|
||||
// type: "pve",
|
||||
// params: { client: "vnc", hostId: "01jc3wp2b3zvgr777f4e3caw4w" },
|
||||
// },
|
||||
// {
|
||||
// id: "3",
|
||||
// type: "pve",
|
||||
// params: { client: "xtermjs", hostId: "01jc3z3yyn2fgb77tyfxc1tkfy" },
|
||||
// },
|
||||
// {
|
||||
// id: "4",
|
||||
// type: "incus",
|
||||
// params: {
|
||||
// client: "xtermjs",
|
||||
// hostId: "01jc3xz9db0v54dg10hk70a13b",
|
||||
// shell: "fish",
|
||||
// },
|
||||
// },
|
||||
]);
|
||||
const [curSession, setSession] = useState(0);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Stack.Screen options={{ title: "Home", headerShown: false }} />
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{ flexGrow: 0, backgroundColor: "#111" }}
|
||||
contentContainerStyle={{ flexDirection: "row", gap: 8 }}
|
||||
>
|
||||
{sessions.map((session, idx) => (
|
||||
<View
|
||||
key={session.id}
|
||||
style={{ flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Button
|
||||
title={"Session " + session.id}
|
||||
color="#333"
|
||||
onPress={() => setSession(idx)}
|
||||
/>
|
||||
<Button
|
||||
title="X"
|
||||
onPress={() => {
|
||||
const newSessions = sessions.filter((s) => s.id !== session.id);
|
||||
setSessions(newSessions);
|
||||
setSession(
|
||||
Math.min(Math.max(curSession, 0), newSessions.length - 1)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* <Button
|
||||
title="[ + ]"
|
||||
onPress={() => {
|
||||
nextSession += 1;
|
||||
setSessions([...sessions, nextSession.toString()]);
|
||||
setSession(sessions.length);
|
||||
}}
|
||||
/> */}
|
||||
</ScrollView>
|
||||
|
||||
<PagerView style={{ flex: 1 }} page={curSession}>
|
||||
{sessions.map((session) => (
|
||||
<InteractiveSession key={session.id} {...session} />
|
||||
))}
|
||||
</PagerView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import SessionsPage from "@/pages/terminal/sessions-page";
|
||||
|
||||
export default SessionsPage;
|
||||
Reference in New Issue
Block a user