mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-21 00:05:25 +00:00
🚧 WIP: showing labels in proxy resources table
This commit is contained in:
@@ -325,24 +325,6 @@ export async function listResources(
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (query) {
|
|
||||||
conditions.push(
|
|
||||||
or(
|
|
||||||
like(
|
|
||||||
sql`LOWER(${resources.name})`,
|
|
||||||
"%" + query.toLowerCase() + "%"
|
|
||||||
),
|
|
||||||
like(
|
|
||||||
sql`LOWER(${resources.niceId})`,
|
|
||||||
"%" + query.toLowerCase() + "%"
|
|
||||||
),
|
|
||||||
like(
|
|
||||||
sql`LOWER(${resources.fullDomain})`,
|
|
||||||
"%" + query.toLowerCase() + "%"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof enabled !== "undefined") {
|
if (typeof enabled !== "undefined") {
|
||||||
conditions.push(eq(resources.enabled, enabled));
|
conditions.push(eq(resources.enabled, enabled));
|
||||||
}
|
}
|
||||||
@@ -386,6 +368,24 @@ export async function listResources(
|
|||||||
.where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId)));
|
.where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId)));
|
||||||
conditions.push(inArray(resources.resourceId, resourcesWithSite));
|
conditions.push(inArray(resources.resourceId, resourcesWithSite));
|
||||||
}
|
}
|
||||||
|
if (query) {
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
like(
|
||||||
|
sql`LOWER(${resources.name})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
),
|
||||||
|
like(
|
||||||
|
sql`LOWER(${resources.niceId})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
),
|
||||||
|
like(
|
||||||
|
sql`LOWER(${resources.fullDomain})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const baseQuery = queryResourcesBase().where(and(...conditions));
|
const baseQuery = queryResourcesBase().where(and(...conditions));
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
|
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
|
||||||
import {
|
import {
|
||||||
ResourceSitesStatusCell,
|
ResourceSitesStatusCell,
|
||||||
type ResourceSiteRow
|
type ResourceSiteRow
|
||||||
} from "@app/components/ResourceSitesStatusCell";
|
} from "@app/components/ResourceSitesStatusCell";
|
||||||
|
import { Selectedsite, SitesSelector } from "@app/components/site-selector";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||||
@@ -24,12 +26,14 @@ import {
|
|||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
import { Selectedsite, SitesSelector } from "@app/components/site-selector";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
|
import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover";
|
||||||
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { build } from "@server/build";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
@@ -64,8 +68,6 @@ import z from "zod";
|
|||||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
import UptimeMiniBar from "./UptimeMiniBar";
|
import UptimeMiniBar from "./UptimeMiniBar";
|
||||||
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
export type TargetHealth = {
|
export type TargetHealth = {
|
||||||
targetId: number;
|
targetId: number;
|
||||||
@@ -97,31 +99,13 @@ export type ResourceRow = {
|
|||||||
health?: "healthy" | "degraded" | "unhealthy" | "unknown";
|
health?: "healthy" | "degraded" | "unhealthy" | "unknown";
|
||||||
sites: ResourceSiteRow[];
|
sites: ResourceSiteRow[];
|
||||||
wildcard?: boolean;
|
wildcard?: boolean;
|
||||||
|
labels?: Array<{
|
||||||
|
labelId: number;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function StatusIcon({
|
|
||||||
status,
|
|
||||||
className = ""
|
|
||||||
}: {
|
|
||||||
status: string | undefined | null;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const iconClass = `h-4 w-4 ${className}`;
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case "healthy":
|
|
||||||
return <CheckCircle2 className={`${iconClass} text-green-500`} />;
|
|
||||||
case "degraded":
|
|
||||||
return <CheckCircle2 className={`${iconClass} text-yellow-500`} />;
|
|
||||||
case "unhealthy":
|
|
||||||
return <XCircle className={`${iconClass} text-destructive`} />;
|
|
||||||
case "unknown":
|
|
||||||
return <Clock className={`${iconClass} text-muted-foreground`} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyResourcesTableProps = {
|
type ProxyResourcesTableProps = {
|
||||||
resources: ResourceRow[];
|
resources: ResourceRow[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -153,6 +137,9 @@ export default function ProxyResourcesTable({
|
|||||||
const [selectedResource, setSelectedResource] =
|
const [selectedResource, setSelectedResource] =
|
||||||
useState<ResourceRow | null>();
|
useState<ResourceRow | null>();
|
||||||
|
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
||||||
|
|
||||||
const [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||||
const [siteFilterOpen, setSiteFilterOpen] = useState(false);
|
const [siteFilterOpen, setSiteFilterOpen] = useState(false);
|
||||||
@@ -233,120 +220,6 @@ export default function ProxyResourcesTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TargetStatusCell({
|
|
||||||
targets,
|
|
||||||
healthStatus
|
|
||||||
}: {
|
|
||||||
targets?: TargetHealth[];
|
|
||||||
healthStatus?: string;
|
|
||||||
}) {
|
|
||||||
const overallStatus = healthStatus;
|
|
||||||
|
|
||||||
if (!targets || targets.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<StatusIcon status="unknown" />
|
|
||||||
<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"
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="flex items-center gap-2 h-8 px-0 font-normal"
|
|
||||||
>
|
|
||||||
<StatusIcon status={overallStatus} />
|
|
||||||
<span className="text-sm">
|
|
||||||
{overallStatus === "healthy" &&
|
|
||||||
t("resourcesTableHealthy")}
|
|
||||||
{overallStatus === "degraded" &&
|
|
||||||
t("resourcesTableDegraded")}
|
|
||||||
{overallStatus === "unhealthy" &&
|
|
||||||
t("resourcesTableUnhealthy")}
|
|
||||||
{overallStatus === "unknown" &&
|
|
||||||
t("resourcesTableUnknown")}
|
|
||||||
</span>
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="min-w-70">
|
|
||||||
{monitoredTargets.length > 0 && (
|
|
||||||
<>
|
|
||||||
{monitoredTargets.map((target) => (
|
|
||||||
<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"
|
|
||||||
}
|
|
||||||
className="h-3 w-3"
|
|
||||||
/>
|
|
||||||
{target.siteName
|
|
||||||
? `${target.siteName} (${target.ip}:${target.port})`
|
|
||||||
: `${target.ip}:${target.port}`}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`capitalize ${
|
|
||||||
target.healthStatus === "healthy"
|
|
||||||
? "text-green-500"
|
|
||||||
: "text-destructive"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{target.healthStatus}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{unknownTargets.length > 0 && (
|
|
||||||
<>
|
|
||||||
{unknownTargets.map((target) => (
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
{target.siteName
|
|
||||||
? `${target.siteName} (${target.ip}:${target.port})`
|
|
||||||
: `${target.ip}:${target.port}`}
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{!target.enabled
|
|
||||||
? t("disabled")
|
|
||||||
: t("resourcesTableNotMonitored")}
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyColumns: ExtendedColumnDef<ResourceRow>[] = [
|
const proxyColumns: ExtendedColumnDef<ResourceRow>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
@@ -653,6 +526,28 @@ export default function ProxyResourcesTable({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
...(isLabelFeatureEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "labels",
|
||||||
|
accessorKey: "labels",
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3 text-end w-full inline-block">
|
||||||
|
{t("labels")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }: { row: { original: ResourceRow } }) => {
|
||||||
|
return (
|
||||||
|
// <SiteLabelCell
|
||||||
|
// site={row.original}
|
||||||
|
// orgId={orgId}
|
||||||
|
// />
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@@ -800,7 +695,11 @@ export default function ProxyResourcesTable({
|
|||||||
isRefreshing={isRefreshing || isFiltering}
|
isRefreshing={isRefreshing || isFiltering}
|
||||||
isNavigatingToAddPage={isNavigatingToAddPage}
|
isNavigatingToAddPage={isNavigatingToAddPage}
|
||||||
enableColumnVisibility
|
enableColumnVisibility
|
||||||
columnVisibility={{ niceId: false, protocol: false }}
|
columnVisibility={{
|
||||||
|
niceId: false,
|
||||||
|
protocol: false,
|
||||||
|
labels: false
|
||||||
|
}}
|
||||||
stickyLeftColumn="name"
|
stickyLeftColumn="name"
|
||||||
stickyRightColumn="actions"
|
stickyRightColumn="actions"
|
||||||
/>
|
/>
|
||||||
@@ -808,6 +707,118 @@ export default function ProxyResourcesTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TargetStatusCell({
|
||||||
|
targets,
|
||||||
|
healthStatus
|
||||||
|
}: {
|
||||||
|
targets?: TargetHealth[];
|
||||||
|
healthStatus?: string;
|
||||||
|
}) {
|
||||||
|
const overallStatus = healthStatus;
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
if (!targets || targets.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusIcon status="unknown" />
|
||||||
|
<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"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2 h-8 px-0 font-normal"
|
||||||
|
>
|
||||||
|
<StatusIcon status={overallStatus} />
|
||||||
|
<span className="text-sm">
|
||||||
|
{overallStatus === "healthy" &&
|
||||||
|
t("resourcesTableHealthy")}
|
||||||
|
{overallStatus === "degraded" &&
|
||||||
|
t("resourcesTableDegraded")}
|
||||||
|
{overallStatus === "unhealthy" &&
|
||||||
|
t("resourcesTableUnhealthy")}
|
||||||
|
{overallStatus === "unknown" &&
|
||||||
|
t("resourcesTableUnknown")}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="min-w-70">
|
||||||
|
{monitoredTargets.length > 0 && (
|
||||||
|
<>
|
||||||
|
{monitoredTargets.map((target) => (
|
||||||
|
<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"
|
||||||
|
}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
{target.siteName
|
||||||
|
? `${target.siteName} (${target.ip}:${target.port})`
|
||||||
|
: `${target.ip}:${target.port}`}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`capitalize ${
|
||||||
|
target.healthStatus === "healthy"
|
||||||
|
? "text-green-500"
|
||||||
|
: "text-destructive"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{target.healthStatus}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{unknownTargets.length > 0 && (
|
||||||
|
<>
|
||||||
|
{unknownTargets.map((target) => (
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
{target.siteName
|
||||||
|
? `${target.siteName} (${target.ip}:${target.port})`
|
||||||
|
: `${target.ip}:${target.port}`}
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{!target.enabled
|
||||||
|
? t("disabled")
|
||||||
|
: t("resourcesTableNotMonitored")}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type ResourceEnabledFormProps = {
|
type ResourceEnabledFormProps = {
|
||||||
resource: ResourceRow;
|
resource: ResourceRow;
|
||||||
onToggleResourceEnabled: (
|
onToggleResourceEnabled: (
|
||||||
@@ -847,3 +858,26 @@ function ResourceEnabledForm({
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusIcon({
|
||||||
|
status,
|
||||||
|
className = ""
|
||||||
|
}: {
|
||||||
|
status: string | undefined | null;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const iconClass = `h-4 w-4 ${className}`;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "healthy":
|
||||||
|
return <CheckCircle2 className={`${iconClass} text-green-500`} />;
|
||||||
|
case "degraded":
|
||||||
|
return <CheckCircle2 className={`${iconClass} text-yellow-500`} />;
|
||||||
|
case "unhealthy":
|
||||||
|
return <XCircle className={`${iconClass} text-destructive`} />;
|
||||||
|
case "unknown":
|
||||||
|
return <Clock className={`${iconClass} text-muted-foreground`} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user