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 (
+ <>
+
+
+
+
+
+ }
+ onPress={() => logout()}
+ >
+ Logout
+
+
+ >
+ );
+};
+
+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;