mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: update ui
This commit is contained in:
parent
887cb64878
commit
334d90e691
26
frontend/app/(drawer)/_layout.tsx
Normal file
26
frontend/app/(drawer)/_layout.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
3
frontend/app/(drawer)/hosts.tsx
Normal file
3
frontend/app/(drawer)/hosts.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import HostsPage from "@/pages/hosts/page";
|
||||||
|
|
||||||
|
export default HostsPage;
|
3
frontend/app/(drawer)/terminal.tsx
Normal file
3
frontend/app/(drawer)/terminal.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import TerminalPage from "@/pages/terminal/page";
|
||||||
|
|
||||||
|
export default TerminalPage;
|
@ -27,7 +27,12 @@ export default function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<Providers>
|
<Providers>
|
||||||
<Stack>
|
<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>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
</Providers>
|
</Providers>
|
||||||
|
@ -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 tamaguiConfig from "@/tamagui.config";
|
||||||
import {
|
import {
|
||||||
DarkTheme,
|
DarkTheme,
|
||||||
@ -8,6 +8,8 @@ import {
|
|||||||
import { TamaguiProvider, Theme } from "@tamagui/core";
|
import { TamaguiProvider, Theme } from "@tamagui/core";
|
||||||
import useThemeStore from "@/stores/theme";
|
import useThemeStore from "@/stores/theme";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { router, usePathname, useRootNavigationState } from "expo-router";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
type Props = PropsWithChildren;
|
type Props = PropsWithChildren;
|
||||||
|
|
||||||
@ -33,16 +35,39 @@ const Providers = ({ children }: Props) => {
|
|||||||
}, [theme, colorScheme]);
|
}, [theme, colorScheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={navTheme}>
|
<>
|
||||||
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
<AuthProvider />
|
||||||
<Theme name="blue">
|
<ThemeProvider value={navTheme}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
|
||||||
{children}
|
<Theme name="blue">
|
||||||
</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
</Theme>
|
{children}
|
||||||
</TamaguiProvider>
|
</QueryClientProvider>
|
||||||
</ThemeProvider>
|
</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;
|
export default Providers;
|
||||||
|
14
frontend/app/auth/login.tsx
Normal file
14
frontend/app/auth/login.tsx
Normal file
@ -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,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import HostForm from "./_comp/host-form";
|
import HostForm from "@/pages/hosts/components/form";
|
||||||
|
|
||||||
export default function CreateHostPage() {
|
export default function CreateHostPage() {
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import HostForm from "./_comp/host-form";
|
import HostForm from "@/pages/hosts/components/form";
|
||||||
|
|
||||||
export default function EditHostPage() {
|
export default function EditHostPage() {
|
||||||
return (
|
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 React from "react";
|
||||||
import { Redirect } from "expo-router";
|
import { Redirect } from "expo-router";
|
||||||
|
import { useTermSession } from "@/stores/terminal-sessions";
|
||||||
|
|
||||||
export default function index() {
|
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;
|
|
3
frontend/app/terminal/sessions.tsx
Normal file
3
frontend/app/terminal/sessions.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SessionsPage from "@/pages/terminal/sessions-page";
|
||||||
|
|
||||||
|
export default SessionsPage;
|
@ -22,11 +22,10 @@ type IncusSessionProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InteractiveSessionProps = { params: { hostId: string } } & (
|
export type InteractiveSessionProps = {
|
||||||
| SSHSessionProps
|
label: string;
|
||||||
| PVESessionProps
|
params: { hostId: string };
|
||||||
| IncusSessionProps
|
} & (SSHSessionProps | PVESessionProps | IncusSessionProps);
|
||||||
);
|
|
||||||
|
|
||||||
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
||||||
const query = new URLSearchParams(params);
|
const query = new URLSearchParams(params);
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
import React, { ComponentPropsWithoutRef } from "react";
|
import React, { ComponentPropsWithoutRef } from "react";
|
||||||
import XTermJs, { XTermRef } from "./xtermjs";
|
import XTermJs, { XTermRef } from "./xtermjs";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import {
|
import { ScrollView, Text, TextStyle, View } from "tamagui";
|
||||||
Pressable,
|
import Pressable from "../ui/pressable";
|
||||||
ScrollView,
|
import Icons from "../ui/icons";
|
||||||
StyleProp,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TextStyle,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
|
|
||||||
const Keys = {
|
const Keys = {
|
||||||
ArrowLeft: "\x1b[D",
|
ArrowLeft: "\x1b[D",
|
||||||
@ -45,7 +39,7 @@ const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]} {...props}>
|
<View flex={1} bg="$background" {...props}>
|
||||||
{client === "xtermjs" && (
|
{client === "xtermjs" && (
|
||||||
<XTermJs
|
<XTermJs
|
||||||
ref={xtermRef}
|
ref={xtermRef}
|
||||||
@ -56,32 +50,32 @@ const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
|
|||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
style={{ flexGrow: 0 }}
|
flexGrow={0}
|
||||||
contentContainerStyle={styles.buttons}
|
contentContainerStyle={{ flexDirection: "row" }}
|
||||||
>
|
>
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
title={<Ionicons name="swap-horizontal" color="white" size={16} />}
|
title={<Icons name="swap-horizontal" size={16} />}
|
||||||
onPress={() => send(Keys.Tab)}
|
// onPress={() => send(Keys.Tab)}
|
||||||
/>
|
/>
|
||||||
<TerminalButton title="ESC" onPress={() => send(Keys.Escape)} />
|
<TerminalButton title="ESC" onPress={() => send(Keys.Escape)} />
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
title={<Ionicons name="home" color="white" size={16} />}
|
title={<Icons name="home" size={16} />}
|
||||||
onPress={() => send(Keys.Home)}
|
onPress={() => send(Keys.Home)}
|
||||||
/>
|
/>
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
title={<Ionicons name="arrow-back" color="white" size={18} />}
|
title={<Icons name="arrow-left" size={18} />}
|
||||||
onPress={() => send(Keys.ArrowLeft)}
|
onPress={() => send(Keys.ArrowLeft)}
|
||||||
/>
|
/>
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
title={<Ionicons name="arrow-up" color="white" size={18} />}
|
title={<Icons name="arrow-up" size={18} />}
|
||||||
onPress={() => send(Keys.ArrowUp)}
|
onPress={() => send(Keys.ArrowUp)}
|
||||||
/>
|
/>
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
title={<Ionicons name="arrow-down" color="white" size={18} />}
|
title={<Icons name="arrow-down" size={18} />}
|
||||||
onPress={() => send(Keys.ArrowDown)}
|
onPress={() => send(Keys.ArrowDown)}
|
||||||
/>
|
/>
|
||||||
<TerminalButton
|
<TerminalButton
|
||||||
title={<Ionicons name="arrow-forward" color="white" size={18} />}
|
title={<Icons name="arrow-right" size={18} />}
|
||||||
onPress={() => send(Keys.ArrowRight)}
|
onPress={() => send(Keys.ArrowRight)}
|
||||||
/>
|
/>
|
||||||
<TerminalButton title="Enter" onPress={() => send(Keys.Enter)} />
|
<TerminalButton title="Enter" onPress={() => send(Keys.Enter)} />
|
||||||
@ -108,45 +102,11 @@ const TerminalButton = ({
|
|||||||
...props
|
...props
|
||||||
}: ComponentPropsWithoutRef<typeof Pressable> & {
|
}: ComponentPropsWithoutRef<typeof Pressable> & {
|
||||||
title: string | React.ReactNode;
|
title: string | React.ReactNode;
|
||||||
textStyle?: StyleProp<TextStyle>;
|
textStyle?: TextStyle;
|
||||||
}) => (
|
}) => (
|
||||||
<Pressable
|
<Pressable px="$4" py="$3" $hover={{ bg: "$blue3" }} {...props}>
|
||||||
style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]}
|
{typeof title === "string" ? <Text {...textStyle}>{title}</Text> : title}
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{typeof title === "string" ? (
|
|
||||||
<Text style={[styles.btnText, textStyle]}>{title}</Text>
|
|
||||||
) : (
|
|
||||||
title
|
|
||||||
)}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#232323",
|
|
||||||
},
|
|
||||||
buttons: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "stretch",
|
|
||||||
backgroundColor: "#232323",
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
},
|
|
||||||
btnPressed: {
|
|
||||||
backgroundColor: "#3a3a3a",
|
|
||||||
},
|
|
||||||
btnText: {
|
|
||||||
color: "white",
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Terminal;
|
export default Terminal;
|
||||||
|
@ -20,6 +20,29 @@ type XTermJsProps = {
|
|||||||
wsUrl: string;
|
wsUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// vscode-snazzy https://github.com/Tyriar/vscode-snazzy
|
||||||
|
const snazzyTheme = {
|
||||||
|
foreground: "#eff0eb",
|
||||||
|
background: "#282a36",
|
||||||
|
selection: "#97979b33",
|
||||||
|
black: "#282a36",
|
||||||
|
brightBlack: "#686868",
|
||||||
|
red: "#ff5c57",
|
||||||
|
brightRed: "#ff5c57",
|
||||||
|
green: "#5af78e",
|
||||||
|
brightGreen: "#5af78e",
|
||||||
|
yellow: "#f3f99d",
|
||||||
|
brightYellow: "#f3f99d",
|
||||||
|
blue: "#57c7ff",
|
||||||
|
brightBlue: "#57c7ff",
|
||||||
|
magenta: "#ff6ac1",
|
||||||
|
brightMagenta: "#ff6ac1",
|
||||||
|
cyan: "#9aedfe",
|
||||||
|
brightCyan: "#9aedfe",
|
||||||
|
white: "#f1f1f0",
|
||||||
|
brightWhite: "#eff0eb",
|
||||||
|
};
|
||||||
|
|
||||||
export interface XTermRef extends DOMImperativeFactory {
|
export interface XTermRef extends DOMImperativeFactory {
|
||||||
send: (...args: JSONValue[]) => void;
|
send: (...args: JSONValue[]) => void;
|
||||||
}
|
}
|
||||||
@ -35,7 +58,11 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const xterm = new XTerm();
|
const xterm = new XTerm({
|
||||||
|
fontFamily: '"Cascadia Code", Menlo, monospace',
|
||||||
|
theme: snazzyTheme,
|
||||||
|
cursorBlink: true,
|
||||||
|
});
|
||||||
xterm.open(container);
|
xterm.open(container);
|
||||||
|
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
@ -131,6 +158,8 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{
|
style={{
|
||||||
|
background: snazzyTheme.background,
|
||||||
|
padding: 12,
|
||||||
flex: !IS_DOM ? 1 : undefined,
|
flex: !IS_DOM ? 1 : undefined,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: IS_DOM ? "100vh" : undefined,
|
height: IS_DOM ? "100vh" : undefined,
|
||||||
|
@ -1,26 +1,51 @@
|
|||||||
|
import { useDebounceCallback } from "@/hooks/useDebounce";
|
||||||
import React, { ComponentPropsWithoutRef, useEffect, useRef } from "react";
|
import React, { ComponentPropsWithoutRef, useEffect, useRef } from "react";
|
||||||
import RNPagerView from "react-native-pager-view";
|
import RNPagerView from "react-native-pager-view";
|
||||||
|
|
||||||
export type PagerViewProps = ComponentPropsWithoutRef<typeof RNPagerView> & {
|
export type PagerViewProps = ComponentPropsWithoutRef<typeof RNPagerView> & {
|
||||||
page?: number;
|
page?: number;
|
||||||
onChangePage?: (page: number) => void;
|
onChangePage?: (page: number) => void;
|
||||||
|
EmptyComponent?: () => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PagerView = ({ page, onChangePage, ...props }: PagerViewProps) => {
|
const PagerView = ({
|
||||||
|
page,
|
||||||
|
onChangePage,
|
||||||
|
EmptyComponent,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PagerViewProps) => {
|
||||||
const ref = useRef<RNPagerView>(null);
|
const ref = useRef<RNPagerView>(null);
|
||||||
|
|
||||||
|
const [onPageSelect, clearPageSelectDebounce] = useDebounceCallback(
|
||||||
|
(page) => onChangePage?.(page),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
const [setPage] = useDebounceCallback((page) => {
|
||||||
|
ref.current?.setPage(page);
|
||||||
|
clearPageSelectDebounce();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
ref.current?.setPage(page);
|
const npage = EmptyComponent != null ? page + 1 : page;
|
||||||
|
setPage(npage);
|
||||||
}
|
}
|
||||||
}, [page]);
|
}, [page, EmptyComponent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RNPagerView
|
<RNPagerView
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
onPageSelected={(e) => onChangePage?.(e.nativeEvent.position)}
|
onPageSelected={(e) => {
|
||||||
/>
|
const pos = e.nativeEvent.position;
|
||||||
|
onPageSelect(EmptyComponent ? pos - 1 : pos);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{EmptyComponent ? <EmptyComponent key="-1" /> : null}
|
||||||
|
{children}
|
||||||
|
</RNPagerView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { View } from "react-native";
|
|||||||
import { PagerViewProps } from "./pager-view";
|
import { PagerViewProps } from "./pager-view";
|
||||||
|
|
||||||
const PagerView = ({
|
const PagerView = ({
|
||||||
className,
|
EmptyComponent,
|
||||||
children,
|
children,
|
||||||
page,
|
page,
|
||||||
initialPage,
|
initialPage,
|
||||||
@ -33,7 +33,16 @@ const PagerView = ({
|
|||||||
});
|
});
|
||||||
}, [curPage, children]);
|
}, [curPage, children]);
|
||||||
|
|
||||||
return content;
|
const pageElement = useMemo(() => {
|
||||||
|
return Array.isArray(children) ? children[curPage] : null;
|
||||||
|
}, [curPage, children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!pageElement && EmptyComponent ? <EmptyComponent key="-1" /> : null}
|
||||||
|
{content}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PagerView;
|
export default PagerView;
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import { Pressable as BasePressable } from "react-native";
|
import { useRef } from "react";
|
||||||
import { GetProps, styled, ViewStyle } from "tamagui";
|
import {
|
||||||
|
TapGestureHandler,
|
||||||
|
State as GestureState,
|
||||||
|
} from "react-native-gesture-handler";
|
||||||
|
import { Button, GetProps, styled, View, ViewStyle } from "tamagui";
|
||||||
|
|
||||||
|
const StyledPressable = styled(Button, {
|
||||||
|
unstyled: true,
|
||||||
|
backgroundColor: "$colorTransparent",
|
||||||
|
borderWidth: 0,
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
||||||
const StyledPressable = styled(BasePressable);
|
|
||||||
export type PressableProps = GetProps<typeof StyledPressable> & {
|
export type PressableProps = GetProps<typeof StyledPressable> & {
|
||||||
$hover?: ViewStyle;
|
$hover?: ViewStyle;
|
||||||
$pressed?: ViewStyle;
|
$pressed?: ViewStyle;
|
||||||
@ -13,7 +23,49 @@ const Pressable = ({
|
|||||||
...props
|
...props
|
||||||
}: PressableProps) => {
|
}: PressableProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledPressable pressStyle={$pressed} hoverStyle={$hover} {...props} />
|
<StyledPressable
|
||||||
|
hoverStyle={$hover}
|
||||||
|
pressStyle={$pressed}
|
||||||
|
{...(props as any)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type MultiTapPressableProps = GetProps<typeof View> & {
|
||||||
|
numberOfTaps: number;
|
||||||
|
onTap?: () => void;
|
||||||
|
onMultiTap?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiTapPressable = ({
|
||||||
|
numberOfTaps,
|
||||||
|
onTap,
|
||||||
|
onMultiTap,
|
||||||
|
...props
|
||||||
|
}: MultiTapPressableProps) => {
|
||||||
|
const tapRef = useRef<any>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TapGestureHandler
|
||||||
|
onHandlerStateChange={(e) => {
|
||||||
|
if (e.nativeEvent.state === GestureState.ACTIVE) {
|
||||||
|
onTap?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
waitFor={tapRef}
|
||||||
|
>
|
||||||
|
<TapGestureHandler
|
||||||
|
onHandlerStateChange={(e) => {
|
||||||
|
if (e.nativeEvent.state === GestureState.ACTIVE) {
|
||||||
|
onMultiTap?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
numberOfTaps={numberOfTaps}
|
||||||
|
ref={tapRef}
|
||||||
|
>
|
||||||
|
<View pressStyle={{ opacity: 0.5 }} {...props} />
|
||||||
|
</TapGestureHandler>
|
||||||
|
</TapGestureHandler>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
25
frontend/components/ui/search-input.tsx
Normal file
25
frontend/components/ui/search-input.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { GetProps, Input, View, ViewStyle } from "tamagui";
|
||||||
|
import Icons from "./icons";
|
||||||
|
|
||||||
|
type SearchInputProps = GetProps<typeof Input> & {
|
||||||
|
_container?: ViewStyle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchInput = ({ _container, ...props }: SearchInputProps) => {
|
||||||
|
return (
|
||||||
|
<View position="relative" {..._container}>
|
||||||
|
<Icons
|
||||||
|
name="magnify"
|
||||||
|
size={20}
|
||||||
|
position="absolute"
|
||||||
|
top={11}
|
||||||
|
left="$3"
|
||||||
|
zIndex={1}
|
||||||
|
/>
|
||||||
|
<Input pl="$7" placeholder="Search..." {...props} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchInput;
|
28
frontend/hooks/useDebounce.ts
Normal file
28
frontend/hooks/useDebounce.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
|
|
||||||
|
export const useDebounceCallback = <T extends (...args: any[]) => any>(
|
||||||
|
callback: T,
|
||||||
|
delay: number = 300
|
||||||
|
) => {
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fn = useCallback(
|
||||||
|
(...args: Parameters<T>) => {
|
||||||
|
clear();
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
timeoutRef.current = null;
|
||||||
|
callback(...args);
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
[delay, clear]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [fn, clear] as const;
|
||||||
|
};
|
@ -20,6 +20,7 @@
|
|||||||
"@novnc/novnc": "^1.5.0",
|
"@novnc/novnc": "^1.5.0",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-navigation/bottom-tabs": "7.0.0-rc.36",
|
"@react-navigation/bottom-tabs": "7.0.0-rc.36",
|
||||||
|
"@react-navigation/drawer": "^7.0.0",
|
||||||
"@react-navigation/native": "7.0.0-rc.21",
|
"@react-navigation/native": "7.0.0-rc.21",
|
||||||
"@tamagui/config": "^1.116.14",
|
"@tamagui/config": "^1.116.14",
|
||||||
"@tanstack/react-query": "^5.59.20",
|
"@tanstack/react-query": "^5.59.20",
|
||||||
@ -64,5 +65,10 @@
|
|||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true,
|
||||||
|
"pnpm": {
|
||||||
|
"patchedDependencies": {
|
||||||
|
"react-native-drawer-layout": "patches/react-native-drawer-layout.patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
124
frontend/pages/hosts/components/hosts-list.tsx
Normal file
124
frontend/pages/hosts/components/hosts-list.tsx
Normal file
@ -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
frontend/pages/hosts/page.tsx
Normal file
25
frontend/pages/hosts/page.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
63
frontend/pages/terminal/components/session-tabs.tsx
Normal file
63
frontend/pages/terminal/components/session-tabs.tsx
Normal file
@ -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
frontend/pages/terminal/page.tsx
Normal file
48
frontend/pages/terminal/page.tsx
Normal 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
frontend/pages/terminal/sessions-page.tsx
Normal file
93
frontend/pages/terminal/sessions-page.tsx
Normal 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;
|
25
frontend/patches/react-native-drawer-layout.patch
Normal file
25
frontend/patches/react-native-drawer-layout.patch
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
diff --git a/lib/commonjs/views/Drawer.native.js b/lib/commonjs/views/Drawer.native.js
|
||||||
|
index 7bac11bd3e76fc78284aabd81b6a94446c64813f..6cf9d65503f5ec35cea276bdc6803adfc60d2c0e 100644
|
||||||
|
--- a/lib/commonjs/views/Drawer.native.js
|
||||||
|
+++ b/lib/commonjs/views/Drawer.native.js
|
||||||
|
@@ -159,16 +159,20 @@ function Drawer({
|
||||||
|
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
|
||||||
|
const startX = (0, _reactNativeReanimated.useSharedValue)(0);
|
||||||
|
let pan = _GestureHandler.Gesture?.Pan().onBegin(event => {
|
||||||
|
+ 'worklet';
|
||||||
|
startX.value = translationX.value;
|
||||||
|
gestureState.value = event.state;
|
||||||
|
touchStartX.value = event.x;
|
||||||
|
}).onStart(() => {
|
||||||
|
+ 'worklet';
|
||||||
|
(0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
|
||||||
|
}).onChange(event => {
|
||||||
|
+ 'worklet';
|
||||||
|
touchX.value = event.x;
|
||||||
|
translationX.value = startX.value + event.translationX;
|
||||||
|
gestureState.value = event.state;
|
||||||
|
}).onEnd((event, success) => {
|
||||||
|
+ 'worklet';
|
||||||
|
gestureState.value = event.state;
|
||||||
|
if (!success) {
|
||||||
|
(0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
|
76
frontend/pnpm-lock.yaml
generated
76
frontend/pnpm-lock.yaml
generated
@ -4,6 +4,11 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
patchedDependencies:
|
||||||
|
react-native-drawer-layout:
|
||||||
|
hash: ipghvwpiqcl5liuijnfvmjzcvq
|
||||||
|
path: patches/react-native-drawer-layout.patch
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@ -20,6 +25,9 @@ importers:
|
|||||||
'@react-navigation/bottom-tabs':
|
'@react-navigation/bottom-tabs':
|
||||||
specifier: 7.0.0-rc.36
|
specifier: 7.0.0-rc.36
|
||||||
version: 7.0.0-rc.36(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-screens@4.0.0-beta.16(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
version: 7.0.0-rc.36(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-screens@4.0.0-beta.16(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
'@react-navigation/drawer':
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.0.0(s2kwfzlicenreg74lts3a6znsu)
|
||||||
'@react-navigation/native':
|
'@react-navigation/native':
|
||||||
specifier: 7.0.0-rc.21
|
specifier: 7.0.0-rc.21
|
||||||
version: 7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
version: 7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
@ -58,7 +66,7 @@ importers:
|
|||||||
version: 7.0.2(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
version: 7.0.2(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
expo-router:
|
expo-router:
|
||||||
specifier: ~4.0.0-preview.12
|
specifier: ~4.0.0-preview.12
|
||||||
version: 4.0.0-preview.12(yd2wh2xxaopmvq6w6kpgmfoxyy)
|
version: 4.0.0-preview.12(dw2oobptqthz2eo3bvppba5ugq)
|
||||||
expo-splash-screen:
|
expo-splash-screen:
|
||||||
specifier: ~0.29.1
|
specifier: ~0.29.1
|
||||||
version: 0.29.1(expo-modules-autolinking@2.0.0-preview.3)(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))
|
version: 0.29.1(expo-modules-autolinking@2.0.0-preview.3)(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))
|
||||||
@ -1399,6 +1407,29 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '*'
|
react: '*'
|
||||||
|
|
||||||
|
'@react-navigation/drawer@7.0.0':
|
||||||
|
resolution: {integrity: sha512-JbJ2ziSFVTV/qr2ffs2qMhziQJ8XHzRJhsF+PH2zFu4FCguRYaPqtf3Kl//tS4QWXVhpO4g5jweJQ7CfSousgw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@react-navigation/native': ^7.0.0
|
||||||
|
react: '>= 18.2.0'
|
||||||
|
react-native: '*'
|
||||||
|
react-native-gesture-handler: '>= 2.0.0'
|
||||||
|
react-native-reanimated: '>= 2.0.0'
|
||||||
|
react-native-safe-area-context: '>= 4.0.0'
|
||||||
|
react-native-screens: '>= 4.0.0'
|
||||||
|
|
||||||
|
'@react-navigation/elements@2.0.0':
|
||||||
|
resolution: {integrity: sha512-kt2Q5WLJ9jjJMA/Jt8S3z3Jub2V+HIJ2LM4z+dZqL00FVsTfa4rSk3BTktI3MmBiUCgzUo6jPOxkxsUbjoL/ig==}
|
||||||
|
peerDependencies:
|
||||||
|
'@react-native-masked-view/masked-view': '>= 0.2.0'
|
||||||
|
'@react-navigation/native': ^7.0.0
|
||||||
|
react: '>= 18.2.0'
|
||||||
|
react-native: '*'
|
||||||
|
react-native-safe-area-context: '>= 4.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@react-native-masked-view/masked-view':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@react-navigation/elements@2.0.0-rc.26':
|
'@react-navigation/elements@2.0.0-rc.26':
|
||||||
resolution: {integrity: sha512-omtEkb2E8j3dYLq08YGsDykQoVLtTLWAQXp0ql6cB8qjtMhP7rMhoBU50veh0Tes/96Sm3X0e3WZPQMVBKrSSg==}
|
resolution: {integrity: sha512-omtEkb2E8j3dYLq08YGsDykQoVLtTLWAQXp0ql6cB8qjtMhP7rMhoBU50veh0Tes/96Sm3X0e3WZPQMVBKrSSg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4611,6 +4642,14 @@ packages:
|
|||||||
react-is@18.3.1:
|
react-is@18.3.1:
|
||||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||||
|
|
||||||
|
react-native-drawer-layout@4.0.0:
|
||||||
|
resolution: {integrity: sha512-l9xu7YDXHImg3wpLjD12CokWV2H4Nu/Uc9EVxg/DFqEwgyDbZqE/8IGhQhN32TiZPgelAZNj5c3MBE2yTR1ivw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 18.2.0'
|
||||||
|
react-native: '*'
|
||||||
|
react-native-gesture-handler: '>= 2.0.0'
|
||||||
|
react-native-reanimated: '>= 2.0.0'
|
||||||
|
|
||||||
react-native-gesture-handler@2.20.2:
|
react-native-gesture-handler@2.20.2:
|
||||||
resolution: {integrity: sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg==}
|
resolution: {integrity: sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -7456,6 +7495,30 @@ snapshots:
|
|||||||
use-latest-callback: 0.2.1(react@18.3.1)
|
use-latest-callback: 0.2.1(react@18.3.1)
|
||||||
use-sync-external-store: 1.2.2(react@18.3.1)
|
use-sync-external-store: 1.2.2(react@18.3.1)
|
||||||
|
|
||||||
|
'@react-navigation/drawer@7.0.0(s2kwfzlicenreg74lts3a6znsu)':
|
||||||
|
dependencies:
|
||||||
|
'@react-navigation/elements': 2.0.0(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
'@react-navigation/native': 7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
color: 4.2.3
|
||||||
|
react: 18.3.1
|
||||||
|
react-native: 0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
react-native-drawer-layout: 4.0.0(patch_hash=ipghvwpiqcl5liuijnfvmjzcvq)(react-native-gesture-handler@2.20.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.1(@babel/core@7.26.0)(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
react-native-gesture-handler: 2.20.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
react-native-reanimated: 3.16.1(@babel/core@7.26.0)(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
react-native-safe-area-context: 4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
react-native-screens: 4.0.0-beta.16(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
use-latest-callback: 0.2.1(react@18.3.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@react-native-masked-view/masked-view'
|
||||||
|
|
||||||
|
'@react-navigation/elements@2.0.0(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@react-navigation/native': 7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
color: 4.2.3
|
||||||
|
react: 18.3.1
|
||||||
|
react-native: 0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
react-native-safe-area-context: 4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
|
||||||
'@react-navigation/elements@2.0.0-rc.26(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)':
|
'@react-navigation/elements@2.0.0-rc.26(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-navigation/native': 7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
'@react-navigation/native': 7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
@ -9846,7 +9909,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
invariant: 2.2.4
|
invariant: 2.2.4
|
||||||
|
|
||||||
expo-router@4.0.0-preview.12(yd2wh2xxaopmvq6w6kpgmfoxyy):
|
expo-router@4.0.0-preview.12(dw2oobptqthz2eo3bvppba5ugq):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@expo/metro-runtime': 4.0.0-preview.1(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))
|
'@expo/metro-runtime': 4.0.0-preview.1(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))
|
||||||
'@expo/server': 0.5.0-preview.0(typescript@5.6.3)
|
'@expo/server': 0.5.0-preview.0(typescript@5.6.3)
|
||||||
@ -9866,6 +9929,7 @@ snapshots:
|
|||||||
schema-utils: 4.2.0
|
schema-utils: 4.2.0
|
||||||
server-only: 0.0.1
|
server-only: 0.0.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@react-navigation/drawer': 7.0.0(s2kwfzlicenreg74lts3a6znsu)
|
||||||
react-native-reanimated: 3.16.1(@babel/core@7.26.0)(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
react-native-reanimated: 3.16.1(@babel/core@7.26.0)(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@react-native-masked-view/masked-view'
|
- '@react-native-masked-view/masked-view'
|
||||||
@ -11706,6 +11770,14 @@ snapshots:
|
|||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
|
|
||||||
|
react-native-drawer-layout@4.0.0(patch_hash=ipghvwpiqcl5liuijnfvmjzcvq)(react-native-gesture-handler@2.20.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.1(@babel/core@7.26.0)(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-native: 0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
react-native-gesture-handler: 2.20.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
react-native-reanimated: 3.16.1(@babel/core@7.26.0)(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
|
||||||
|
use-latest-callback: 0.2.1(react@18.3.1)
|
||||||
|
|
||||||
react-native-gesture-handler@2.20.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1):
|
react-native-gesture-handler@2.20.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@egjs/hammerjs': 2.0.17
|
'@egjs/hammerjs': 2.0.17
|
||||||
|
26
frontend/stores/auth.ts
Normal file
26
frontend/stores/auth.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { createStore, useStore } from "zustand";
|
||||||
|
import { persist, createJSONStorage } from "zustand/middleware";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
|
type AuthStore = {
|
||||||
|
token?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const authStore = createStore(
|
||||||
|
persist<AuthStore>(
|
||||||
|
() => ({
|
||||||
|
token: null,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "auth",
|
||||||
|
storage: createJSONStorage(() => AsyncStorage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useAuthStore = () => {
|
||||||
|
const state = useStore(authStore);
|
||||||
|
return { ...state, isLoggedIn: state.token != null };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authStore;
|
43
frontend/stores/terminal-sessions.ts
Normal file
43
frontend/stores/terminal-sessions.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { InteractiveSessionProps } from "@/components/containers/interactive-session";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export type Session = InteractiveSessionProps & { id: string };
|
||||||
|
|
||||||
|
type TerminalSessionsStore = {
|
||||||
|
sessions: Session[];
|
||||||
|
curSession: number;
|
||||||
|
push: (session: Session) => void;
|
||||||
|
remove: (idx: number) => void;
|
||||||
|
setSession: (idx: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTermSession = create(
|
||||||
|
persist<TerminalSessionsStore>(
|
||||||
|
(set) => ({
|
||||||
|
sessions: [],
|
||||||
|
curSession: 0,
|
||||||
|
push: (session: Session) => {
|
||||||
|
set((state) => ({
|
||||||
|
sessions: [
|
||||||
|
...state.sessions,
|
||||||
|
{ ...session, id: session.id + "." + Date.now() },
|
||||||
|
],
|
||||||
|
curSession: state.sessions.length,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
remove: (idx: number) => {
|
||||||
|
set((state) => {
|
||||||
|
const sessions = [...state.sessions];
|
||||||
|
sessions.splice(idx, 1);
|
||||||
|
return { sessions, curSession: Math.min(idx, sessions.length - 1) };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setSession: (idx: number) => {
|
||||||
|
set({ curSession: idx });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "term-sessions", storage: createJSONStorage(() => AsyncStorage) }
|
||||||
|
)
|
||||||
|
);
|
@ -11,7 +11,7 @@ type Store = {
|
|||||||
const useThemeStore = create(
|
const useThemeStore = create(
|
||||||
persist<Store>(
|
persist<Store>(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
theme: "light",
|
theme: "dark",
|
||||||
setTheme: (theme: "light" | "dark") => {
|
setTheme: (theme: "light" | "dark") => {
|
||||||
set({ theme });
|
set({ theme });
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user