Commit 78c7c2f8 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: implement a new data table component with drag-and-drop functionality...

feat: implement a new data table component with drag-and-drop functionality and filtering options and varius other improvements
parent 15b26f90
Loading
Loading
Loading
Loading
+0 −212
Changes for portal-gui/src/components/applications/app-deployment-table.tsx: 0 added lines, 212 removed lines.
Original line number Diff line number Diff line
import * as React from 'react';
import {
  closestCenter,
  DndContext,
  KeyboardSensor,
  MouseSensor,
  TouchSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
  arrayMove,
  SortableContext,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
  type ColumnFiltersState,
  type SortingState,
  type VisibilityState,
} from '@tanstack/react-table';

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { TablePagination } from '../data-table/table-pagination';
import { DraggableRow } from '../data-table/dragable-row';
import { getAppDeploymentColumns } from './app-deployment-columns';

import type { AppDeployTableProps } from '@/types/application';

import { AppDeploymentToolbar } from './app-deployment-toolbar';

export function AppDepploymentTable({
  data: initialData,
  onDetails,
  onDelete,
  appId,
  setAppId,
  edgeZoneId,
  setEdgeZoneId,
  apps,
  zones,
  onRefresh,
}: AppDeployTableProps) {
  const [data, setData] = React.useState(initialData);
  React.useEffect(() => {
    setData((currentData) => {
      const currentIds = new Set(
        currentData.map((item) => item.appDeploymentId),
      );

      const newIds = new Set(initialData.map((item) => item.appDeploymentId));

      const idsChanged =
        currentIds.size !== newIds.size ||
        [...currentIds].some((id) => !newIds.has(id));

      return idsChanged ? initialData : currentData;
    });
  }, [initialData]);

  const [sorting, setSorting] = React.useState<SortingState>([]);
  const sortableId = React.useId();
  const sensors = useSensors(
    useSensor(MouseSensor, {}),
    useSensor(TouchSensor, {}),
    useSensor(KeyboardSensor, {}),
  );

  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    [],
  );
  const [columnVisibility, setColumnVisibility] =
    React.useState<VisibilityState>({});
  const [rowSelection, setRowSelection] = React.useState({});
  const columns = React.useMemo(
    () => getAppDeploymentColumns(onDetails, onDelete),
    [onDetails, onDelete],
  );
  const table = useReactTable({
    data,
    columns,
    getRowId: (row) => row.appDeploymentId,
    state: {
      sorting,
      columnVisibility,
      rowSelection,
      columnFilters,
    },
    enableRowSelection: true,
    onRowSelectionChange: setRowSelection,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;

    if (!over || active.id === over.id) {
      return;
    }

    setData((currentData) => {
      const oldIndex = currentData.findIndex(
        (deployment) => deployment.appDeploymentId === active.id,
      );

      const newIndex = currentData.findIndex(
        (deployment) => deployment.appDeploymentId === over.id,
      );

      if (oldIndex === -1 || newIndex === -1) {
        return currentData;
      }

      return arrayMove(currentData, oldIndex, newIndex);
    });
  }

  return (
    <>
      <AppDeploymentToolbar
        table={table}
        apps={apps}
        zones={zones}
        appId={appId}
        onAppChange={setAppId}
        edgeZoneId={edgeZoneId}
        onEdgeZoneChange={setEdgeZoneId}
        onRefresh={() => void onRefresh()}
      />
      <div className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
        <div className="overflow-x-auto rounded-lg border">
          <DndContext
            collisionDetection={closestCenter}
            modifiers={[restrictToVerticalAxis]}
            onDragEnd={handleDragEnd}
            sensors={sensors}
            id={sortableId}
          >
            <Table className="min-w-[700px]">
              <TableHeader className="sticky top-0 z-10 bg-muted">
                {table.getHeaderGroups().map((headerGroup) => (
                  <TableRow key={headerGroup.id}>
                    {headerGroup.headers.map((header) => {
                      return (
                        <TableHead
                          key={header.id}
                          colSpan={header.colSpan}
                          className={
                            header.column.columnDef.meta?.headerClassName
                          }
                        >
                          {header.isPlaceholder
                            ? null
                            : flexRender(
                                header.column.columnDef.header,
                                header.getContext(),
                              )}
                        </TableHead>
                      );
                    })}
                  </TableRow>
                ))}
              </TableHeader>
              <TableBody className="**:data-[slot=table-cell]:first:w-8">
                {table.getRowModel().rows?.length ? (
                  <SortableContext
                    items={data.map((deploment) => deploment.appDeploymentId)}
                    strategy={verticalListSortingStrategy}
                  >
                    {table.getRowModel().rows.map((row) => (
                      <DraggableRow key={row.id} row={row} />
                    ))}
                  </SortableContext>
                ) : (
                  <TableRow>
                    <TableCell
                      colSpan={columns.length}
                      className="h-24 text-center"
                    >
                      No results.
                    </TableCell>
                  </TableRow>
                )}
              </TableBody>
            </Table>
          </DndContext>
        </div>
        <TablePagination table={table} />
      </div>
    </>
  );
}
+0 −137
Changes for portal-gui/src/components/applications/app-deployment-toolbar.tsx: 0 added lines, 137 removed lines.
Original line number Diff line number Diff line
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

import { Columns3Icon, ChevronDownIcon, SearchIcon } from 'lucide-react';
import { RefreshCw } from 'lucide-react';

import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

import type { AppDeploymentToolbarProps } from '@/types/application';

const selectClass = 'h-9 w-full rounded-md border bg-background px-3 text-sm';
export function AppDeploymentToolbar({
  table,
  appId,
  onAppChange,
  apps,
  edgeZoneId,
  onEdgeZoneChange,
  zones,
  onRefresh,
}: AppDeploymentToolbarProps) {
  return (
    <>
      <div className="flex items-center justify-between px-4 lg:px-6">
        <div className="flex flex-1 items-center gap-3">
          <div className="relative max-w-sm flex-1">
            {/* Search Field*/}

            <SearchIcon className="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />

            <Input
              placeholder="Search Deployments..."
              className="pl-9"
              value={
                (table.getColumn('name')?.getFilterValue() as string) ?? ''
              }
              onChange={(e) =>
                table.getColumn('name')?.setFilterValue(e.target.value)
              }
            />
          </div>
        </div>

        {/* Columns */}
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="outline" size="sm">
              <Columns3Icon data-icon="inline-start" />
              <span className="hidden sm:inline">Columns</span>

              <ChevronDownIcon data-icon="inline-end" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="w-32">
            {table
              .getAllColumns()
              .filter((column) => column.getCanHide())
              .map((column) => {
                return (
                  <DropdownMenuCheckboxItem
                    key={column.id}
                    className="capitalize"
                    checked={column.getIsVisible()}
                    onCheckedChange={(value) =>
                      column.toggleVisibility(!!value)
                    }
                  >
                    {typeof column.columnDef.header === 'string'
                      ? column.columnDef.header
                      : column.id}
                  </DropdownMenuCheckboxItem>
                );
              })}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>

      <div className="flex items-end justify-between px-4 lg:px-6">
        <div className="flex flex-wrap items-end gap-3">
          {/* Application filter */}
          <label className="w-48 space-y-1.5 text-sm">
            <span className="block font-medium">Application</span>

            <select
              className={selectClass}
              value={appId}
              onChange={(event) => onAppChange(event.target.value)}
            >
              <option value="">All applications</option>

              {apps.map((app) => (
                <option key={app.appId} value={app.appId}>
                  {app.name}
                </option>
              ))}
            </select>
          </label>

          {/* Edge Zone filter */}
          <label className="w-48 space-y-1.5 text-sm">
            <span className="block font-medium">Edge Zone</span>

            <select
              className={selectClass}
              value={edgeZoneId}
              onChange={(event) => onEdgeZoneChange(event.target.value)}
            >
              <option value="">All edge zones</option>

              {zones.map((zone) => (
                <option key={zone.edgeCloudZoneId} value={zone.edgeCloudZoneId}>
                  {zone.edgeCloudZoneName}
                </option>
              ))}
            </select>
          </label>

          {/* Refresh */}

          <Button
            variant="outline"
            className="h-9"
            onClick={() => void onRefresh()}
          >
            <RefreshCw className="mr-2 h-4 w-4" />
            Refresh
          </Button>
        </div>
      </div>
    </>
  );
}
+0 −210
Changes for portal-gui/src/components/applications/app-instance-table.tsx: 0 added lines, 210 removed lines.
Original line number Diff line number Diff line
import * as React from 'react';
import {
  closestCenter,
  DndContext,
  KeyboardSensor,
  MouseSensor,
  TouchSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
  arrayMove,
  SortableContext,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
  type ColumnFiltersState,
  type SortingState,
  type VisibilityState,
} from '@tanstack/react-table';

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { TablePagination } from '../data-table/table-pagination';
import { DraggableRow } from '../data-table/dragable-row';
import { getAppInstanceColumns } from './app-instance-columns';
import { AppInstanceToolbar } from './app-instance-toolbar';
import type { AppInstanceTableProps } from '@/types/application';

// import { AppInstanceToolbar } from './app-instance-toolbar';

export function AppInstanceTable({
  data: initialData,
  onDetails,
  onDelete,
  appId,
  setAppId,
  edgeZoneId,
  setEdgeZoneId,
  apps,
  zones,
  onRefresh,
}: AppInstanceTableProps) {
  const [data, setData] = React.useState(initialData);
  React.useEffect(() => {
    setData((currentData) => {
      const currentIds = new Set(currentData.map((item) => item.appInstanceId));

      const newIds = new Set(initialData.map((item) => item.appInstanceId));

      const idsChanged =
        currentIds.size !== newIds.size ||
        [...currentIds].some((id) => !newIds.has(id));

      return idsChanged ? initialData : currentData;
    });
  }, [initialData]);

  const [sorting, setSorting] = React.useState<SortingState>([]);
  const sortableId = React.useId();
  const sensors = useSensors(
    useSensor(MouseSensor, {}),
    useSensor(TouchSensor, {}),
    useSensor(KeyboardSensor, {}),
  );

  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
    [],
  );
  const [columnVisibility, setColumnVisibility] =
    React.useState<VisibilityState>({});
  const [rowSelection, setRowSelection] = React.useState({});
  const columns = React.useMemo(
    () => getAppInstanceColumns(onDetails, onDelete),
    [onDetails, onDelete],
  );
  const table = useReactTable({
    data,
    columns,
    getRowId: (row) => row.appInstanceId,
    state: {
      sorting,
      columnVisibility,
      rowSelection,
      columnFilters,
    },
    enableRowSelection: true,
    onRowSelectionChange: setRowSelection,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;

    if (!over || active.id === over.id) {
      return;
    }

    setData((currentData) => {
      const oldIndex = currentData.findIndex(
        (deployment) => deployment.appInstanceId === active.id,
      );

      const newIndex = currentData.findIndex(
        (deployment) => deployment.appInstanceId === over.id,
      );

      if (oldIndex === -1 || newIndex === -1) {
        return currentData;
      }

      return arrayMove(currentData, oldIndex, newIndex);
    });
  }
  // ++++++++++++++++++++++++++
  return (
    <>
      <AppInstanceToolbar
        table={table}
        apps={apps}
        zones={zones}
        appId={appId}
        onAppChange={setAppId}
        edgeZoneId={edgeZoneId}
        onEdgeZoneChange={setEdgeZoneId}
        onRefresh={() => void onRefresh()}
      />
      <div className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
        <div className="overflow-x-auto rounded-lg border">
          <DndContext
            collisionDetection={closestCenter}
            modifiers={[restrictToVerticalAxis]}
            onDragEnd={handleDragEnd}
            sensors={sensors}
            id={sortableId}
          >
            <Table className="min-w-[700px]">
              <TableHeader className="sticky top-0 z-10 bg-muted">
                {table.getHeaderGroups().map((headerGroup) => (
                  <TableRow key={headerGroup.id}>
                    {headerGroup.headers.map((header) => {
                      return (
                        <TableHead
                          key={header.id}
                          colSpan={header.colSpan}
                          className={
                            header.column.columnDef.meta?.headerClassName
                          }
                        >
                          {header.isPlaceholder
                            ? null
                            : flexRender(
                                header.column.columnDef.header,
                                header.getContext(),
                              )}
                        </TableHead>
                      );
                    })}
                  </TableRow>
                ))}
              </TableHeader>
              <TableBody className="**:data-[slot=table-cell]:first:w-8">
                {table.getRowModel().rows?.length ? (
                  <SortableContext
                    items={data.map((instance) => instance.appInstanceId)}
                    strategy={verticalListSortingStrategy}
                  >
                    {table.getRowModel().rows.map((row) => (
                      <DraggableRow key={row.id} row={row} />
                    ))}
                  </SortableContext>
                ) : (
                  <TableRow>
                    <TableCell
                      colSpan={columns.length}
                      className="h-24 text-center"
                    >
                      No results.
                    </TableCell>
                  </TableRow>
                )}
              </TableBody>
            </Table>
          </DndContext>
        </div>
        <TablePagination table={table} />
      </div>
    </>
  );
}
+0 −137
Changes for portal-gui/src/components/applications/app-instance-toolbar.tsx: 0 added lines, 137 removed lines.
Original line number Diff line number Diff line
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

import { Columns3Icon, ChevronDownIcon, SearchIcon } from 'lucide-react';
import { RefreshCw } from 'lucide-react';

import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

import type { AppInstanceToolbarProps } from '@/types/application';

const selectClass = 'h-9 w-full rounded-md border bg-background px-3 text-sm';
export function AppInstanceToolbar({
  table,
  appId,
  onAppChange,
  apps,
  edgeZoneId,
  onEdgeZoneChange,
  zones,
  onRefresh,
}: AppInstanceToolbarProps) {
  return (
    <>
      <div className="flex items-center justify-between px-4 lg:px-6">
        <div className="flex flex-1 items-center gap-3">
          <div className="relative max-w-sm flex-1">
            {/* Search Field*/}

            <SearchIcon className="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />

            <Input
              placeholder="Search Deployments..."
              className="pl-9"
              value={
                (table.getColumn('name')?.getFilterValue() as string) ?? ''
              }
              onChange={(e) =>
                table.getColumn('name')?.setFilterValue(e.target.value)
              }
            />
          </div>
        </div>

        {/* Columns */}
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="outline" size="sm">
              <Columns3Icon data-icon="inline-start" />
              <span className="hidden sm:inline">Columns</span>

              <ChevronDownIcon data-icon="inline-end" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="w-32">
            {table
              .getAllColumns()
              .filter((column) => column.getCanHide())
              .map((column) => {
                return (
                  <DropdownMenuCheckboxItem
                    key={column.id}
                    className="capitalize"
                    checked={column.getIsVisible()}
                    onCheckedChange={(value) =>
                      column.toggleVisibility(!!value)
                    }
                  >
                    {typeof column.columnDef.header === 'string'
                      ? column.columnDef.header
                      : column.id}
                  </DropdownMenuCheckboxItem>
                );
              })}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>

      <div className="flex items-end justify-between px-4 lg:px-6">
        <div className="flex flex-wrap items-end gap-3">
          {/* Application filter */}
          <label className="w-48 space-y-1.5 text-sm">
            <span className="block font-medium">Application</span>

            <select
              className={selectClass}
              value={appId}
              onChange={(event) => onAppChange(event.target.value)}
            >
              <option value="">All applications</option>

              {apps.map((app) => (
                <option key={app.appId} value={app.appId}>
                  {app.name}
                </option>
              ))}
            </select>
          </label>

          {/* Edge Zone filter */}
          <label className="w-48 space-y-1.5 text-sm">
            <span className="block font-medium">Edge Zone</span>

            <select
              className={selectClass}
              value={edgeZoneId}
              onChange={(event) => onEdgeZoneChange(event.target.value)}
            >
              <option value="">All edge zones</option>

              {zones.map((zone) => (
                <option key={zone.edgeCloudZoneId} value={zone.edgeCloudZoneId}>
                  {zone.edgeCloudZoneName}
                </option>
              ))}
            </select>
          </label>

          {/* Refresh */}

          <Button
            variant="outline"
            className="h-9"
            onClick={() => void onRefresh()}
          >
            <RefreshCw className="mr-2 h-4 w-4" />
            Refresh
          </Button>
        </div>
      </div>
    </>
  );
}
+2 −0
Changes for portal-gui/src/components/applications/app-profile-columns .tsx: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -42,7 +42,9 @@ export function getAppProfileColumns(
    },
    {
      id: 'package',
      accessorKey: 'packageType',
      header: 'Package',
      filterFn: 'equalsString',

      cell: ({ row }) => (
        <Badge variant="outline">{row.original.packageType}</Badge>
Loading