From 6a7525acd2fe85bc221ea702574aa5ac6726692c Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 27 Dec 2024 10:23:31 +0000 Subject: [PATCH 1/9] new routes table --- .../20241227084426_routes_table/migration.sql | 22 +++ prisma/schema.prisma | 11 +- .../networkRoutes/collumns.tsx | 57 +++++++ .../{ => networkRoutes}/networkRoutes.tsx | 31 ++-- .../networkRoutes/networkRoutesTable.tsx | 88 +++++++++++ .../networkRoutes/routesEditCell.tsx | 141 ++++++++++++++++++ src/pages/central/[id].tsx | 2 +- src/pages/network/[id].tsx | 4 +- src/pages/organization/[orgid]/[id].tsx | 2 +- src/server/api/routers/networkRouter.ts | 52 +++++-- src/server/api/services/networkService.ts | 7 +- src/types/local/network.d.ts | 1 + 12 files changed, 380 insertions(+), 38 deletions(-) create mode 100644 prisma/migrations/20241227084426_routes_table/migration.sql create mode 100644 src/components/networkByIdPage/networkRoutes/collumns.tsx rename src/components/networkByIdPage/{ => networkRoutes}/networkRoutes.tsx (90%) create mode 100644 src/components/networkByIdPage/networkRoutes/networkRoutesTable.tsx create mode 100644 src/components/networkByIdPage/networkRoutes/routesEditCell.tsx diff --git a/prisma/migrations/20241227084426_routes_table/migration.sql b/prisma/migrations/20241227084426_routes_table/migration.sql new file mode 100644 index 00000000..cecb4cd9 --- /dev/null +++ b/prisma/migrations/20241227084426_routes_table/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - You are about to drop the column `routes` on the `network` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "network" DROP COLUMN "routes"; + +-- CreateTable +CREATE TABLE "Routes" ( + "id" TEXT NOT NULL, + "target" TEXT NOT NULL, + "via" TEXT, + "networkId" TEXT NOT NULL, + "notes" TEXT, + + CONSTRAINT "Routes_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Routes" ADD CONSTRAINT "Routes_networkId_fkey" FOREIGN KEY ("networkId") REFERENCES "network"("nwid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 672c1510..c31337c2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -110,7 +110,16 @@ model network { organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) networkMembers network_members[] notations Notation[] - routes Json? + routes Routes[] +} + +model Routes { + id String @id @default(cuid()) + target String + via String? + networkId String + notes String? + network network @relation(fields: [networkId], references: [nwid], onDelete: Cascade) } model Notation { diff --git a/src/components/networkByIdPage/networkRoutes/collumns.tsx b/src/components/networkByIdPage/networkRoutes/collumns.tsx new file mode 100644 index 00000000..bea37a09 --- /dev/null +++ b/src/components/networkByIdPage/networkRoutes/collumns.tsx @@ -0,0 +1,57 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { MemberEntity } from "~/types/local/member"; +import { RoutesEntity } from "~/types/local/network"; + +const columnHelper = createColumnHelper(); + +export const networkRoutesColumns = ( + deleteRoute: (route: RoutesEntity) => void, + isUpdating: boolean, + members: MemberEntity[], +) => [ + columnHelper.accessor("target", { + header: "Destination", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("via", { + header: "Via", + cell: (info) => ( +
+ {info.row.original.via || "LAN"} +
+ ), + }), + columnHelper.accessor("nodeName", { + header: "Node Name", + cell: (info) => { + // check if ipAssignments has the via ip and return the node name + const node = members.find((member) => + member.ipAssignments.includes(info.row.original.via), + ); + return node?.name || "N/A"; + }, + }), + columnHelper.accessor("notes", { + header: "Notes", + }), + columnHelper.accessor("actions", { + header: "", + cell: (info) => ( + !isUpdating && deleteRoute(info.row.original)} + > + + + ), + }), +]; diff --git a/src/components/networkByIdPage/networkRoutes.tsx b/src/components/networkByIdPage/networkRoutes/networkRoutes.tsx similarity index 90% rename from src/components/networkByIdPage/networkRoutes.tsx rename to src/components/networkByIdPage/networkRoutes/networkRoutes.tsx index 567efc35..87c702cf 100644 --- a/src/components/networkByIdPage/networkRoutes.tsx +++ b/src/components/networkByIdPage/networkRoutes/networkRoutes.tsx @@ -7,6 +7,7 @@ import { useTrpcApiErrorHandler, useTrpcApiSuccessHandler, } from "~/hooks/useTrpcApiHandler"; +import { NetworkRoutesTable } from "./networkRoutesTable"; const initialRouteInput = { target: "", @@ -18,7 +19,7 @@ interface IProp { organizationId?: string; } -export const NettworkRoutes = ({ central = false, organizationId }: IProp) => { +export const NetworkRoutes = ({ central = false, organizationId }: IProp) => { const b = useTranslations("commonButtons"); const t = useTranslations("networkById"); @@ -40,24 +41,13 @@ export const NettworkRoutes = ({ central = false, organizationId }: IProp) => { }, { enabled: !!query.id }, ); + const { network } = networkById || {}; - const { mutate: updateManageRoutes, isLoading: isUpdating } = - api.network.managedRoutes.useMutation({ - onError: handleApiError, - onSuccess: handleApiSuccess({ actions: [refecthNetworkById] }), - }); - - const deleteRoute = (route: RoutesEntity) => { - const _routes = [...(network.routes as RoutesEntity[])]; - const newRouteArr = _routes.filter((r) => r.target !== route.target); + const { mutate: updateManageRoutes } = api.network.managedRoutes.useMutation({ + onError: handleApiError, + onSuccess: handleApiSuccess({ actions: [refecthNetworkById] }), + }); - updateManageRoutes({ - updateParams: { routes: [...newRouteArr] }, - organizationId, - nwid: query.id as string, - central, - }); - }; const routeHandler = (event: ChangeEvent) => { setRouteInput({ ...routeInput, @@ -85,7 +75,7 @@ export const NettworkRoutes = ({ central = false, organizationId }: IProp) => { }, ); }; - const { network } = networkById || {}; + if (isLoading) return
Loading
; return ( @@ -118,6 +108,9 @@ export const NettworkRoutes = ({ central = false, organizationId }: IProp) => { ) : null}
+ +
+ {/*
{(network?.routes as RoutesEntity[]).map((route) => { return (
{
); })} -
+ */} {showRouteInput ? (
{ + const handleApiError = useTrpcApiErrorHandler(); + const handleApiSuccess = useTrpcApiSuccessHandler(); + + const { query } = useRouter(); + const { data: networkById, refetch: refecthNetworkById } = + api.network.getNetworkById.useQuery( + { + nwid: query.id as string, + central, + }, + { enabled: !!query.id }, + ); + const { network, members } = networkById || {}; + + const { mutate: updateManageRoutes, isLoading: isUpdating } = + api.network.managedRoutes.useMutation({ + onError: handleApiError, + onSuccess: handleApiSuccess({ actions: [refecthNetworkById] }), + }); + + const deleteRoute = (route: RoutesEntity) => { + const _routes = [...(network.routes as RoutesEntity[])]; + const newRouteArr = _routes.filter((r) => r.target !== route.target); + + updateManageRoutes({ + updateParams: { routes: [...newRouteArr] }, + organizationId, + nwid: query.id as string, + central, + }); + }; + + const defaultColumn = useEditableColumn({ refecthNetworkById }); + + const table = useReactTable({ + data: network?.routes ?? [], + columns: networkRoutesColumns(deleteRoute, isUpdating, members), + defaultColumn, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); +}; diff --git a/src/components/networkByIdPage/networkRoutes/routesEditCell.tsx b/src/components/networkByIdPage/networkRoutes/routesEditCell.tsx new file mode 100644 index 00000000..a1d4efd2 --- /dev/null +++ b/src/components/networkByIdPage/networkRoutes/routesEditCell.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useRef, useState } from "react"; +import { type ColumnDef } from "@tanstack/react-table"; +import { RoutesEntity } from "~/types/local/network"; +import { api } from "~/utils/api"; +import { useRouter } from "next/router"; + +interface EditableColumnConfig { + id: string; + updateParam: string; + placeholder?: string; +} + +const EDITABLE_COLUMNS: EditableColumnConfig[] = [ + { + id: "notes", + updateParam: "note", + placeholder: "Click to add notes", + }, +]; + +interface useEditableColumnProps { + refecthNetworkById: () => void; +} +export const useEditableColumn = ({ refecthNetworkById }: useEditableColumnProps) => { + const { mutate: updateManageRoutes, isLoading: isUpdating } = + api.network.managedRoutes.useMutation({ + onSuccess: refecthNetworkById, + }); + + const { query } = useRouter(); + const defaultColumn: Partial> = { + cell: ({ getValue, row: { original }, column: { id } }) => { + // Check if this column is editable + const columnConfig = EDITABLE_COLUMNS.find((col) => col.id === id); + if (!columnConfig) { + return getValue(); + } + + return ( + + ); + }, + }; + + return defaultColumn; +}; + +interface EditableCellProps { + nwid: string; + // biome-ignore lint/suspicious/noExplicitAny: + getValue: () => any; + original: RoutesEntity; + columnConfig: EditableColumnConfig; + isUpdating: boolean; + updateManageRoutes: (params: { + nwid: string; + // biome-ignore lint/suspicious/noExplicitAny: + updateParams: Record; + }) => void; +} + +const EditableCell: React.FC = ({ + nwid, + getValue, + original, + columnConfig, + isUpdating, + updateManageRoutes, +}) => { + const initialValue = getValue(); + const inputRef = useRef(null); + const [value, setValue] = useState(initialValue ?? ""); + const [isEditing, setIsEditing] = useState(false); + + useEffect(() => { + setValue(initialValue ?? ""); + }, [initialValue]); + + const handleUpdate = () => { + if (value !== initialValue) { + updateManageRoutes({ + nwid, + updateParams: { + [columnConfig.updateParam]: value, + routeId: original.id, + }, + }); + } + }; + + const onBlur = () => { + setIsEditing(false); + handleUpdate(); + }; + + const submitUpdate = (e: React.MouseEvent) => { + e.preventDefault(); + setIsEditing(false); + handleUpdate(); + inputRef.current?.blur(); + }; + + if (isEditing) { + return ( + + setValue(e.target.value)} + onBlur={onBlur} + className="input-primary input-sm m-0 border-0 bg-transparent p-0 min-w-full" + disabled={isUpdating} + /> +