improve spacing and colors

This commit is contained in:
miloschwartz
2025-11-13 22:04:29 -05:00
parent 0798a0c6c2
commit d9564ed6fe
3 changed files with 143 additions and 93 deletions

View File

@@ -1525,6 +1525,12 @@
"resourcesTableTheseResourcesForUseWith": "These resources are for use with", "resourcesTableTheseResourcesForUseWith": "These resources are for use with",
"resourcesTableClients": "Clients", "resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
"resourcesTableNoTargets": "No targets",
"resourcesTableHealthy": "Healthy",
"resourcesTableDegraded": "Degraded",
"resourcesTableOffline": "Offline",
"resourcesTableUnknown": "Unknown",
"resourcesTableNotMonitored": "Not monitored",
"editInternalResourceDialogEditClientResource": "Edit Client Resource", "editInternalResourceDialogEditClientResource": "Edit Client Resource",
"editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.",
"editInternalResourceDialogResourceProperties": "Resource Properties", "editInternalResourceDialogResourceProperties": "Resource Properties",

View File

@@ -53,8 +53,8 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
authInfo.sso || authInfo.sso ||
authInfo.whitelist || authInfo.whitelist ||
authInfo.headerAuth ? ( authInfo.headerAuth ? (
<div className="flex items-start space-x-2 text-green-500"> <div className="flex items-start space-x-2">
<ShieldCheck className="w-4 h-4 mt-0.5 flex-shrink-0" /> <ShieldCheck className="w-4 h-4 mt-0.5 flex-shrink-0 text-green-500" />
<span>{t("protected")}</span> <span>{t("protected")}</span>
</div> </div>
) : ( ) : (
@@ -163,13 +163,13 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle> <InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{resource.enabled ? ( {resource.enabled ? (
<div className="flex items-center space-x-2 text-green-500"> <div className="flex items-center space-x-2">
<Eye className="w-4 h-4 flex-shrink-0" /> <Eye className="w-4 h-4 flex-shrink-0 text-green-500" />
<span>{t("enabled")}</span> <span>{t("enabled")}</span>
</div> </div>
) : ( ) : (
<div className="flex items-center space-x-2 text-neutral-500"> <div className="flex items-center space-x-2">
<EyeOff className="w-4 h-4 flex-shrink-0" /> <EyeOff className="w-4 h-4 flex-shrink-0 text-neutral-500" />
<span>{t("disabled")}</span> <span>{t("disabled")}</span>
</div> </div>
)} )}

View File

@@ -17,7 +17,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
@@ -36,7 +36,7 @@ import {
Wifi, Wifi,
WifiOff, WifiOff,
CheckCircle2, CheckCircle2,
XCircle, XCircle
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -75,13 +75,12 @@ import EditInternalResourceDialog from "@app/components/EditInternalResourceDial
import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
export type TargetHealth = { export type TargetHealth = {
targetId: number; targetId: number;
ip: string; ip: string;
port: number; port: number;
enabled: boolean; enabled: boolean;
healthStatus?: 'healthy' | 'unhealthy' | 'unknown'; healthStatus?: "healthy" | "unhealthy" | "unknown";
}; };
export type ResourceRow = { export type ResourceRow = {
@@ -102,45 +101,55 @@ export type ResourceRow = {
targets?: TargetHealth[]; targets?: TargetHealth[];
}; };
function getOverallHealthStatus(
function getOverallHealthStatus(targets?: TargetHealth[]): 'online' | 'degraded' | 'offline' | 'unknown' { targets?: TargetHealth[]
): "online" | "degraded" | "offline" | "unknown" {
if (!targets || targets.length === 0) { 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) { if (monitoredTargets.length === 0) {
return 'unknown'; return "unknown";
} }
const healthyCount = monitoredTargets.filter(t => t.healthStatus === 'healthy').length; const healthyCount = monitoredTargets.filter(
const unhealthyCount = monitoredTargets.filter(t => t.healthStatus === 'unhealthy').length; (t) => t.healthStatus === "healthy"
).length;
const unhealthyCount = monitoredTargets.filter(
(t) => t.healthStatus === "unhealthy"
).length;
if (healthyCount === monitoredTargets.length) { if (healthyCount === monitoredTargets.length) {
return 'online'; return "online";
} else if (unhealthyCount === monitoredTargets.length) { } else if (unhealthyCount === monitoredTargets.length) {
return 'offline'; return "offline";
} else { } else {
return 'degraded'; return "degraded";
} }
} }
function StatusIcon({ status, className = "" }: { function StatusIcon({
status: 'online' | 'degraded' | 'offline' | 'unknown'; status,
className = ""
}: {
status: "online" | "degraded" | "offline" | "unknown";
className?: string; className?: string;
}) { }) {
const iconClass = `h-4 w-4 ${className}`; const iconClass = `h-4 w-4 ${className}`;
switch (status) { switch (status) {
case 'online': case "online":
return <CheckCircle2 className={`${iconClass} text-green-500`} />; return <CheckCircle2 className={`${iconClass} text-green-500`} />;
case 'degraded': case "degraded":
return <CheckCircle2 className={`${iconClass} text-yellow-500`} />; return <CheckCircle2 className={`${iconClass} text-yellow-500`} />;
case 'offline': case "offline":
return <XCircle className={`${iconClass} text-destructive`} />; return <XCircle className={`${iconClass} text-destructive`} />;
case 'unknown': case "unknown":
return <Clock className={`${iconClass} text-gray-400`} />; return <Clock className={`${iconClass} text-muted-foreground`} />;
default: default:
return null; return null;
} }
@@ -171,15 +180,14 @@ type ResourcesTableProps = {
}; };
}; };
const STORAGE_KEYS = { const STORAGE_KEYS = {
PAGE_SIZE: 'datatable-page-size', PAGE_SIZE: "datatable-page-size",
getTablePageSize: (tableId?: string) => getTablePageSize: (tableId?: string) =>
tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE
}; };
const getStoredPageSize = (tableId?: string, defaultSize = 20): number => { const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
if (typeof window === 'undefined') return defaultSize; if (typeof window === "undefined") return defaultSize;
try { try {
const key = STORAGE_KEYS.getTablePageSize(tableId); const key = STORAGE_KEYS.getTablePageSize(tableId);
@@ -191,24 +199,22 @@ const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
} }
} }
} catch (error) { } catch (error) {
console.warn('Failed to read page size from localStorage:', error); console.warn("Failed to read page size from localStorage:", error);
} }
return defaultSize; return defaultSize;
}; };
const setStoredPageSize = (pageSize: number, tableId?: string): void => { const setStoredPageSize = (pageSize: number, tableId?: string): void => {
if (typeof window === 'undefined') return; if (typeof window === "undefined") return;
try { try {
const key = STORAGE_KEYS.getTablePageSize(tableId); const key = STORAGE_KEYS.getTablePageSize(tableId);
localStorage.setItem(key, pageSize.toString()); localStorage.setItem(key, pageSize.toString());
} catch (error) { } 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({ export default function ResourcesTable({
resources, resources,
internalResources, internalResources,
@@ -224,12 +230,11 @@ export default function ResourcesTable({
const api = createApiClient({ env }); const api = createApiClient({ env });
const [proxyPageSize, setProxyPageSize] = useState<number>(() => const [proxyPageSize, setProxyPageSize] = useState<number>(() =>
getStoredPageSize('proxy-resources', 20) getStoredPageSize("proxy-resources", 20)
); );
const [internalPageSize, setInternalPageSize] = useState<number>(() => const [internalPageSize, setInternalPageSize] = useState<number>(() =>
getStoredPageSize('internal-resources', 20) getStoredPageSize("internal-resources", 20)
); );
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -247,8 +252,10 @@ export default function ResourcesTable({
defaultSort ? [defaultSort] : [] defaultSort ? [defaultSort] : []
); );
const [proxyColumnVisibility, setProxyColumnVisibility] = useState<VisibilityState>({}); const [proxyColumnVisibility, setProxyColumnVisibility] =
const [internalColumnVisibility, setInternalColumnVisibility] = useState<VisibilityState>({}); useState<VisibilityState>({});
const [internalColumnVisibility, setInternalColumnVisibility] =
useState<VisibilityState>({});
const [proxyColumnFilters, setProxyColumnFilters] = const [proxyColumnFilters, setProxyColumnFilters] =
useState<ColumnFiltersState>([]); useState<ColumnFiltersState>([]);
@@ -427,24 +434,34 @@ export default function ResourcesTable({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusIcon status="unknown" /> <StatusIcon status="unknown" />
<span className="text-sm text-muted-foreground">No targets</span> <span className="text-sm">
{t("resourcesTableNoTargets")}
</span>
</div> </div>
); );
} }
const monitoredTargets = targets.filter(t => t.enabled && t.healthStatus && t.healthStatus !== 'unknown'); const monitoredTargets = targets.filter(
const unknownTargets = targets.filter(t => !t.enabled || !t.healthStatus || t.healthStatus === 'unknown'); (t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown"
);
const unknownTargets = targets.filter(
(t) => !t.enabled || !t.healthStatus || t.healthStatus === "unknown"
);
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <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} /> <StatusIcon status={overallStatus} />
<span className="text-sm"> <span className="text-sm">
{overallStatus === 'online' && 'Healthy'} {overallStatus === "online" && t("resourcesTableHealthy")}
{overallStatus === 'degraded' && 'Degraded'} {overallStatus === "degraded" && t("resourcesTableDegraded")}
{overallStatus === 'offline' && 'Offline'} {overallStatus === "offline" && t("resourcesTableOffline")}
{overallStatus === 'unknown' && 'Unknown'} {overallStatus === "unknown" && t("resourcesTableUnknown")}
</span> </span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</Button> </Button>
@@ -453,16 +470,29 @@ export default function ResourcesTable({
{monitoredTargets.length > 0 && ( {monitoredTargets.length > 0 && (
<> <>
{monitoredTargets.map((target) => ( {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"> <div className="flex items-center gap-2">
<StatusIcon <StatusIcon
status={target.healthStatus === 'healthy' ? 'online' : 'offline'} status={
target.healthStatus ===
"healthy"
? "online"
: "offline"
}
className="h-3 w-3" className="h-3 w-3"
/> />
{`${target.ip}:${target.port}`} {`${target.ip}:${target.port}`}
</div> </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} {target.healthStatus}
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
@@ -472,13 +502,21 @@ export default function ResourcesTable({
{unknownTargets.length > 0 && ( {unknownTargets.length > 0 && (
<> <>
{unknownTargets.map((target) => ( {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"> <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}`} {`${target.ip}:${target.port}`}
</div> </div>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{!target.enabled ? 'Disabled' : 'Not monitored'} {!target.enabled
? t("disabled")
: t("resourcesTableNotMonitored")}
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
@@ -489,7 +527,6 @@ export default function ResourcesTable({
); );
} }
const proxyColumns: ColumnDef<ResourceRow>[] = [ const proxyColumns: ColumnDef<ResourceRow>[] = [
{ {
accessorKey: "name", accessorKey: "name",
@@ -512,7 +549,15 @@ export default function ResourcesTable({
header: t("protocol"), header: t("protocol"),
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; 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>
);
} }
}, },
{ {
@@ -538,7 +583,12 @@ export default function ResourcesTable({
sortingFn: (rowA, rowB) => { sortingFn: (rowA, rowB) => {
const statusA = getOverallHealthStatus(rowA.original.targets); const statusA = getOverallHealthStatus(rowA.original.targets);
const statusB = getOverallHealthStatus(rowB.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]; return statusOrder[statusA] - statusOrder[statusB];
} }
}, },
@@ -589,13 +639,13 @@ export default function ResourcesTable({
return ( return (
<div> <div>
{resourceRow.authState === "protected" ? ( {resourceRow.authState === "protected" ? (
<span className="text-green-500 flex items-center space-x-2"> <span className="flex items-center space-x-2">
<ShieldCheck className="w-4 h-4" /> <ShieldCheck className="w-4 h-4 text-green-500" />
<span>{t("protected")}</span> <span>{t("protected")}</span>
</span> </span>
) : resourceRow.authState === "not_protected" ? ( ) : resourceRow.authState === "not_protected" ? (
<span className="text-yellow-500 flex items-center space-x-2"> <span className="flex items-center space-x-2">
<ShieldOff className="w-4 h-4" /> <ShieldOff className="w-4 h-4 text-yellow-500" />
<span>{t("notProtected")}</span> <span>{t("notProtected")}</span>
</span> </span>
) : ( ) : (
@@ -841,12 +891,12 @@ export default function ResourcesTable({
const handleProxyPageSizeChange = (newPageSize: number) => { const handleProxyPageSizeChange = (newPageSize: number) => {
setProxyPageSize(newPageSize); setProxyPageSize(newPageSize);
setStoredPageSize(newPageSize, 'proxy-resources'); setStoredPageSize(newPageSize, "proxy-resources");
}; };
const handleInternalPageSizeChange = (newPageSize: number) => { const handleInternalPageSizeChange = (newPageSize: number) => {
setInternalPageSize(newPageSize); setInternalPageSize(newPageSize);
setStoredPageSize(newPageSize, 'internal-resources'); setStoredPageSize(newPageSize, "internal-resources");
}; };
return ( return (
@@ -860,12 +910,8 @@ export default function ResourcesTable({
}} }}
dialog={ dialog={
<div> <div>
<p> <p>{t("resourceQuestionRemove")}</p>
{t("resourceQuestionRemove")} <p>{t("resourceMessageRemove")}</p>
</p>
<p>
{t("resourceMessageRemove")}
</p>
</div> </div>
} }
buttonText={t("resourceDeleteConfirm")} buttonText={t("resourceDeleteConfirm")}
@@ -884,12 +930,8 @@ export default function ResourcesTable({
}} }}
dialog={ dialog={
<div> <div>
<p> <p>{t("resourceQuestionRemove")}</p>
{t("resourceQuestionRemove")} <p>{t("resourceMessageRemove")}</p>
</p>
<p>
{t("resourceMessageRemove")}
</p>
</div> </div>
} }
buttonText={t("resourceDeleteConfirm")} buttonText={t("resourceDeleteConfirm")}
@@ -939,9 +981,7 @@ export default function ResourcesTable({
{t("refresh")} {t("refresh")}
</Button> </Button>
</div> </div>
<div> <div>{getActionButton()}</div>
{getActionButton()}
</div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -960,12 +1000,12 @@ export default function ResourcesTable({
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header header
.column .column
.columnDef .columnDef
.header, .header,
header.getContext() header.getContext()
)} )}
</TableHead> </TableHead>
) )
)} )}
@@ -1023,7 +1063,9 @@ export default function ResourcesTable({
<div className="mt-4"> <div className="mt-4">
<DataTablePagination <DataTablePagination
table={proxyTable} table={proxyTable}
onPageSizeChange={handleProxyPageSizeChange} onPageSizeChange={
handleProxyPageSizeChange
}
/> />
</div> </div>
</TabsContent> </TabsContent>
@@ -1061,12 +1103,12 @@ export default function ResourcesTable({
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header header
.column .column
.columnDef .columnDef
.header, .header,
header.getContext() header.getContext()
)} )}
</TableHead> </TableHead>
) )
)} )}
@@ -1124,7 +1166,9 @@ export default function ResourcesTable({
<div className="mt-4"> <div className="mt-4">
<DataTablePagination <DataTablePagination
table={internalTable} table={internalTable}
onPageSizeChange={handleInternalPageSizeChange} onPageSizeChange={
handleInternalPageSizeChange
}
/> />
</div> </div>
</TabsContent> </TabsContent>