From 8159b65605b1c44ecf9075171d86d8d1e93e3aab Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Tue, 12 Nov 2024 04:43:59 +0700 Subject: [PATCH] feat: add drawer --- frontend/app/(drawer)/_layout.tsx | 31 ++++++- frontend/app/_providers.tsx | 4 +- frontend/app/index.tsx | 6 +- frontend/components/containers/drawer.tsx | 88 +++++++++++++++++++ .../containers/interactive-session.tsx | 9 +- frontend/lib/api.ts | 13 ++- frontend/pages/auth/login.tsx | 13 ++- frontend/stores/app.ts | 15 ++-- frontend/stores/auth.ts | 6 ++ frontend/stores/terminal-sessions.ts | 6 +- 10 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 frontend/components/containers/drawer.tsx diff --git a/frontend/app/(drawer)/_layout.tsx b/frontend/app/(drawer)/_layout.tsx index fc20c2d..be5a791 100644 --- a/frontend/app/(drawer)/_layout.tsx +++ b/frontend/app/(drawer)/_layout.tsx @@ -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 ( null, }} > - - + ( + + ), + }} + /> + ( + + ), + }} + /> ( + + ), + }} /> diff --git a/frontend/app/_providers.tsx b/frontend/app/_providers.tsx index 56da1e1..02a867b 100644 --- a/frontend/app/_providers.tsx +++ b/frontend/app/_providers.tsx @@ -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) { diff --git a/frontend/app/index.tsx b/frontend/app/index.tsx index 0b7d8d9..0814aba 100644 --- a/frontend/app/index.tsx +++ b/frontend/app/index.tsx @@ -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 ; } diff --git a/frontend/components/containers/drawer.tsx b/frontend/components/containers/drawer.tsx new file mode 100644 index 0000000..b1ffa3a --- /dev/null +++ b/frontend/components/containers/drawer.tsx @@ -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 ( + <> + + + + + + + + + ); +}; + +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 ( + + + + ); + }) as React.ReactNode as React.ReactElement; +}; + +export default Drawer; diff --git a/frontend/components/containers/interactive-session.tsx b/frontend/components/containers/interactive-session.tsx index 81f7d34..79d5df1 100644 --- a/frontend/components/containers/interactive-session.tsx +++ b/frontend/components/containers/interactive-session.tsx @@ -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; diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index e5975cf..b276f4b 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -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}`); diff --git a/frontend/pages/auth/login.tsx b/frontend/pages/auth/login.tsx index dbab297..73eae9e 100644 --- a/frontend/pages/auth/login.tsx +++ b/frontend/pages/auth/login.tsx @@ -66,10 +66,19 @@ export default function LoginPage() { - + - + diff --git a/frontend/stores/app.ts b/frontend/stores/app.ts index ad0fb09..2177025 100644 --- a/frontend/stores/app.ts +++ b/frontend/stores/app.ts @@ -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; diff --git a/frontend/stores/auth.ts b/frontend/stores/auth.ts index 9d812e5..eeb9303 100644 --- a/frontend/stores/auth.ts +++ b/frontend/stores/auth.ts @@ -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; diff --git a/frontend/stores/terminal-sessions.ts b/frontend/stores/terminal-sessions.ts index 55e47be..6e49548 100644 --- a/frontend/stores/terminal-sessions.ts +++ b/frontend/stores/terminal-sessions.ts @@ -13,7 +13,7 @@ type TerminalSessionsStore = { setSession: (idx: number) => void; }; -export const useTermSession = create( +const termSessionStore = create( persist( (set) => ({ sessions: [], @@ -44,3 +44,7 @@ export const useTermSession = create( } ) ); + +export const useTermSession = termSessionStore; + +export default termSessionStore;