mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-06 23:59:02 +00:00
Merge branch 'dev' into refactor/loading-animation-on-request-logs
This commit is contained in:
63
src/app/[orgId]/settings/(private)/labels/page.tsx
Normal file
63
src/app/[orgId]/settings/(private)/labels/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
||||
import { AxiosResponse } from "axios";
|
||||
import OrgLabelsTable from "@app/components/OrgLabelsTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Labels"
|
||||
};
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function LabelsPage({ params, searchParams }: Props) {
|
||||
const { orgId } = await params;
|
||||
|
||||
const searchParamsObj = new URLSearchParams(await searchParams);
|
||||
|
||||
let labels: ListOrgLabelsResponse["labels"] = [];
|
||||
let pagination: ListOrgLabelsResponse["pagination"] = {
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<ListOrgLabelsResponse>>(
|
||||
`/org/${orgId}/labels?${searchParamsObj.toString()}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
const responseData = res.data.data;
|
||||
labels = responseData.labels;
|
||||
pagination = responseData.pagination;
|
||||
} catch (e) {}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("labels")}
|
||||
description={t("orgLabelsDescription")}
|
||||
/>
|
||||
|
||||
<OrgLabelsTable
|
||||
labels={labels}
|
||||
orgId={orgId}
|
||||
rowCount={pagination.total}
|
||||
pagination={{
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.pageSize
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +76,8 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
||||
agent: client.agent,
|
||||
archived: client.archived || false,
|
||||
blocked: client.blocked || false,
|
||||
approvalState: client.approvalState ?? "approved"
|
||||
approvalState: client.approvalState ?? "approved",
|
||||
labels: client.labels ?? []
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -153,6 +153,65 @@ export default function GeneralPage() {
|
||||
const [approvalId, setApprovalId] = useState<number | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [, startTransition] = useTransition();
|
||||
const [cacheCheck, setCacheCheck] = useState<null | {
|
||||
consistent: boolean;
|
||||
missingSiteResourceIds: number[];
|
||||
extraSiteResourceIds: number[];
|
||||
missingSiteIds: number[];
|
||||
extraSiteIds: number[];
|
||||
expectedSiteResourceIds: number[];
|
||||
actualSiteResourceIds: number[];
|
||||
expectedSiteIds: number[];
|
||||
actualSiteIds: number[];
|
||||
}>(null);
|
||||
const [isCheckingCache, setIsCheckingCache] = useState(false);
|
||||
const [isRebuildingCache, setIsRebuildingCache] = useState(false);
|
||||
|
||||
const handleRebuildCache = async () => {
|
||||
if (!client.clientId) return;
|
||||
setIsRebuildingCache(true);
|
||||
try {
|
||||
await api.post(
|
||||
`/client/${client.clientId}/rebuild-associations-cache`
|
||||
);
|
||||
// Re-verify after rebuild so the result refreshes
|
||||
const res = await api.get(
|
||||
`/client/${client.clientId}/verify-associations-cache`
|
||||
);
|
||||
setCacheCheck(res.data.data);
|
||||
toast({
|
||||
title: "Cache rebuilt",
|
||||
description: "Association cache rebuilt successfully."
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Rebuild failed",
|
||||
description: formatAxiosError(e, "Failed to rebuild cache")
|
||||
});
|
||||
} finally {
|
||||
setIsRebuildingCache(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyCache = async () => {
|
||||
if (!client.clientId) return;
|
||||
setIsCheckingCache(true);
|
||||
try {
|
||||
const res = await api.get(
|
||||
`/client/${client.clientId}/verify-associations-cache`
|
||||
);
|
||||
setCacheCheck(res.data.data);
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Cache check failed",
|
||||
description: formatAxiosError(e, "Failed to verify cache")
|
||||
});
|
||||
} finally {
|
||||
setIsCheckingCache(false);
|
||||
}
|
||||
};
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const showApprovalFeatures =
|
||||
@@ -844,6 +903,75 @@ export default function GeneralPage() {
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Hidden cache verification — subtle button, dev/admin diagnostic */}
|
||||
<div className="mt-8 flex flex-col gap-2 items-start opacity-30 hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleVerifyCache}
|
||||
disabled={isCheckingCache}
|
||||
className="text-xs text-muted-foreground underline disabled:opacity-50"
|
||||
title="Verify the client's site association cache against current permissions (read-only)"
|
||||
>
|
||||
{isCheckingCache
|
||||
? "Checking cache…"
|
||||
: "Verify association cache"}
|
||||
</button>
|
||||
{cacheCheck && (
|
||||
<div
|
||||
className={
|
||||
"text-xs rounded border px-2 py-1 " +
|
||||
(cacheCheck.consistent
|
||||
? "border-green-600 text-green-700"
|
||||
: "border-red-600 text-red-700")
|
||||
}
|
||||
>
|
||||
{cacheCheck.consistent ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
Cache is consistent
|
||||
</span>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1 font-semibold">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Cache is INCONSISTENT
|
||||
</div>
|
||||
<div>
|
||||
Missing site resources: [
|
||||
{cacheCheck.missingSiteResourceIds.join(
|
||||
", "
|
||||
)}
|
||||
]
|
||||
</div>
|
||||
<div>
|
||||
Extra site resources: [
|
||||
{cacheCheck.extraSiteResourceIds.join(", ")}
|
||||
]
|
||||
</div>
|
||||
<div>
|
||||
Missing sites: [
|
||||
{cacheCheck.missingSiteIds.join(", ")}]
|
||||
</div>
|
||||
<div>
|
||||
Extra sites: [
|
||||
{cacheCheck.extraSiteIds.join(", ")}]
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRebuildCache}
|
||||
disabled={isRebuildingCache}
|
||||
className="mt-1 text-xs underline font-semibold disabled:opacity-50"
|
||||
>
|
||||
{isRebuildingCache
|
||||
? "Rebuilding…"
|
||||
: "Rebuild cache now"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -294,7 +294,7 @@ export default function ConnectionLogsPage() {
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("Failed to filter logs"),
|
||||
description: formatAxiosError(error),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -127,7 +127,8 @@ export default async function ClientResourcesPage(
|
||||
authDaemonPort: siteResource.authDaemonPort ?? null,
|
||||
subdomain: siteResource.subdomain ?? null,
|
||||
domainId: siteResource.domainId ?? null,
|
||||
fullDomain: siteResource.fullDomain ?? null
|
||||
fullDomain: siteResource.fullDomain ?? null,
|
||||
labels: siteResource.labels ?? []
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -111,6 +111,7 @@ export default async function ProxyResourcesPage(
|
||||
protocol: resource.protocol,
|
||||
proxyPort: resource.proxyPort,
|
||||
http: resource.http,
|
||||
labels: resource.labels,
|
||||
authState: !resource.http
|
||||
? "none"
|
||||
: resource.sso ||
|
||||
|
||||
@@ -60,6 +60,7 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
return {
|
||||
name: site.name,
|
||||
id: site.siteId,
|
||||
labels: site.labels,
|
||||
nice: site.niceId.toString(),
|
||||
address: site.address?.split("/")[0],
|
||||
mbIn: formatSize(site.megabytesIn || 0, site.type),
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Server,
|
||||
Settings,
|
||||
SquareMousePointer,
|
||||
TagIcon,
|
||||
TicketCheck,
|
||||
Unplug,
|
||||
User,
|
||||
@@ -99,7 +100,7 @@ export const orgNavSections = (
|
||||
href: "/{orgId}/settings/domains",
|
||||
icon: <Globe className="size-4 flex-none" />
|
||||
},
|
||||
...(build == "saas"
|
||||
...(build === "saas"
|
||||
? [
|
||||
{
|
||||
title: "sidebarRemoteExitNodes",
|
||||
@@ -237,10 +238,19 @@ export const orgNavSections = (
|
||||
title: "sidebarApiKeys",
|
||||
href: "/{orgId}/settings/api-keys",
|
||||
icon: <KeyRound className="size-4 flex-none" />
|
||||
}
|
||||
},
|
||||
...(build !== "oss"
|
||||
? [
|
||||
{
|
||||
title: "labels",
|
||||
href: "/{orgId}/settings/labels",
|
||||
icon: <TagIcon className="size-4 flex-none" />
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
...(build == "saas" && options?.isPrimaryOrg
|
||||
...(build === "saas" && options?.isPrimaryOrg
|
||||
? [
|
||||
{
|
||||
title: "sidebarBillingAndLicenses",
|
||||
|
||||
Reference in New Issue
Block a user