mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: multi tab session
This commit is contained in:
parent
86eb5eb4e6
commit
5b37d7bae5
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
EXPO_PUBLIC_API_URL=http://localhost:3000
|
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -18,3 +18,5 @@ web-build/
|
|||||||
|
|
||||||
expo-env.d.ts
|
expo-env.d.ts
|
||||||
# @end expo-cli
|
# @end expo-cli
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
@ -1,14 +1,66 @@
|
|||||||
import { View, Text } from "react-native";
|
import { View, Text, ScrollView, Button } from "react-native";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import Terminal from "@/components/containers/terminal";
|
import InteractiveSession from "@/components/containers/interactive-session";
|
||||||
|
import PagerView from "@/components/ui/pager-view";
|
||||||
|
|
||||||
|
let nextSession = 1;
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
|
const [sessions, setSessions] = useState<string[]>(["1"]);
|
||||||
|
const [curSession, setSession] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Stack.Screen options={{ title: "Home" }} />
|
<Stack.Screen options={{ title: "Home" }} />
|
||||||
|
|
||||||
<Terminal wsUrl="ws://10.0.0.100:3000/ws" />
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
style={{ flexGrow: 0 }}
|
||||||
|
contentContainerStyle={{ flexDirection: "row", gap: 8 }}
|
||||||
|
>
|
||||||
|
{sessions.map((session, idx) => (
|
||||||
|
<View
|
||||||
|
key={session}
|
||||||
|
style={{ flexDirection: "row", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
title={"Session " + session}
|
||||||
|
color="#222"
|
||||||
|
onPress={() => setSession(idx)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title="X"
|
||||||
|
onPress={() => {
|
||||||
|
const newSessions = sessions.filter((s) => s !== session);
|
||||||
|
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}
|
||||||
|
type="ssh"
|
||||||
|
options={{ serverId: session }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</PagerView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
31
frontend/components/containers/interactive-session.tsx
Normal file
31
frontend/components/containers/interactive-session.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { View, Text } from "react-native";
|
||||||
|
import React from "react";
|
||||||
|
import Terminal from "./terminal";
|
||||||
|
import { BASE_WS_URL } from "@/lib/api";
|
||||||
|
|
||||||
|
type SSHSessionProps = {
|
||||||
|
type: "ssh";
|
||||||
|
options: {
|
||||||
|
serverId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = SSHSessionProps;
|
||||||
|
|
||||||
|
const InteractiveSession = ({ type, options }: Props) => {
|
||||||
|
switch (type) {
|
||||||
|
case "ssh":
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
serverId: options.serverId,
|
||||||
|
token: "token",
|
||||||
|
});
|
||||||
|
return <Terminal wsUrl={BASE_WS_URL + "/ws/ssh?" + params} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown interactive session type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InteractiveSession;
|
@ -69,10 +69,13 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
console.log("WS Open");
|
||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClose(e: CloseEvent) {
|
function onClose(e: CloseEvent) {
|
||||||
|
console.log("WS Closed", e.reason, e.code);
|
||||||
|
|
||||||
// Check if the close event was abnormal
|
// Check if the close event was abnormal
|
||||||
if (!e.wasClean) {
|
if (!e.wasClean) {
|
||||||
const reason = e.reason || `Code: ${e.code}`;
|
const reason = e.reason || `Code: ${e.code}`;
|
||||||
|
27
frontend/components/ui/pager-view.tsx
Normal file
27
frontend/components/ui/pager-view.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { ComponentPropsWithoutRef, useEffect, useRef } from "react";
|
||||||
|
import RNPagerView from "react-native-pager-view";
|
||||||
|
|
||||||
|
export type PagerViewProps = ComponentPropsWithoutRef<typeof RNPagerView> & {
|
||||||
|
page?: number;
|
||||||
|
onChangePage?: (page: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PagerView = ({ page, onChangePage, ...props }: PagerViewProps) => {
|
||||||
|
const ref = useRef<RNPagerView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (page != null) {
|
||||||
|
ref.current?.setPage(page);
|
||||||
|
}
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RNPagerView
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
onPageSelected={(e) => onChangePage?.(e.nativeEvent.position)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PagerView;
|
39
frontend/components/ui/pager-view.web.tsx
Normal file
39
frontend/components/ui/pager-view.web.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { PagerViewProps } from "./pager-view";
|
||||||
|
|
||||||
|
const PagerView = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
page,
|
||||||
|
initialPage,
|
||||||
|
}: PagerViewProps) => {
|
||||||
|
const [curPage, setPage] = useState<number>(page || initialPage || 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (page != null) {
|
||||||
|
setPage(page);
|
||||||
|
}
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (!Array.isArray(children)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children.map((element, index) => {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={element.key || index}
|
||||||
|
style={{ display: index === curPage ? "flex" : "none", flex: 1 }}
|
||||||
|
>
|
||||||
|
{element}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [curPage, children]);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PagerView;
|
2
frontend/lib/api.ts
Normal file
2
frontend/lib/api.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
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");
|
@ -38,6 +38,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.1",
|
"react-native": "0.76.1",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
|
"react-native-pager-view": "6.4.1",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "4.0.0-beta.16",
|
"react-native-screens": "4.0.0-beta.16",
|
||||||
|
14
frontend/pnpm-lock.yaml
generated
14
frontend/pnpm-lock.yaml
generated
@ -74,6 +74,9 @@ importers:
|
|||||||
react-native-gesture-handler:
|
react-native-gesture-handler:
|
||||||
specifier: ~2.20.2
|
specifier: ~2.20.2
|
||||||
version: 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)
|
version: 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-pager-view:
|
||||||
|
specifier: 6.4.1
|
||||||
|
version: 6.4.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-reanimated:
|
react-native-reanimated:
|
||||||
specifier: ~3.16.1
|
specifier: ~3.16.1
|
||||||
version: 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)
|
version: 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)
|
||||||
@ -3667,6 +3670,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.6.0 || ^17.0.0 || ^18.0.0
|
react: ^16.6.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
|
react-native-pager-view@6.4.1:
|
||||||
|
resolution: {integrity: sha512-HnDxXTRHnR6WJ/vnOitv0C32KG9MJjxLnxswuQlBJmQ7RxF2GWOHSPIRAdZ9fLxdLstV38z9Oz1C95+t+yXkcg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
react-native: '*'
|
||||||
|
|
||||||
react-native-reanimated@3.16.1:
|
react-native-reanimated@3.16.1:
|
||||||
resolution: {integrity: sha512-Wnbo7toHZ6kPLAD8JWKoKCTfNoqYOMW5vUEP76Rr4RBmJCrdXj6oauYP0aZnZq8NCbiP5bwwu7+RECcWtoetnQ==}
|
resolution: {integrity: sha512-Wnbo7toHZ6kPLAD8JWKoKCTfNoqYOMW5vUEP76Rr4RBmJCrdXj6oauYP0aZnZq8NCbiP5bwwu7+RECcWtoetnQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -9141,6 +9150,11 @@ snapshots:
|
|||||||
react-fast-compare: 3.2.2
|
react-fast-compare: 3.2.2
|
||||||
shallowequal: 1.1.0
|
shallowequal: 1.1.0
|
||||||
|
|
||||||
|
react-native-pager-view@6.4.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-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):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
|
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
tmp/
|
@ -121,10 +121,12 @@ func sshWebSocketHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Handle WebSocket to SSH data streaming
|
// Handle WebSocket to SSH data streaming
|
||||||
go func() {
|
go func() {
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, msg, err := wsConn.Read(context.Background())
|
_, msg, err := wsConn.Read(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(string(msg), "\x01") {
|
if strings.HasPrefix(string(msg), "\x01") {
|
||||||
@ -153,7 +155,7 @@ func sshWebSocketHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
http.HandleFunc("/ws", sshWebSocketHandler)
|
http.HandleFunc("/ws/ssh", sshWebSocketHandler)
|
||||||
log.Println("Server started on :3000")
|
log.Println("Server started on :3000")
|
||||||
log.Fatal(http.ListenAndServe(":3000", nil))
|
log.Fatal(http.ListenAndServe(":3000", nil))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user