Settings as panels, the way the portal does it
Four tabs over three cards was navigation for its own sake, and the two shells disagreed about what a settings screen looks like. It is one grid of cards now, matching the portal: the account card carries changing a password and deleting the account in its footer, appearance is a card with one row, and remote access — an operator's concern, not a personal preference — is a card of its own for a superuser. SettingRow, alert-dialog and UserAvatar come across from the portal, so an account renders the same face in both shells. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,13 @@ import { useState } from "react"
|
||||
|
||||
import { CloudService } from "@/client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -13,6 +20,7 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import useCustomToast from "@/hooks/useCustomToast"
|
||||
import { handleError } from "@/utils"
|
||||
|
||||
@@ -96,125 +104,130 @@ export function RemoteAccess() {
|
||||
if (!status) return null
|
||||
|
||||
return (
|
||||
<div className="flex max-w-2xl flex-col gap-6 py-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Remote access</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Remote access</CardTitle>
|
||||
<CardDescription>
|
||||
Reach this installation from fluksio.com. Entirely optional — without
|
||||
it, this installation talks to nothing outside your network.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status.enrolled ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
|
||||
<dl className="grid gap-3 sm:grid-cols-2">
|
||||
<Field label="Status">
|
||||
{status.connected
|
||||
? "Connected"
|
||||
: status.last_error
|
||||
? `Reconnecting — ${status.last_error}`
|
||||
: "Reconnecting…"}
|
||||
</Field>
|
||||
<Field label="Portal">{status.portal_url ?? "—"}</Field>
|
||||
<Field label="Acting as">
|
||||
{status.portal_account ?? "—"}
|
||||
<span className="mt-1 block text-xs text-muted-foreground">
|
||||
Portal sessions of this account get this account's rights
|
||||
here. Anyone else gets in only once added below, as their own
|
||||
user.
|
||||
</span>
|
||||
</Field>
|
||||
<Field label="Installation">
|
||||
<span className="font-mono text-xs">
|
||||
{status.installation_id ?? "—"}
|
||||
</span>
|
||||
</Field>
|
||||
</dl>
|
||||
<div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="rounded-full"
|
||||
onClick={() => setConfirmDisconnect(true)}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
{status.enrolled ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<dl className="grid gap-3 sm:grid-cols-2">
|
||||
<Field label="Status">
|
||||
{status.connected
|
||||
? "Connected"
|
||||
: status.last_error
|
||||
? `Reconnecting — ${status.last_error}`
|
||||
: "Reconnecting…"}
|
||||
</Field>
|
||||
<Field label="Portal">{status.portal_url ?? "—"}</Field>
|
||||
<Field label="Acting as">
|
||||
{status.portal_account ?? "—"}
|
||||
<span className="mt-1 block text-xs text-muted-foreground">
|
||||
Portal sessions of this account get this account's rights
|
||||
here. Anyone else gets in only once added below, as their
|
||||
own user.
|
||||
</span>
|
||||
</Field>
|
||||
<Field label="Installation">
|
||||
<span className="font-mono text-xs">
|
||||
{status.installation_id ?? "—"}
|
||||
</span>
|
||||
</Field>
|
||||
</dl>
|
||||
<div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="rounded-full"
|
||||
onClick={() => setConfirmDisconnect(true)}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
|
||||
<div>
|
||||
<h3 className="font-medium">Remote users</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Let someone else reach this installation through the portal.
|
||||
They get a user of their own here — not yours, and never a
|
||||
superuser, so they cannot pass access on.
|
||||
</p>
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 className="font-medium">Remote users</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Let someone else reach this installation through the portal.
|
||||
They get a user of their own here — not yours, and never a
|
||||
superuser, so they cannot pass access on.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="join-code">Code</Label>
|
||||
<Input
|
||||
id="join-code"
|
||||
value={joinCode}
|
||||
placeholder="XXXX-XXXX"
|
||||
className="font-mono tracking-widest"
|
||||
onChange={(event) =>
|
||||
setJoinCode(event.target.value.toUpperCase())
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
They get a code at fluksio.com → Installations → Join an
|
||||
installation. Added users appear under Admin → Users; deleting
|
||||
them there ends their access.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
variant="outline"
|
||||
disabled={!joinCode.trim() || addRemoteUser.isPending}
|
||||
onClick={() => addRemoteUser.mutate()}
|
||||
>
|
||||
{addRemoteUser.isPending ? "Adding…" : "Add remote user"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="portal-url">Portal</Label>
|
||||
<Input
|
||||
id="portal-url"
|
||||
value={portalUrl}
|
||||
onChange={(event) => setPortalUrl(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="join-code">Code</Label>
|
||||
<Label htmlFor="claim-code">Code</Label>
|
||||
<Input
|
||||
id="join-code"
|
||||
value={joinCode}
|
||||
id="claim-code"
|
||||
value={code}
|
||||
placeholder="XXXX-XXXX"
|
||||
className="font-mono tracking-widest"
|
||||
onChange={(event) =>
|
||||
setJoinCode(event.target.value.toUpperCase())
|
||||
}
|
||||
onChange={(event) => setCode(event.target.value.toUpperCase())}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
They get a code at fluksio.com → Installations → Join an
|
||||
installation. Added users appear under Admin → Users; deleting
|
||||
them there ends their access.
|
||||
Get a code at fluksio.com → Installations → Add installation.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="brand"
|
||||
className="rounded-full"
|
||||
variant="outline"
|
||||
disabled={!joinCode.trim() || addRemoteUser.isPending}
|
||||
onClick={() => addRemoteUser.mutate()}
|
||||
disabled={
|
||||
!code.trim() || !portalUrl.trim() || connect.isPending
|
||||
}
|
||||
onClick={() => connect.mutate()}
|
||||
>
|
||||
{addRemoteUser.isPending ? "Adding…" : "Add remote user"}
|
||||
{connect.isPending ? "Connecting…" : "Connect"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="portal-url">Portal</Label>
|
||||
<Input
|
||||
id="portal-url"
|
||||
value={portalUrl}
|
||||
onChange={(event) => setPortalUrl(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="claim-code">Code</Label>
|
||||
<Input
|
||||
id="claim-code"
|
||||
value={code}
|
||||
placeholder="XXXX-XXXX"
|
||||
className="font-mono tracking-widest"
|
||||
onChange={(event) => setCode(event.target.value.toUpperCase())}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Get a code at fluksio.com → Installations → Add installation.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="brand"
|
||||
className="rounded-full"
|
||||
disabled={!code.trim() || !portalUrl.trim() || connect.isPending}
|
||||
onClick={() => connect.mutate()}
|
||||
>
|
||||
{connect.isPending ? "Connecting…" : "Connect"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<Dialog open={confirmDisconnect} onOpenChange={setConfirmDisconnect}>
|
||||
<DialogContent>
|
||||
@@ -245,7 +258,7 @@ export function RemoteAccess() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user