mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 06:39:52 +02:00
rework ui
This commit is contained in:
+6
-8
@@ -2388,23 +2388,21 @@
|
|||||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeId": "ID",
|
"remoteExitNodeId": "ID",
|
||||||
"remoteExitNodeSecretKey": "Secret",
|
"remoteExitNodeSecretKey": "Secret",
|
||||||
|
"remoteExitNodeNetworkingTitle": "Network Settings",
|
||||||
|
"remoteExitNodeNetworkingDescription": "Configure how this remote exit node routes traffic and which sites prefer to connect through it.",
|
||||||
|
"remoteExitNodeNetworkingSave": "Save Settings",
|
||||||
|
"remoteExitNodeNetworkingSaveSuccessTitle": "Network settings saved",
|
||||||
|
"remoteExitNodeNetworkingSaveSuccessDescription": "Network settings have been updated successfully.",
|
||||||
|
"remoteExitNodeNetworkingSaveError": "Failed to save network settings",
|
||||||
"remoteExitNodeNetworkingSubnetsTitle": "Remote Subnets",
|
"remoteExitNodeNetworkingSubnetsTitle": "Remote Subnets",
|
||||||
"remoteExitNodeNetworkingSubnetsDescription": "Define the CIDR ranges that this remote exit node will route traffic to. Type a valid CIDR (e.g. <code>10.0.0.0/8</code>) and press Enter to add.",
|
"remoteExitNodeNetworkingSubnetsDescription": "Define the CIDR ranges that this remote exit node will route traffic to. Type a valid CIDR (e.g. <code>10.0.0.0/8</code>) and press Enter to add.",
|
||||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
|
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
|
||||||
"remoteExitNodeNetworkingSubnetsSave": "Save Subnets",
|
|
||||||
"remoteExitNodeNetworkingSubnetsSaveSuccessTitle": "Subnets saved",
|
|
||||||
"remoteExitNodeNetworkingSubnetsSaveSuccessDescription": "Remote subnets have been updated successfully.",
|
|
||||||
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
|
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
|
||||||
"remoteExitNodeNetworkingSubnetsSaveError": "Failed to save subnets",
|
|
||||||
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
|
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
|
||||||
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
|
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
|
||||||
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
|
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
|
||||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
|
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
|
||||||
"remoteExitNodeNetworkingLabelsSave": "Save Labels",
|
|
||||||
"remoteExitNodeNetworkingLabelsSaveSuccessTitle": "Labels saved",
|
|
||||||
"remoteExitNodeNetworkingLabelsSaveSuccessDescription": "Preference labels have been updated successfully.",
|
|
||||||
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
|
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
|
||||||
"remoteExitNodeNetworkingLabelsSaveError": "Failed to save labels",
|
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Create Remote Node",
|
"title": "Create Remote Node",
|
||||||
"description": "Create a new self-hosted remote relay and proxy server node",
|
"description": "Create a new self-hosted remote relay and proxy server node",
|
||||||
|
|||||||
+106
-100
@@ -3,14 +3,18 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
|
SettingsFormCell,
|
||||||
|
SettingsFormGrid,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionFooter,
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { Label } from "@app/components/ui/label";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
@@ -47,13 +51,13 @@ export default function NetworkingPage() {
|
|||||||
const [subnets, setSubnets] = useState<Tag[]>([]);
|
const [subnets, setSubnets] = useState<Tag[]>([]);
|
||||||
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
|
||||||
const [loadingSubnets, setLoadingSubnets] = useState(true);
|
const [loadingSubnets, setLoadingSubnets] = useState(true);
|
||||||
const [savingSubnets, setSavingSubnets] = useState(false);
|
|
||||||
|
|
||||||
// Labels state
|
// Labels state
|
||||||
const [selectedLabels, setSelectedLabels] = useState<TagValue[]>([]);
|
const [selectedLabels, setSelectedLabels] = useState<TagValue[]>([]);
|
||||||
const [labelSearchQuery, setLabelSearchQuery] = useState("");
|
const [labelSearchQuery, setLabelSearchQuery] = useState("");
|
||||||
const [loadingLabels, setLoadingLabels] = useState(true);
|
const [loadingLabels, setLoadingLabels] = useState(true);
|
||||||
const [savingLabels, setSavingLabels] = useState(false);
|
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const [debouncedLabelQuery] = useDebounce(labelSearchQuery, 150);
|
const [debouncedLabelQuery] = useDebounce(labelSearchQuery, 150);
|
||||||
|
|
||||||
@@ -135,17 +139,27 @@ export default function NetworkingPage() {
|
|||||||
loadLabels();
|
loadLabels();
|
||||||
}, [remoteExitNode.remoteExitNodeId]);
|
}, [remoteExitNode.remoteExitNodeId]);
|
||||||
|
|
||||||
const handleSaveSubnets = async () => {
|
const handleSave = async () => {
|
||||||
setSavingSubnets(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await api.post<AxiosResponse<SetRemoteExitNodeResourcesResponse>>(
|
await Promise.all([
|
||||||
`/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/resources`,
|
api.post<
|
||||||
{ destinations: subnets.map((s) => s.text) }
|
AxiosResponse<SetRemoteExitNodeResourcesResponse>
|
||||||
);
|
>(
|
||||||
|
`/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/resources`,
|
||||||
|
{ destinations: subnets.map((s) => s.text) }
|
||||||
|
),
|
||||||
|
api.post<
|
||||||
|
AxiosResponse<SetRemoteExitNodePreferenceLabelsResponse>
|
||||||
|
>(
|
||||||
|
`/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/preference-labels`,
|
||||||
|
{ labelIds: selectedLabels.map((l) => parseInt(l.id)) }
|
||||||
|
)
|
||||||
|
]);
|
||||||
toast({
|
toast({
|
||||||
title: t("remoteExitNodeNetworkingSubnetsSaveSuccessTitle"),
|
title: t("remoteExitNodeNetworkingSaveSuccessTitle"),
|
||||||
description: t(
|
description: t(
|
||||||
"remoteExitNodeNetworkingSubnetsSaveSuccessDescription"
|
"remoteExitNodeNetworkingSaveSuccessDescription"
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -154,38 +168,10 @@ export default function NetworkingPage() {
|
|||||||
title: t("error"),
|
title: t("error"),
|
||||||
description:
|
description:
|
||||||
formatAxiosError(error) ||
|
formatAxiosError(error) ||
|
||||||
t("remoteExitNodeNetworkingSubnetsSaveError")
|
t("remoteExitNodeNetworkingSaveError")
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSavingSubnets(false);
|
setSaving(false);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveLabels = async () => {
|
|
||||||
setSavingLabels(true);
|
|
||||||
try {
|
|
||||||
await api.post<
|
|
||||||
AxiosResponse<SetRemoteExitNodePreferenceLabelsResponse>
|
|
||||||
>(
|
|
||||||
`/org/${orgId}/remote-exit-node/${remoteExitNode.remoteExitNodeId}/preference-labels`,
|
|
||||||
{ labelIds: selectedLabels.map((l) => parseInt(l.id)) }
|
|
||||||
);
|
|
||||||
toast({
|
|
||||||
title: t("remoteExitNodeNetworkingLabelsSaveSuccessTitle"),
|
|
||||||
description: t(
|
|
||||||
"remoteExitNodeNetworkingLabelsSaveSuccessDescription"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("error"),
|
|
||||||
description:
|
|
||||||
formatAxiosError(error) ||
|
|
||||||
t("remoteExitNodeNetworkingLabelsSaveError")
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setSavingLabels(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,73 +180,93 @@ export default function NetworkingPage() {
|
|||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("remoteExitNodeNetworkingSubnetsTitle")}
|
{t("remoteExitNodeNetworkingTitle")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t.rich("remoteExitNodeNetworkingSubnetsDescription", {
|
{t("remoteExitNodeNetworkingDescription")}
|
||||||
code: (chunks) => <code>{chunks}</code>
|
|
||||||
})}{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.pangolin.net/placeholder"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{t("learnMore")}
|
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
|
||||||
</a>
|
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<TagInput
|
<SettingsSectionForm variant="half">
|
||||||
tags={subnets}
|
<SettingsFormGrid>
|
||||||
setTags={setSubnets}
|
<SettingsFormCell span="half">
|
||||||
placeholder={t(
|
<div className="grid gap-2">
|
||||||
"remoteExitNodeNetworkingSubnetsPlaceholder"
|
<Label>
|
||||||
)}
|
{t(
|
||||||
validateTag={(tag) => cidrRegex.test(tag.trim())}
|
"remoteExitNodeNetworkingSubnetsTitle"
|
||||||
activeTagIndex={activeTagIndex}
|
)}
|
||||||
setActiveTagIndex={setActiveTagIndex}
|
</Label>
|
||||||
disabled={loadingSubnets}
|
<TagInput
|
||||||
allowDuplicates={false}
|
tags={subnets}
|
||||||
inlineTags={true}
|
setTags={setSubnets}
|
||||||
/>
|
placeholder={t(
|
||||||
|
"remoteExitNodeNetworkingSubnetsPlaceholder"
|
||||||
|
)}
|
||||||
|
validateTag={(tag) =>
|
||||||
|
cidrRegex.test(tag.trim())
|
||||||
|
}
|
||||||
|
activeTagIndex={activeTagIndex}
|
||||||
|
setActiveTagIndex={setActiveTagIndex}
|
||||||
|
disabled={loadingSubnets}
|
||||||
|
allowDuplicates={false}
|
||||||
|
size="sm"
|
||||||
|
inlineTags={true}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t.rich(
|
||||||
|
"remoteExitNodeNetworkingSubnetsDescription",
|
||||||
|
{
|
||||||
|
code: (chunks) => (
|
||||||
|
<code>{chunks}</code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.pangolin.net/placeholder"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{t("learnMore")}
|
||||||
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SettingsFormCell>
|
||||||
|
<SettingsFormCell span="half">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
{t(
|
||||||
|
"remoteExitNodeNetworkingLabelsTitle"
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<MultiSelectTagInput
|
||||||
|
value={selectedLabels}
|
||||||
|
options={labelsShown}
|
||||||
|
onChange={setSelectedLabels}
|
||||||
|
onSearch={setLabelSearchQuery}
|
||||||
|
searchQuery={labelSearchQuery}
|
||||||
|
disabled={loadingLabels}
|
||||||
|
buttonText={t(
|
||||||
|
"remoteExitNodeNetworkingLabelsButtonText"
|
||||||
|
)}
|
||||||
|
searchPlaceholder={t(
|
||||||
|
"remoteExitNodeNetworkingLabelsSearchPlaceholder"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"remoteExitNodeNetworkingLabelsDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SettingsFormCell>
|
||||||
|
</SettingsFormGrid>
|
||||||
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button onClick={handleSaveSubnets} loading={savingSubnets}>
|
<Button onClick={handleSave} loading={saving}>
|
||||||
{t("remoteExitNodeNetworkingSubnetsSave")}
|
{t("remoteExitNodeNetworkingSave")}
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("remoteExitNodeNetworkingLabelsTitle")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("remoteExitNodeNetworkingLabelsDescription")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<MultiSelectTagInput
|
|
||||||
value={selectedLabels}
|
|
||||||
options={labelsShown}
|
|
||||||
onChange={setSelectedLabels}
|
|
||||||
onSearch={setLabelSearchQuery}
|
|
||||||
searchQuery={labelSearchQuery}
|
|
||||||
disabled={loadingLabels}
|
|
||||||
buttonText={t(
|
|
||||||
"remoteExitNodeNetworkingLabelsButtonText"
|
|
||||||
)}
|
|
||||||
searchPlaceholder={t(
|
|
||||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
<SettingsSectionFooter>
|
|
||||||
<Button onClick={handleSaveLabels} loading={savingLabels}>
|
|
||||||
{t("remoteExitNodeNetworkingLabelsSave")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|||||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
import { Button as ButtonUI } from "@/components/ui/button";
|
import { Button as ButtonUI } from "@/components/ui/button";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string().nonempty("Name is required"),
|
name: z.string().nonempty("Name is required"),
|
||||||
@@ -73,23 +72,6 @@ export default function GeneralPage() {
|
|||||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [isRestartDialogOpen, setIsRestartDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
async function restartSite() {
|
|
||||||
try {
|
|
||||||
await api.post(`/site/${site?.siteId}/restart`);
|
|
||||||
toast({
|
|
||||||
title: t("siteRestarted"),
|
|
||||||
description: t("siteRestartedDescription")
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("siteErrorRestart"),
|
|
||||||
description: formatAxiosError(e, t("siteErrorRestartDescription"))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgAutoUpdate = org.org.settingsEnableGlobalNewtAutoUpdate ?? false;
|
const orgAutoUpdate = org.org.settingsEnableGlobalNewtAutoUpdate ?? false;
|
||||||
|
|
||||||
@@ -367,52 +349,6 @@ export default function GeneralPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
{site && site.type === "newt" && (
|
|
||||||
<>
|
|
||||||
<ConfirmDeleteDialog
|
|
||||||
open={isRestartDialogOpen}
|
|
||||||
setOpen={setIsRestartDialogOpen}
|
|
||||||
dialog={
|
|
||||||
<p>
|
|
||||||
{t.rich("siteRestartDialogMessage", {
|
|
||||||
name: site.name,
|
|
||||||
b: (chunks) => <b>{chunks}</b>
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
buttonText={t("siteRestartButton")}
|
|
||||||
onConfirm={restartSite}
|
|
||||||
string={site.name}
|
|
||||||
warningText={t("siteRestartWarning")}
|
|
||||||
title={t("siteRestartTitle")}
|
|
||||||
/>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("siteRestartTitle")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
{t("siteRestartDescription")}
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<SettingsSectionForm>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("siteRestartBody")}
|
|
||||||
</p>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
<SettingsSectionFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsRestartDialogOpen(true)}
|
|
||||||
>
|
|
||||||
{t("siteRestartButton")}
|
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
</SettingsSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export default function SitesTable({
|
|||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [deleteWithResources, setDeleteWithResources] = useState(false);
|
const [deleteWithResources, setDeleteWithResources] = useState(false);
|
||||||
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
|
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
|
||||||
|
const [restartingSite, setRestartingSite] = useState<SiteRow | null>(null);
|
||||||
const [resourcesDialogSite, setResourcesDialogSite] =
|
const [resourcesDialogSite, setResourcesDialogSite] =
|
||||||
useState<SiteRow | null>(null);
|
useState<SiteRow | null>(null);
|
||||||
const [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
@@ -159,6 +160,24 @@ export default function SitesTable({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function restartSite(siteId: number) {
|
||||||
|
try {
|
||||||
|
await api.post(`/site/${siteId}/restart`);
|
||||||
|
toast({
|
||||||
|
title: t("siteRestarted"),
|
||||||
|
description: t("siteRestartedDescription")
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("siteErrorRestart"),
|
||||||
|
description: formatAxiosError(e, t("siteErrorRestartDescription"))
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setRestartingSite(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteSite(siteId: number, withResources: boolean) {
|
function deleteSite(siteId: number, withResources: boolean) {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
await api
|
await api
|
||||||
@@ -526,6 +545,20 @@ export default function SitesTable({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
{siteRow.type === "newt" && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
setRestartingSite(siteRow)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="text-orange-500">
|
||||||
|
{t("siteRestartButton")}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSite(siteRow);
|
setSelectedSite(siteRow);
|
||||||
@@ -654,6 +687,28 @@ export default function SitesTable({
|
|||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
||||||
|
{restartingSite && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={Boolean(restartingSite)}
|
||||||
|
setOpen={(val) => {
|
||||||
|
if (!val) setRestartingSite(null);
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<p>
|
||||||
|
{t.rich("siteRestartDialogMessage", {
|
||||||
|
name: restartingSite.name,
|
||||||
|
b: (chunks) => <b>{chunks}</b>
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
buttonText={t("siteRestartButton")}
|
||||||
|
onConfirm={() => restartSite(restartingSite.id)}
|
||||||
|
string={restartingSite.name}
|
||||||
|
warningText={t("siteRestartWarning")}
|
||||||
|
title={t("siteRestartTitle")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedSite && (
|
{selectedSite && (
|
||||||
<ConfirmDeleteDialog
|
<ConfirmDeleteDialog
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user