Rename terminals, add welcome card for the survey, add buttons to make it clear how to open the sidebar/topbar after closing.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
import {Homepage} from "@/apps/Homepage/Homepage.tsx"
|
import {Homepage} from "@/apps/Homepage/Homepage.tsx"
|
||||||
import {SSH} from "@/apps/SSH/Terminal/SSH.tsx"
|
import {Terminal} from "@/apps/SSH/Terminal/Terminal.tsx"
|
||||||
import {SSHTunnel} from "@/apps/SSH/Tunnel/SSHTunnel.tsx";
|
import {SSHTunnel} from "@/apps/SSH/Tunnel/SSHTunnel.tsx";
|
||||||
import {ConfigEditor} from "@/apps/SSH/Config Editor/ConfigEditor.tsx";
|
import {ConfigEditor} from "@/apps/SSH/Config Editor/ConfigEditor.tsx";
|
||||||
import {SSHManager} from "@/apps/SSH/Manager/SSHManager.tsx"
|
import {SSHManager} from "@/apps/SSH/Manager/SSHManager.tsx"
|
||||||
@@ -35,7 +35,7 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
{mountedViews.has("terminal") && (
|
{mountedViews.has("terminal") && (
|
||||||
<div style={{display: view === "terminal" ? "block" : "none"}}>
|
<div style={{display: view === "terminal" ? "block" : "none"}}>
|
||||||
<SSH onSelectView={handleSelectView} />
|
<Terminal onSelectView={handleSelectView} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mountedViews.has("tunnel") && (
|
{mountedViews.has("tunnel") && (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, {useEffect, useState} from "react";
|
|||||||
import {HomepageAuth} from "@/apps/Homepage/HomepageAuth.tsx";
|
import {HomepageAuth} from "@/apps/Homepage/HomepageAuth.tsx";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {HomepageUpdateLog} from "@/apps/Homepage/HompageUpdateLog.tsx";
|
import {HomepageUpdateLog} from "@/apps/Homepage/HompageUpdateLog.tsx";
|
||||||
|
import {HomepageWelcomeCard} from "@/apps/Homepage/HomepageWelcomeCard.tsx";
|
||||||
|
|
||||||
interface HomepageProps {
|
interface HomepageProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -32,9 +33,12 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
|||||||
const [username, setUsername] = useState<string | null>(null);
|
const [username, setUsername] = useState<string | null>(null);
|
||||||
const [authLoading, setAuthLoading] = useState(true);
|
const [authLoading, setAuthLoading] = useState(true);
|
||||||
const [dbError, setDbError] = useState<string | null>(null);
|
const [dbError, setDbError] = useState<string | null>(null);
|
||||||
|
const [showWelcomeCard, setShowWelcomeCard] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
|
const welcomeHidden = getCookie("welcome_hidden");
|
||||||
|
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
setAuthLoading(true);
|
setAuthLoading(true);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -46,6 +50,7 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
|||||||
setIsAdmin(!!meRes.data.is_admin);
|
setIsAdmin(!!meRes.data.is_admin);
|
||||||
setUsername(meRes.data.username || null);
|
setUsername(meRes.data.username || null);
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
|
setShowWelcomeCard(welcomeHidden !== "true");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
@@ -64,6 +69,11 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleHideWelcomeCard = () => {
|
||||||
|
setShowWelcomeCard(false);
|
||||||
|
setCookie("welcome_hidden", "true", 365 * 10);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomepageSidebar
|
<HomepageSidebar
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView}
|
||||||
@@ -86,6 +96,13 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
|||||||
loggedIn={loggedIn}
|
loggedIn={loggedIn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{loggedIn && !authLoading && showWelcomeCard && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-10">
|
||||||
|
<HomepageWelcomeCard onHidePermanently={handleHideWelcomeCard}/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</HomepageSidebar>
|
</HomepageSidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
58
src/apps/Homepage/HomepageWelcomeCard.tsx
Normal file
58
src/apps/Homepage/HomepageWelcomeCard.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface HomepageWelcomeCardProps {
|
||||||
|
onHidePermanently: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HomepageWelcomeCard({onHidePermanently}: HomepageWelcomeCardProps): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold text-center">
|
||||||
|
The Future of Termix
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground text-center leading-relaxed">
|
||||||
|
Please checkout the linked survey{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.google.com/forms/d/e/1FAIpQLSeGvnQODFtnpjmJsMKgASbaQ87CLQEBCcnzK_Vuw5TdfbfIyA/viewform?usp=sharing&ouid=107601685503825301492"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline hover:text-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
. The purpose of this survey is to gather feedback from users on what the future UI of Termix could
|
||||||
|
look like to optimize server management. Please take a minute or two to read the survey questions
|
||||||
|
and answer them to the best of your ability. Thank you!
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-center leading-relaxed mt-6">
|
||||||
|
A special thanks to those in Asia who recently joined Termix through various forum posts, keep
|
||||||
|
sharing it! A Chinese translation is planned for Termix, but since I don’t speak Chinese, I’ll need
|
||||||
|
to hire someone to help with the translation. If you’d like to support me financially, you can do
|
||||||
|
so{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/LukeGus"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline hover:text-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
here.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onHidePermanently}
|
||||||
|
className="w-full max-w-xs"
|
||||||
|
>
|
||||||
|
Hide Permanently
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, {useState, useRef, useEffect} from "react";
|
import React, {useState, useRef, useEffect} from "react";
|
||||||
import {SSHSidebar} from "@/apps/SSH/Terminal/SSHSidebar.tsx";
|
import {TerminalSidebar} from "@/apps/SSH/Terminal/TerminalSidebar.tsx";
|
||||||
import {SSHTerminal} from "./SSHTerminal.tsx";
|
import {TerminalComponent} from "./TerminalComponent.tsx";
|
||||||
import {SSHTopbar} from "@/apps/SSH/Terminal/SSHTopbar.tsx";
|
import {TerminalTopbar} from "@/apps/SSH/Terminal/TerminalTopbar.tsx";
|
||||||
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
|
import {ResizablePanelGroup, ResizablePanel, ResizableHandle} from '@/components/ui/resizable.tsx';
|
||||||
import * as ResizablePrimitive from "react-resizable-panels";
|
import * as ResizablePrimitive from "react-resizable-panels";
|
||||||
|
import {ChevronDown, ChevronRight} from "lucide-react";
|
||||||
|
|
||||||
interface ConfigEditorProps {
|
interface ConfigEditorProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -16,7 +17,7 @@ type Tab = {
|
|||||||
terminalRef: React.RefObject<any>;
|
terminalRef: React.RefObject<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
export function Terminal({onSelectView}: ConfigEditorProps): React.ReactElement {
|
||||||
const [allTabs, setAllTabs] = useState<Tab[]>([]);
|
const [allTabs, setAllTabs] = useState<Tab[]>([]);
|
||||||
const [currentTab, setCurrentTab] = useState<number | null>(null);
|
const [currentTab, setCurrentTab] = useState<number | null>(null);
|
||||||
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
|
const [allSplitScreenTab, setAllSplitScreenTab] = useState<number[]>([]);
|
||||||
@@ -25,7 +26,7 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
||||||
const SIDEBAR_WIDTH = 256;
|
const SIDEBAR_WIDTH = 256;
|
||||||
const HANDLE_THICKNESS = 6;
|
const HANDLE_THICKNESS = 10;
|
||||||
|
|
||||||
const [panelRects, setPanelRects] = useState<Record<string, DOMRect | null>>({});
|
const [panelRects, setPanelRects] = useState<Record<string, DOMRect | null>>({});
|
||||||
const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
const panelRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
@@ -160,7 +161,7 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
const isVisible = !!layoutStyles[tab.id];
|
const isVisible = !!layoutStyles[tab.id];
|
||||||
return (
|
return (
|
||||||
<div key={tab.id} style={style} data-terminal-id={tab.id}>
|
<div key={tab.id} style={style} data-terminal-id={tab.id}>
|
||||||
<SSHTerminal
|
<TerminalComponent
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
ref={tab.terminalRef}
|
ref={tab.terminalRef}
|
||||||
hostConfig={tab.hostConfig}
|
hostConfig={tab.hostConfig}
|
||||||
@@ -593,7 +594,6 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', width: '100vw', height: '100vh', overflow: 'hidden', position: 'relative'}}>
|
<div style={{display: 'flex', width: '100vw', height: '100vh', overflow: 'hidden', position: 'relative'}}>
|
||||||
{/* Sidebar (collapsible) */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: isSidebarOpen ? SIDEBAR_WIDTH : 0,
|
width: isSidebarOpen ? SIDEBAR_WIDTH : 0,
|
||||||
@@ -609,7 +609,7 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
willChange: 'width',
|
willChange: 'width',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SSHSidebar
|
<TerminalSidebar
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView}
|
||||||
onHostConnect={onHostConnect}
|
onHostConnect={onHostConnect}
|
||||||
allTabs={allTabs}
|
allTabs={allTabs}
|
||||||
@@ -655,7 +655,7 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
willChange: 'height',
|
willChange: 'height',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SSHTopbar
|
<TerminalTopbar
|
||||||
allTabs={allTabs}
|
allTabs={allTabs}
|
||||||
currentTab={currentTab ?? -1}
|
currentTab={currentTab ?? -1}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
@@ -677,12 +677,15 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
background: '#222224',
|
background: '#222224',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
zIndex: 12,
|
zIndex: 12,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
title="Show top bar"
|
title="Show top bar">
|
||||||
/>
|
<ChevronDown size={HANDLE_THICKNESS} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main terminal area (height adapts to topbar) */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: isTopbarOpen ? 'calc(100% - 46px)' : '100%',
|
height: isTopbarOpen ? 'calc(100% - 46px)' : '100%',
|
||||||
@@ -756,7 +759,6 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar reopen handle */}
|
|
||||||
{!isSidebarOpen && (
|
{!isSidebarOpen && (
|
||||||
<div
|
<div
|
||||||
onClick={() => setIsSidebarOpen(true)}
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
@@ -769,9 +771,13 @@ export function SSH({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|||||||
background: '#222224',
|
background: '#222224',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
title="Show sidebar"
|
title="Show sidebar">
|
||||||
/>
|
<ChevronRight size={HANDLE_THICKNESS} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -13,7 +13,7 @@ interface SSHTerminalProps {
|
|||||||
splitScreen?: boolean;
|
splitScreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SSHTerminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
export const TerminalComponent = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||||
{hostConfig, isVisible, splitScreen = false},
|
{hostConfig, isVisible, splitScreen = false},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
@@ -74,7 +74,7 @@ export interface SidebarProps {
|
|||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHSidebar({
|
export function TerminalSidebar({
|
||||||
onSelectView,
|
onSelectView,
|
||||||
onHostConnect,
|
onHostConnect,
|
||||||
allTabs,
|
allTabs,
|
||||||
@@ -16,7 +16,7 @@ interface SSHTabListProps {
|
|||||||
setCloseTab: (tab: number) => void;
|
setCloseTab: (tab: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHTabList({
|
export function TerminalTabList({
|
||||||
allTabs,
|
allTabs,
|
||||||
currentTab,
|
currentTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {SSHTabList} from "@/apps/SSH/Terminal/SSHTabList.tsx";
|
import {TerminalTabList} from "@/apps/SSH/Terminal/TerminalTabList.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {ChevronUp} from "lucide-react";
|
import {ChevronUp} from "lucide-react";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ interface SSHTopbarProps {
|
|||||||
onHideTopbar?: () => void;
|
onHideTopbar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SSHTopbar({
|
export function TerminalTopbar({
|
||||||
allTabs,
|
allTabs,
|
||||||
currentTab,
|
currentTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
@@ -38,7 +38,7 @@ export function SSHTopbar({
|
|||||||
}}>
|
}}>
|
||||||
<div style={{flex: 1, minWidth: 0, height: '100%', overflowX: 'auto'}}>
|
<div style={{flex: 1, minWidth: 0, height: '100%', overflowX: 'auto'}}>
|
||||||
<div style={{minWidth: 'max-content', height: '100%', paddingLeft: 8, overflowY: 'hidden'}}>
|
<div style={{minWidth: 'max-content', height: '100%', paddingLeft: 8, overflowY: 'hidden'}}>
|
||||||
<SSHTabList
|
<TerminalTabList
|
||||||
allTabs={allTabs}
|
allTabs={allTabs}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
Reference in New Issue
Block a user