feat: add drawer

This commit is contained in:
Khairul Hidayat 2024-11-12 04:43:59 +07:00
parent 38e81049a1
commit 8159b65605
10 changed files with 168 additions and 23 deletions

View File

@ -2,6 +2,8 @@ import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Drawer } from "expo-router/drawer";
import React from "react";
import { useMedia } from "tamagui";
import DrawerContent from "@/components/containers/drawer";
import Icons from "@/components/ui/icons";
export default function Layout() {
const media = useMedia();
@ -9,17 +11,40 @@ export default function Layout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Drawer
drawerContent={DrawerContent}
screenOptions={{
drawerType: media.sm ? "front" : "permanent",
drawerStyle: { width: 250 },
headerLeft: media.sm ? undefined : () => null,
}}
>
<Drawer.Screen name="hosts" options={{ title: "Hosts" }} />
<Drawer.Screen name="keychains" options={{ title: "Keychains" }} />
<Drawer.Screen
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
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>
</GestureHandlerRootView>

View File

@ -12,7 +12,7 @@ import { router, usePathname, useRootNavigationState } from "expo-router";
import { useAuthStore } from "@/stores/auth";
import { PortalProvider } from "tamagui";
import { queryClient } from "@/lib/api";
import { useAppStore } from "@/stores/app";
import { useServer } from "@/stores/app";
type Props = PropsWithChildren;
@ -54,7 +54,7 @@ const AuthProvider = () => {
const pathname = usePathname();
const rootNavigationState = useRootNavigationState();
const { isLoggedIn } = useAuthStore();
const { curServer } = useAppStore();
const curServer = useServer();
useEffect(() => {
if (!rootNavigationState?.key) {

View File

@ -1,13 +1,13 @@
import React from "react";
import { Redirect } from "expo-router";
import { useTermSession } from "@/stores/terminal-sessions";
import { useAppStore } from "@/stores/app";
import { useServer } from "@/stores/app";
export default function index() {
const { sessions, curSession } = useTermSession();
const { servers, curServer } = useAppStore();
const curServer = useServer();
if (!servers.length || !curServer) {
if (!curServer) {
return <Redirect href="/server" />;
}

View 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;

View File

@ -1,8 +1,8 @@
import React from "react";
import Terminal from "./terminal";
import { BASE_WS_URL } from "@/lib/api";
import VNCViewer from "./vncviewer";
import { useAuthStore } from "@/stores/auth";
import { AppServer, useServer } from "@/stores/app";
type SSHSessionProps = {
type: "ssh";
@ -30,8 +30,9 @@ export type InteractiveSessionProps = {
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
const { token } = useAuthStore();
const server = useServer();
const query = new URLSearchParams({ ...params, sid: token || "" });
const url = `${BASE_WS_URL}/ws/term?${query}`;
const url = `${getBaseUrl(server)}/ws/term?${query}`;
switch (type) {
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;

View File

@ -1,13 +1,18 @@
import { getCurrentServer } from "@/stores/app";
import authStore from "@/stores/auth";
import { QueryClient } from "@tanstack/react-query";
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({
baseURL: BASE_API_URL,
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;
if (authToken) {
config.options.headers.set("Authorization", `Bearer ${authToken}`);

View File

@ -66,10 +66,19 @@ export default function LoginPage() {
<ErrorAlert error={login.error} />
<FormField vertical label="Username/Email">
<InputField form={form} name="username" />
<InputField
form={form}
name="username"
onSubmitEditing={onSubmit}
/>
</FormField>
<FormField vertical label="Password">
<InputField form={form} name="password" secureTextEntry />
<InputField
form={form}
name="password"
secureTextEntry
onSubmitEditing={onSubmit}
/>
</FormField>
<Separator />

View File

@ -2,7 +2,7 @@ import { createStore, useStore } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
type AppServer = {
export type AppServer = {
name?: string;
url: string;
};
@ -51,12 +51,15 @@ export function setActiveServer(idx: number) {
appStore.setState({ curServerIdx: idx });
}
export const useAppStore = () => {
const state = useStore(appStore);
const curServer =
state.curServerIdx != null ? state.servers[state.curServerIdx] : null;
export function getCurrentServer() {
const state = appStore.getState();
return 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;

View File

@ -1,6 +1,7 @@
import { createStore, useStore } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";
import termSessionStore from "./terminal-sessions";
type AuthStore = {
token?: string | null;
@ -23,4 +24,9 @@ export const useAuthStore = () => {
return { ...state, isLoggedIn: state.token != null };
};
export const logout = () => {
authStore.setState({ token: null });
termSessionStore.setState({ sessions: [], curSession: 0 });
};
export default authStore;

View File

@ -13,7 +13,7 @@ type TerminalSessionsStore = {
setSession: (idx: number) => void;
};
export const useTermSession = create(
const termSessionStore = create(
persist<TerminalSessionsStore>(
(set) => ({
sessions: [],
@ -44,3 +44,7 @@ export const useTermSession = create(
}
)
);
export const useTermSession = termSessionStore;
export default termSessionStore;