mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: move session sharing and collab rooms into a plugin (B12)
This commit is contained in:
1 parent
791d8656a1
commit
a276f6898c
207 files changed
+35771
-11072
No files matched your search
@@ -54,10 +54,6 @@ import {
|
||||
type HostDefaults,
|
||||
type BrandingSettings,
|
||||
} from "@/api/settings-api";
|
||||
import {
|
||||
getSessionSharingGloballyEnabled,
|
||||
updateSessionSharingGloballyEnabled,
|
||||
} from "@/api/session-sharing-api";
|
||||
import {
|
||||
getAcmeSslSettings,
|
||||
updateAcmeSslSettings,
|
||||
@@ -148,8 +144,6 @@ export function AdminSettingsPanel({
|
||||
const [analyticsLocked, setAnalyticsLocked] = useState(false);
|
||||
const [oidcSilentLoginDefaultLocked, setOidcSilentLoginDefaultLocked] =
|
||||
useState(false);
|
||||
const [sessionSharingGloballyEnabled, setSessionSharingGloballyEnabled] =
|
||||
useState(true);
|
||||
const [stepCaPrivateEndpoints, setStepCaPrivateEndpoints] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
@@ -361,7 +355,6 @@ export function AdminSettingsPanel({
|
||||
secondFactorExternal,
|
||||
oidcSilent,
|
||||
analytics,
|
||||
sessionSharingEnabled,
|
||||
notificationEndpoints,
|
||||
stepCaEndpoints,
|
||||
secretSourceEndpoints,
|
||||
@@ -378,7 +371,6 @@ export function AdminSettingsPanel({
|
||||
getSecondFactorAfterExternalLogin(),
|
||||
getOidcSilentLoginDefault(),
|
||||
getAnalyticsEnabled(),
|
||||
getSessionSharingGloballyEnabled(),
|
||||
getNotificationPrivateEndpoints(),
|
||||
getStepCaPrivateEndpoints(),
|
||||
getSecretSourcePrivateEndpoints(),
|
||||
@@ -418,9 +410,6 @@ export function AdminSettingsPanel({
|
||||
setAnalyticsEnabled(analytics.value.enabled);
|
||||
setAnalyticsLocked(analytics.value.locked ?? false);
|
||||
}
|
||||
if (sessionSharingEnabled.status === "fulfilled") {
|
||||
setSessionSharingGloballyEnabled(sessionSharingEnabled.value.enabled);
|
||||
}
|
||||
if (stepCaEndpoints.status === "fulfilled") {
|
||||
setStepCaPrivateEndpoints(stepCaEndpoints.value);
|
||||
}
|
||||
@@ -553,17 +542,6 @@ export function AdminSettingsPanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleSessionSharingGloballyEnabled() {
|
||||
const newVal = !sessionSharingGloballyEnabled;
|
||||
setSessionSharingGloballyEnabled(newVal);
|
||||
try {
|
||||
await updateSessionSharingGloballyEnabled(newVal);
|
||||
} catch {
|
||||
setSessionSharingGloballyEnabled(!newVal);
|
||||
toast.error(t("admin.updateSessionSharingFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveStepCaSettings() {
|
||||
try {
|
||||
await updateStepCaSettings(stepCaSettings);
|
||||
@@ -1088,7 +1066,6 @@ export function AdminSettingsPanel({
|
||||
analyticsEnabled={analyticsEnabled}
|
||||
analyticsLocked={analyticsLocked}
|
||||
handleToggleAnalytics={handleToggleAnalytics}
|
||||
sessionSharingGloballyEnabled={sessionSharingGloballyEnabled}
|
||||
notificationPrivateEndpoints={notificationPrivateEndpoints}
|
||||
stepCaPrivateEndpoints={stepCaPrivateEndpoints}
|
||||
onSaveStepCaPrivateEndpoints={handleSaveStepCaPrivateEndpoints}
|
||||
@@ -1102,9 +1079,6 @@ export function AdminSettingsPanel({
|
||||
onSaveNotificationPrivateEndpoints={
|
||||
handleSaveNotificationPrivateEndpoints
|
||||
}
|
||||
handleToggleSessionSharingGloballyEnabled={
|
||||
handleToggleSessionSharingGloballyEnabled
|
||||
}
|
||||
allowRegistration={allowRegistration}
|
||||
handleToggleRegistration={handleToggleRegistration}
|
||||
allowPasswordLogin={allowPasswordLogin}
|
||||
|
||||
@@ -27,7 +27,6 @@ type GeneralSettingsSectionProps = {
|
||||
analyticsEnabled: boolean;
|
||||
analyticsLocked: boolean;
|
||||
handleToggleAnalytics: () => void;
|
||||
sessionSharingGloballyEnabled: boolean;
|
||||
notificationPrivateEndpoints: string[];
|
||||
onSaveNotificationPrivateEndpoints: (hosts: string[]) => void;
|
||||
stepCaPrivateEndpoints: string[];
|
||||
@@ -39,7 +38,6 @@ type GeneralSettingsSectionProps = {
|
||||
SetStateAction<{ caUrl: string; fingerprint: string; provisioner: string }>
|
||||
>;
|
||||
handleSaveStepCaSettings: () => void;
|
||||
handleToggleSessionSharingGloballyEnabled: () => void;
|
||||
allowRegistration: boolean;
|
||||
handleToggleRegistration: () => void;
|
||||
allowPasswordLogin: boolean;
|
||||
@@ -77,7 +75,6 @@ export function AdminGeneralSettingsSection({
|
||||
analyticsEnabled,
|
||||
analyticsLocked,
|
||||
handleToggleAnalytics,
|
||||
sessionSharingGloballyEnabled,
|
||||
notificationPrivateEndpoints,
|
||||
onSaveNotificationPrivateEndpoints,
|
||||
stepCaPrivateEndpoints,
|
||||
@@ -87,7 +84,6 @@ export function AdminGeneralSettingsSection({
|
||||
stepCaSettings,
|
||||
setStepCaSettings,
|
||||
handleSaveStepCaSettings,
|
||||
handleToggleSessionSharingGloballyEnabled,
|
||||
allowRegistration,
|
||||
handleToggleRegistration,
|
||||
allowPasswordLogin,
|
||||
@@ -142,15 +138,6 @@ export function AdminGeneralSettingsSection({
|
||||
disabled={analyticsLocked}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("admin.sessionSharingGloballyEnabled")}
|
||||
description={t("admin.sessionSharingGloballyEnabledDesc")}
|
||||
>
|
||||
<AdminToggle
|
||||
on={sessionSharingGloballyEnabled}
|
||||
onToggle={handleToggleSessionSharingGloballyEnabled}
|
||||
/>
|
||||
</SettingRow>
|
||||
<div className="flex flex-col gap-1.5 py-2">
|
||||
<span className="text-xs font-medium">
|
||||
{t("admin.notificationPrivateEndpoints")}
|
||||
|
||||
@@ -25,8 +25,7 @@ export type CoreRailView =
|
||||
| "connections"
|
||||
| "session-logs"
|
||||
| "user-profile"
|
||||
| "admin-settings"
|
||||
| "collab";
|
||||
| "admin-settings";
|
||||
|
||||
export type RailView = CoreRailView | (string & {});
|
||||
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2, Plus, Presentation, RefreshCw } from "lucide-react";
|
||||
import { Button } from "@/components/button";
|
||||
import { Input } from "@/components/input";
|
||||
import { Badge } from "@/components/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/dialog";
|
||||
import {
|
||||
createCollabRoom,
|
||||
listCollabRooms,
|
||||
type CollabRoom,
|
||||
} from "@/api/collab-api";
|
||||
import { getErrorMessage } from "@/lib/error-message";
|
||||
|
||||
export function CollabPanel({
|
||||
onOpenRoom,
|
||||
}: {
|
||||
onOpenRoom: (room: CollabRoom) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [rooms, setRooms] = useState<CollabRoom[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [persistent, setPersistent] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const result = await listCollabRooms();
|
||||
setRooms(result.rooms ?? []);
|
||||
} catch {
|
||||
/* the list stays as-is */
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
const timer = window.setInterval(() => void refresh(), 30000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [refresh]);
|
||||
|
||||
async function handleCreate() {
|
||||
if (!name.trim()) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const { room } = await createCollabRoom(name.trim(), persistent);
|
||||
toast.success(t("collab.created"));
|
||||
setCreateOpen(false);
|
||||
setName("");
|
||||
setPersistent(false);
|
||||
await refresh();
|
||||
onOpenRoom(room);
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 text-xs flex-1 border border-accent-brand/40 bg-accent-brand/10 text-accent-brand font-semibold hover:bg-accent-brand/20"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="size-3.5 mr-1" />
|
||||
{t("collab.createRoom")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 px-2"
|
||||
onClick={() => void refresh()}
|
||||
aria-label={t("common.refresh")}
|
||||
title={t("common.refresh")}
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-6">
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : rooms.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground px-1 py-4 text-center">
|
||||
{t("collab.noRooms")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{rooms.map((room) => (
|
||||
<button
|
||||
key={room.id}
|
||||
type="button"
|
||||
onClick={() => onOpenRoom(room)}
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-left border border-border hover:bg-muted/50"
|
||||
>
|
||||
<Presentation className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1 text-xs truncate">{room.name}</span>
|
||||
{room.presenterUserId && (
|
||||
<span
|
||||
className="size-1.5 rounded-full bg-red-500 shrink-0"
|
||||
role="img"
|
||||
aria-label={t("collab.presenterBadge")}
|
||||
title={t("collab.presenterBadge")}
|
||||
/>
|
||||
)}
|
||||
{room.persistent && (
|
||||
<Badge variant="outline" className="text-[9px] px-1 py-0">
|
||||
{t("collab.persistentRoom")}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("collab.createRoom")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label htmlFor="collab-room-name" className="text-xs font-medium">
|
||||
{t("collab.roomName")}
|
||||
</label>
|
||||
<Input
|
||||
id="collab-room-name"
|
||||
placeholder={t("collab.roomName")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void handleCreate();
|
||||
}}
|
||||
/>
|
||||
<label className="flex items-start gap-2 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5"
|
||||
checked={persistent}
|
||||
onChange={(e) => setPersistent(e.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
<span className="font-medium">
|
||||
{t("collab.persistentRoom")}
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-muted-foreground">
|
||||
{t("collab.persistentRoomHint")}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCreateOpen(false)}
|
||||
disabled={creating}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={creating || !name.trim()}
|
||||
>
|
||||
{creating && <Loader2 className="size-3.5 mr-1 animate-spin" />}
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1810,15 +1810,6 @@ export function HostEditor({
|
||||
onChange={(v) => setField("enableSessionLogging", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.allowSessionSharing")}
|
||||
description={t("hosts.allowSessionSharingDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.allowSessionSharing}
|
||||
onChange={(v) => setField("allowSessionSharing", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.localEchoLabel")}
|
||||
|
||||
@@ -199,7 +199,6 @@ export function createHostEditorForm(
|
||||
enableTmuxMonitor: host?.enableTmuxMonitor ?? false,
|
||||
enableTerminalToolbar: host?.enableTerminalToolbar ?? true,
|
||||
enableAiAssistant: host?.enableAiAssistant ?? false,
|
||||
allowSessionSharing: host?.allowSessionSharing ?? true,
|
||||
enableProxmox: (proxmoxSettings.enableProxmox as boolean) ?? false,
|
||||
proxmoxConfig:
|
||||
((proxmoxSettings.proxmoxConfig as Host["proxmoxConfig"]) ?? {
|
||||
@@ -509,7 +508,6 @@ export function buildHostEditorPayload(
|
||||
enableTmuxMonitor: form.enableTmuxMonitor,
|
||||
enableTerminalToolbar: form.enableTerminalToolbar,
|
||||
enableAiAssistant: form.enableAiAssistant,
|
||||
allowSessionSharing: form.allowSessionSharing,
|
||||
enableProxmox: form.enableProxmox,
|
||||
proxmoxConfig:
|
||||
form.enableProxmox || form.proxmoxConfig?.source
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Settings,
|
||||
User,
|
||||
Zap,
|
||||
Presentation,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
@@ -90,12 +89,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
|
||||
separatorAfter: true,
|
||||
rightDockable: true,
|
||||
},
|
||||
{
|
||||
id: "collab",
|
||||
icon: Presentation,
|
||||
labelKey: "nav.collab",
|
||||
separatorAfter: true,
|
||||
},
|
||||
{
|
||||
id: "quick-connect",
|
||||
icon: Zap,
|
||||
|
||||
Reference in new issue
Block a user