From f9d6fd85ddf5ba292bcb775a835ab30148ba9914 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:08:09 +0300 Subject: [PATCH 1/7] commit --- .../app/(authenticated)/clients/columns.tsx | 74 +++++++++ .../(authenticated)/clients/data-table.tsx | 100 +++++++++++++ .../app/(authenticated)/clients/dialogs.tsx | 56 +++++++ .../app/(authenticated)/clients/page.tsx | 98 ++++++++++++ netmanager-app/components/ui/alert-dialog.tsx | 141 ++++++++++++++++++ netmanager-app/components/ui/button.tsx | 22 +-- netmanager-app/core/apis/analytics.ts | 12 +- netmanager-app/package-lock.json | 98 +++++++++++- netmanager-app/package.json | 2 + 9 files changed, 585 insertions(+), 18 deletions(-) create mode 100644 netmanager-app/app/(authenticated)/clients/columns.tsx create mode 100644 netmanager-app/app/(authenticated)/clients/data-table.tsx create mode 100644 netmanager-app/app/(authenticated)/clients/dialogs.tsx create mode 100644 netmanager-app/app/(authenticated)/clients/page.tsx create mode 100644 netmanager-app/components/ui/alert-dialog.tsx diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx new file mode 100644 index 0000000000..5a223c6ca1 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/columns.tsx @@ -0,0 +1,74 @@ +"use client" + +import type { ColumnDef } from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { ArrowUpDown, MoreHorizontal } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ) + }, + }, + { + accessorKey: "_id", + header: "Client ID", + }, + { + accessorKey: "ip_address", + header: "Client IP", + }, + { + accessorKey: "isActive", + header: "Status", + cell: ({ row }) => { + const isActive = row.getValue("isActive") + return ( +
+ {isActive ? "Activated" : "Not Activated"} +
+ ) + }, + }, + { + id: "actions", + cell: ({ row }) => { + const client = row.original + + return ( + + + + + + Actions + navigator.clipboard.writeText(client._id)}> + Copy client ID + + + View client details + Update client information + + + ) + }, + }, +] + diff --git a/netmanager-app/app/(authenticated)/clients/data-table.tsx b/netmanager-app/app/(authenticated)/clients/data-table.tsx new file mode 100644 index 0000000000..ecf9395330 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/data-table.tsx @@ -0,0 +1,100 @@ +"use client" + +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + getFilteredRowModel, + type ColumnFiltersState, +} from "@tanstack/react-table" + +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import React from "react" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ columns, data }: DataTableProps) { + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, + }) + + return ( +
+
+ table.getColumn("name")?.setFilterValue(event.target.value)} + className="max-w-sm" + /> +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ + +
+
+ ) +} + diff --git a/netmanager-app/app/(authenticated)/clients/dialogs.tsx b/netmanager-app/app/(authenticated)/clients/dialogs.tsx new file mode 100644 index 0000000000..43445b6653 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/dialogs.tsx @@ -0,0 +1,56 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + } from "@/components/ui/alert-dialog" + + interface DialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void + } + + export function ActivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { + return ( + + + + Activate Client + + Are you sure you want to activate this client? This action cannot be undone. + + + + Cancel + Activate + + + + ) + } + + export function DeactivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { + return ( + + + + Deactivate Client + + Are you sure you want to deactivate this client? This action cannot be undone. + + + + Cancel + Deactivate + + + + ) + } + + \ No newline at end of file diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx new file mode 100644 index 0000000000..dfbfe9dab4 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -0,0 +1,98 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { DataTable } from "./data-table" +import { columns } from "./columns" +import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" +import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" +import { useToast } from "@/components/ui/use-toast" + +const ClientManagement = () => { + const [clients, setClients] = useState<{ _id: string; isActive: boolean }[]>([]) + const [loading, setLoading] = useState(false) + const [selectedClient, setSelectedClient] = useState(null) + const [activateDialogOpen, setActivateDialogOpen] = useState(false) + const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) + const { toast } = useToast() + + const fetchClients = async () => { + setLoading(true) + try { + const response = await getClientsApi() + setClients(response.clients) + } catch (error) { + toast({ + title: "Error", + description: "Failed to fetch clients", + variant: "destructive", + }) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchClients() + }, []) + + const handleActivateDeactivate = async (clientId: string, activate: boolean) => { + try { + await activateUserClientApi({ _id: clientId, isActive: activate }) + await fetchClients() + toast({ + title: "Success", + description: `Client ${activate ? "activated" : "deactivated"} successfully`, + }) + } catch (error) { + toast({ + title: "Error", + description: `Failed to ${activate ? "activate" : "deactivate"} client`, + variant: "destructive", + }) + } finally { + setActivateDialogOpen(false) + setDeactivateDialogOpen(false) + } + } + + const activatedClients = clients.filter((client) => client.isActive).length + const deactivatedClients = clients.filter((client) => !client.isActive).length + + return ( +
+
+

Client Management

+ +
+ +
+
+

Activated Clients

+

{activatedClients}

+
+
+

Deactivated Clients

+

{deactivatedClients}

+
+
+ + + + handleActivateDeactivate(selectedClient?._id, true)} + /> + + handleActivateDeactivate(selectedClient?._id, false)} + /> +
+ ) +} + +export default ClientManagement + diff --git a/netmanager-app/components/ui/alert-dialog.tsx b/netmanager-app/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000..25e7b47446 --- /dev/null +++ b/netmanager-app/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/netmanager-app/components/ui/button.tsx b/netmanager-app/components/ui/button.tsx index f2329bdfe6..36496a2872 100644 --- a/netmanager-app/components/ui/button.tsx +++ b/netmanager-app/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", @@ -31,26 +31,26 @@ const buttonVariants = cva( size: "default", }, } -); +) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; + asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot : "button" return ( - ); + ) } -); -Button.displayName = "Button"; +) +Button.displayName = "Button" -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/netmanager-app/core/apis/analytics.ts b/netmanager-app/core/apis/analytics.ts index 9f189c4cbc..b760d9ab6a 100644 --- a/netmanager-app/core/apis/analytics.ts +++ b/netmanager-app/core/apis/analytics.ts @@ -1,5 +1,5 @@ import createAxiosInstance from "./axiosConfig"; -import { ANALYTICS_MGT_URL } from "@/core/urls"; +import { ANALYTICS_MGT_URL, USERS_MGT_URL } from "@/core/urls"; const axiosInstance = createAxiosInstance(); @@ -26,3 +26,13 @@ export const dataExport = async (data: DataExportForm) => { { headers } ); }; + +export const getClientsApi = async () => { + return axiosInstance.get(`${USERS_MGT_URL}/clients`).then((response) => response.data); +}; + +export const activateUserClientApi = async (data: { _id: string; isActive: boolean }) => { + return axiosInstance + .put(`${USERS_MGT_URL}/clients/activate`, data) + .then((response) => response.data); +}; diff --git a/netmanager-app/package-lock.json b/netmanager-app/package-lock.json index 70377ac0f0..fecd809959 100644 --- a/netmanager-app/package-lock.json +++ b/netmanager-app/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", @@ -23,6 +24,7 @@ "@reduxjs/toolkit": "^2.5.0", "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", + "@tanstack/react-table": "^8.20.6", "@types/leaflet": "^1.9.15", "@types/react-redux": "^7.1.34", "apexcharts": "^4.3.0", @@ -513,6 +515,33 @@ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.5.tgz", + "integrity": "sha512-1Y2sI17QzSZP58RjGtrklfSGIf3AF7U/HkD3aAcAnhOUJrm7+7GG1wRDFaUlSe0nW5B/t4mYd/+7RNbP2Wexug==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.5", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", @@ -643,14 +672,14 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", - "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz", + "integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", @@ -659,8 +688,34 @@ "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz", + "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -1467,6 +1522,37 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", + "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", diff --git a/netmanager-app/package.json b/netmanager-app/package.json index ac64760639..d252c5532a 100644 --- a/netmanager-app/package.json +++ b/netmanager-app/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", @@ -24,6 +25,7 @@ "@reduxjs/toolkit": "^2.5.0", "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", + "@tanstack/react-table": "^8.20.6", "@types/leaflet": "^1.9.15", "@types/react-redux": "^7.1.34", "apexcharts": "^4.3.0", From 3cabe59b3349bb03070abaf31a63ba2ea6da6522 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:17:12 +0300 Subject: [PATCH 2/7] Clients Activation --- .../app/(authenticated)/clients/columns.tsx | 23 +++- .../(authenticated)/clients/data-table.tsx | 13 ++- .../app/(authenticated)/clients/dialogs.tsx | 110 +++++++++--------- .../app/(authenticated)/clients/page.tsx | 29 ++++- netmanager-app/app/types/clients.ts | 27 +++++ 5 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 netmanager-app/app/types/clients.ts diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx index 5a223c6ca1..ac2a1a57c2 100644 --- a/netmanager-app/app/(authenticated)/clients/columns.tsx +++ b/netmanager-app/app/(authenticated)/clients/columns.tsx @@ -11,8 +11,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import type { Client } from "@/app/types/clients" -export const columns: ColumnDef[] = [ +interface ColumnProps { + onActivate: (client: Client) => void + onDeactivate: (client: Client) => void +} + +export const columns = ({ onActivate, onDeactivate }: ColumnProps): ColumnDef[] => [ { accessorKey: "name", header: ({ column }) => { @@ -29,8 +35,16 @@ export const columns: ColumnDef[] = [ header: "Client ID", }, { - accessorKey: "ip_address", - header: "Client IP", + accessorKey: "user.email", + header: "User Email", + }, + { + accessorKey: "access_token.expires", + header: "Token Expiry", + cell: ({ row }) => { + const expires = new Date(row.getValue("access_token.expires")) + return expires.toLocaleDateString() + }, }, { accessorKey: "isActive", @@ -63,6 +77,9 @@ export const columns: ColumnDef[] = [ Copy client ID + (client.isActive ? onDeactivate(client) : onActivate(client))}> + {client.isActive ? "Deactivate" : "Activate"} + View client details Update client information diff --git a/netmanager-app/app/(authenticated)/clients/data-table.tsx b/netmanager-app/app/(authenticated)/clients/data-table.tsx index ecf9395330..716e5a6389 100644 --- a/netmanager-app/app/(authenticated)/clients/data-table.tsx +++ b/netmanager-app/app/(authenticated)/clients/data-table.tsx @@ -17,19 +17,22 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import React from "react" +import type { Client } from "@/app/types/clients" -interface DataTableProps { - columns: ColumnDef[] - data: TData[] +interface DataTableProps { + columns: ColumnDef[] + data: Client[] + onActivate: (client: Client) => void + onDeactivate: (client: Client) => void } -export function DataTable({ columns, data }: DataTableProps) { +export function DataTable({ columns, data, onActivate, onDeactivate }: DataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) const table = useReactTable({ data, - columns, + columns: columns({ onActivate, onDeactivate }), getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, diff --git a/netmanager-app/app/(authenticated)/clients/dialogs.tsx b/netmanager-app/app/(authenticated)/clients/dialogs.tsx index 43445b6653..9cfd7d51b1 100644 --- a/netmanager-app/app/(authenticated)/clients/dialogs.tsx +++ b/netmanager-app/app/(authenticated)/clients/dialogs.tsx @@ -1,56 +1,56 @@ import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - } from "@/components/ui/alert-dialog" - - interface DialogProps { - open: boolean - onOpenChange: (open: boolean) => void - onConfirm: () => void - } - - export function ActivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { - return ( - - - - Activate Client - - Are you sure you want to activate this client? This action cannot be undone. - - - - Cancel - Activate - - - - ) - } - - export function DeactivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { - return ( - - - - Deactivate Client - - Are you sure you want to deactivate this client? This action cannot be undone. - - - - Cancel - Deactivate - - - - ) - } - - \ No newline at end of file + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" + +interface DialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void + clientName?: string +} + +export function ActivateClientDialog({ open, onOpenChange, onConfirm, clientName }: DialogProps) { + return ( + + + + Activate Client + + Are you sure you want to activate the client {clientName}? This action cannot be undone. + + + + Cancel + Activate + + + + ) +} + +export function DeactivateClientDialog({ open, onOpenChange, onConfirm, clientName }: DialogProps) { + return ( + + + + Deactivate Client + + Are you sure you want to deactivate the client {clientName}? This action cannot be undone. + + + + Cancel + Deactivate + + + + ) +} + diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index dfbfe9dab4..adcce2d85f 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -7,11 +7,12 @@ import { columns } from "./columns" import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" +import type { Client } from "@/app/types/clients" const ClientManagement = () => { - const [clients, setClients] = useState<{ _id: string; isActive: boolean }[]>([]) + const [clients, setClients] = useState([]) const [loading, setLoading] = useState(false) - const [selectedClient, setSelectedClient] = useState(null) + const [selectedClient, setSelectedClient] = useState(null) const [activateDialogOpen, setActivateDialogOpen] = useState(false) const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) const { toast } = useToast() @@ -53,12 +54,23 @@ const ClientManagement = () => { } finally { setActivateDialogOpen(false) setDeactivateDialogOpen(false) + setSelectedClient(null) } } const activatedClients = clients.filter((client) => client.isActive).length const deactivatedClients = clients.filter((client) => !client.isActive).length + const handleActivateClick = (client: Client) => { + setSelectedClient(client) + setActivateDialogOpen(true) + } + + const handleDeactivateClick = (client: Client) => { + setSelectedClient(client) + setDeactivateDialogOpen(true) + } + return (
@@ -77,18 +89,25 @@ const ClientManagement = () => {
- + handleActivateDeactivate(selectedClient?._id, true)} + onConfirm={() => selectedClient && handleActivateDeactivate(selectedClient._id, true)} + clientName={selectedClient?.name} /> handleActivateDeactivate(selectedClient?._id, false)} + onConfirm={() => selectedClient && handleActivateDeactivate(selectedClient._id, false)} + clientName={selectedClient?.name} />
) diff --git a/netmanager-app/app/types/clients.ts b/netmanager-app/app/types/clients.ts new file mode 100644 index 0000000000..9e0235a51c --- /dev/null +++ b/netmanager-app/app/types/clients.ts @@ -0,0 +1,27 @@ +import { UserDetails } from './users'; + + export interface AccessToken { + _id: string + permissions: string[] + scopes: string[] + expiredEmailSent: boolean + token: string + client_id: string + name: string + expires: string + createdAt: string + updatedAt: string + __v: number + } + + export interface Client { + _id: string + isActive: boolean + ip_addresses: string[] + name: string + client_secret: string + user: UserDetails + access_token: AccessToken + } + + \ No newline at end of file From dde8e137b3d36503f4c1ccc11a0ec2d554b7cadd Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:28:09 +0300 Subject: [PATCH 3/7] commit --- .../app/(authenticated)/clients/columns.tsx | 15 ++++++- .../app/(authenticated)/clients/page.tsx | 40 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx index ac2a1a57c2..398a793ae3 100644 --- a/netmanager-app/app/(authenticated)/clients/columns.tsx +++ b/netmanager-app/app/(authenticated)/clients/columns.tsx @@ -42,8 +42,19 @@ export const columns = ({ onActivate, onDeactivate }: ColumnProps): ColumnDef { - const expires = new Date(row.getValue("access_token.expires")) - return expires.toLocaleDateString() + const accessToken = row.original.access_token + if (!accessToken || !accessToken.expires) return "N/A" + const expires = new Date(accessToken.expires) + if (isNaN(expires.getTime())) return "Invalid Date" + + const now = new Date() + const diffTime = expires.getTime() - now.getTime() + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + + if (diffDays < 0) return "Expired" + if (diffDays === 0) return "Expires today" + if (diffDays === 1) return "Expires tomorrow" + return `Expires in ${diffDays} days` }, }, { diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index adcce2d85f..ae10c6142a 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -9,6 +9,21 @@ import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" +const formatDate = (dateString: string | undefined): string => { + if (!dateString) return "N/A" + const date = new Date(dateString) + if (isNaN(date.getTime())) return "Invalid Date" + + const now = new Date() + const diffTime = date.getTime() - now.getTime() + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + + if (diffDays < 0) return "Expired" + if (diffDays === 0) return "Expires today" + if (diffDays === 1) return "Expires tomorrow" + return `Expires in ${diffDays} days` +} + const ClientManagement = () => { const [clients, setClients] = useState([]) const [loading, setLoading] = useState(false) @@ -71,6 +86,22 @@ const ClientManagement = () => { setDeactivateDialogOpen(true) } + const nearestExpiringToken = clients + .filter((client) => client.access_token && client.access_token.expires) + .filter((client) => { + const expiryDate = new Date(client.access_token.expires) + return expiryDate > new Date() + }) + .sort((a, b) => { + const dateA = new Date(a.access_token.expires).getTime() + const dateB = new Date(b.access_token.expires).getTime() + return dateA - dateB + })[0] + + const nearestExpiryDate = nearestExpiringToken + ? formatDate(nearestExpiringToken.access_token.expires) + : "No upcoming expiries" + return (
@@ -78,7 +109,7 @@ const ClientManagement = () => {
-
+

Activated Clients

{activatedClients}

@@ -87,6 +118,13 @@ const ClientManagement = () => {

Deactivated Clients

{deactivatedClients}

+
+

Nearest Token Expiry

+

+ {nearestExpiringToken ? formatDate(nearestExpiringToken.access_token.expires) : "No upcoming expiries"} +

+

{nearestExpiringToken?.name || "N/A"}

+
Date: Fri, 24 Jan 2025 11:55:57 +0300 Subject: [PATCH 4/7] commit --- .../app/(authenticated)/clients/page.tsx | 3 +- netmanager-app/app/pageAccess.tsx | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 netmanager-app/app/pageAccess.tsx diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index ae10c6142a..b8c0f38088 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -8,6 +8,7 @@ import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" +import withPermission from "@/app/pageAccess" const formatDate = (dateString: string | undefined): string => { if (!dateString) return "N/A" @@ -151,5 +152,5 @@ const ClientManagement = () => { ) } -export default ClientManagement +export default withPermission(ClientManagement, 'CREATE_UPDATE_AND_DELETE_NETWORK_DEVICES'); diff --git a/netmanager-app/app/pageAccess.tsx b/netmanager-app/app/pageAccess.tsx new file mode 100644 index 0000000000..4ca6d4dca3 --- /dev/null +++ b/netmanager-app/app/pageAccess.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAppSelector } from "@/core/redux/hooks"; +import type { RootState } from "@/core/redux/store"; + +const withPermission =

( + Component: React.ComponentType

, + requiredPermission: string +): React.FC

=> { + const WithPermission: React.FC

= (props) => { + const router = useRouter(); + const currentRole = useAppSelector( + (state: RootState) => state.user.currentRole + ); + const [hasPermission, setHasPermission] = useState(false); + + useEffect(() => { + if (currentRole) { + const permissionExists = currentRole.permissions.includes( + requiredPermission + ); + setHasPermission(permissionExists); + + if (!permissionExists) { + router.push("/permission-denied"); + } + } + }, [currentRole, requiredPermission, router]); + + if (!hasPermission) return null; + + return ; + }; + + return WithPermission; +}; + +export default withPermission; From cf84b1a2a9826c4fcd971cb72abe1ad6ca83a894 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 12:19:48 +0300 Subject: [PATCH 5/7] Sidebar Client --- .../app/(authenticated)/clients/page.tsx | 3 +- netmanager-app/app/pageAccess.tsx | 38 ------------------- netmanager-app/components/sidebar.tsx | 16 ++++++++ 3 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 netmanager-app/app/pageAccess.tsx diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index b8c0f38088..f5aa65dcaa 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -8,7 +8,6 @@ import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" -import withPermission from "@/app/pageAccess" const formatDate = (dateString: string | undefined): string => { if (!dateString) return "N/A" @@ -152,5 +151,5 @@ const ClientManagement = () => { ) } -export default withPermission(ClientManagement, 'CREATE_UPDATE_AND_DELETE_NETWORK_DEVICES'); +export default ClientManagement; diff --git a/netmanager-app/app/pageAccess.tsx b/netmanager-app/app/pageAccess.tsx deleted file mode 100644 index 4ca6d4dca3..0000000000 --- a/netmanager-app/app/pageAccess.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { useAppSelector } from "@/core/redux/hooks"; -import type { RootState } from "@/core/redux/store"; - -const withPermission =

( - Component: React.ComponentType

, - requiredPermission: string -): React.FC

=> { - const WithPermission: React.FC

= (props) => { - const router = useRouter(); - const currentRole = useAppSelector( - (state: RootState) => state.user.currentRole - ); - const [hasPermission, setHasPermission] = useState(false); - - useEffect(() => { - if (currentRole) { - const permissionExists = currentRole.permissions.includes( - requiredPermission - ); - setHasPermission(permissionExists); - - if (!permissionExists) { - router.push("/permission-denied"); - } - } - }, [currentRole, requiredPermission, router]); - - if (!hasPermission) return null; - - return ; - }; - - return WithPermission; -}; - -export default withPermission; diff --git a/netmanager-app/components/sidebar.tsx b/netmanager-app/components/sidebar.tsx index c419bf1987..2dc2b65a10 100644 --- a/netmanager-app/components/sidebar.tsx +++ b/netmanager-app/components/sidebar.tsx @@ -18,6 +18,7 @@ import { Map, ChevronDown, Check, + Hospital, PlusCircle, MonitorSmartphone, LogOut, @@ -210,6 +211,21 @@ const Sidebar = () => { + +

  • + + + Clients + +
  • +
  • Date: Tue, 28 Jan 2025 21:42:36 +0300 Subject: [PATCH 6/7] Add client activation and deactivation dialogs; remove old dialog components --- .../app/(authenticated)/clients/columns.tsx | 102 ------- .../(authenticated)/clients/data-table.tsx | 103 ------- .../app/(authenticated)/clients/page.tsx | 251 ++++++++++++++---- .../clients/dialogs.tsx | 0 4 files changed, 199 insertions(+), 257 deletions(-) delete mode 100644 netmanager-app/app/(authenticated)/clients/columns.tsx delete mode 100644 netmanager-app/app/(authenticated)/clients/data-table.tsx rename netmanager-app/{app/(authenticated) => components}/clients/dialogs.tsx (100%) diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx deleted file mode 100644 index 398a793ae3..0000000000 --- a/netmanager-app/app/(authenticated)/clients/columns.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import type { ColumnDef } from "@tanstack/react-table" -import { Button } from "@/components/ui/button" -import { ArrowUpDown, MoreHorizontal } from "lucide-react" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import type { Client } from "@/app/types/clients" - -interface ColumnProps { - onActivate: (client: Client) => void - onDeactivate: (client: Client) => void -} - -export const columns = ({ onActivate, onDeactivate }: ColumnProps): ColumnDef[] => [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ) - }, - }, - { - accessorKey: "_id", - header: "Client ID", - }, - { - accessorKey: "user.email", - header: "User Email", - }, - { - accessorKey: "access_token.expires", - header: "Token Expiry", - cell: ({ row }) => { - const accessToken = row.original.access_token - if (!accessToken || !accessToken.expires) return "N/A" - const expires = new Date(accessToken.expires) - if (isNaN(expires.getTime())) return "Invalid Date" - - const now = new Date() - const diffTime = expires.getTime() - now.getTime() - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - - if (diffDays < 0) return "Expired" - if (diffDays === 0) return "Expires today" - if (diffDays === 1) return "Expires tomorrow" - return `Expires in ${diffDays} days` - }, - }, - { - accessorKey: "isActive", - header: "Status", - cell: ({ row }) => { - const isActive = row.getValue("isActive") - return ( -
    - {isActive ? "Activated" : "Not Activated"} -
    - ) - }, - }, - { - id: "actions", - cell: ({ row }) => { - const client = row.original - - return ( - - - - - - Actions - navigator.clipboard.writeText(client._id)}> - Copy client ID - - - (client.isActive ? onDeactivate(client) : onActivate(client))}> - {client.isActive ? "Deactivate" : "Activate"} - - View client details - Update client information - - - ) - }, - }, -] - diff --git a/netmanager-app/app/(authenticated)/clients/data-table.tsx b/netmanager-app/app/(authenticated)/clients/data-table.tsx deleted file mode 100644 index 716e5a6389..0000000000 --- a/netmanager-app/app/(authenticated)/clients/data-table.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client" - -import { - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, - getPaginationRowModel, - getSortedRowModel, - type SortingState, - getFilteredRowModel, - type ColumnFiltersState, -} from "@tanstack/react-table" - -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" - -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import React from "react" -import type { Client } from "@/app/types/clients" - -interface DataTableProps { - columns: ColumnDef[] - data: Client[] - onActivate: (client: Client) => void - onDeactivate: (client: Client) => void -} - -export function DataTable({ columns, data, onActivate, onDeactivate }: DataTableProps) { - const [sorting, setSorting] = React.useState([]) - const [columnFilters, setColumnFilters] = React.useState([]) - - const table = useReactTable({ - data, - columns: columns({ onActivate, onDeactivate }), - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - onColumnFiltersChange: setColumnFilters, - getFilteredRowModel: getFilteredRowModel(), - state: { - sorting, - columnFilters, - }, - }) - - return ( -
    -
    - table.getColumn("name")?.setFilterValue(event.target.value)} - className="max-w-sm" - /> -
    -
    - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - - )) - ) : ( - - - No results. - - - )} - -
    -
    -
    - - -
    -
    - ) -} - diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index f5aa65dcaa..def9e5c7df 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -2,12 +2,24 @@ import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" -import { DataTable } from "./data-table" -import { columns } from "./columns" -import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" +import { Input } from "@/components/ui/input" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { ActivateClientDialog, DeactivateClientDialog } from "../../../components/clients/dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" +import { Search, ArrowUpDown, Loader2 } from "lucide-react" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination" + +const ITEMS_PER_PAGE = 8 const formatDate = (dateString: string | undefined): string => { if (!dateString) return "N/A" @@ -24,18 +36,24 @@ const formatDate = (dateString: string | undefined): string => { return `Expires in ${diffDays} days` } + const ClientManagement = () => { const [clients, setClients] = useState([]) const [loading, setLoading] = useState(false) const [selectedClient, setSelectedClient] = useState(null) const [activateDialogOpen, setActivateDialogOpen] = useState(false) const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [currentPage, setCurrentPage] = useState(1) + const [sortField, setSortField] = useState("name") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") const { toast } = useToast() const fetchClients = async () => { setLoading(true) try { const response = await getClientsApi() + console.log(response.clients) setClients(response.clients) } catch (error) { toast({ @@ -48,6 +66,14 @@ const ClientManagement = () => { } } + const handleCopyClientId = (clientId: string) => { + navigator.clipboard.writeText(clientId) + toast({ + title: "Client ID Copied", + description: "The client ID has been copied to your clipboard.", + }) + } + useEffect(() => { fetchClients() }, []) @@ -73,9 +99,6 @@ const ClientManagement = () => { } } - const activatedClients = clients.filter((client) => client.isActive).length - const deactivatedClients = clients.filter((client) => !client.isActive).length - const handleActivateClick = (client: Client) => { setSelectedClient(client) setActivateDialogOpen(true) @@ -86,6 +109,33 @@ const ClientManagement = () => { setDeactivateDialogOpen(true) } + const handleSort = (field: keyof Client) => { + if (sortField === field) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortField(field) + setSortOrder("asc") + } + } + + const filteredClients = clients.filter( + (client) => + client.name.toLowerCase().includes(searchQuery.toLowerCase()) || + client.user.email.toLowerCase().includes(searchQuery.toLowerCase()), + ) + + const sortedClients = [...filteredClients].sort((a, b) => { + if (a[sortField] < b[sortField]) return sortOrder === "asc" ? -1 : 1 + if (a[sortField] > b[sortField]) return sortOrder === "asc" ? 1 : -1 + return 0 + }) + + const totalPages = Math.ceil(sortedClients.length / ITEMS_PER_PAGE) + const paginatedClients = sortedClients.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE) + + const activatedClients = clients.filter((client) => client.isActive).length + const deactivatedClients = clients.filter((client) => !client.isActive).length + const nearestExpiringToken = clients .filter((client) => client.access_token && client.access_token.expires) .filter((client) => { @@ -98,58 +148,155 @@ const ClientManagement = () => { return dateA - dateB })[0] - const nearestExpiryDate = nearestExpiringToken - ? formatDate(nearestExpiringToken.access_token.expires) - : "No upcoming expiries" return (
    -
    -

    Client Management

    - -
    - -
    -
    -

    Activated Clients

    -

    {activatedClients}

    + {loading ? ( +
    +
    -
    -

    Deactivated Clients

    -

    {deactivatedClients}

    -
    -
    -

    Nearest Token Expiry

    -

    - {nearestExpiringToken ? formatDate(nearestExpiringToken.access_token.expires) : "No upcoming expiries"} -

    -

    {nearestExpiringToken?.name || "N/A"}

    -
    -
    - - - - selectedClient && handleActivateDeactivate(selectedClient._id, true)} - clientName={selectedClient?.name} - /> - - selectedClient && handleActivateDeactivate(selectedClient._id, false)} - clientName={selectedClient?.name} - /> + ) : ( + <> +
    +

    Client Management

    + +
    + +
    +
    +

    Activated Clients

    +

    {activatedClients}

    +
    +
    +

    Deactivated Clients

    +

    {deactivatedClients}

    +
    + +
    + +
    +
    + + setSearchQuery(e.target.value)} + /> +
    + + + + + + handleSort("name")}> + Name {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("isActive")}> + Status {sortField === "isActive" && (sortOrder === "asc" ? "↑" : "↓")} + + + +
    + +
    + + + + Client Name + User Email + Token Expiry + Status + Actions + + + + {paginatedClients.map((client) => ( + + +
    {client.name}
    +
    {client._id}
    +
    + {client.user.email} + + {client.access_token?.expires + ? formatDate(client.access_token.expires) + : "N/A"} + + + + {client.isActive ? "Activated" : "Not Activated"} + + + + + + +
    + ))} +
    +
    +
    + +
    + + + + setCurrentPage((prev) => Math.max(prev - 1, 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + setCurrentPage(page)} isActive={currentPage === page}> + {page} + + + ))} + + setCurrentPage((prev) => Math.min(prev + 1, totalPages))} + className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
    + + selectedClient && handleActivateDeactivate(selectedClient._id, true)} + clientName={selectedClient?.name} + /> + + selectedClient && handleActivateDeactivate(selectedClient._id, false)} + clientName={selectedClient?.name} + /> + + )}
    ) } -export default ClientManagement; +export default ClientManagement diff --git a/netmanager-app/app/(authenticated)/clients/dialogs.tsx b/netmanager-app/components/clients/dialogs.tsx similarity index 100% rename from netmanager-app/app/(authenticated)/clients/dialogs.tsx rename to netmanager-app/components/clients/dialogs.tsx From 2376cb43ddd29f0ba373c676a2a9186018548395 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Wed, 29 Jan 2025 16:06:29 +0300 Subject: [PATCH 7/7] Remove unused token expiry logic and update package dependencies --- .../app/(authenticated)/clients/page.tsx | 12 ------- netmanager-app/package-lock.json | 32 ------------------- netmanager-app/package.json | 1 - 3 files changed, 45 deletions(-) diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index def9e5c7df..224632669a 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -53,7 +53,6 @@ const ClientManagement = () => { setLoading(true) try { const response = await getClientsApi() - console.log(response.clients) setClients(response.clients) } catch (error) { toast({ @@ -136,17 +135,6 @@ const ClientManagement = () => { const activatedClients = clients.filter((client) => client.isActive).length const deactivatedClients = clients.filter((client) => !client.isActive).length - const nearestExpiringToken = clients - .filter((client) => client.access_token && client.access_token.expires) - .filter((client) => { - const expiryDate = new Date(client.access_token.expires) - return expiryDate > new Date() - }) - .sort((a, b) => { - const dateA = new Date(a.access_token.expires).getTime() - const dateB = new Date(b.access_token.expires).getTime() - return dateA - dateB - })[0] return ( diff --git a/netmanager-app/package-lock.json b/netmanager-app/package-lock.json index fecd809959..81afcd13ee 100644 --- a/netmanager-app/package-lock.json +++ b/netmanager-app/package-lock.json @@ -24,7 +24,6 @@ "@reduxjs/toolkit": "^2.5.0", "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", - "@tanstack/react-table": "^8.20.6", "@types/leaflet": "^1.9.15", "@types/react-redux": "^7.1.34", "apexcharts": "^4.3.0", @@ -1522,37 +1521,6 @@ "react": "^18 || ^19" } }, - "node_modules/@tanstack/react-table": { - "version": "8.20.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", - "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==", - "dependencies": { - "@tanstack/table-core": "8.20.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.20.5", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", - "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", diff --git a/netmanager-app/package.json b/netmanager-app/package.json index d252c5532a..507d6a40b7 100644 --- a/netmanager-app/package.json +++ b/netmanager-app/package.json @@ -25,7 +25,6 @@ "@reduxjs/toolkit": "^2.5.0", "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", - "@tanstack/react-table": "^8.20.6", "@types/leaflet": "^1.9.15", "@types/react-redux": "^7.1.34", "apexcharts": "^4.3.0",