Settings: connect this installation to a portal
A superuser-only tab that redeems a claim code and shows the link's state. It names the account a remote session acts as, because that is the thing being granted and the person granting it should see it spelled out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtBzdDyLsmDaF1W7DLYtYM
This commit is contained in:
@@ -634,6 +634,26 @@ these so a value never appears to come from nowhere — or worse, appears to
|
|||||||
come from whichever node happens to be drawn as a producer.`
|
come from whichever node happens to be drawn as a producer.`
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const EnrollBodySchema = {
|
||||||
|
properties: {
|
||||||
|
portal_url: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 255,
|
||||||
|
minLength: 1,
|
||||||
|
title: 'Portal Url'
|
||||||
|
},
|
||||||
|
claim_code: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 32,
|
||||||
|
minLength: 1,
|
||||||
|
title: 'Claim Code'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
required: ['portal_url', 'claim_code'],
|
||||||
|
title: 'EnrollBody'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const EventRowSchema = {
|
export const EventRowSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { CancelablePromise } from './core/CancelablePromise';
|
import type { CancelablePromise } from './core/CancelablePromise';
|
||||||
import { OpenAPI } from './core/OpenAPI';
|
import { OpenAPI } from './core/OpenAPI';
|
||||||
import { request as __request } from './core/request';
|
import { request as __request } from './core/request';
|
||||||
import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, ArtifactsPutArtifactData, ArtifactsPutArtifactResponse, ArtifactsGetArtifactData, ArtifactsGetArtifactResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsPublishDashboardData, DashboardsPublishDashboardResponse, DashboardsDiscardDashboardDraftData, DashboardsDiscardDashboardDraftResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadGraphResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, PrivateCreateUserData, PrivateCreateUserResponse, RunsCreateRunData, RunsCreateRunResponse, RunsCreateSweepData, RunsCreateSweepResponse, RunsReadRunsData, RunsReadRunsResponse, RunsReadRunData, RunsReadRunResponse, RunsCancelRunData, RunsCancelRunResponse, RunsReadMetricsData, RunsReadMetricsResponse, RunsCompareMetricData, RunsCompareMetricResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse, WorkersReadWorkersResponse, WorkersIssueTokenData, WorkersIssueTokenResponse, WorkersReadRuntimeResponse } from './types.gen';
|
import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, ArtifactsPutArtifactData, ArtifactsPutArtifactResponse, ArtifactsGetArtifactData, ArtifactsGetArtifactResponse, CloudReadStatusResponse, CloudEnrollData, CloudEnrollResponse, CloudDisconnectResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsPublishDashboardData, DashboardsPublishDashboardResponse, DashboardsDiscardDashboardDraftData, DashboardsDiscardDashboardDraftResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadGraphResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, PrivateCreateUserData, PrivateCreateUserResponse, RunsCreateRunData, RunsCreateRunResponse, RunsCreateSweepData, RunsCreateSweepResponse, RunsReadRunsData, RunsReadRunsResponse, RunsReadRunData, RunsReadRunResponse, RunsCancelRunData, RunsCancelRunResponse, RunsReadMetricsData, RunsReadMetricsResponse, RunsCompareMetricData, RunsCompareMetricResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse, WorkersReadWorkersResponse, WorkersIssueTokenData, WorkersIssueTokenResponse, WorkersReadRuntimeResponse } from './types.gen';
|
||||||
|
|
||||||
export class AlertsService {
|
export class AlertsService {
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +107,64 @@ export class ArtifactsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CloudService {
|
||||||
|
/**
|
||||||
|
* Read Status
|
||||||
|
* Whether this installation is enrolled, and whether the link is up.
|
||||||
|
*
|
||||||
|
* Readable by any signed-in user: everyone here has a right to know whether
|
||||||
|
* the machine they are using can be reached from outside.
|
||||||
|
* @returns unknown Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static readStatus(): CancelablePromise<CloudReadStatusResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/cloud/status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enroll
|
||||||
|
* Redeem a claim code and start dialling the portal.
|
||||||
|
*
|
||||||
|
* The account performing this is recorded as the one every portal session
|
||||||
|
* will act as. There is no way to widen that later from the portal side.
|
||||||
|
* @param data The data for the request.
|
||||||
|
* @param data.requestBody
|
||||||
|
* @returns Message Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static enroll(data: CloudEnrollData): CancelablePromise<CloudEnrollResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/v1/cloud/enroll',
|
||||||
|
body: data.requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
422: 'Validation Error'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect
|
||||||
|
* Sever the connection from this side.
|
||||||
|
*
|
||||||
|
* Unilateral and immediate: the config is the only reason portal tokens
|
||||||
|
* verify here, so deleting it ends remote access whatever the portal still
|
||||||
|
* has on file.
|
||||||
|
* @returns Message Successful Response
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static disconnect(): CancelablePromise<CloudDisconnectResponse> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/api/v1/cloud'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DashboardsService {
|
export class DashboardsService {
|
||||||
/**
|
/**
|
||||||
* Read Dashboards
|
* Read Dashboards
|
||||||
|
|||||||
@@ -268,6 +268,11 @@ export type Endpoint = {
|
|||||||
requires?: Array<(string)>;
|
requires?: Array<(string)>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EnrollBody = {
|
||||||
|
portal_url: string;
|
||||||
|
claim_code: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EventRow = {
|
export type EventRow = {
|
||||||
id: number;
|
id: number;
|
||||||
ts: string;
|
ts: string;
|
||||||
@@ -1012,6 +1017,18 @@ export type ArtifactsGetArtifactData = {
|
|||||||
|
|
||||||
export type ArtifactsGetArtifactResponse = (unknown);
|
export type ArtifactsGetArtifactResponse = (unknown);
|
||||||
|
|
||||||
|
export type CloudReadStatusResponse = ({
|
||||||
|
[key: string]: unknown;
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CloudEnrollData = {
|
||||||
|
requestBody: EnrollBody;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CloudEnrollResponse = (Message);
|
||||||
|
|
||||||
|
export type CloudDisconnectResponse = (Message);
|
||||||
|
|
||||||
export type DashboardsReadDashboardsResponse = (DashboardsPublic);
|
export type DashboardsReadDashboardsResponse = (DashboardsPublic);
|
||||||
|
|
||||||
export type DashboardsReadDashboardData = {
|
export type DashboardsReadDashboardData = {
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { CloudService } from "@/client"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
|
import { handleError } from "@/utils"
|
||||||
|
|
||||||
|
const DEFAULT_PORTAL = "https://hub.fluksio.com"
|
||||||
|
|
||||||
|
type CloudStatus = {
|
||||||
|
enrolled: boolean
|
||||||
|
connected: boolean
|
||||||
|
portal_url: string | null
|
||||||
|
portal_account: string | null
|
||||||
|
installation_id: string | null
|
||||||
|
last_error: string | null
|
||||||
|
connected_since: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connecting this installation to a Fluksio portal, or cutting it loose.
|
||||||
|
*
|
||||||
|
* Deliberately blunt about what it grants: a remote session acts as the
|
||||||
|
* account that enrolled, and this screen says which one. Everything here is
|
||||||
|
* optional — an installation nobody enrolls never contacts anything.
|
||||||
|
*/
|
||||||
|
export function RemoteAccess() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { showErrorToast, showSuccessToast } = useCustomToast()
|
||||||
|
const [portalUrl, setPortalUrl] = useState(DEFAULT_PORTAL)
|
||||||
|
const [code, setCode] = useState("")
|
||||||
|
const [confirmDisconnect, setConfirmDisconnect] = useState(false)
|
||||||
|
|
||||||
|
const { data: status } = useQuery<CloudStatus>({
|
||||||
|
queryKey: ["cloud", "status"],
|
||||||
|
// The endpoint returns a plain object; the generated type is `unknown`
|
||||||
|
// because it has no response model of its own.
|
||||||
|
queryFn: async () => (await CloudService.readStatus()) as CloudStatus,
|
||||||
|
// Often enough that "connecting…" resolves while someone is watching it.
|
||||||
|
refetchInterval: 5000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const invalidate = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["cloud", "status"] })
|
||||||
|
|
||||||
|
const connect = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
CloudService.enroll({
|
||||||
|
requestBody: { portal_url: portalUrl.trim(), claim_code: code.trim() },
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
setCode("")
|
||||||
|
showSuccessToast("Connected to the portal")
|
||||||
|
invalidate()
|
||||||
|
},
|
||||||
|
onError: handleError.bind(showErrorToast),
|
||||||
|
})
|
||||||
|
|
||||||
|
const disconnect = useMutation({
|
||||||
|
mutationFn: () => CloudService.disconnect(),
|
||||||
|
onSuccess: () => {
|
||||||
|
setConfirmDisconnect(false)
|
||||||
|
showSuccessToast("Disconnected from the portal")
|
||||||
|
invalidate()
|
||||||
|
},
|
||||||
|
onError: handleError.bind(showErrorToast),
|
||||||
|
})
|
||||||
|
|
||||||
|
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">
|
||||||
|
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">
|
||||||
|
Anyone signed in to the portal for this installation gets this
|
||||||
|
account's rights here.
|
||||||
|
</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 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
|
||||||
|
className="rounded-full bg-[--brand-secondary] text-white hover:opacity-90"
|
||||||
|
disabled={!code.trim() || !portalUrl.trim() || connect.isPending}
|
||||||
|
onClick={() => connect.mutate()}
|
||||||
|
>
|
||||||
|
{connect.isPending ? "Connecting…" : "Connect"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog open={confirmDisconnect} onOpenChange={setConfirmDisconnect}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Disconnect from the portal?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Remote access ends immediately and the portal's credentials stop
|
||||||
|
working here. Nothing on this installation is changed or deleted,
|
||||||
|
and you can connect again with a new code.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full"
|
||||||
|
onClick={() => setConfirmDisconnect(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="rounded-full"
|
||||||
|
disabled={disconnect.isPending}
|
||||||
|
onClick={() => disconnect.mutate()}
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm">{children}</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoteAccess
|
||||||
@@ -3,6 +3,7 @@ import { createFileRoute } from "@tanstack/react-router"
|
|||||||
import Appearance from "@/components/UserSettings/Appearance"
|
import Appearance from "@/components/UserSettings/Appearance"
|
||||||
import ChangePassword from "@/components/UserSettings/ChangePassword"
|
import ChangePassword from "@/components/UserSettings/ChangePassword"
|
||||||
import DeleteAccount from "@/components/UserSettings/DeleteAccount"
|
import DeleteAccount from "@/components/UserSettings/DeleteAccount"
|
||||||
|
import RemoteAccess from "@/components/UserSettings/RemoteAccess"
|
||||||
import UserInformation from "@/components/UserSettings/UserInformation"
|
import UserInformation from "@/components/UserSettings/UserInformation"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import useAuth from "@/hooks/useAuth"
|
import useAuth from "@/hooks/useAuth"
|
||||||
@@ -14,6 +15,12 @@ const tabsConfig = [
|
|||||||
{ value: "danger-zone", title: "Danger zone", component: DeleteAccount },
|
{ value: "danger-zone", title: "Danger zone", component: DeleteAccount },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Whether this whole installation can be reached from outside is not a
|
||||||
|
// personal preference, so the tab only exists for an operator.
|
||||||
|
const superuserTabs = [
|
||||||
|
{ value: "remote-access", title: "Remote access", component: RemoteAccess },
|
||||||
|
]
|
||||||
|
|
||||||
export const Route = createFileRoute("/_layout/settings")({
|
export const Route = createFileRoute("/_layout/settings")({
|
||||||
component: UserSettings,
|
component: UserSettings,
|
||||||
head: () => ({
|
head: () => ({
|
||||||
@@ -29,7 +36,10 @@ function UserSettings() {
|
|||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
// A superuser deleting its own account would lock everyone out.
|
// A superuser deleting its own account would lock everyone out.
|
||||||
const finalTabs = currentUser?.is_superuser
|
const finalTabs = currentUser?.is_superuser
|
||||||
? tabsConfig.filter((tab) => tab.value !== "danger-zone")
|
? [
|
||||||
|
...tabsConfig.filter((tab) => tab.value !== "danger-zone"),
|
||||||
|
...superuserTabs,
|
||||||
|
]
|
||||||
: tabsConfig
|
: tabsConfig
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
|
|||||||
Reference in New Issue
Block a user