Initial UI commit for 1.3
This commit is contained in:
123
src/App.tsx
123
src/App.tsx
@@ -1,14 +1,64 @@
|
|||||||
import React from "react"
|
import React, { useState, useEffect } from "react"
|
||||||
|
import { Sidebar } from "@/ui/Navigation/Sidebar.tsx"
|
||||||
|
import { Homepage } from "@/ui/Homepage/Homepage.tsx"
|
||||||
|
import { Terminal } from "@/ui/SSH/Terminal/Terminal.tsx"
|
||||||
|
import { SSHTunnel } from "@/ui/SSH/Tunnel/SSHTunnel.tsx"
|
||||||
|
import { ConfigEditor } from "@/ui/SSH/Config Editor/ConfigEditor.tsx"
|
||||||
|
import { SSHManager } from "@/ui/SSH/Manager/SSHManager.tsx"
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
import {Homepage} from "@/apps/Homepage/Homepage.tsx"
|
const apiBase = import.meta.env.DEV ? "http://localhost:8081/users" : "/users";
|
||||||
import {Terminal} from "@/apps/SSH/Terminal/Terminal.tsx"
|
const API = axios.create({ baseURL: apiBase });
|
||||||
import {SSHTunnel} from "@/apps/SSH/Tunnel/SSHTunnel.tsx";
|
|
||||||
import {ConfigEditor} from "@/apps/SSH/Config Editor/ConfigEditor.tsx";
|
function getCookie(name: string) {
|
||||||
import {SSHManager} from "@/apps/SSH/Manager/SSHManager.tsx"
|
return document.cookie.split('; ').reduce((r, v) => {
|
||||||
|
const parts = v.split('=');
|
||||||
|
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
||||||
|
}, "");
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [view, setView] = React.useState<string>("homepage")
|
const [view, setView] = React.useState<string>("homepage")
|
||||||
const [mountedViews, setMountedViews] = React.useState<Set<string>>(new Set(["homepage"]))
|
const [mountedViews, setMountedViews] = React.useState<Set<string>>(new Set(["homepage"]))
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
|
const [username, setUsername] = useState<string | null>(null)
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
const [authLoading, setAuthLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = () => {
|
||||||
|
const jwt = getCookie("jwt");
|
||||||
|
if (jwt) {
|
||||||
|
setAuthLoading(true);
|
||||||
|
API.get("/me", {headers: {Authorization: `Bearer ${jwt}`}})
|
||||||
|
.then((meRes) => {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
setIsAdmin(!!meRes.data.is_admin);
|
||||||
|
setUsername(meRes.data.username || null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setIsAdmin(false);
|
||||||
|
setUsername(null);
|
||||||
|
// Clear invalid JWT
|
||||||
|
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
|
})
|
||||||
|
.finally(() => setAuthLoading(false));
|
||||||
|
} else {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setIsAdmin(false);
|
||||||
|
setUsername(null);
|
||||||
|
setAuthLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAuth()
|
||||||
|
|
||||||
|
const handleStorageChange = () => checkAuth()
|
||||||
|
window.addEventListener('storage', handleStorageChange)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('storage', handleStorageChange)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSelectView = (nextView: string) => {
|
const handleSelectView = (nextView: string) => {
|
||||||
setMountedViews((prev) => {
|
setMountedViews((prev) => {
|
||||||
@@ -21,35 +71,38 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh w-full">
|
<Sidebar
|
||||||
<main className="flex-1 w-full">
|
onSelectView={handleSelectView}
|
||||||
{mountedViews.has("homepage") && (
|
disabled={!isAuthenticated || authLoading}
|
||||||
<div style={{display: view === "homepage" ? "block" : "none"}}>
|
isAdmin={isAdmin}
|
||||||
<Homepage onSelectView={handleSelectView} />
|
username={username}
|
||||||
</div>
|
>
|
||||||
)}
|
{mountedViews.has("homepage") && (
|
||||||
{mountedViews.has("ssh_manager") && (
|
<div style={{display: view === "homepage" ? "block" : "none"}}>
|
||||||
<div style={{display: view === "ssh_manager" ? "block" : "none"}}>
|
<Homepage onSelectView={handleSelectView} />
|
||||||
<SSHManager onSelectView={handleSelectView} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{mountedViews.has("ssh_manager") && (
|
||||||
{mountedViews.has("terminal") && (
|
<div style={{display: view === "ssh_manager" ? "block" : "none"}}>
|
||||||
<div style={{display: view === "terminal" ? "block" : "none"}}>
|
<SSHManager onSelectView={handleSelectView} />
|
||||||
<Terminal onSelectView={handleSelectView} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{mountedViews.has("terminal") && (
|
||||||
{mountedViews.has("tunnel") && (
|
<div style={{display: view === "terminal" ? "block" : "none"}}>
|
||||||
<div style={{display: view === "tunnel" ? "block" : "none"}}>
|
<Terminal onSelectView={handleSelectView} />
|
||||||
<SSHTunnel onSelectView={handleSelectView} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{mountedViews.has("tunnel") && (
|
||||||
{mountedViews.has("config_editor") && (
|
<div style={{display: view === "tunnel" ? "block" : "none"}}>
|
||||||
<div style={{display: view === "config_editor" ? "block" : "none"}}>
|
<SSHTunnel onSelectView={handleSelectView} />
|
||||||
<ConfigEditor onSelectView={handleSelectView} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{mountedViews.has("config_editor") && (
|
||||||
</main>
|
<div style={{display: view === "config_editor" ? "block" : "none"}}>
|
||||||
</div>
|
<ConfigEditor onSelectView={handleSelectView} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Sidebar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CornerDownLeft
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button
|
|
||||||
} from "@/components/ui/button.tsx"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuItem, SidebarProvider,
|
|
||||||
} from "@/components/ui/sidebar.tsx"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Separator,
|
|
||||||
} from "@/components/ui/separator.tsx"
|
|
||||||
|
|
||||||
interface SidebarProps {
|
|
||||||
onSelectView: (view: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SSHManagerSidebar({onSelectView}: SidebarProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<SidebarProvider>
|
|
||||||
<Sidebar>
|
|
||||||
<SidebarContent>
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel className="text-lg font-bold text-white flex items-center gap-2">
|
|
||||||
Termix / SSH Manager
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
<SidebarGroupContent className="flex flex-col flex-grow">
|
|
||||||
<SidebarMenu>
|
|
||||||
|
|
||||||
{/* Sidebar Items */}
|
|
||||||
<SidebarMenuItem key={"Homepage"}>
|
|
||||||
<Button className="w-full mt-2 mb-2 h-8" onClick={() => onSelectView("homepage")}
|
|
||||||
variant="outline">
|
|
||||||
<CornerDownLeft/>
|
|
||||||
Return
|
|
||||||
</Button>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
|
||||||
</Sidebar>
|
|
||||||
</SidebarProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {TemplateSidebar} from "@/apps/Template/TemplateSidebar.tsx";
|
|
||||||
|
|
||||||
interface ConfigEditorProps {
|
|
||||||
onSelectView: (view: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Template({onSelectView}: ConfigEditorProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TemplateSidebar
|
|
||||||
onSelectView={onSelectView}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Template
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CornerDownLeft
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button
|
|
||||||
} from "@/components/ui/button.tsx"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuItem, SidebarProvider,
|
|
||||||
} from "@/components/ui/sidebar.tsx"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Separator,
|
|
||||||
} from "@/components/ui/separator.tsx"
|
|
||||||
|
|
||||||
interface SidebarProps {
|
|
||||||
onSelectView: (view: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TemplateSidebar({onSelectView}: SidebarProps): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<SidebarProvider>
|
|
||||||
<Sidebar>
|
|
||||||
<SidebarContent>
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel className="text-lg font-bold text-white flex items-center gap-2">
|
|
||||||
Termix / Template
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
<SidebarGroupContent className="flex flex-col flex-grow">
|
|
||||||
<SidebarMenu>
|
|
||||||
|
|
||||||
<SidebarMenuItem key={"Homepage"}>
|
|
||||||
<Button className="w-full mt-2 mb-2 h-8" onClick={() => onSelectView("homepage")}
|
|
||||||
variant="outline">
|
|
||||||
<CornerDownLeft/>
|
|
||||||
Return
|
|
||||||
</Button>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
|
||||||
</Sidebar>
|
|
||||||
</SidebarProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -234,7 +234,7 @@ function Sidebar({
|
|||||||
// Adjust the padding for floating and inset variants.
|
// Adjust the padding for floating and inset variants.
|
||||||
variant === "floating" || variant === "inset"
|
variant === "floating" || variant === "inset"
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -242,7 +242,7 @@ function Sidebar({
|
|||||||
<div
|
<div
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar-inner"
|
data-slot="sidebar-inner"
|
||||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
className="bg-sidebar flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import {HomepageSidebar} from "@/apps/Homepage/HomepageSidebar.tsx";
|
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {HomepageAuth} from "@/apps/Homepage/HomepageAuth.tsx";
|
import {HomepageAuth} from "@/ui/Homepage/HomepageAuth.tsx";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {HomepageUpdateLog} from "@/apps/Homepage/HompageUpdateLog.tsx";
|
import {HomepageUpdateLog} from "@/ui/Homepage/HompageUpdateLog.tsx";
|
||||||
import {AlertManager} from "@/apps/Homepage/AlertManager.tsx";
|
import {HomepageAlertManager} from "@/ui/Homepage/HomepageAlertManager.tsx";
|
||||||
|
|
||||||
interface HomepageProps {
|
interface HomepageProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -70,35 +69,27 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomepageSidebar
|
<div className="w-full min-h-svh grid place-items-center">
|
||||||
onSelectView={onSelectView}
|
<div className="flex flex-row items-center justify-center gap-8">
|
||||||
disabled={!loggedIn || authLoading}
|
<HomepageAuth
|
||||||
isAdmin={isAdmin}
|
setLoggedIn={setLoggedIn}
|
||||||
username={loggedIn ? username : null}
|
setIsAdmin={setIsAdmin}
|
||||||
>
|
setUsername={setUsername}
|
||||||
<div className="w-full min-h-svh grid place-items-center">
|
setUserId={setUserId}
|
||||||
<div className="flex flex-row items-center justify-center gap-8">
|
loggedIn={loggedIn}
|
||||||
<HomepageAuth
|
authLoading={authLoading}
|
||||||
setLoggedIn={setLoggedIn}
|
dbError={dbError}
|
||||||
setIsAdmin={setIsAdmin}
|
setDbError={setDbError}
|
||||||
setUsername={setUsername}
|
/>
|
||||||
setUserId={setUserId}
|
<HomepageUpdateLog
|
||||||
loggedIn={loggedIn}
|
|
||||||
authLoading={authLoading}
|
|
||||||
dbError={dbError}
|
|
||||||
setDbError={setDbError}
|
|
||||||
/>
|
|
||||||
<HomepageUpdateLog
|
|
||||||
loggedIn={loggedIn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Alert Manager - replaces the old welcome card */}
|
|
||||||
<AlertManager
|
|
||||||
userId={userId}
|
|
||||||
loggedIn={loggedIn}
|
loggedIn={loggedIn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</HomepageSidebar>
|
|
||||||
|
<HomepageAlertManager
|
||||||
|
userId={userId}
|
||||||
|
loggedIn={loggedIn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card.tsx";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge.tsx";
|
||||||
import {X, ExternalLink, AlertTriangle, Info, CheckCircle, AlertCircle} from "lucide-react";
|
import {X, ExternalLink, AlertTriangle, Info, CheckCircle, AlertCircle} from "lucide-react";
|
||||||
|
|
||||||
interface TermixAlert {
|
interface TermixAlert {
|
||||||
@@ -63,7 +63,7 @@ const getTypeBadgeVariant = (type?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AlertCard({alert, onDismiss, onClose}: AlertCardProps): React.ReactElement {
|
export function HomepageAlertCard({alert, onDismiss, onClose}: AlertCardProps): React.ReactElement {
|
||||||
if (!alert) {
|
if (!alert) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {AlertCard} from "./AlertCard";
|
import {HomepageAlertCard} from "./HomepageAlertCard.tsx";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
interface TermixAlert {
|
interface TermixAlert {
|
||||||
@@ -25,7 +25,7 @@ const API = axios.create({
|
|||||||
baseURL: apiBase,
|
baseURL: apiBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AlertManager({userId, loggedIn}: AlertManagerProps): React.ReactElement {
|
export function HomepageAlertManager({userId, loggedIn}: AlertManagerProps): React.ReactElement {
|
||||||
const [alerts, setAlerts] = useState<TermixAlert[]>([]);
|
const [alerts, setAlerts] = useState<TermixAlert[]>([]);
|
||||||
const [currentAlertIndex, setCurrentAlertIndex] = useState(0);
|
const [currentAlertIndex, setCurrentAlertIndex] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -154,14 +154,12 @@ export function AlertManager({userId, loggedIn}: AlertManagerProps): React.React
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-10">
|
<div className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-10">
|
||||||
<div className="relative w-full max-w-2xl mx-4">
|
<div className="relative w-full max-w-2xl mx-4">
|
||||||
{/* Current Alert */}
|
<HomepageAlertCard
|
||||||
<AlertCard
|
|
||||||
alert={currentAlert}
|
alert={currentAlert}
|
||||||
onDismiss={handleDismissAlert}
|
onDismiss={handleDismissAlert}
|
||||||
onClose={handleCloseCurrentAlert}
|
onClose={handleCloseCurrentAlert}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Navigation Controls */}
|
|
||||||
{hasMultipleAlerts && (
|
{hasMultipleAlerts && (
|
||||||
<div className="absolute -bottom-16 left-1/2 transform -translate-x-1/2 flex items-center gap-2">
|
<div className="absolute -bottom-16 left-1/2 transform -translate-x-1/2 flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -188,7 +186,6 @@ export function AlertManager({userId, loggedIn}: AlertManagerProps): React.React
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error Display */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="absolute -bottom-20 left-1/2 transform -translate-x-1/2">
|
<div className="absolute -bottom-20 left-1/2 transform -translate-x-1/2">
|
||||||
<div className="bg-destructive text-destructive-foreground px-3 py-1 rounded text-sm">
|
<div className="bg-destructive text-destructive-foreground px-3 py-1 rounded text-sm">
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils.ts";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
import {Label} from "@/components/ui/label";
|
import {Label} from "@/components/ui/label.tsx";
|
||||||
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert";
|
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {Separator} from "@/components/ui/separator";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
function setCookie(name: string, value: string, days = 7) {
|
function setCookie(name: string, value: string, days = 7) {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React, {useState} from 'react';
|
||||||
import {
|
import {
|
||||||
Computer,
|
Computer,
|
||||||
Server,
|
Server,
|
||||||
File,
|
File,
|
||||||
Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings
|
Hammer, ChevronUp, User2, HardDrive, Trash2, Users, Shield, Settings, Menu, ChevronRight
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem, SidebarProvider, SidebarInset,
|
SidebarMenuItem, SidebarProvider, SidebarInset, SidebarHeader,
|
||||||
} from "@/components/ui/sidebar.tsx"
|
} from "@/components/ui/sidebar.tsx"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -74,7 +74,7 @@ const API = axios.create({
|
|||||||
baseURL: apiBase,
|
baseURL: apiBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function HomepageSidebar({
|
export function Sidebar({
|
||||||
onSelectView,
|
onSelectView,
|
||||||
getView,
|
getView,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -117,6 +117,8 @@ export function HomepageSidebar({
|
|||||||
const [makeAdminError, setMakeAdminError] = React.useState<string | null>(null);
|
const [makeAdminError, setMakeAdminError] = React.useState<string | null>(null);
|
||||||
const [makeAdminSuccess, setMakeAdminSuccess] = React.useState<string | null>(null);
|
const [makeAdminSuccess, setMakeAdminSuccess] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (adminSheetOpen) {
|
if (adminSheetOpen) {
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
@@ -341,14 +343,23 @@ export function HomepageSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-svh">
|
<div className="min-h-svh">
|
||||||
<SidebarProvider>
|
<SidebarProvider open={isSidebarOpen}>
|
||||||
<Sidebar>
|
<Sidebar variant="floating">
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarGroupLabel className="text-lg font-bold text-white">
|
||||||
|
Termix
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
|
className="w-[28px] h-[28px] absolute right-5"
|
||||||
|
>
|
||||||
|
<Menu className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
</SidebarHeader>
|
||||||
|
<Separator className="p-0.25"/>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel className="text-lg font-bold text-white flex items-center gap-2">
|
|
||||||
Termix
|
|
||||||
</SidebarGroupLabel>
|
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem key={"SSH Manager"}>
|
<SidebarMenuItem key={"SSH Manager"}>
|
||||||
@@ -893,6 +904,14 @@ export function HomepageSidebar({
|
|||||||
{children}
|
{children}
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
||||||
|
{!isSidebarOpen && (
|
||||||
|
<div
|
||||||
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
|
className="absolute top-0 left-0 w-[10px] h-full bg-[#18181b] cursor-pointer z-20 flex items-center justify-center">
|
||||||
|
<ChevronRight size={10} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, {useState, useEffect, useRef} from "react";
|
import React, {useState, useEffect, useRef} from "react";
|
||||||
import {ConfigEditorSidebar} from "@/apps/SSH/Config Editor/ConfigEditorSidebar.tsx";
|
import {ConfigEditorSidebar} from "@/ui/SSH/Config Editor/ConfigEditorSidebar.tsx";
|
||||||
import {ConfigTabList} from "@/apps/SSH/Config Editor/ConfigTabList.tsx";
|
import {ConfigTabList} from "@/ui/SSH/Config Editor/ConfigTabList.tsx";
|
||||||
import {ConfigHomeView} from "@/apps/SSH/Config Editor/ConfigHomeView.tsx";
|
import {ConfigHomeView} from "@/ui/SSH/Config Editor/ConfigHomeView.tsx";
|
||||||
import {ConfigCodeEditor} from "@/apps/SSH/Config Editor/ConfigCodeEditor.tsx";
|
import {ConfigCodeEditor} from "@/ui/SSH/Config Editor/ConfigCodeEditor.tsx";
|
||||||
import {Button} from '@/components/ui/button.tsx';
|
import {Button} from '@/components/ui/button.tsx';
|
||||||
import {ConfigTopbar} from "@/apps/SSH/Config Editor/ConfigTopbar.tsx";
|
import {ConfigTopbar} from "@/ui/SSH/Config Editor/ConfigTopbar.tsx";
|
||||||
import {cn} from '@/lib/utils.ts';
|
import {cn} from '@/lib/utils.ts';
|
||||||
import {
|
import {
|
||||||
getConfigEditorRecent,
|
getConfigEditorRecent,
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
writeSSHFile,
|
writeSSHFile,
|
||||||
getSSHStatus,
|
getSSHStatus,
|
||||||
connectSSH
|
connectSSH
|
||||||
} from '@/apps/SSH/ssh-axios.ts';
|
} from '@/ui/SSH/ssh-axios.ts';
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
getConfigEditorPinned,
|
getConfigEditorPinned,
|
||||||
addConfigEditorPinned,
|
addConfigEditorPinned,
|
||||||
removeConfigEditorPinned
|
removeConfigEditorPinned
|
||||||
} from '@/apps/SSH/ssh-axios.ts';
|
} from '@/ui/SSH/ssh-axios.ts';
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {SSHManagerSidebar} from "@/apps/SSH/Manager/SSHManagerSidebar.tsx";
|
import {SSHManagerHostViewer} from "@/ui/SSH/Manager/SSHManagerHostViewer.tsx"
|
||||||
import {SSHManagerHostViewer} from "@/apps/SSH/Manager/SSHManagerHostViewer.tsx"
|
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {SSHManagerHostEditor} from "@/apps/SSH/Manager/SSHManagerHostEditor.tsx";
|
import {SSHManagerHostEditor} from "@/ui/SSH/Manager/SSHManagerHostEditor.tsx";
|
||||||
|
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
||||||
|
|
||||||
interface ConfigEditorProps {
|
interface ConfigEditorProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -35,6 +35,7 @@ interface SSHHost {
|
|||||||
export function SSHManager({onSelectView}: ConfigEditorProps): React.ReactElement {
|
export function SSHManager({onSelectView}: ConfigEditorProps): React.ReactElement {
|
||||||
const [activeTab, setActiveTab] = useState("host_viewer");
|
const [activeTab, setActiveTab] = useState("host_viewer");
|
||||||
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
||||||
|
const {state: sidebarState} = useSidebar();
|
||||||
|
|
||||||
const handleEditHost = (host: SSHHost) => {
|
const handleEditHost = (host: SSHHost) => {
|
||||||
setEditingHost(host);
|
setEditingHost(host);
|
||||||
@@ -55,29 +56,25 @@ export function SSHManager({onSelectView}: ConfigEditorProps): React.ReactElemen
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SSHManagerSidebar
|
<div className="flex w-full h-screen overflow-hidden">
|
||||||
onSelectView={onSelectView}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex w-screen h-screen overflow-hidden">
|
|
||||||
<div className="w-[256px]"/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex-1 bg-[#18181b] m-[35px] text-white p-4 rounded-md w-[1200px] border h-[calc(100vh-70px)] flex flex-col min-h-0">
|
className={`flex-1 bg-[#18181b] m-[8px] text-white p-4 pt-0 rounded-lg border border-[#303032] flex flex-col min-h-0 ${
|
||||||
|
sidebarState === 'collapsed' ? 'ml-6' : ''
|
||||||
|
}`}>
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange}
|
<Tabs value={activeTab} onValueChange={handleTabChange}
|
||||||
className="flex-1 flex flex-col h-full min-h-0">
|
className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<TabsList>
|
<TabsList className="mt-1.5">
|
||||||
<TabsTrigger value="host_viewer">Host Viewer</TabsTrigger>
|
<TabsTrigger value="host_viewer">Host Viewer</TabsTrigger>
|
||||||
<TabsTrigger value="add_host">
|
<TabsTrigger value="add_host">
|
||||||
{editingHost ? "Edit Host" : "Add Host"}
|
{editingHost ? "Edit Host" : "Add Host"}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
|
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
||||||
<SSHManagerHostViewer onEditHost={handleEditHost}/>
|
<SSHManagerHostViewer onEditHost={handleEditHost}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="add_host" className="flex-1 flex flex-col h-full min-h-0">
|
<TabsContent value="add_host" className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<Separator className="p-0.25 mt-1 mb-1"/>
|
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
||||||
<div className="flex flex-col h-full min-h-0">
|
<div className="flex flex-col h-full min-h-0">
|
||||||
<SSHManagerHostEditor
|
<SSHManagerHostEditor
|
||||||
editingHost={editingHost}
|
editingHost={editingHost}
|
||||||
@@ -19,7 +19,7 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx
|
|||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {Switch} from "@/components/ui/switch.tsx";
|
import {Switch} from "@/components/ui/switch.tsx";
|
||||||
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/apps/SSH/ssh-axios';
|
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/SSH/ssh-axios';
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -388,7 +388,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
<div className="flex-1 flex flex-col h-full min-h-0 w-full">
|
<div className="flex-1 flex flex-col h-full min-h-0 w-full">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0 h-full">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0 h-full">
|
||||||
<ScrollArea className="flex-1 min-h-0 w-full my-1">
|
<ScrollArea className="flex-1 min-h-0 w-full my-1 pb-2">
|
||||||
<Tabs defaultValue="general" className="w-full">
|
<Tabs defaultValue="general" className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@@ -396,7 +396,7 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
<TabsTrigger value="tunnel">Tunnel</TabsTrigger>
|
<TabsTrigger value="tunnel">Tunnel</TabsTrigger>
|
||||||
<TabsTrigger value="config_editor">Config Editor</TabsTrigger>
|
<TabsTrigger value="config_editor">Config Editor</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general">
|
<TabsContent value="general" className="pt-2">
|
||||||
<FormLabel className="mb-3 font-bold">Connection Details</FormLabel>
|
<FormLabel className="mb-3 font-bold">Connection Details</FormLabel>
|
||||||
<div className="grid grid-cols-12 gap-4">
|
<div className="grid grid-cols-12 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1025,9 +1025,18 @@ export function SSHManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHost
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<footer className="shrink-0 w-full">
|
<footer className="shrink-0 w-full pb-0">
|
||||||
<Separator className="p-0.25 mt-1 mb-3"/>
|
<Separator className="p-0.25"/>
|
||||||
<Button type="submit" variant="outline">{editingHost ? "Update Host" : "Add Host"}</Button>
|
<Button
|
||||||
|
className=""
|
||||||
|
type="submit"
|
||||||
|
variant="outline"
|
||||||
|
style={{
|
||||||
|
transform: 'translateY(8px)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editingHost ? "Update Host" : "Add Host"}
|
||||||
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -6,7 +6,7 @@ import {ScrollArea} from "@/components/ui/scroll-area";
|
|||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/apps/SSH/ssh-axios";
|
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/SSH/ssh-axios";
|
||||||
import {
|
import {
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, {useState, useRef, useEffect} from "react";
|
import React, {useState, useRef, useEffect} from "react";
|
||||||
import {TerminalSidebar} from "@/apps/SSH/Terminal/TerminalSidebar.tsx";
|
import {TerminalSidebar} from "@/ui/SSH/Terminal/TerminalSidebar.tsx";
|
||||||
import {TerminalComponent} from "./TerminalComponent.tsx";
|
import {TerminalComponent} from "./TerminalComponent.tsx";
|
||||||
import {TerminalTopbar} from "@/apps/SSH/Terminal/TerminalTopbar.tsx";
|
import {TerminalTopbar} from "@/ui/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";
|
import {ChevronDown, ChevronRight} from "lucide-react";
|
||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
} from "@/components/ui/accordion.tsx";
|
} from "@/components/ui/accordion.tsx";
|
||||||
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
|
import {ScrollArea} from "@/components/ui/scroll-area.tsx";
|
||||||
import {Input} from "@/components/ui/input.tsx";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
import {getSSHHosts} from "@/apps/SSH/ssh-axios";
|
import {getSSHHosts} from "@/ui/SSH/ssh-axios";
|
||||||
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {TerminalTabList} from "@/apps/SSH/Terminal/TerminalTabList.tsx";
|
import {TerminalTabList} from "@/ui/SSH/Terminal/TerminalTabList.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {ChevronUp} from "lucide-react";
|
import {ChevronUp} from "lucide-react";
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, {useState, useEffect, useCallback} from "react";
|
import React, {useState, useEffect, useCallback} from "react";
|
||||||
import {SSHTunnelSidebar} from "@/apps/SSH/Tunnel/SSHTunnelSidebar.tsx";
|
import {SSHTunnelSidebar} from "@/ui/SSH/Tunnel/SSHTunnelSidebar.tsx";
|
||||||
import {SSHTunnelViewer} from "@/apps/SSH/Tunnel/SSHTunnelViewer.tsx";
|
import {SSHTunnelViewer} from "@/ui/SSH/Tunnel/SSHTunnelViewer.tsx";
|
||||||
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/apps/SSH/ssh-axios";
|
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/SSH/ssh-axios";
|
||||||
|
|
||||||
interface ConfigEditorProps {
|
interface ConfigEditorProps {
|
||||||
onSelectView: (view: string) => void;
|
onSelectView: (view: string) => void;
|
||||||
@@ -6,16 +6,12 @@
|
|||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting - Made extremely permissive */
|
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
@@ -31,8 +27,6 @@
|
|||||||
"allowUnreachableCode": true,
|
"allowUnreachableCode": true,
|
||||||
"noImplicitOverride": false,
|
"noImplicitOverride": false,
|
||||||
"noEmitOnError": false,
|
"noEmitOnError": false,
|
||||||
|
|
||||||
/* shadcn */
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
|
|||||||
Reference in New Issue
Block a user