mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: add drawer
This commit is contained in:
parent
38e81049a1
commit
8159b65605
@ -2,6 +2,8 @@ import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|||||||
import { Drawer } from "expo-router/drawer";
|
import { Drawer } from "expo-router/drawer";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useMedia } from "tamagui";
|
import { useMedia } from "tamagui";
|
||||||
|
import DrawerContent from "@/components/containers/drawer";
|
||||||
|
import Icons from "@/components/ui/icons";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const media = useMedia();
|
const media = useMedia();
|
||||||
@ -9,17 +11,40 @@ export default function Layout() {
|
|||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<Drawer
|
<Drawer
|
||||||
|
drawerContent={DrawerContent}
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
drawerType: media.sm ? "front" : "permanent",
|
drawerType: media.sm ? "front" : "permanent",
|
||||||
drawerStyle: { width: 250 },
|
drawerStyle: { width: 250 },
|
||||||
headerLeft: media.sm ? undefined : () => null,
|
headerLeft: media.sm ? undefined : () => null,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Drawer.Screen name="hosts" options={{ title: "Hosts" }} />
|
<Drawer.Screen
|
||||||
<Drawer.Screen name="keychains" options={{ title: "Keychains" }} />
|
name="hosts"
|
||||||
|
options={{
|
||||||
|
title: "Hosts",
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Icons name="server" size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="keychains"
|
||||||
|
options={{
|
||||||
|
title: "Keychains",
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Icons name="key" size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="terminal"
|
name="terminal"
|
||||||
options={{ title: "Terminal", headerShown: media.sm }}
|
options={{
|
||||||
|
title: "Terminal",
|
||||||
|
headerShown: media.sm,
|
||||||
|
drawerIcon: ({ size, color }) => (
|
||||||
|
<Icons name="console-line" size={size} color={color} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
@ -12,7 +12,7 @@ import { router, usePathname, useRootNavigationState } from "expo-router";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { PortalProvider } from "tamagui";
|
import { PortalProvider } from "tamagui";
|
||||||
import { queryClient } from "@/lib/api";
|
import { queryClient } from "@/lib/api";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useServer } from "@/stores/app";
|
||||||
|
|
||||||
type Props = PropsWithChildren;
|
type Props = PropsWithChildren;
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ const AuthProvider = () => {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const rootNavigationState = useRootNavigationState();
|
const rootNavigationState = useRootNavigationState();
|
||||||
const { isLoggedIn } = useAuthStore();
|
const { isLoggedIn } = useAuthStore();
|
||||||
const { curServer } = useAppStore();
|
const curServer = useServer();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rootNavigationState?.key) {
|
if (!rootNavigationState?.key) {
|
||||||
|
@ -1,13 +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";
|
import { useTermSession } from "@/stores/terminal-sessions";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useServer } from "@/stores/app";
|
||||||
|
|
||||||
export default function index() {
|
export default function index() {
|
||||||
const { sessions, curSession } = useTermSession();
|
const { sessions, curSession } = useTermSession();
|
||||||
const { servers, curServer } = useAppStore();
|
const curServer = useServer();
|
||||||
|
|
||||||
if (!servers.length || !curServer) {
|
if (!curServer) {
|
||||||
return <Redirect href="/server" />;
|
return <Redirect href="/server" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
88
frontend/components/containers/drawer.tsx
Normal file
88
frontend/components/containers/drawer.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
DrawerContentComponentProps,
|
||||||
|
DrawerContentScrollView,
|
||||||
|
} from "@react-navigation/drawer";
|
||||||
|
import { Button, View } from "tamagui";
|
||||||
|
import {
|
||||||
|
CommonActions,
|
||||||
|
DrawerActions,
|
||||||
|
useLinkBuilder,
|
||||||
|
} from "@react-navigation/native";
|
||||||
|
import { Link } from "expo-router";
|
||||||
|
import Icons from "../ui/icons";
|
||||||
|
import { logout } from "@/stores/auth";
|
||||||
|
|
||||||
|
const Drawer = (props: DrawerContentComponentProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DrawerContentScrollView
|
||||||
|
contentContainerStyle={{ padding: 18 }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<DrawerItemList {...props} />
|
||||||
|
</DrawerContentScrollView>
|
||||||
|
|
||||||
|
<View p="$4">
|
||||||
|
<Button
|
||||||
|
justifyContent="flex-start"
|
||||||
|
icon={<Icons name="logout" size={16} />}
|
||||||
|
onPress={() => logout()}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawerItemList = ({
|
||||||
|
state,
|
||||||
|
navigation,
|
||||||
|
descriptors,
|
||||||
|
}: DrawerContentComponentProps) => {
|
||||||
|
const { buildHref } = useLinkBuilder();
|
||||||
|
|
||||||
|
return state.routes.map((route, i) => {
|
||||||
|
const focused = i === state.index;
|
||||||
|
|
||||||
|
const onPress = () => {
|
||||||
|
const event = navigation.emit({
|
||||||
|
type: "drawerItemPress",
|
||||||
|
target: route.key,
|
||||||
|
canPreventDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!event.defaultPrevented) {
|
||||||
|
navigation.dispatch({
|
||||||
|
...(focused
|
||||||
|
? DrawerActions.closeDrawer()
|
||||||
|
: CommonActions.navigate(route)),
|
||||||
|
target: state.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { title, drawerLabel, drawerIcon } = descriptors[route.key].options;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link key={route.key} href={buildHref(route.name, route.params) as never}>
|
||||||
|
<Button
|
||||||
|
w="100%"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
bg={focused ? "$background" : "$colorTransparent"}
|
||||||
|
onPress={onPress}
|
||||||
|
icon={drawerIcon?.({ size: 16, color: "$color", focused }) as never}
|
||||||
|
>
|
||||||
|
{drawerLabel !== undefined
|
||||||
|
? drawerLabel
|
||||||
|
: title !== undefined
|
||||||
|
? title
|
||||||
|
: route.name}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}) as React.ReactNode as React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Drawer;
|
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Terminal from "./terminal";
|
import Terminal from "./terminal";
|
||||||
import { BASE_WS_URL } from "@/lib/api";
|
|
||||||
import VNCViewer from "./vncviewer";
|
import VNCViewer from "./vncviewer";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { AppServer, useServer } from "@/stores/app";
|
||||||
|
|
||||||
type SSHSessionProps = {
|
type SSHSessionProps = {
|
||||||
type: "ssh";
|
type: "ssh";
|
||||||
@ -30,8 +30,9 @@ export type InteractiveSessionProps = {
|
|||||||
|
|
||||||
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
||||||
const { token } = useAuthStore();
|
const { token } = useAuthStore();
|
||||||
|
const server = useServer();
|
||||||
const query = new URLSearchParams({ ...params, sid: token || "" });
|
const query = new URLSearchParams({ ...params, sid: token || "" });
|
||||||
const url = `${BASE_WS_URL}/ws/term?${query}`;
|
const url = `${getBaseUrl(server)}/ws/term?${query}`;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ssh":
|
case "ssh":
|
||||||
@ -50,4 +51,8 @@ const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getBaseUrl(server?: AppServer | null) {
|
||||||
|
return server?.url.replace("http://", "ws://") || "";
|
||||||
|
}
|
||||||
|
|
||||||
export default InteractiveSession;
|
export default InteractiveSession;
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
|
import { getCurrentServer } from "@/stores/app";
|
||||||
import authStore from "@/stores/auth";
|
import authStore from "@/stores/auth";
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
import { ofetch } from "ofetch";
|
import { ofetch } from "ofetch";
|
||||||
|
|
||||||
export const BASE_API_URL = process.env.EXPO_PUBLIC_API_URL || ""; //"http://10.0.0.100:3000";
|
|
||||||
export const BASE_WS_URL = BASE_API_URL.replace("http", "ws");
|
|
||||||
|
|
||||||
const api = ofetch.create({
|
const api = ofetch.create({
|
||||||
baseURL: BASE_API_URL,
|
|
||||||
onRequest: (config) => {
|
onRequest: (config) => {
|
||||||
|
const server = getCurrentServer();
|
||||||
|
if (!server) {
|
||||||
|
throw new Error("No server selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set server url
|
||||||
|
config.options.baseURL = server.url;
|
||||||
|
|
||||||
const authToken = authStore.getState().token;
|
const authToken = authStore.getState().token;
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
config.options.headers.set("Authorization", `Bearer ${authToken}`);
|
config.options.headers.set("Authorization", `Bearer ${authToken}`);
|
||||||
|
@ -66,10 +66,19 @@ export default function LoginPage() {
|
|||||||
<ErrorAlert error={login.error} />
|
<ErrorAlert error={login.error} />
|
||||||
|
|
||||||
<FormField vertical label="Username/Email">
|
<FormField vertical label="Username/Email">
|
||||||
<InputField form={form} name="username" />
|
<InputField
|
||||||
|
form={form}
|
||||||
|
name="username"
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField vertical label="Password">
|
<FormField vertical label="Password">
|
||||||
<InputField form={form} name="password" secureTextEntry />
|
<InputField
|
||||||
|
form={form}
|
||||||
|
name="password"
|
||||||
|
secureTextEntry
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
@ -2,7 +2,7 @@ import { createStore, useStore } from "zustand";
|
|||||||
import { persist, createJSONStorage } from "zustand/middleware";
|
import { persist, createJSONStorage } from "zustand/middleware";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
type AppServer = {
|
export type AppServer = {
|
||||||
name?: string;
|
name?: string;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
@ -51,12 +51,15 @@ export function setActiveServer(idx: number) {
|
|||||||
appStore.setState({ curServerIdx: idx });
|
appStore.setState({ curServerIdx: idx });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppStore = () => {
|
export function getCurrentServer() {
|
||||||
const state = useStore(appStore);
|
const state = appStore.getState();
|
||||||
const curServer =
|
return state.curServerIdx != null ? state.servers[state.curServerIdx] : null;
|
||||||
state.curServerIdx != null ? state.servers[state.curServerIdx] : null;
|
}
|
||||||
|
|
||||||
return { ...state, curServer };
|
export const useServer = () => {
|
||||||
|
const servers = useStore(appStore, (i) => i.servers);
|
||||||
|
const idx = useStore(appStore, (i) => i.curServerIdx);
|
||||||
|
return servers.length > 0 && idx != null ? servers[idx] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default appStore;
|
export default appStore;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createStore, useStore } from "zustand";
|
import { createStore, useStore } from "zustand";
|
||||||
import { persist, createJSONStorage } from "zustand/middleware";
|
import { persist, createJSONStorage } from "zustand/middleware";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import termSessionStore from "./terminal-sessions";
|
||||||
|
|
||||||
type AuthStore = {
|
type AuthStore = {
|
||||||
token?: string | null;
|
token?: string | null;
|
||||||
@ -23,4 +24,9 @@ export const useAuthStore = () => {
|
|||||||
return { ...state, isLoggedIn: state.token != null };
|
return { ...state, isLoggedIn: state.token != null };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
authStore.setState({ token: null });
|
||||||
|
termSessionStore.setState({ sessions: [], curSession: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
export default authStore;
|
export default authStore;
|
||||||
|
@ -13,7 +13,7 @@ type TerminalSessionsStore = {
|
|||||||
setSession: (idx: number) => void;
|
setSession: (idx: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTermSession = create(
|
const termSessionStore = create(
|
||||||
persist<TerminalSessionsStore>(
|
persist<TerminalSessionsStore>(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
sessions: [],
|
sessions: [],
|
||||||
@ -44,3 +44,7 @@ export const useTermSession = create(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const useTermSession = termSessionStore;
|
||||||
|
|
||||||
|
export default termSessionStore;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user