feat: move session sharing and collab rooms into a plugin (B12)

This commit is contained in:
LukeGus committed 2026-09-23 23:42:46 -05:00
1 parent 791d8656a1
commit a276f6898c
207 files changed
+35771 -11072

No files matched your search

-26
View File
@@ -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}
-13
View File
@@ -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")}
+1 -2
View File
@@ -25,8 +25,7 @@ export type CoreRailView =
| "connections"
| "session-logs"
| "user-profile"
| "admin-settings"
| "collab";
| "admin-settings";
export type RailView = CoreRailView | (string & {});
-186
View File
@@ -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>
);
}
-9
View File
@@ -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")}
-2
View File
@@ -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
-7
View File
@@ -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,