mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-09 15:39:47 +02:00
migrate private resources to page
This commit is contained in:
@@ -1,206 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useTransition } from "react";
|
||||
import {
|
||||
cleanForFQDN,
|
||||
PrivateResourceForm,
|
||||
isHostname,
|
||||
type InternalResourceFormValues
|
||||
} from "./PrivateResourceForm";
|
||||
import type { Selectedsite } from "./site-selector";
|
||||
|
||||
type CreateInternalResourceDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (val: boolean) => void;
|
||||
orgId: string;
|
||||
onSuccess?: () => void;
|
||||
initialSites?: Selectedsite[];
|
||||
};
|
||||
|
||||
export default function CreatePrivateResourceDialog({
|
||||
open,
|
||||
setOpen,
|
||||
orgId,
|
||||
onSuccess,
|
||||
initialSites
|
||||
}: CreateInternalResourceDialogProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [isHttpModeDisabled, setIsHttpModeDisabled] = useState(false);
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
|
||||
function handleSubmit(values: InternalResourceFormValues) {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
let data = { ...values };
|
||||
if (
|
||||
(data.mode === "host" ||
|
||||
data.mode === "http" ||
|
||||
data.mode === "ssh") &&
|
||||
isHostname(data.destination)
|
||||
) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
if (!currentAlias) {
|
||||
let aliasValue = data.destination;
|
||||
if (data.destination?.toLowerCase() === "localhost") {
|
||||
aliasValue = `${cleanForFQDN(data.name)}.internal`;
|
||||
}
|
||||
data = { ...data, alias: aliasValue };
|
||||
}
|
||||
}
|
||||
|
||||
await api.put<
|
||||
AxiosResponse<{ data: { siteResourceId: number } }>
|
||||
>(`/org/${orgId}/site-resource`, {
|
||||
name: data.name,
|
||||
siteIds: data.siteIds,
|
||||
mode: data.mode,
|
||||
destination: data.destination ?? undefined,
|
||||
enabled: true,
|
||||
...(data.mode === "http" && {
|
||||
scheme: data.scheme,
|
||||
ssl: data.ssl ?? false,
|
||||
destinationPort: data.destinationPort ?? undefined,
|
||||
domainId: data.httpConfigDomainId
|
||||
? data.httpConfigDomainId
|
||||
: undefined,
|
||||
subdomain: data.httpConfigSubdomain
|
||||
? data.httpConfigSubdomain
|
||||
: undefined
|
||||
}),
|
||||
...(data.mode === "host" && {
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: undefined,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" &&
|
||||
data.authDaemonPort != null && {
|
||||
authDaemonPort: data.authDaemonPort
|
||||
})
|
||||
}),
|
||||
...(data.mode === "ssh" && {
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: undefined,
|
||||
destinationPort: data.destinationPort ?? undefined,
|
||||
pamMode: data.pamMode ?? undefined,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" &&
|
||||
data.authDaemonPort != null && {
|
||||
authDaemonPort: data.authDaemonPort
|
||||
})
|
||||
}),
|
||||
...((data.mode === "host" || data.mode === "cidr") && {
|
||||
tcpPortRangeString: data.tcpPortRangeString,
|
||||
udpPortRangeString: data.udpPortRangeString,
|
||||
disableIcmp: data.disableIcmp ?? false
|
||||
}),
|
||||
...(data.mode === "ssh" && {
|
||||
disableIcmp: data.disableIcmp ?? false
|
||||
}),
|
||||
roleIds: data.roles
|
||||
? data.roles.map((r) => parseInt(r.id))
|
||||
: [],
|
||||
userIds: data.users ? data.users.map((u) => u.id) : [],
|
||||
clientIds: data.clients
|
||||
? data.clients.map((c) => parseInt(c.id))
|
||||
: []
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t("createInternalResourceDialogSuccess"),
|
||||
description: t(
|
||||
"createInternalResourceDialogInternalResourceCreatedSuccessfully"
|
||||
),
|
||||
variant: "default"
|
||||
});
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("createInternalResourceDialogError"),
|
||||
description: formatAxiosError(
|
||||
error,
|
||||
t(
|
||||
"createInternalResourceDialogFailedToCreateInternalResource"
|
||||
)
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent className="max-w-3xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("createInternalResourceDialogCreateClientResource")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t(
|
||||
"createInternalResourceDialogCreateClientResourceDescription"
|
||||
)}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<PrivateResourceForm
|
||||
variant="create"
|
||||
open={open}
|
||||
orgId={orgId}
|
||||
formId="create-internal-resource-form"
|
||||
onSubmit={handleSubmit}
|
||||
onSubmitDisabledChange={setIsHttpModeDisabled}
|
||||
initialSites={initialSites}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("createInternalResourceDialogCancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="create-internal-resource-form"
|
||||
disabled={isSubmitting || isHttpModeDisabled}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t("createInternalResourceDialogCreateResource")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { resourceQueries } from "@app/lib/queries";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useTransition } from "react";
|
||||
import {
|
||||
cleanForFQDN,
|
||||
PrivateResourceForm,
|
||||
type InternalResourceData,
|
||||
type InternalResourceFormValues,
|
||||
isHostname
|
||||
} from "./PrivateResourceForm";
|
||||
|
||||
type EditInternalResourceDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (val: boolean) => void;
|
||||
resource: InternalResourceData;
|
||||
orgId: string;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export default function EditPrivateResourceDialog({
|
||||
open,
|
||||
setOpen,
|
||||
resource,
|
||||
orgId,
|
||||
onSuccess
|
||||
}: EditInternalResourceDialogProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const queryClient = useQueryClient();
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
const [isHttpModeDisabled, setIsHttpModeDisabled] = useState(false);
|
||||
|
||||
async function handleSubmit(values: InternalResourceFormValues) {
|
||||
try {
|
||||
let data = { ...values };
|
||||
if (
|
||||
(data.mode === "host" ||
|
||||
data.mode === "http" ||
|
||||
data.mode === "ssh") &&
|
||||
isHostname(data.destination)
|
||||
) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
if (!currentAlias) {
|
||||
let aliasValue = data.destination;
|
||||
if (data.destination?.toLowerCase() === "localhost") {
|
||||
aliasValue = `${cleanForFQDN(data.name)}.internal`;
|
||||
}
|
||||
data = { ...data, alias: aliasValue };
|
||||
}
|
||||
}
|
||||
|
||||
await api.post(`/site-resource/${resource.id}`, {
|
||||
name: data.name,
|
||||
siteIds: data.siteIds,
|
||||
mode: data.mode,
|
||||
niceId: data.niceId,
|
||||
destination: data.destination ?? undefined,
|
||||
...(data.mode === "http" && {
|
||||
scheme: data.scheme,
|
||||
ssl: data.ssl ?? false,
|
||||
destinationPort: data.destinationPort ?? null,
|
||||
domainId: data.httpConfigDomainId
|
||||
? data.httpConfigDomainId
|
||||
: undefined,
|
||||
subdomain: data.httpConfigSubdomain
|
||||
? data.httpConfigSubdomain
|
||||
: undefined
|
||||
}),
|
||||
...(data.mode === "host" && {
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: null,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" && {
|
||||
authDaemonPort: data.authDaemonPort || null
|
||||
})
|
||||
}),
|
||||
...(data.mode === "ssh" && {
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: null,
|
||||
destinationPort: data.destinationPort ?? null,
|
||||
pamMode: data.pamMode ?? undefined,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" && {
|
||||
authDaemonPort: data.authDaemonPort || null
|
||||
})
|
||||
}),
|
||||
...((data.mode === "host" || data.mode === "cidr") && {
|
||||
tcpPortRangeString: data.tcpPortRangeString,
|
||||
udpPortRangeString: data.udpPortRangeString,
|
||||
disableIcmp: data.disableIcmp ?? false
|
||||
}),
|
||||
...(data.mode === "ssh" && {
|
||||
disableIcmp: data.disableIcmp ?? false
|
||||
}),
|
||||
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
|
||||
userIds: (data.users || []).map((u) => u.id),
|
||||
clientIds: (data.clients || []).map((c) => parseInt(c.id))
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries(
|
||||
resourceQueries.siteResourceRoles({
|
||||
siteResourceId: resource.id
|
||||
})
|
||||
);
|
||||
await queryClient.invalidateQueries(
|
||||
resourceQueries.siteResourceUsers({
|
||||
siteResourceId: resource.id
|
||||
})
|
||||
);
|
||||
await queryClient.invalidateQueries(
|
||||
resourceQueries.siteResourceClients({
|
||||
siteResourceId: resource.id
|
||||
})
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("editInternalResourceDialogSuccess"),
|
||||
description: t(
|
||||
"editInternalResourceDialogInternalResourceUpdatedSuccessfully"
|
||||
),
|
||||
variant: "default"
|
||||
});
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("editInternalResourceDialogError"),
|
||||
description: formatAxiosError(
|
||||
error,
|
||||
t(
|
||||
"editInternalResourceDialogFailedToUpdateInternalResource"
|
||||
)
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) setOpen(false);
|
||||
}}
|
||||
>
|
||||
<CredenzaContent className="max-w-3xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("editInternalResourceDialogEditClientResource")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t(
|
||||
"editInternalResourceDialogUpdateResourceProperties",
|
||||
{
|
||||
resourceName: resource.name
|
||||
}
|
||||
)}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<PrivateResourceForm
|
||||
variant="edit"
|
||||
open={open}
|
||||
resource={resource}
|
||||
orgId={orgId}
|
||||
siteResourceId={resource.id}
|
||||
formId="edit-internal-resource-form"
|
||||
onSubmit={(values) =>
|
||||
startTransition(() => handleSubmit(values))
|
||||
}
|
||||
onSubmitDisabledChange={setIsHttpModeDisabled}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("editInternalResourceDialogCancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="edit-internal-resource-form"
|
||||
disabled={isSubmitting || isHttpModeDisabled}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t("editInternalResourceDialogSaveResource")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function InfoSections({
|
||||
cols?: number;
|
||||
/** content (default): fixed gap, columns hug content, left-aligned; fill: equal-width columns across the row */
|
||||
columnSizing?: "fill" | "content";
|
||||
/** panel: 2 columns until xl for narrow containers such as side panels */
|
||||
/** panel: fixed 2-column grid for narrow containers such as side panels */
|
||||
layout?: "default" | "panel";
|
||||
}) {
|
||||
const n = cols || 1;
|
||||
@@ -24,11 +24,11 @@ export function InfoSections({
|
||||
className={cn(
|
||||
"grid w-full min-w-0 gap-4",
|
||||
layout === "panel"
|
||||
? "grid-cols-2 xl:grid-cols-(--columns) xl:items-start xl:space-x-16"
|
||||
? "grid-cols-2 items-start"
|
||||
: "grid-cols-2 md:grid-cols-(--columns) md:items-start md:space-x-16",
|
||||
columnSizing === "content" &&
|
||||
(layout === "panel"
|
||||
? "xl:justify-items-start xl:justify-start"
|
||||
? "justify-items-start justify-start"
|
||||
: "md:justify-items-start md:justify-start")
|
||||
)}
|
||||
style={{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,6 @@
|
||||
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import CreatePrivateResourceDialog from "@app/components/CreatePrivateResourceDialog";
|
||||
import EditPrivateResourceDialog from "@app/components/EditPrivateResourceDialog";
|
||||
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
|
||||
import {
|
||||
ResourceSitesStatusCell,
|
||||
@@ -34,12 +32,14 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
|
||||
import { formatSiteResourceDestinationDisplay } from "@app/lib/formatSiteResourceAccess";
|
||||
import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHref";
|
||||
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowDown01Icon,
|
||||
ArrowRight,
|
||||
ArrowUp10Icon,
|
||||
ArrowUpDown,
|
||||
ChevronsUpDownIcon,
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
MoreHorizontal
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { startTransition, useMemo, useState, useTransition } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
@@ -78,6 +79,7 @@ export type InternalResourceRow = {
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
niceId: string;
|
||||
enabled: boolean;
|
||||
tcpPortRangeString: string | null;
|
||||
udpPortRangeString: string | null;
|
||||
disableIcmp: boolean;
|
||||
@@ -140,13 +142,10 @@ export default function PrivateResourcesTable({
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||
|
||||
const [selectedInternalResource, setSelectedInternalResource] =
|
||||
useState<InternalResourceRow | null>();
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
const [editingResource, setEditingResource] =
|
||||
useState<InternalResourceRow | null>();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
|
||||
const [isRefreshing, startRefreshTransition] = useTransition();
|
||||
|
||||
@@ -450,6 +449,17 @@ export default function PrivateResourcesTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={getPrivateResourceSettingsHref(
|
||||
resourceRow.orgId,
|
||||
resourceRow.niceId
|
||||
)}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedInternalResource(
|
||||
@@ -464,15 +474,17 @@ export default function PrivateResourcesTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
setEditingResource(resourceRow);
|
||||
setIsEditDialogOpen(true);
|
||||
}}
|
||||
<Link
|
||||
href={getPrivateResourceSettingsHref(
|
||||
resourceRow.orgId,
|
||||
resourceRow.niceId
|
||||
)}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -580,8 +592,15 @@ export default function PrivateResourcesTable({
|
||||
tableId="internal-resources"
|
||||
searchPlaceholder={t("resourcesSearch")}
|
||||
searchQuery={searchParams.get("query")?.toString()}
|
||||
onAdd={() => setIsCreateDialogOpen(true)}
|
||||
onAdd={() =>
|
||||
startNavigation(() =>
|
||||
router.push(
|
||||
`/${orgId}/settings/resources/private/create`
|
||||
)
|
||||
)
|
||||
}
|
||||
addButtonText={t("resourceAdd")}
|
||||
isNavigatingToAddPage={isNavigatingToAddPage}
|
||||
onSearch={handleSearchChange}
|
||||
onRefresh={refreshData}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
@@ -597,34 +616,6 @@ export default function PrivateResourcesTable({
|
||||
stickyLeftColumn="name"
|
||||
stickyRightColumn="actions"
|
||||
/>
|
||||
|
||||
{editingResource && (
|
||||
<EditPrivateResourceDialog
|
||||
open={isEditDialogOpen}
|
||||
setOpen={setIsEditDialogOpen}
|
||||
resource={editingResource}
|
||||
orgId={orgId}
|
||||
onSuccess={() => {
|
||||
// Delay refresh to allow modal to close smoothly
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
setEditingResource(null);
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreatePrivateResourceDialog
|
||||
open={isCreateDialogOpen}
|
||||
setOpen={setIsCreateDialogOpen}
|
||||
orgId={orgId}
|
||||
onSuccess={() => {
|
||||
// Delay refresh to allow modal to close smoothly
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { useSiteResourceContext } from "@app/hooks/useSiteResourceContext";
|
||||
import { formatPortRestrictionDisplay } from "@app/lib/launcherResourceDetails";
|
||||
import {
|
||||
formatSiteResourceAccess,
|
||||
formatSiteResourceDestinationDisplay,
|
||||
isSafeUrlForLink,
|
||||
type LauncherAccessFields
|
||||
} from "@app/lib/launcherResourceAccess";
|
||||
import type { PrivateResourceMode } from "@app/lib/privateResourceForm";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type SiteResourceInfoInput = {
|
||||
mode: PrivateResourceMode;
|
||||
destination: string | null;
|
||||
destinationPort: number | null;
|
||||
scheme: "http" | "https" | null;
|
||||
ssl: boolean;
|
||||
fullDomain?: string | null;
|
||||
alias?: string | null;
|
||||
aliasAddress?: string | null;
|
||||
authDaemonMode?: "site" | "remote" | "native" | null;
|
||||
tcpPortRangeString?: string | null;
|
||||
udpPortRangeString?: string | null;
|
||||
};
|
||||
|
||||
type SiteResourceInfoBoxVariant = "settings" | "panel";
|
||||
|
||||
type SiteResourceInfoSectionsProps = {
|
||||
siteResource: SiteResourceInfoInput;
|
||||
access: LauncherAccessFields;
|
||||
variant: SiteResourceInfoBoxVariant;
|
||||
accessClassName?: string;
|
||||
};
|
||||
|
||||
function AccessMethodContent({
|
||||
accessDisplay,
|
||||
accessCopyValue,
|
||||
accessUrl,
|
||||
className
|
||||
}: LauncherAccessFields & { className?: string }) {
|
||||
if (!accessDisplay) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
const href = accessUrl ?? undefined;
|
||||
const canLink = Boolean(href && isSafeUrlForLink(href));
|
||||
|
||||
if (canLink && href) {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={href}
|
||||
displayText={accessDisplay}
|
||||
isLink
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={accessCopyValue || accessDisplay}
|
||||
displayText={accessDisplay}
|
||||
isLink={false}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SiteResourceInfoSections({
|
||||
siteResource,
|
||||
access,
|
||||
variant,
|
||||
accessClassName
|
||||
}: SiteResourceInfoSectionsProps) {
|
||||
const t = useTranslations();
|
||||
const isPanel = variant === "panel";
|
||||
|
||||
const modeLabel: Record<PrivateResourceMode, string> = {
|
||||
host: t("editInternalResourceDialogModeHost"),
|
||||
cidr: t("editInternalResourceDialogModeCidr"),
|
||||
http: t("editInternalResourceDialogModeHttp"),
|
||||
ssh: t("editInternalResourceDialogModeSsh")
|
||||
};
|
||||
|
||||
const destination = formatSiteResourceDestinationDisplay({
|
||||
mode: siteResource.mode,
|
||||
destination: siteResource.destination,
|
||||
destinationPort: siteResource.destinationPort,
|
||||
scheme: siteResource.scheme
|
||||
});
|
||||
|
||||
const portRestrictions = formatPortRestrictionDisplay({
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString ?? "*",
|
||||
udpPortRangeString: siteResource.udpPortRangeString ?? "*"
|
||||
});
|
||||
const showAlias =
|
||||
siteResource.mode !== "cidr" && siteResource.mode !== "http";
|
||||
const showDestination = !(
|
||||
siteResource.mode === "ssh" && siteResource.authDaemonMode === "native"
|
||||
);
|
||||
|
||||
const numSections =
|
||||
2 + (showDestination ? 1 : 0) + (showAlias ? 1 : 0) + (isPanel ? 1 : 0);
|
||||
|
||||
const sections = (
|
||||
<InfoSections cols={numSections} layout={isPanel ? "panel" : "default"}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("type")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{modeLabel[siteResource.mode]}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("access")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<AccessMethodContent
|
||||
accessDisplay={access.accessDisplay}
|
||||
accessCopyValue={access.accessCopyValue}
|
||||
accessUrl={access.accessUrl}
|
||||
className={accessClassName}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
|
||||
{showDestination ? (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("editInternalResourceDialogDestination")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{destination || "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
) : null}
|
||||
|
||||
{showAlias ? (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("editInternalResourceDialogAlias")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{siteResource.alias?.trim() ? siteResource.alias : "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
) : null}
|
||||
|
||||
{isPanel ? (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("portRestrictions")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{!portRestrictions.hasNonDefaultPorts ? (
|
||||
<span>
|
||||
{t("resourceLauncherNoPortRestrictions")}
|
||||
</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{portRestrictions.tcp.state !== "all" ? (
|
||||
<div>
|
||||
{t("resourceLauncherTcp")}:{" "}
|
||||
{portRestrictions.tcp.state ===
|
||||
"blocked"
|
||||
? t("blocked")
|
||||
: portRestrictions.tcp.ports}
|
||||
</div>
|
||||
) : null}
|
||||
{portRestrictions.udp.state !== "all" ? (
|
||||
<div>
|
||||
{t("resourceLauncherUdp")}:{" "}
|
||||
{portRestrictions.udp.state ===
|
||||
"blocked"
|
||||
? t("blocked")
|
||||
: portRestrictions.udp.ports}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
) : null}
|
||||
</InfoSections>
|
||||
);
|
||||
|
||||
if (isPanel) {
|
||||
return sections;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>{sections}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
type SiteResourceInfoBoxProps = {
|
||||
variant?: "settings";
|
||||
};
|
||||
|
||||
export default function SiteResourceInfoBox({
|
||||
variant = "settings"
|
||||
}: SiteResourceInfoBoxProps) {
|
||||
const { siteResource } = useSiteResourceContext();
|
||||
|
||||
const access = formatSiteResourceAccess({
|
||||
mode: siteResource.mode,
|
||||
destination: siteResource.destination,
|
||||
destinationPort: siteResource.destinationPort,
|
||||
scheme: siteResource.scheme,
|
||||
ssl: siteResource.ssl,
|
||||
fullDomain: siteResource.fullDomain ?? null,
|
||||
alias: siteResource.alias ?? null,
|
||||
aliasAddress: siteResource.aliasAddress ?? null
|
||||
});
|
||||
|
||||
return (
|
||||
<SiteResourceInfoSections
|
||||
siteResource={siteResource}
|
||||
access={access}
|
||||
variant={variant}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { SettingsContainer } from "@app/components/Settings";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { formatSiteResourceDestinationDisplay } from "@app/lib/formatSiteResourceAccess";
|
||||
import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHref";
|
||||
import type { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource";
|
||||
import type { ListResourcesResponse } from "@server/routers/resource";
|
||||
import type ResponseT from "@server/types/Response";
|
||||
@@ -432,19 +433,13 @@ export default function SiteResourcesOverview({
|
||||
editHref: `/${orgId}/settings/resources/public/${r.niceId}`
|
||||
}));
|
||||
|
||||
const privateRows = privateList.map((row) => {
|
||||
const qs = new URLSearchParams({
|
||||
siteId: String(siteId),
|
||||
query: row.niceId
|
||||
});
|
||||
return {
|
||||
key: row.siteResourceId,
|
||||
meta: <PrivateResourceMeta row={row} />,
|
||||
name: row.name,
|
||||
access: <PrivateAccessMethod row={row} />,
|
||||
editHref: `/${orgId}/settings/resources/private?${qs.toString()}`
|
||||
};
|
||||
});
|
||||
const privateRows = privateList.map((row) => ({
|
||||
key: row.siteResourceId,
|
||||
meta: <PrivateResourceMeta row={row} />,
|
||||
name: row.name,
|
||||
access: <PrivateAccessMethod row={row} />,
|
||||
editHref: getPrivateResourceSettingsHref(orgId, row.niceId)
|
||||
}));
|
||||
|
||||
if (showEmptyPlaceholder) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import { SettingsFormCell } from "@app/components/Settings";
|
||||
import {
|
||||
StrategySelect,
|
||||
type StrategyOption
|
||||
} from "@app/components/StrategySelect";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type SshServerSettingsFormFields = {
|
||||
pamMode: "passthrough" | "push";
|
||||
standardDaemonLocation: "site" | "remote";
|
||||
authDaemonPort: string;
|
||||
};
|
||||
|
||||
type SshServerSettingsFieldsProps = {
|
||||
pamMode: "passthrough" | "push";
|
||||
standardDaemonLocation: "site" | "remote";
|
||||
authDaemonPort: string;
|
||||
onPamModeChange: (value: "passthrough" | "push") => void;
|
||||
onStandardDaemonLocationChange: (value: "site" | "remote") => void;
|
||||
onAuthDaemonPortChange: (value: string) => void;
|
||||
authDaemonPortError?: string;
|
||||
sshServerMode: "standard" | "native";
|
||||
serverModeDisplay: "badge" | "select";
|
||||
onServerModeChange?: (mode: "standard" | "native") => void;
|
||||
sshServerModeOptions?: StrategyOption<"standard" | "native">[];
|
||||
idPrefix?: string;
|
||||
};
|
||||
|
||||
export function SshServerSettingsFields({
|
||||
pamMode,
|
||||
standardDaemonLocation,
|
||||
authDaemonPort,
|
||||
onPamModeChange,
|
||||
onStandardDaemonLocationChange,
|
||||
onAuthDaemonPortChange,
|
||||
authDaemonPortError,
|
||||
sshServerMode,
|
||||
serverModeDisplay,
|
||||
onServerModeChange,
|
||||
sshServerModeOptions,
|
||||
idPrefix = "ssh-server"
|
||||
}: SshServerSettingsFieldsProps) {
|
||||
const t = useTranslations();
|
||||
const isNative = sshServerMode === "native";
|
||||
const showDaemonLocation = !isNative && pamMode === "push";
|
||||
const showDaemonPort =
|
||||
!isNative && pamMode === "push" && standardDaemonLocation === "remote";
|
||||
|
||||
const authMethodOptions = useMemo(
|
||||
(): StrategyOption<"passthrough" | "push">[] => [
|
||||
{
|
||||
id: "passthrough",
|
||||
title: t("sshAuthMethodManual"),
|
||||
description: t("sshAuthMethodManualDescription")
|
||||
},
|
||||
{
|
||||
id: "push",
|
||||
title: t("sshAuthMethodAutomated"),
|
||||
description: t("sshAuthMethodAutomatedDescription")
|
||||
}
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const daemonLocationOptions = useMemo(
|
||||
(): StrategyOption<"site" | "remote">[] => [
|
||||
{
|
||||
id: "site",
|
||||
title: t("internalResourceAuthDaemonSite"),
|
||||
description: t("sshDaemonLocationSiteDescription")
|
||||
},
|
||||
{
|
||||
id: "remote",
|
||||
title: t("sshDaemonLocationRemote"),
|
||||
description: t("sshDaemonLocationRemoteDescription")
|
||||
}
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const defaultSshServerModeOptions = useMemo(
|
||||
(): StrategyOption<"standard" | "native">[] => [
|
||||
{
|
||||
id: "native",
|
||||
title: t("sshServerModePangolin"),
|
||||
description: t("sshServerModeNativeDescription")
|
||||
},
|
||||
{
|
||||
id: "standard",
|
||||
title: t("sshServerModeStandard"),
|
||||
description: t("sshServerModeStandardDescription")
|
||||
}
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const modeOptions = sshServerModeOptions ?? defaultSshServerModeOptions;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshServerMode")}
|
||||
</p>
|
||||
{serverModeDisplay === "badge" ? (
|
||||
<Badge variant="secondary">
|
||||
{sshServerMode === "standard"
|
||||
? t("sshServerModeStandard")
|
||||
: t("sshServerModePangolin")}
|
||||
</Badge>
|
||||
) : (
|
||||
<StrategySelect<"standard" | "native">
|
||||
idPrefix={`${idPrefix}-mode`}
|
||||
value={sshServerMode}
|
||||
options={modeOptions}
|
||||
onChange={(value) => onServerModeChange?.(value)}
|
||||
cols={2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshAuthenticationMethod")}
|
||||
</p>
|
||||
<StrategySelect<"passthrough" | "push">
|
||||
idPrefix={`${idPrefix}-auth`}
|
||||
value={pamMode}
|
||||
options={authMethodOptions}
|
||||
onChange={onPamModeChange}
|
||||
cols={2}
|
||||
/>
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
|
||||
{showDaemonLocation && (
|
||||
<SettingsFormCell span="full">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshAuthDaemonLocation")}
|
||||
</p>
|
||||
<StrategySelect<"site" | "remote">
|
||||
idPrefix={`${idPrefix}-daemon`}
|
||||
value={standardDaemonLocation}
|
||||
options={daemonLocationOptions}
|
||||
onChange={onStandardDaemonLocationChange}
|
||||
cols={2}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshDaemonDisclaimer")}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/ssh"
|
||||
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>
|
||||
)}
|
||||
|
||||
{showDaemonPort && (
|
||||
<SettingsFormCell span="half">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor={`${idPrefix}-daemon-port`}>
|
||||
{t("sshDaemonPort")}
|
||||
</Label>
|
||||
<Input
|
||||
id={`${idPrefix}-daemon-port`}
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
value={authDaemonPort}
|
||||
onChange={(e) =>
|
||||
onAuthDaemonPortChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
{authDaemonPortError ? (
|
||||
<p className="text-destructive text-sm">
|
||||
{authDaemonPortError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ interface StrategySelectProps<TValue extends string> {
|
||||
defaultValue?: TValue;
|
||||
onChange?: (value: TValue) => void;
|
||||
cols?: number;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export function StrategySelect<TValue extends string>({
|
||||
@@ -25,7 +26,8 @@ export function StrategySelect<TValue extends string>({
|
||||
value: controlledValue,
|
||||
defaultValue,
|
||||
onChange,
|
||||
cols = 1
|
||||
cols = 1,
|
||||
idPrefix = "strategy"
|
||||
}: StrategySelectProps<TValue>) {
|
||||
const [uncontrolledSelected, setUncontrolledSelected] = useState<
|
||||
TValue | undefined
|
||||
@@ -49,43 +51,49 @@ export function StrategySelect<TValue extends string>({
|
||||
}}
|
||||
className="grid md:grid-cols-(--cols) gap-4"
|
||||
>
|
||||
{options.map((option: StrategyOption<TValue>) => (
|
||||
<label
|
||||
key={option.id}
|
||||
htmlFor={option.id}
|
||||
data-state={
|
||||
selected === option.id ? "checked" : "unchecked"
|
||||
}
|
||||
className={cn(
|
||||
"relative flex rounded-lg border p-4 transition-colors cursor-pointer",
|
||||
option.disabled
|
||||
? "border-input text-muted-foreground cursor-not-allowed opacity-50"
|
||||
: selected === option.id
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-input hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={option.id}
|
||||
id={option.id}
|
||||
disabled={option.disabled}
|
||||
className="absolute left-4 top-5 h-4 w-4 border-primary text-primary"
|
||||
/>
|
||||
<div className="flex gap-3 pl-7">
|
||||
{option.icon && (
|
||||
<div className="mt-1">{option.icon}</div>
|
||||
{options.map((option: StrategyOption<TValue>) => {
|
||||
const optionId = `${idPrefix}-${option.id}`;
|
||||
|
||||
return (
|
||||
<label
|
||||
key={option.id}
|
||||
htmlFor={optionId}
|
||||
data-state={
|
||||
selected === option.id ? "checked" : "unchecked"
|
||||
}
|
||||
className={cn(
|
||||
"relative flex rounded-lg border p-4 transition-colors cursor-pointer",
|
||||
option.disabled
|
||||
? "border-input text-muted-foreground cursor-not-allowed opacity-50"
|
||||
: selected === option.id
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-input hover:bg-accent"
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{option.title}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{typeof option.description === "string"
|
||||
? option.description
|
||||
: option.description}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={option.id}
|
||||
id={optionId}
|
||||
disabled={option.disabled}
|
||||
className="absolute left-4 top-5 h-4 w-4 border-primary text-primary"
|
||||
/>
|
||||
<div className="flex gap-3 pl-7">
|
||||
{option.icon && (
|
||||
<div className="mt-1">{option.icon}</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">
|
||||
{option.title}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{typeof option.description === "string"
|
||||
? option.description
|
||||
: option.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { SiteResourceInfoSections } from "@app/components/SiteResourceInfoBox";
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
@@ -26,14 +27,10 @@ import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
derivePublicAuthState,
|
||||
formatPortRestrictionDisplay,
|
||||
formatPublicResourceType
|
||||
} from "@app/lib/launcherResourceDetails";
|
||||
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
|
||||
import {
|
||||
formatSiteResourceDestinationDisplay,
|
||||
isSafeUrlForLink
|
||||
} from "@app/lib/launcherResourceAccess";
|
||||
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
|
||||
import { launcherQueries } from "@app/lib/queries";
|
||||
import type { LauncherResource } from "@server/routers/launcher/types";
|
||||
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
|
||||
@@ -322,25 +319,6 @@ function PrivateResourceDetails({
|
||||
resource: GetSiteResourceResponse;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const modeLabel: Record<GetSiteResourceResponse["mode"], string> = {
|
||||
host: t("editInternalResourceDialogModeHost"),
|
||||
cidr: t("editInternalResourceDialogModeCidr"),
|
||||
http: t("editInternalResourceDialogModeHttp"),
|
||||
ssh: t("editInternalResourceDialogModeSsh")
|
||||
};
|
||||
const destination = formatSiteResourceDestinationDisplay({
|
||||
mode: resource.mode,
|
||||
destination: resource.destination,
|
||||
destinationPort: resource.destinationPort,
|
||||
scheme: resource.scheme
|
||||
});
|
||||
const portRestrictions = formatPortRestrictionDisplay(resource);
|
||||
const showAlias = resource.mode !== "cidr" && resource.mode !== "http";
|
||||
const showDestination = !(
|
||||
resource.mode === "ssh" && resource.authDaemonMode === "native"
|
||||
);
|
||||
const infoSectionCount =
|
||||
2 + (showDestination ? 1 : 0) + (showAlias ? 1 : 0) + 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -375,89 +353,16 @@ function PrivateResourceDetails({
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={infoSectionCount} layout="panel">
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("type")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{modeLabel[resource.mode]}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("access")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<AccessMethodContent
|
||||
accessDisplay={
|
||||
launcherResource.accessDisplay
|
||||
}
|
||||
accessCopyValue={
|
||||
launcherResource.accessCopyValue
|
||||
}
|
||||
accessUrl={launcherResource.accessUrl}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{showDestination ? (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("editInternalResourceDialogDestination")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{destination || "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
) : null}
|
||||
{showAlias ? (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("editInternalResourceDialogAlias")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{resource.alias?.trim()
|
||||
? resource.alias
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
) : null}
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("portRestrictions")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{!portRestrictions.hasNonDefaultPorts ? (
|
||||
<span>
|
||||
{t(
|
||||
"resourceLauncherNoPortRestrictions"
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{portRestrictions.tcp.state !==
|
||||
"all" ? (
|
||||
<div>
|
||||
{t("resourceLauncherTcp")}:{" "}
|
||||
{portRestrictions.tcp.state ===
|
||||
"blocked"
|
||||
? t("blocked")
|
||||
: portRestrictions.tcp
|
||||
.ports}
|
||||
</div>
|
||||
) : null}
|
||||
{portRestrictions.udp.state !==
|
||||
"all" ? (
|
||||
<div>
|
||||
{t("resourceLauncherUdp")}:{" "}
|
||||
{portRestrictions.udp.state ===
|
||||
"blocked"
|
||||
? t("blocked")
|
||||
: portRestrictions.udp
|
||||
.ports}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
<SiteResourceInfoSections
|
||||
siteResource={resource}
|
||||
access={{
|
||||
accessDisplay: launcherResource.accessDisplay,
|
||||
accessCopyValue: launcherResource.accessCopyValue,
|
||||
accessUrl: launcherResource.accessUrl
|
||||
}}
|
||||
variant="panel"
|
||||
accessClassName="text-base"
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user