From f21188000e4164f979adc055078634075dccf8ef Mon Sep 17 00:00:00 2001 From: Pallavi Date: Wed, 3 Sep 2025 00:15:02 +0530 Subject: [PATCH] remove status check and add column filtering on all of the tables --- server/auth/actions.ts | 2 - server/routers/external.ts | 14 -- server/routers/resource/index.ts | 1 - server/routers/resource/tcpCheck.ts | 290 ------------------------- src/components/DataTablePagination.tsx | 9 +- src/components/ResourcesTable.tsx | 184 +++++----------- src/components/ui/data-table.tsx | 59 ++++- src/hooks/useResourceHealth.ts | 104 --------- 8 files changed, 119 insertions(+), 544 deletions(-) delete mode 100644 server/routers/resource/tcpCheck.ts delete mode 100644 src/hooks/useResourceHealth.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 4e271de9..d08457e5 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -23,8 +23,6 @@ export enum ActionsEnum { deleteResource = "deleteResource", getResource = "getResource", listResources = "listResources", - tcpCheck = "tcpCheck", - batchTcpCheck = "batchTcpCheck", updateResource = "updateResource", createTarget = "createTarget", deleteTarget = "deleteTarget", diff --git a/server/routers/external.ts b/server/routers/external.ts index 26254802..5c235902 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -306,20 +306,6 @@ authenticated.get( resource.listResources ); -authenticated.post( - "/org/:orgId/resources/tcp-check", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.tcpCheck), - resource.tcpCheck -); - -authenticated.post( - "/org/:orgId/resources/tcp-check-batch", - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.batchTcpCheck), - resource.batchTcpCheck -); - authenticated.get( "/org/:orgId/user-resources", verifyOrgAccess, diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index a757cae3..d1c7011d 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -25,4 +25,3 @@ export * from "./getUserResources"; export * from "./setResourceHeaderAuth"; export * from "./addEmailToResourceWhitelist"; export * from "./removeEmailFromResourceWhitelist"; -export * from "./tcpCheck"; diff --git a/server/routers/resource/tcpCheck.ts b/server/routers/resource/tcpCheck.ts deleted file mode 100644 index 1779cc10..00000000 --- a/server/routers/resource/tcpCheck.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import * as net from "net"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; -import { OpenAPITags, registry } from "@server/openApi"; - -const tcpCheckSchema = z - .object({ - host: z.string().min(1, "Host is required"), - port: z.number().int().min(1).max(65535), - timeout: z.number().int().min(1000).max(30000).optional().default(5000) - }) - .strict(); - -export type TcpCheckResponse = { - connected: boolean; - host: string; - port: number; - responseTime?: number; - error?: string; -}; - -registry.registerPath({ - method: "post", - path: "/org/{orgId}/resources/tcp-check", - description: "Check TCP connectivity to a host and port", - tags: [OpenAPITags.Resource], - request: { - body: { - content: { - "application/json": { - schema: tcpCheckSchema - } - } - } - }, - responses: { - 200: { - description: "TCP check result", - content: { - "application/json": { - schema: z.object({ - success: z.boolean(), - data: z.object({ - connected: z.boolean(), - host: z.string(), - port: z.number(), - responseTime: z.number().optional(), - error: z.string().optional() - }), - message: z.string() - }) - } - } - } - } -}); - -function checkTcpConnection(host: string, port: number, timeout: number): Promise { - return new Promise((resolve) => { - const startTime = Date.now(); - const socket = new net.Socket(); - - const cleanup = () => { - socket.removeAllListeners(); - if (!socket.destroyed) { - socket.destroy(); - } - }; - - const timer = setTimeout(() => { - cleanup(); - resolve({ - connected: false, - host, - port, - error: 'Connection timeout' - }); - }, timeout); - - socket.setTimeout(timeout); - - socket.on('connect', () => { - const responseTime = Date.now() - startTime; - clearTimeout(timer); - cleanup(); - resolve({ - connected: true, - host, - port, - responseTime - }); - }); - - socket.on('error', (error) => { - clearTimeout(timer); - cleanup(); - resolve({ - connected: false, - host, - port, - error: error.message - }); - }); - - socket.on('timeout', () => { - clearTimeout(timer); - cleanup(); - resolve({ - connected: false, - host, - port, - error: 'Socket timeout' - }); - }); - - try { - socket.connect(port, host); - } catch (error) { - clearTimeout(timer); - cleanup(); - resolve({ - connected: false, - host, - port, - error: error instanceof Error ? error.message : 'Unknown connection error' - }); - } - }); -} - -export async function tcpCheck( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = tcpCheckSchema.safeParse(req.body); - - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { host, port, timeout } = parsedBody.data; - - - const result = await checkTcpConnection(host, port, timeout); - - logger.info(`TCP check for ${host}:${port} - Connected: ${result.connected}`, { - host, - port, - connected: result.connected, - responseTime: result.responseTime, - error: result.error - }); - - return response(res, { - data: result, - success: true, - error: false, - message: `TCP check completed for ${host}:${port}`, - status: HttpCode.OK - }); - } catch (error) { - logger.error("TCP check error:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred during TCP check" - ) - ); - } -} - -// Batch TCP check endpoint for checking multiple resources at once -const batchTcpCheckSchema = z - .object({ - checks: z.array(z.object({ - id: z.number().int().positive(), - host: z.string().min(1), - port: z.number().int().min(1).max(65535) - })).max(50), // Limit to 50 concurrent checks - timeout: z.number().int().min(1000).max(30000).optional().default(5000) - }) - .strict(); - -export type BatchTcpCheckResponse = { - results: Array; -}; - -registry.registerPath({ - method: "post", - path: "/org/{orgId}/resources/tcp-check-batch", - description: "Check TCP connectivity to multiple hosts and ports", - tags: [OpenAPITags.Resource], - request: { - body: { - content: { - "application/json": { - schema: batchTcpCheckSchema - } - } - } - }, - responses: { - 200: { - description: "Batch TCP check results", - content: { - "application/json": { - schema: z.object({ - success: z.boolean(), - data: z.object({ - results: z.array(z.object({ - id: z.number(), - connected: z.boolean(), - host: z.string(), - port: z.number(), - responseTime: z.number().optional(), - error: z.string().optional() - })) - }), - message: z.string() - }) - } - } - } - } -}); - -export async function batchTcpCheck( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedBody = batchTcpCheckSchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { checks, timeout } = parsedBody.data; - - // all TCP checks concurrently - const checkPromises = checks.map(async (check) => { - const result = await checkTcpConnection(check.host, check.port, timeout); - return { - id: check.id, - ...result - }; - }); - - const results = await Promise.all(checkPromises); - - logger.info(`Batch TCP check completed for ${checks.length} resources`, { - totalChecks: checks.length, - successfulConnections: results.filter(r => r.connected).length, - failedConnections: results.filter(r => !r.connected).length - }); - - return response(res, { - data: { results }, - success: true, - error: false, - message: `Batch TCP check completed for ${checks.length} resources`, - status: HttpCode.OK - }); - } catch (error) { - logger.error("Batch TCP check error:", error); - return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred during batch TCP check" - ) - ); - } -} \ No newline at end of file diff --git a/src/components/DataTablePagination.tsx b/src/components/DataTablePagination.tsx index 70d64f0c..79b09f20 100644 --- a/src/components/DataTablePagination.tsx +++ b/src/components/DataTablePagination.tsx @@ -24,6 +24,7 @@ interface DataTablePaginationProps { isServerPagination?: boolean; isLoading?: boolean; disabled?: boolean; + renderAdditionalControls?: () => React.ReactNode; } export function DataTablePagination({ @@ -33,7 +34,8 @@ export function DataTablePagination({ totalCount, isServerPagination = false, isLoading = false, - disabled = false + disabled = false, + renderAdditionalControls }: DataTablePaginationProps) { const t = useTranslations(); @@ -113,6 +115,11 @@ export function DataTablePagination({ ))} + {renderAdditionalControls && ( +
+ {renderAdditionalControls()} +
+ )}
diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index d2cf4384..3238d1e4 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -18,7 +18,6 @@ import { DropdownMenuItem, DropdownMenuTrigger, DropdownMenuCheckboxItem, - DropdownMenuSeparator } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { @@ -30,9 +29,6 @@ import { ShieldCheck, RefreshCw, Settings2, - Wifi, - WifiOff, - Clock, Plus, Search, ChevronDown, @@ -73,14 +69,6 @@ import { useSearchParams } from "next/navigation"; import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog"; import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { Badge } from "@app/components/ui/badge"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "@app/components/ui/tooltip"; -import { useResourceHealth } from "@app/hooks/useResourceHealth"; export type ResourceRow = { id: number; @@ -162,25 +150,6 @@ const setStoredPageSize = (pageSize: number, tableId?: string): void => { }; -function StatusIcon({ status, className = "" }: { - status: 'checking' | 'online' | 'offline' | undefined; - className?: string; -}) { - const iconClass = `h-4 w-4 ${className}`; - - switch (status) { - case 'checking': - return ; - case 'online': - return ; - case 'offline': - return ; - default: - return null; - } -} - - export default function ResourcesTable({ resources, internalResources, @@ -204,9 +173,6 @@ export default function ResourcesTable({ getStoredPageSize('internal-resources', 20) ); - const { resourceStatus, targetStatus } = useResourceHealth(orgId, resources); - - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = useState(); @@ -318,39 +284,6 @@ export default function ResourcesTable({ ); }; - const getColumnToggle = () => { - const table = currentView === "internal" ? internalTable : proxyTable; - - return ( - - - - - - {table.getAllColumns() - .filter(column => column.getCanHide()) - .map(column => ( - column.toggleVisibility(!!value)} - > - {column.id === "target" ? t("target") : - column.id === "authState" ? t("authentication") : - column.id === "enabled" ? t("enabled") : - column.id === "status" ? t("status") : - column.id} - - ))} - - - ); - }; - const getActionButton = () => { if (currentView === "internal") { return ( @@ -514,18 +447,9 @@ export default function ResourcesTable({ {targets.map((target, idx) => { const key = `${resourceRow.id}:${target.host}:${target.port}`; - const status = targetStatus[key]; - - const color = - status === "online" - ? "bg-green-500" - : status === "offline" - ? "bg-red-500 " - : "bg-gray-400"; return ( -
{ - const resourceRow = row.original; - const status = resourceStatus[resourceRow.id]; - - if (!resourceRow.enabled) { - return ( - - - - - {t("disabled")} - - - -

{t("resourceDisabled")}

-
-
-
- ); - } - - return ( - - - -
- - - {status === 'checking' ? t("checking") : - status === 'online' ? t("online") : - status === 'offline' ? t("offline") : '-'} - -
-
- -

- {status === 'checking' ? t("checkingConnection") : - status === 'online' ? t("connectionSuccessful") : - status === 'offline' ? t("connectionFailed") : - t("statusUnknown")} -

-
-
-
- ); - } - }, { accessorKey: "domain", header: t("access"), @@ -987,7 +860,6 @@ export default function ResourcesTable({
- {getColumnToggle()} {getActionButton()}
@@ -1072,6 +944,34 @@ export default function ResourcesTable({ ( + + + + + + {proxyTable.getAllColumns() + .filter(column => column.getCanHide()) + .map(column => ( + column.toggleVisibility(!!value)} + > + {column.id === "target" ? t("target") : + column.id === "authState" ? t("authentication") : + column.id === "enabled" ? t("enabled") : + column.id === "status" ? t("status") : + column.id} + + ))} + + + )} /> @@ -1173,6 +1073,34 @@ export default function ResourcesTable({ ( + + + + + + {internalTable.getAllColumns() + .filter(column => column.getCanHide()) + .map(column => ( + column.toggleVisibility(!!value)} + > + {column.id === "target" ? t("target") : + column.id === "authState" ? t("authentication") : + column.id === "enabled" ? t("enabled") : + column.id === "status" ? t("status") : + column.id} + + ))} + + + )} /> diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index ae94b12e..9c2cab88 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -9,7 +9,9 @@ import { SortingState, getSortedRowModel, ColumnFiltersState, - getFilteredRowModel + getFilteredRowModel, + VisibilityState, + Column } from "@tanstack/react-table"; import { Table, @@ -23,7 +25,7 @@ import { Button } from "@app/components/ui/button"; import { useEffect, useMemo, useState } from "react"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; -import { Plus, Search, RefreshCw } from "lucide-react"; +import { Plus, Search, RefreshCw, Settings2 } from "lucide-react"; import { Card, CardContent, @@ -32,6 +34,12 @@ import { } from "@app/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; import { useTranslations } from "next-intl"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; const STORAGE_KEYS = { PAGE_SIZE: 'datatable-page-size', @@ -93,6 +101,7 @@ type DataTableProps = { defaultTab?: string; persistPageSize?: boolean | string; defaultPageSize?: number; + enableColumnToggle?: boolean; }; export function DataTable({ @@ -109,7 +118,8 @@ export function DataTable({ tabs, defaultTab, persistPageSize = false, - defaultPageSize = 20 + defaultPageSize = 20, + enableColumnToggle = true }: DataTableProps) { const t = useTranslations(); @@ -129,6 +139,7 @@ export function DataTable({ ); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); const [activeTab, setActiveTab] = useState( defaultTab || tabs?.[0]?.id || "" ); @@ -157,6 +168,7 @@ export function DataTable({ onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setGlobalFilter, + onColumnVisibilityChange: setColumnVisibility, initialState: { pagination: { pageSize: pageSize, @@ -166,7 +178,8 @@ export function DataTable({ state: { sorting, columnFilters, - globalFilter + globalFilter, + columnVisibility } }); @@ -199,6 +212,43 @@ export function DataTable({ } }; + const getColumnLabel = (column: Column) => { + return typeof column.columnDef.header === "string" ? + column.columnDef.header : + column.id; // fallback to id if header is JSX + }; + + + const renderColumnToggle = () => { + if (!enableColumnToggle) return null; + + return ( + + + + + + {table.getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => ( + column.toggleVisibility(!!value)} + > + {getColumnLabel(column)} + + ))} + + + ); + }; + + return (
@@ -312,6 +362,7 @@ export function DataTable({
diff --git a/src/hooks/useResourceHealth.ts b/src/hooks/useResourceHealth.ts deleted file mode 100644 index 315f0a00..00000000 --- a/src/hooks/useResourceHealth.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { useState, useEffect } from "react"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; - -type Target = { - host: string; - port: number; -}; - -type ResourceRow = { - id: number; - enabled: boolean; - targets?: Target[]; -}; - -type Status = "checking" | "online" | "offline"; - -export function useResourceHealth(orgId: string, resources: ResourceRow[]) { - const { env } = useEnvContext(); - const api = createApiClient({ env }); - - const [resourceStatus, setResourceStatus] = useState>({}); - const [targetStatus, setTargetStatus] = useState>({}); - - useEffect(() => { - if (!orgId || resources.length === 0) return; - - // init all as "checking" - const initialRes: Record = {}; - const initialTargets: Record = {}; - resources.forEach((r) => { - initialRes[r.id] = "checking"; - r.targets?.forEach((t) => { - const key = `${r.id}:${t.host}:${t.port}`; - initialTargets[key] = "checking"; - }); - }); - setResourceStatus(initialRes); - setTargetStatus(initialTargets); - - // build batch checks - const checks = resources.flatMap((r) => - r.enabled && r.targets?.length - ? r.targets.map((t) => ({ - id: r.id, - host: t.host, - port: t.port, - })) - : [] - ); - - if (checks.length === 0) return; - - api.post(`/org/${orgId}/resources/tcp-check-batch`, { - checks, - timeout: 5000, - }) - .then((res) => { - const results = res.data.data.results as Array<{ - id: number; - host: string; - port: number; - connected: boolean; - }>; - - // build maps - const newTargetStatus: Record = {}; - const grouped: Record = {}; - - results.forEach((r) => { - const key = `${r.id}:${r.host}:${r.port}`; - newTargetStatus[key] = r.connected ? "online" : "offline"; - - if (!grouped[r.id]) grouped[r.id] = []; - grouped[r.id].push(r.connected); - }); - - const newResourceStatus: Record = {}; - Object.entries(grouped).forEach(([id, arr]) => { - newResourceStatus[+id] = arr.some(Boolean) ? "online" : "offline"; - }); - - setTargetStatus((prev) => ({ ...prev, ...newTargetStatus })); - setResourceStatus((prev) => ({ ...prev, ...newResourceStatus })); - }) - .catch(() => { - // fallback all offline - const fallbackRes: Record = {}; - const fallbackTargets: Record = {}; - resources.forEach((r) => { - if (r.enabled) { - fallbackRes[r.id] = "offline"; - r.targets?.forEach((t) => { - fallbackTargets[`${r.id}:${t.host}:${t.port}`] = "offline"; - }); - } - }); - setResourceStatus((prev) => ({ ...prev, ...fallbackRes })); - setTargetStatus((prev) => ({ ...prev, ...fallbackTargets })); - }); - }, [orgId, resources]); - - return { resourceStatus, targetStatus }; -}