mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-15 01:16:38 +00:00
Merge branch 'dev' into feat/update-popup
This commit is contained in:
@@ -232,6 +232,7 @@ export default function ExitNodesTable({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const nodeRow = row.original;
|
||||
const remoteExitNodeId = nodeRow.id;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
@@ -242,6 +243,14 @@ export default function ExitNodesTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedNode(nodeRow);
|
||||
@@ -254,6 +263,14 @@ export default function ExitNodesTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
|
||||
>
|
||||
<Button variant={"secondary"} size="sm">
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
PickRemoteExitNodeDefaultsResponse,
|
||||
QuickStartRemoteExitNodeResponse
|
||||
} from "@server/routers/remoteExitNode/types";
|
||||
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { remoteExitNode } = useRemoteExitNodeContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [credentials, setCredentials] = useState<PickRemoteExitNodeDefaultsResponse | null>(null);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
|
||||
const response = await api.get<AxiosResponse<PickRemoteExitNodeDefaultsResponse>>(
|
||||
`/org/${orgId}/pick-remote-exit-node-defaults`
|
||||
);
|
||||
|
||||
const data = response.data.data;
|
||||
setCredentials(data);
|
||||
|
||||
await api.put<AxiosResponse<QuickStartRemoteExitNodeResponse>>(
|
||||
`/re-key/${orgId}/reGenerate-remote-exit-node-secret`,
|
||||
{
|
||||
remoteExitNodeId: remoteExitNode.remoteExitNodeId,
|
||||
secret: data.secret,
|
||||
}
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const getCredentials = () => {
|
||||
if (credentials) {
|
||||
return {
|
||||
Id: remoteExitNode.remoteExitNodeId,
|
||||
Secret: credentials.secret
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-block">
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regeneratecredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isSecurityFeatureDisabled() && (
|
||||
<TooltipContent side="top">
|
||||
{t("featureDisabledTooltip")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<RegenerateCredentialsModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
type="remote-exit-node"
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function GeneralPage() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import RemoteExitNodeProvider from "@app/providers/RemoteExitNodeProvider";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import ExitNodeInfoCard from "@app/components/ExitNodeInfoCard";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -31,6 +33,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('credentials'),
|
||||
href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/credentials"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
@@ -39,7 +48,10 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
/>
|
||||
|
||||
<RemoteExitNodeProvider remoteExitNode={remoteExitNode}>
|
||||
<div className="space-y-6">{children}</div>
|
||||
<div className="space-y-6">
|
||||
<ExitNodeInfoCard />
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</div>
|
||||
</RemoteExitNodeProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,6 @@ export default async function RemoteExitNodePage(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(
|
||||
`/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/general`
|
||||
`/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/credentials`
|
||||
);
|
||||
}
|
||||
|
||||
124
src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx
Normal file
124
src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PickClientDefaultsResponse } from "@server/routers/client";
|
||||
import { useClientContext } from "@app/hooks/useClientContext";
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
import { build } from "@server/build";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { client } = useClientContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [clientDefaults, setClientDefaults] = useState<PickClientDefaultsResponse | null>(null);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
|
||||
const res = await api.get(`/org/${orgId}/pick-client-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setClientDefaults(data);
|
||||
|
||||
await api.post(`/re-key/${client?.clientId}/regenerate-client-secret`, {
|
||||
olmId: data.olmId,
|
||||
secret: data.olmSecret,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const getCredentials = () => {
|
||||
if (clientDefaults) {
|
||||
return {
|
||||
Id: clientDefaults.olmId,
|
||||
Secret: clientDefaults.olmSecret
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-block">
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}>
|
||||
{t("regeneratecredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isSecurityFeatureDisabled() && (
|
||||
<TooltipContent side="top">
|
||||
{t("featureDisabledTooltip")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<RegenerateCredentialsModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
type="client-olm"
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import ClientInfoCard from "../../../../../components/ClientInfoCard";
|
||||
import ClientProvider from "@app/providers/ClientProvider";
|
||||
import { redirect } from "next/navigation";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { build } from "@server/build";
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -30,11 +32,20 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
redirect(`/${params.orgId}/settings/clients`);
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "General",
|
||||
title: t('general'),
|
||||
href: `/{orgId}/settings/clients/{clientId}/general`
|
||||
}
|
||||
},
|
||||
...(build === 'enterprise'
|
||||
? [{
|
||||
title: t('credentials'),
|
||||
href: `/{orgId}/settings/clients/{clientId}/credentials`
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -73,7 +73,7 @@ export default async function DomainSettingsPage({
|
||||
|
||||
<DNSRecordsTable records={dnsRecords} type={domain.type} />
|
||||
|
||||
{domain.type == "wildcard" && (
|
||||
{domain.type == "wildcard" && !domain.configManaged && (
|
||||
<DomainCertForm
|
||||
orgId={orgId}
|
||||
domainId={domain.domainId}
|
||||
|
||||
@@ -68,9 +68,9 @@ export default function GeneralForm() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const [editDomainOpen, setEditDomainOpen] = useState(false);
|
||||
const {licenseStatus } = useLicenseStatusContext();
|
||||
const { licenseStatus } = useLicenseStatusContext();
|
||||
const subscriptionStatus = useSubscriptionStatusContext();
|
||||
const {user} = useUserContext();
|
||||
const { user } = useUserContext();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
|
||||
@@ -102,6 +102,7 @@ export default function GeneralForm() {
|
||||
enabled: z.boolean(),
|
||||
subdomain: z.string().optional(),
|
||||
name: z.string().min(1).max(255),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
domainId: z.string().optional(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||
// enableProxy: z.boolean().optional()
|
||||
@@ -130,6 +131,7 @@ export default function GeneralForm() {
|
||||
defaultValues: {
|
||||
enabled: resource.enabled,
|
||||
name: resource.name,
|
||||
niceId: resource.niceId,
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
domainId: resource.domainId || undefined,
|
||||
proxyPort: resource.proxyPort || undefined,
|
||||
@@ -192,6 +194,7 @@ export default function GeneralForm() {
|
||||
{
|
||||
enabled: data.enabled,
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
|
||||
domainId: data.domainId,
|
||||
proxyPort: data.proxyPort,
|
||||
@@ -212,16 +215,12 @@ export default function GeneralForm() {
|
||||
});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
title: t("resourceUpdated"),
|
||||
description: t("resourceUpdatedDescription")
|
||||
});
|
||||
|
||||
const resource = res.data.data;
|
||||
const updated = res.data.data;
|
||||
|
||||
updateResource({
|
||||
enabled: data.enabled,
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain,
|
||||
fullDomain: resource.fullDomain,
|
||||
proxyPort: data.proxyPort,
|
||||
@@ -230,8 +229,20 @@ export default function GeneralForm() {
|
||||
// })
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
toast({
|
||||
title: t("resourceUpdated"),
|
||||
description: t("resourceUpdatedDescription")
|
||||
});
|
||||
|
||||
if (data.niceId && data.niceId !== resource?.niceId) {
|
||||
router.replace(`/${updated.orgId}/settings/resources/${data.niceId}/general`);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
setSaveLoading(false);
|
||||
}
|
||||
|
||||
setSaveLoading(false);
|
||||
}
|
||||
|
||||
@@ -304,6 +315,24 @@ export default function GeneralForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("identifier")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("enterIdentifier")}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!resource.http && (
|
||||
<>
|
||||
<FormField
|
||||
|
||||
@@ -501,25 +501,6 @@ export default function ReverseProxyTargets(props: {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target with same IP, port and method already exists
|
||||
const isDuplicate = targets.some(
|
||||
(t) =>
|
||||
t.targetId !== target.targetId &&
|
||||
t.ip === target.ip &&
|
||||
t.port === target.port &&
|
||||
t.method === target.method &&
|
||||
t.siteId === target.siteId
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("targetErrorDuplicate"),
|
||||
description: t("targetErrorDuplicateDescription")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setTargetsLoading(true);
|
||||
|
||||
@@ -531,9 +512,18 @@ export default function ReverseProxyTargets(props: {
|
||||
port: target.port,
|
||||
enabled: target.enabled,
|
||||
hcEnabled: target.hcEnabled,
|
||||
hcPath: target.hcPath,
|
||||
hcInterval: target.hcInterval,
|
||||
hcTimeout: target.hcTimeout
|
||||
hcPath: target.hcPath || null,
|
||||
hcScheme: target.hcScheme || null,
|
||||
hcHostname: target.hcHostname || null,
|
||||
hcPort: target.hcPort || null,
|
||||
hcInterval: target.hcInterval || null,
|
||||
hcTimeout: target.hcTimeout || null,
|
||||
hcHeaders: target.hcHeaders || null,
|
||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
||||
hcMethod: target.hcMethod || null,
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
@@ -585,24 +575,6 @@ export default function ReverseProxyTargets(props: {
|
||||
}
|
||||
|
||||
async function addTarget(data: z.infer<typeof addTargetSchema>) {
|
||||
// Check if target with same IP, port and method already exists
|
||||
const isDuplicate = targets.some(
|
||||
(target) =>
|
||||
target.ip === data.ip &&
|
||||
target.port === data.port &&
|
||||
target.method === data.method &&
|
||||
target.siteId === data.siteId
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("targetErrorDuplicate"),
|
||||
description: t("targetErrorDuplicateDescription")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if (site && site.type == "wireguard" && site.subnet) {
|
||||
// // make sure that the target IP is within the site subnet
|
||||
// const targetIp = data.ip;
|
||||
@@ -755,7 +727,9 @@ export default function ReverseProxyTargets(props: {
|
||||
hcHeaders: target.hcHeaders || null,
|
||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
||||
hcMethod: target.hcMethod || null,
|
||||
hcStatus: target.hcStatus || null
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
@@ -899,7 +873,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "healthCheck",
|
||||
header: t("healthCheck"),
|
||||
header: () => (<span className="p-3">{t("healthCheck")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
const isEnabled = row.original.hcEnabled;
|
||||
@@ -971,7 +945,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "path",
|
||||
header: t("matchPath"),
|
||||
header: () => (<span className="p-3">{t("matchPath")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const hasPathMatch = !!(
|
||||
row.original.path || row.original.pathMatchType
|
||||
@@ -1033,7 +1007,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: t("address"),
|
||||
header: () => (<span className="p-3">{t("address")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const selectedSite = sites.find(
|
||||
(site) => site.siteId === row.original.siteId
|
||||
@@ -1052,7 +1026,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input shadow-2xs rounded-md">
|
||||
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
|
||||
{selectedSite &&
|
||||
selectedSite.type === "newt" &&
|
||||
(() => {
|
||||
@@ -1247,7 +1221,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "rewritePath",
|
||||
header: t("rewritePath"),
|
||||
header: () => (<span className="p-3">{t("rewritePath")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const hasRewritePath = !!(
|
||||
row.original.rewritePath || row.original.rewritePathType
|
||||
@@ -1317,7 +1291,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "enabled",
|
||||
header: t("enabled"),
|
||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<Switch
|
||||
@@ -1338,8 +1312,9 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<div className="flex items-center w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => removeTarget(row.original.targetId)}
|
||||
@@ -1850,6 +1825,7 @@ export default function ReverseProxyTargets(props: {
|
||||
30
|
||||
}}
|
||||
onChanges={async (config) => {
|
||||
console.log("here");
|
||||
if (selectedTargetForHealthCheck) {
|
||||
console.log(config);
|
||||
updateTargetHealthCheck(
|
||||
|
||||
@@ -425,24 +425,6 @@ export default function Page() {
|
||||
};
|
||||
|
||||
async function addTarget(data: z.infer<typeof addTargetSchema>) {
|
||||
// Check if target with same IP, port and method already exists
|
||||
const isDuplicate = targets.some(
|
||||
(target) =>
|
||||
target.ip === data.ip &&
|
||||
target.port === data.port &&
|
||||
target.method === data.method &&
|
||||
target.siteId === data.siteId
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("targetErrorDuplicate"),
|
||||
description: t("targetErrorDuplicateDescription")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const site = sites.find((site) => site.siteId === data.siteId);
|
||||
|
||||
const isHttp = baseForm.watch("http");
|
||||
@@ -592,7 +574,9 @@ export default function Page() {
|
||||
hcPort: target.hcPort || null,
|
||||
hcFollowRedirects:
|
||||
target.hcFollowRedirects || null,
|
||||
hcStatus: target.hcStatus || null
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
|
||||
193
src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx
Normal file
193
src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { generateKeypair } from "../wireguardConfig";
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { site } = useSiteContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [siteDefaults, setSiteDefaults] = useState<PickSiteDefaultsResponse | null>(null);
|
||||
const [wgConfig, setWgConfig] = useState("");
|
||||
const [publicKey, setPublicKey] = useState("");
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
|
||||
const hydrateWireGuardConfig = (
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
subnet: string,
|
||||
address: string,
|
||||
endpoint: string,
|
||||
listenPort: string
|
||||
) => {
|
||||
const config = `[Interface]
|
||||
Address = ${subnet}
|
||||
ListenPort = 51820
|
||||
PrivateKey = ${privateKey}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${publicKey}
|
||||
AllowedIPs = ${address.split("/")[0]}/32
|
||||
Endpoint = ${endpoint}:${listenPort}
|
||||
PersistentKeepalive = 5`;
|
||||
setWgConfig(config);
|
||||
return config;
|
||||
};
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
let generatedPublicKey = "";
|
||||
let generatedWgConfig = "";
|
||||
|
||||
if (site?.type === "wireguard") {
|
||||
const generatedKeypair = generateKeypair();
|
||||
generatedPublicKey = generatedKeypair.publicKey;
|
||||
setPublicKey(generatedPublicKey);
|
||||
|
||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setSiteDefaults(data);
|
||||
|
||||
// generate config with the fetched data
|
||||
generatedWgConfig = hydrateWireGuardConfig(
|
||||
generatedKeypair.privateKey,
|
||||
data.publicKey,
|
||||
data.subnet,
|
||||
data.address,
|
||||
data.endpoint,
|
||||
data.listenPort
|
||||
);
|
||||
}
|
||||
|
||||
await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, {
|
||||
type: "wireguard",
|
||||
subnet: res.data.data.subnet,
|
||||
exitNodeId: res.data.data.exitNodeId,
|
||||
pubKey: generatedPublicKey
|
||||
});
|
||||
}
|
||||
|
||||
if (site?.type === "newt") {
|
||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setSiteDefaults(data);
|
||||
|
||||
await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, {
|
||||
type: "newt",
|
||||
newtId: data.newtId,
|
||||
newtSecret: data.newtSecret
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const getCredentialType = () => {
|
||||
if (site?.type === "wireguard") return "site-wireguard";
|
||||
if (site?.type === "newt") return "site-newt";
|
||||
return "site-newt";
|
||||
};
|
||||
|
||||
const getCredentials = () => {
|
||||
if (site?.type === "wireguard" && wgConfig) {
|
||||
return { wgConfig };
|
||||
}
|
||||
if (site?.type === "newt" && siteDefaults) {
|
||||
return {
|
||||
Id: siteDefaults.newtId,
|
||||
Secret: siteDefaults.newtSecret
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-block">
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regeneratecredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isSecurityFeatureDisabled() && (
|
||||
<TooltipContent side="top">
|
||||
{t("featureDisabledTooltip")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<RegenerateCredentialsModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
type={getCredentialType()}
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { toast, useToast } from "@app/hooks/useToast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
SettingsContainer,
|
||||
@@ -37,6 +37,7 @@ import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
remoteSubnets: z
|
||||
.array(
|
||||
@@ -55,19 +56,18 @@ export default function GeneralPage() {
|
||||
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(null);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: site?.name,
|
||||
niceId: site?.niceId || "",
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||
remoteSubnets: site?.remoteSubnets
|
||||
? site.remoteSubnets.split(",").map((subnet, index) => ({
|
||||
@@ -82,37 +82,40 @@ export default function GeneralPage() {
|
||||
async function onSubmit(data: GeneralFormValues) {
|
||||
setLoading(true);
|
||||
|
||||
await api
|
||||
.post(`/site/${site?.siteId}`, {
|
||||
try {
|
||||
await api.post(`/site/${site?.siteId}`, {
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
remoteSubnets:
|
||||
data.remoteSubnets
|
||||
?.map((subnet) => subnet.text)
|
||||
.join(",") || ""
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("siteErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
?.map((subnet) => subnet.text)
|
||||
.join(",") || ""
|
||||
});
|
||||
|
||||
updateSite({
|
||||
name: data.name,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
remoteSubnets:
|
||||
data.remoteSubnets?.map((subnet) => subnet.text).join(",") || ""
|
||||
});
|
||||
updateSite({
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
remoteSubnets:
|
||||
data.remoteSubnets?.map((subnet) => subnet.text).join(",") || ""
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t("siteUpdated"),
|
||||
description: t("siteUpdatedDescription")
|
||||
});
|
||||
if (data.niceId && data.niceId !== site?.niceId) {
|
||||
router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("siteUpdated"),
|
||||
description: t("siteUpdatedDescription")
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorUpdate"),
|
||||
description: formatAxiosError(e, t("siteErrorUpdateDescription"))
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -153,8 +156,25 @@ export default function GeneralPage() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{env.flags.enableClients &&
|
||||
site.type === "newt" ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("identifier")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("enterIdentifier")}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{env.flags.enableClients && site.type === "newt" ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remoteSubnets"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import SiteInfoCard from "../../../../../components/SiteInfoCard";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { build } from "@server/build";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -35,14 +36,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
href: "/{orgId}/settings/sites/{niceId}/general"
|
||||
}
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/general`,
|
||||
},
|
||||
...(site.type !== 'local' && build === 'enterprise'
|
||||
? [
|
||||
{
|
||||
title: t('credentials'),
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/credentials`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('siteSetting', {siteName: site?.name})}
|
||||
title={t('siteSetting', { siteName: site?.name })}
|
||||
description={t('siteSettingDescription')}
|
||||
/>
|
||||
|
||||
|
||||
@@ -54,22 +54,7 @@ export default function BlueprintDetailsForm({
|
||||
<div className="flex flex-col gap-6">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("appliedAt")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<time
|
||||
className="text-muted-foreground"
|
||||
dateTime={blueprint.createdAt.toString()}
|
||||
>
|
||||
{new Date(
|
||||
blueprint.createdAt * 1000
|
||||
).toLocaleString()}
|
||||
</time>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("status")}
|
||||
@@ -88,16 +73,6 @@ export default function BlueprintDetailsForm({
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("message")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<p className="text-muted-foreground">
|
||||
{blueprint.message}
|
||||
</p>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("source")}
|
||||
@@ -106,35 +81,59 @@ export default function BlueprintDetailsForm({
|
||||
{blueprint.source === "API" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="-mx-2"
|
||||
className="inline-flex items-center gap-1 "
|
||||
>
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
API
|
||||
<Webhook className="size-4 flex-none" />
|
||||
</span>
|
||||
API
|
||||
<Webhook className="w-3 h-3 flex-none" />
|
||||
</Badge>
|
||||
)}
|
||||
{blueprint.source === "NEWT" && (
|
||||
<Badge variant="secondary">
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Newt CLI
|
||||
<Terminal className="size-4 flex-none" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="inline-flex items-center gap-1 "
|
||||
>
|
||||
<Terminal className="w-3 h-3 flex-none" />
|
||||
Newt CLI
|
||||
</Badge>
|
||||
)}
|
||||
{blueprint.source === "UI" && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="-mx-1 py-1"
|
||||
className="inline-flex items-center gap-1 "
|
||||
>
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Dashboard{" "}
|
||||
<Globe className="size-4 flex-none" />
|
||||
</span>
|
||||
<Globe className="w-3 h-3 flex-none" />
|
||||
Dashboard
|
||||
</Badge>
|
||||
)}{" "}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("appliedAt")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<time
|
||||
className="text-muted-foreground"
|
||||
dateTime={blueprint.createdAt.toString()}
|
||||
>
|
||||
{new Date(
|
||||
blueprint.createdAt * 1000
|
||||
).toLocaleString()}
|
||||
</time>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{blueprint.message && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("message")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<p className="text-muted-foreground">
|
||||
{blueprint.message}
|
||||
</p>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -169,11 +168,6 @@ export default function BlueprintDetailsForm({
|
||||
<FormLabel>
|
||||
{t("parsedContents")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"blueprintContentsDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -116,10 +116,13 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
}
|
||||
case "UI": {
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="inline-flex items-center gap-1"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1 ">
|
||||
Dashboard{" "}
|
||||
<Globe className="size-4 flex-none" />
|
||||
<Globe className="w-3 h-3" />
|
||||
Dashboard
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
@@ -163,18 +166,14 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="items-center"
|
||||
asChild
|
||||
<Link
|
||||
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
||||
>
|
||||
<Link
|
||||
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
||||
>
|
||||
View details{" "}
|
||||
<ArrowRight className="size-4 flex-none" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" className="items-center">
|
||||
View Details
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { useClientContext } from "@app/hooks/useClientContext";
|
||||
import {
|
||||
InfoSection,
|
||||
@@ -19,9 +18,7 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">{t("clientInformation")}</AlertTitle>
|
||||
<AlertDescription className="mt-4">
|
||||
<AlertDescription>
|
||||
<InfoSections cols={2}>
|
||||
<>
|
||||
<InfoSection>
|
||||
|
||||
@@ -278,14 +278,14 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{/* <Link */}
|
||||
{/* className="block w-full" */}
|
||||
{/* href={`/${clientRow.orgId}/settings/sites/${clientRow.nice}`} */}
|
||||
{/* > */}
|
||||
{/* <DropdownMenuItem> */}
|
||||
{/* View settings */}
|
||||
{/* </DropdownMenuItem> */}
|
||||
{/* </Link> */}
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
View settings
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedClient(clientRow);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { DNSRecordsDataTable } from "./DNSRecordsDataTable";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
export type DNSRecordRow = {
|
||||
id: string;
|
||||
@@ -24,6 +25,30 @@ export default function DNSRecordsTable({
|
||||
type
|
||||
}: Props) {
|
||||
const t = useTranslations();
|
||||
const env = useEnvContext();
|
||||
|
||||
const statusColumn: ColumnDef<DNSRecordRow> = {
|
||||
accessorKey: "verified",
|
||||
header: ({ column }) => {
|
||||
return <div>{t("status")}</div>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const verified = row.original.verified;
|
||||
return verified ? (
|
||||
type === "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{t("manual", { fallback: "Manual" })}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<DNSRecordRow>[] = [
|
||||
{
|
||||
@@ -81,28 +106,7 @@ export default function DNSRecordsTable({
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "verified",
|
||||
header: ({ column }) => {
|
||||
return <div>{t("status")}</div>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const verified = row.original.verified;
|
||||
return verified ? (
|
||||
type === "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{t("manual", { fallback: "Manual" })}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
...(env.env.flags.usePangolinDns ? [statusColumn] : [])
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -124,7 +124,6 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="bg-secondary dark:bg-transparent"
|
||||
>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
type DomainInfoCardProps = {
|
||||
failed: boolean;
|
||||
@@ -22,6 +23,7 @@ export default function DomainInfoCard({
|
||||
type
|
||||
}: DomainInfoCardProps) {
|
||||
const t = useTranslations();
|
||||
const env = useEnvContext();
|
||||
|
||||
const getTypeDisplay = (type: string) => {
|
||||
switch (type) {
|
||||
@@ -46,32 +48,34 @@ export default function DomainInfoCard({
|
||||
<span>{getTypeDisplay(type ? type : "")}</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{failed ? (
|
||||
<Badge variant="red">
|
||||
{t("failed", { fallback: "Failed" })}
|
||||
</Badge>
|
||||
) : verified ? (
|
||||
type === "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{t("manual", {
|
||||
fallback: "Manual"
|
||||
})}
|
||||
{env.env.flags.usePangolinDns && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{failed ? (
|
||||
<Badge variant="red">
|
||||
{t("failed", { fallback: "Failed" })}
|
||||
</Badge>
|
||||
) : verified ? (
|
||||
type === "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">
|
||||
{t("manual", {
|
||||
fallback: "Manual"
|
||||
})}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="green">
|
||||
{t("verified")}
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="green">
|
||||
{t("verified")}
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
</Badge>
|
||||
)
|
||||
) : (
|
||||
<Badge variant="yellow">
|
||||
{t("pending", { fallback: "Pending" })}
|
||||
</Badge>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -55,7 +55,8 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const api = createApiClient(useEnvContext());
|
||||
const env = useEnvContext();
|
||||
const api = createApiClient(env);
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { toast } = useToast();
|
||||
@@ -134,6 +135,41 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const statusColumn: ColumnDef<DomainRow> = {
|
||||
accessorKey: "verified",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("status")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const { verified, failed, type } = row.original;
|
||||
if (verified) {
|
||||
return type == "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">{t("manual")}</Badge>
|
||||
) : (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
);
|
||||
} else if (failed) {
|
||||
return (
|
||||
<Badge variant="red">
|
||||
{t("failed", { fallback: "Failed" })}
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
return <Badge variant="yellow">{t("pending")}</Badge>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<DomainRow>[] = [
|
||||
{
|
||||
accessorKey: "baseDomain",
|
||||
@@ -173,40 +209,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "verified",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("status")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const { verified, failed, type } = row.original;
|
||||
if (verified) {
|
||||
return type == "wildcard" ? (
|
||||
<Badge variant="outlinePrimary">{t("manual")}</Badge>
|
||||
) : (
|
||||
<Badge variant="green">{t("verified")}</Badge>
|
||||
);
|
||||
} else if (failed) {
|
||||
return (
|
||||
<Badge variant="red">
|
||||
{t("failed", { fallback: "Failed" })}
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
return <Badge variant="yellow">{t("pending")}</Badge>;
|
||||
}
|
||||
}
|
||||
},
|
||||
...(env.env.flags.usePangolinDns ? [statusColumn] : []),
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
|
||||
52
src/components/ExitNodeInfoCard.tsx
Normal file
52
src/components/ExitNodeInfoCard.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
|
||||
|
||||
type ExitNodeInfoCardProps = {};
|
||||
|
||||
export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
|
||||
const { remoteExitNode, updateRemoteExitNode } = useRemoteExitNodeContext();
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription className="mt-4">
|
||||
<InfoSections cols={2}>
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{remoteExitNode.online ? (
|
||||
<div className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>{t("online")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>{t("offline")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("address")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{remoteExitNode.address}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -50,5 +50,11 @@ export function InfoSectionContent({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("break-words", className)}>{children}</div>;
|
||||
return (
|
||||
<div className={cn("min-w-0 overflow-hidden", className)}>
|
||||
<div className="w-full truncate [&>div.flex]:min-w-0 [&>div.flex]:!whitespace-normal [&>div.flex>span]:truncate [&>div.flex>a]:truncate">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
216
src/components/RegenerateCredentialsModal.tsx
Normal file
216
src/components/RegenerateCredentialsModal.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon, AlertTriangle } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
|
||||
type CredentialType = "site-wireguard" | "site-newt" | "client-olm" | "remote-exit-node";
|
||||
|
||||
interface RegenerateCredentialsModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
type: CredentialType;
|
||||
onConfirmRegenerate: () => Promise<void>;
|
||||
dashboardUrl: string;
|
||||
credentials?: {
|
||||
// For WireGuard sites
|
||||
wgConfig?: string;
|
||||
|
||||
Id?: string;
|
||||
Secret?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function RegenerateCredentialsModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
type,
|
||||
onConfirmRegenerate,
|
||||
dashboardUrl,
|
||||
credentials
|
||||
}: RegenerateCredentialsModalProps) {
|
||||
const t = useTranslations();
|
||||
const [stage, setStage] = useState<"confirm" | "show">("confirm");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await onConfirmRegenerate();
|
||||
setStage("show");
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setStage("confirm");
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (stage === "confirm") {
|
||||
return t("regeneratecredentials");
|
||||
}
|
||||
switch (type) {
|
||||
case "site-wireguard":
|
||||
return t("WgConfiguration");
|
||||
case "site-newt":
|
||||
return t("siteNewtCredentials");
|
||||
case "client-olm":
|
||||
return t("clientOlmCredentials");
|
||||
case "remote-exit-node":
|
||||
return t("remoteExitNodeCreate.generate.title");
|
||||
}
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
if (stage === "confirm") {
|
||||
return t("regenerateCredentialsWarning");
|
||||
}
|
||||
switch (type) {
|
||||
case "site-wireguard":
|
||||
return t("WgConfigurationDescription");
|
||||
case "site-newt":
|
||||
return t("siteNewtCredentialsDescription");
|
||||
case "client-olm":
|
||||
return t("clientOlmCredentialsDescription");
|
||||
case "remote-exit-node":
|
||||
return t("remoteExitNodeCreate.generate.description");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={onOpenChange}>
|
||||
<CredenzaContent className="max-h-[80vh] flex flex-col">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{getTitle()}</CredenzaTitle>
|
||||
<CredenzaDescription>{getDescription()}</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
|
||||
<CredenzaBody className="overflow-y-auto px-4">
|
||||
{stage === "confirm" ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("warning")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("regenerateCredentialsConfirmation")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
{credentials?.wgConfig && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<CopyTextBox text={credentials.wgConfig} />
|
||||
<div className="relative w-fit border rounded-md">
|
||||
<div className="bg-white p-6 rounded-md">
|
||||
<QRCodeCanvas
|
||||
value={credentials.wgConfig}
|
||||
size={168}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("copyandsavethesecredentials")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("copyandsavethesecredentialsdescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{credentials?.Id && credentials.Secret && (
|
||||
<div className="space-y-4">
|
||||
<InfoSections cols={1}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("endpoint")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard text={dashboardUrl} />
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("Id")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard text={credentials?.Id} />
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("SecretKey")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard text={credentials?.Secret} />
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("copyandsavethesecredentials")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("copyandsavethesecredentialsdescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CredenzaBody>
|
||||
|
||||
<CredenzaFooter>
|
||||
{stage === "confirm" ? (
|
||||
<>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("cancel")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
variant="destructive"
|
||||
>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
{t("close")}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import { ShieldCheck, ShieldOff, Eye, EyeOff } from "lucide-react";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import {
|
||||
@@ -17,21 +17,30 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
type ResourceInfoBoxType = {};
|
||||
|
||||
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
const { resource, authInfo } = useResourceContext();
|
||||
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
||||
const { resource, authInfo, updateResource } = useResourceContext();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
|
||||
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{/* 4 cols because of the certs */}
|
||||
<InfoSections
|
||||
cols={resource.http && env.flags.usePangolinDns ? 4 : 3}
|
||||
cols={resource.http && env.flags.usePangolinDns ? 5 : 4}
|
||||
>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("identifier")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{resource.niceId}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{resource.http ? (
|
||||
<>
|
||||
<InfoSection>
|
||||
@@ -40,17 +49,17 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{authInfo.password ||
|
||||
authInfo.pincode ||
|
||||
authInfo.sso ||
|
||||
authInfo.whitelist ||
|
||||
authInfo.headerAuth ? (
|
||||
<div className="flex items-start space-x-2 text-green-500">
|
||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||
authInfo.pincode ||
|
||||
authInfo.sso ||
|
||||
authInfo.whitelist ||
|
||||
authInfo.headerAuth ? (
|
||||
<div className="flex items-start space-x-2">
|
||||
<ShieldCheck className="w-4 h-4 mt-0.5 flex-shrink-0 text-green-500" />
|
||||
<span>{t("protected")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 text-yellow-500">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<ShieldOff className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t("notProtected")}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -91,9 +100,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
{t("protocol")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span>
|
||||
{resource.protocol.toUpperCase()}
|
||||
</span>
|
||||
{resource.protocol.toUpperCase()}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
@@ -155,11 +162,17 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span>
|
||||
{resource.enabled
|
||||
? t("enabled")
|
||||
: t("disabled")}
|
||||
</span>
|
||||
{resource.enabled ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Eye className="w-4 h-4 flex-shrink-0 text-green-500" />
|
||||
<span>{t("enabled")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<EyeOff className="w-4 h-4 flex-shrink-0 text-neutral-500" />
|
||||
<span>{t("disabled")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuCheckboxItem
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
Wifi,
|
||||
WifiOff,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
XCircle
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -75,13 +75,12 @@ import EditInternalResourceDialog from "@app/components/EditInternalResourceDial
|
||||
import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
|
||||
|
||||
export type TargetHealth = {
|
||||
targetId: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
enabled: boolean;
|
||||
healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||
healthStatus?: "healthy" | "unhealthy" | "unknown";
|
||||
};
|
||||
|
||||
export type ResourceRow = {
|
||||
@@ -102,45 +101,55 @@ export type ResourceRow = {
|
||||
targets?: TargetHealth[];
|
||||
};
|
||||
|
||||
|
||||
function getOverallHealthStatus(targets?: TargetHealth[]): 'online' | 'degraded' | 'offline' | 'unknown' {
|
||||
function getOverallHealthStatus(
|
||||
targets?: TargetHealth[]
|
||||
): "online" | "degraded" | "offline" | "unknown" {
|
||||
if (!targets || targets.length === 0) {
|
||||
return 'unknown';
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const monitoredTargets = targets.filter(t => t.enabled && t.healthStatus && t.healthStatus !== 'unknown');
|
||||
const monitoredTargets = targets.filter(
|
||||
(t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown"
|
||||
);
|
||||
|
||||
if (monitoredTargets.length === 0) {
|
||||
return 'unknown';
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const healthyCount = monitoredTargets.filter(t => t.healthStatus === 'healthy').length;
|
||||
const unhealthyCount = monitoredTargets.filter(t => t.healthStatus === 'unhealthy').length;
|
||||
const healthyCount = monitoredTargets.filter(
|
||||
(t) => t.healthStatus === "healthy"
|
||||
).length;
|
||||
const unhealthyCount = monitoredTargets.filter(
|
||||
(t) => t.healthStatus === "unhealthy"
|
||||
).length;
|
||||
|
||||
if (healthyCount === monitoredTargets.length) {
|
||||
return 'online';
|
||||
return "online";
|
||||
} else if (unhealthyCount === monitoredTargets.length) {
|
||||
return 'offline';
|
||||
return "offline";
|
||||
} else {
|
||||
return 'degraded';
|
||||
return "degraded";
|
||||
}
|
||||
}
|
||||
|
||||
function StatusIcon({ status, className = "" }: {
|
||||
status: 'online' | 'degraded' | 'offline' | 'unknown';
|
||||
function StatusIcon({
|
||||
status,
|
||||
className = ""
|
||||
}: {
|
||||
status: "online" | "degraded" | "offline" | "unknown";
|
||||
className?: string;
|
||||
}) {
|
||||
const iconClass = `h-4 w-4 ${className}`;
|
||||
|
||||
switch (status) {
|
||||
case 'online':
|
||||
case "online":
|
||||
return <CheckCircle2 className={`${iconClass} text-green-500`} />;
|
||||
case 'degraded':
|
||||
case "degraded":
|
||||
return <CheckCircle2 className={`${iconClass} text-yellow-500`} />;
|
||||
case 'offline':
|
||||
case "offline":
|
||||
return <XCircle className={`${iconClass} text-destructive`} />;
|
||||
case 'unknown':
|
||||
return <Clock className={`${iconClass} text-gray-400`} />;
|
||||
case "unknown":
|
||||
return <Clock className={`${iconClass} text-muted-foreground`} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -171,15 +180,14 @@ type ResourcesTableProps = {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
PAGE_SIZE: 'datatable-page-size',
|
||||
PAGE_SIZE: "datatable-page-size",
|
||||
getTablePageSize: (tableId?: string) =>
|
||||
tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE
|
||||
};
|
||||
|
||||
const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
if (typeof window === 'undefined') return defaultSize;
|
||||
if (typeof window === "undefined") return defaultSize;
|
||||
|
||||
try {
|
||||
const key = STORAGE_KEYS.getTablePageSize(tableId);
|
||||
@@ -191,24 +199,22 @@ const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to read page size from localStorage:', error);
|
||||
console.warn("Failed to read page size from localStorage:", error);
|
||||
}
|
||||
return defaultSize;
|
||||
};
|
||||
|
||||
const setStoredPageSize = (pageSize: number, tableId?: string): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const key = STORAGE_KEYS.getTablePageSize(tableId);
|
||||
localStorage.setItem(key, pageSize.toString());
|
||||
} catch (error) {
|
||||
console.warn('Failed to save page size to localStorage:', error);
|
||||
console.warn("Failed to save page size to localStorage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default function ResourcesTable({
|
||||
resources,
|
||||
internalResources,
|
||||
@@ -224,12 +230,11 @@ export default function ResourcesTable({
|
||||
|
||||
const api = createApiClient({ env });
|
||||
|
||||
|
||||
const [proxyPageSize, setProxyPageSize] = useState<number>(() =>
|
||||
getStoredPageSize('proxy-resources', 20)
|
||||
getStoredPageSize("proxy-resources", 20)
|
||||
);
|
||||
const [internalPageSize, setInternalPageSize] = useState<number>(() =>
|
||||
getStoredPageSize('internal-resources', 20)
|
||||
getStoredPageSize("internal-resources", 20)
|
||||
);
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@@ -247,8 +252,10 @@ export default function ResourcesTable({
|
||||
defaultSort ? [defaultSort] : []
|
||||
);
|
||||
|
||||
const [proxyColumnVisibility, setProxyColumnVisibility] = useState<VisibilityState>({});
|
||||
const [internalColumnVisibility, setInternalColumnVisibility] = useState<VisibilityState>({});
|
||||
const [proxyColumnVisibility, setProxyColumnVisibility] =
|
||||
useState<VisibilityState>({});
|
||||
const [internalColumnVisibility, setInternalColumnVisibility] =
|
||||
useState<VisibilityState>({});
|
||||
|
||||
const [proxyColumnFilters, setProxyColumnFilters] =
|
||||
useState<ColumnFiltersState>([]);
|
||||
@@ -427,24 +434,34 @@ export default function ResourcesTable({
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon status="unknown" />
|
||||
<span className="text-sm text-muted-foreground">No targets</span>
|
||||
<span className="text-sm">
|
||||
{t("resourcesTableNoTargets")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const monitoredTargets = targets.filter(t => t.enabled && t.healthStatus && t.healthStatus !== 'unknown');
|
||||
const unknownTargets = targets.filter(t => !t.enabled || !t.healthStatus || t.healthStatus === 'unknown');
|
||||
const monitoredTargets = targets.filter(
|
||||
(t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown"
|
||||
);
|
||||
const unknownTargets = targets.filter(
|
||||
(t) => !t.enabled || !t.healthStatus || t.healthStatus === "unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="flex items-center gap-2 h-8">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 h-8 px-0"
|
||||
>
|
||||
<StatusIcon status={overallStatus} />
|
||||
<span className="text-sm">
|
||||
{overallStatus === 'online' && 'Healthy'}
|
||||
{overallStatus === 'degraded' && 'Degraded'}
|
||||
{overallStatus === 'offline' && 'Offline'}
|
||||
{overallStatus === 'unknown' && 'Unknown'}
|
||||
{overallStatus === "online" && t("resourcesTableHealthy")}
|
||||
{overallStatus === "degraded" && t("resourcesTableDegraded")}
|
||||
{overallStatus === "offline" && t("resourcesTableOffline")}
|
||||
{overallStatus === "unknown" && t("resourcesTableUnknown")}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -453,16 +470,29 @@ export default function ResourcesTable({
|
||||
{monitoredTargets.length > 0 && (
|
||||
<>
|
||||
{monitoredTargets.map((target) => (
|
||||
<DropdownMenuItem key={target.targetId} className="flex items-center justify-between gap-4">
|
||||
<DropdownMenuItem
|
||||
key={target.targetId}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon
|
||||
status={target.healthStatus === 'healthy' ? 'online' : 'offline'}
|
||||
status={
|
||||
target.healthStatus ===
|
||||
"healthy"
|
||||
? "online"
|
||||
: "offline"
|
||||
}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{`${target.ip}:${target.port}`}
|
||||
</div>
|
||||
<span className={`capitalize ${target.healthStatus === 'healthy' ? 'text-green-500' : 'text-destructive'
|
||||
}`}>
|
||||
<span
|
||||
className={`capitalize ${
|
||||
target.healthStatus === "healthy"
|
||||
? "text-green-500"
|
||||
: "text-destructive"
|
||||
}`}
|
||||
>
|
||||
{target.healthStatus}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
@@ -472,13 +502,21 @@ export default function ResourcesTable({
|
||||
{unknownTargets.length > 0 && (
|
||||
<>
|
||||
{unknownTargets.map((target) => (
|
||||
<DropdownMenuItem key={target.targetId} className="flex items-center justify-between gap-4">
|
||||
<DropdownMenuItem
|
||||
key={target.targetId}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon status="unknown" className="h-3 w-3" />
|
||||
<StatusIcon
|
||||
status="unknown"
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{`${target.ip}:${target.port}`}
|
||||
</div>
|
||||
<span className="text-muted-foreground">
|
||||
{!target.enabled ? 'Disabled' : 'Not monitored'}
|
||||
{!target.enabled
|
||||
? t("disabled")
|
||||
: t("resourcesTableNotMonitored")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -489,7 +527,6 @@ export default function ResourcesTable({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const proxyColumns: ColumnDef<ResourceRow>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
@@ -507,28 +544,20 @@ export default function ResourcesTable({
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "nice",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("resource")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "protocol",
|
||||
header: t("protocol"),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return <span>{resourceRow.http ? (resourceRow.ssl ? "HTTPS" : "HTTP") : resourceRow.protocol.toUpperCase()}</span>;
|
||||
return (
|
||||
<span>
|
||||
{resourceRow.http
|
||||
? resourceRow.ssl
|
||||
? "HTTPS"
|
||||
: "HTTP"
|
||||
: resourceRow.protocol.toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -554,7 +583,12 @@ export default function ResourcesTable({
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const statusA = getOverallHealthStatus(rowA.original.targets);
|
||||
const statusB = getOverallHealthStatus(rowB.original.targets);
|
||||
const statusOrder = { online: 3, degraded: 2, offline: 1, unknown: 0 };
|
||||
const statusOrder = {
|
||||
online: 3,
|
||||
degraded: 2,
|
||||
offline: 1,
|
||||
unknown: 0
|
||||
};
|
||||
return statusOrder[statusA] - statusOrder[statusB];
|
||||
}
|
||||
},
|
||||
@@ -605,13 +639,13 @@ export default function ResourcesTable({
|
||||
return (
|
||||
<div>
|
||||
{resourceRow.authState === "protected" ? (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
<span className="flex items-center space-x-2">
|
||||
<ShieldCheck className="w-4 h-4 text-green-500" />
|
||||
<span>{t("protected")}</span>
|
||||
</span>
|
||||
) : resourceRow.authState === "not_protected" ? (
|
||||
<span className="text-yellow-500 flex items-center space-x-2">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<span className="flex items-center space-x-2">
|
||||
<ShieldOff className="w-4 h-4 text-yellow-500" />
|
||||
<span>{t("notProtected")}</span>
|
||||
</span>
|
||||
) : (
|
||||
@@ -857,12 +891,12 @@ export default function ResourcesTable({
|
||||
|
||||
const handleProxyPageSizeChange = (newPageSize: number) => {
|
||||
setProxyPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, 'proxy-resources');
|
||||
setStoredPageSize(newPageSize, "proxy-resources");
|
||||
};
|
||||
|
||||
const handleInternalPageSizeChange = (newPageSize: number) => {
|
||||
setInternalPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, 'internal-resources');
|
||||
setStoredPageSize(newPageSize, "internal-resources");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -876,12 +910,8 @@ export default function ResourcesTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("resourceQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{t("resourceMessageRemove")}
|
||||
</p>
|
||||
<p>{t("resourceQuestionRemove")}</p>
|
||||
<p>{t("resourceMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
@@ -900,12 +930,8 @@ export default function ResourcesTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("resourceQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{t("resourceMessageRemove")}
|
||||
</p>
|
||||
<p>{t("resourceQuestionRemove")}</p>
|
||||
<p>{t("resourceMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
@@ -955,9 +981,7 @@ export default function ResourcesTable({
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{getActionButton()}
|
||||
</div>
|
||||
<div>{getActionButton()}</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -976,12 +1000,12 @@ export default function ResourcesTable({
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
@@ -1039,7 +1063,9 @@ export default function ResourcesTable({
|
||||
<div className="mt-4">
|
||||
<DataTablePagination
|
||||
table={proxyTable}
|
||||
onPageSizeChange={handleProxyPageSizeChange}
|
||||
onPageSizeChange={
|
||||
handleProxyPageSizeChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -1077,12 +1103,12 @@ export default function ResourcesTable({
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
@@ -1140,7 +1166,9 @@ export default function ResourcesTable({
|
||||
<div className="mt-4">
|
||||
<DataTablePagination
|
||||
table={internalTable}
|
||||
onPageSizeChange={handleInternalPageSizeChange}
|
||||
onPageSizeChange={
|
||||
handleInternalPageSizeChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import {
|
||||
InfoSection,
|
||||
@@ -12,9 +11,10 @@ import {
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
|
||||
type SiteInfoCardProps = {};
|
||||
|
||||
export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
export default function SiteInfoCard({ }: SiteInfoCardProps) {
|
||||
const { site, updateSite } = useSiteContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
@@ -31,10 +31,19 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={env.flags.enableClients ? 3 : 2}>
|
||||
<InfoSections cols={env.flags.enableClients ? 4 : 3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("identifier")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{site.niceId}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{(site.type == "newt" || site.type == "wireguard") && (
|
||||
<>
|
||||
<InfoSection>
|
||||
|
||||
@@ -164,30 +164,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "nice",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
className="hidden md:flex whitespace-nowrap"
|
||||
>
|
||||
{t("site")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="hidden md:block whitespace-nowrap">
|
||||
{row.original.nice}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "mbIn",
|
||||
header: ({ column }) => {
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext";
|
||||
import { build } from "@server/build";
|
||||
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||
import { useContext } from "react";
|
||||
|
||||
export function useRemoteExitNodeContext() {
|
||||
if (build == "oss") {
|
||||
return null;
|
||||
return {
|
||||
remoteExitNode: {} as GetRemoteExitNodeResponse,
|
||||
updateRemoteExitNode: () => {},
|
||||
};
|
||||
}
|
||||
const context = useContext(RemoteExitNodeContext);
|
||||
if (context === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user