Commit 20ea2c4c authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: enhance API client management with deletion functionality and confirmation dialog

parent f0dddc8c
Loading
Loading
Loading
Loading
+17 −1
Changes for portal-gui/src/components/api-clients/api-client-columns.tsx: 17 added lines, 1 removed line.
Original line number Diff line number Diff line
import type { ColumnDef } from '@tanstack/react-table';

import { actionsColumn } from '@/components/data-table/actions-column';
import { Badge } from '@/components/ui/badge';
import type { ApiClient } from '@/types/api-clients';

export const apiClientColumns: ColumnDef<ApiClient>[] = [
export function getApiClientColumns(
  onDelete: (client: ApiClient) => void,
): ColumnDef<ApiClient>[] {
  return [
    {
      id: 'drag',
      header: () => null,
@@ -67,4 +71,16 @@ export const apiClientColumns: ColumnDef<ApiClient>[] = [
          : '—',
      size: 120,
    },
    actionsColumn((client) => ({
      id: client.id,
      label: client.name,
      actions: [
        {
          label: 'Delete',
          destructive: true,
          onSelect: () => onDelete(client),
        },
      ],
    })),
  ];
}
+39 −7
Changes for portal-gui/src/features/api-clients/api-clients.tsx: 39 added lines, 7 removed lines.
Original line number Diff line number Diff line
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Plus } from 'lucide-react';

import {
  ApiClientsFootnote,
  ApiClientsNotice,
} from '@/components/api-clients/api-clients-notices';
import { apiClientColumns } from '@/components/api-clients/api-client-columns';
import { getApiClientColumns } from '@/components/api-clients/api-client-columns';
import { ApiClientSecretDialog } from '@/components/api-clients/api-client-secret-dialog';
import { CreateApiClientDialog } from '@/components/api-clients/create-api-client-dialog';
import { Button } from '@/components/ui/button';
import { ConfirmActionDialog } from '@/components/common/confirm-action-dialog';
import { DataTable } from '@/components/data-table/data-table';
import { EmptyState } from '@/components/common/empty-state';
import { PageContent } from '@/components/common/page-content';
import { PageHeader } from '@/components/common/page-header';
import { PageState } from '@/components/common/page-state';
import { useApiClients } from '@/hooks/useApiClients';
import type { CreatedApiClient } from '@/types/api-clients';
import { deleteApiClient } from '@/services/api-clients/api-clients';
import type { ApiClient, CreatedApiClient } from '@/types/api-clients';

export function ApiClientsPage() {
  const { clients, loading, error, unavailable, retry } = useApiClients();
  const { clients, loading, error, unavailable, retry, removeClient } =
    useApiClients();
  const [creating, setCreating] = useState(false);
  const [created, setCreated] = useState<CreatedApiClient | null>(null);
  const [deleting, setDeleting] = useState<ApiClient | null>(null);
  const columns = useMemo(() => getApiClientColumns(setDeleting), []);

  return (
    <>
@@ -36,14 +41,14 @@ export function ApiClientsPage() {
      />

      <PageContent>
        <ApiClientsNotice />

        {unavailable ? (
          <EmptyState
            title="API clients are not available"
            description="This deployment is not set up for API clients."
          />
        ) : (
          <>
            <ApiClientsNotice />
            <PageState
              loading={loading}
              error={error}
@@ -55,13 +60,14 @@ export function ApiClientsPage() {
            >
              <DataTable
                data={clients}
              columns={apiClientColumns}
                columns={columns}
                getRowId={(client) => client.id}
                noun="API clients"
                onRefresh={retry}
              />
              <ApiClientsFootnote />
            </PageState>
          </>
        )}
      </PageContent>

@@ -75,6 +81,32 @@ export function ApiClientsPage() {
          }}
        />
      )}
      {deleting && (
        <ConfirmActionDialog
          title="Delete API client?"
          description={
            <>
              Are you sure you want to delete{' '}
              <span className="font-semibold text-foreground">
                {deleting.name}
              </span>
              ? Its secret stops working at once and its products are revoked.
              This cannot be undone. To replace a leaked secret, delete the
              client and create a new one.
            </>
          }
          confirmLabel="Delete"
          pendingLabel="Deleting…"
          failureMessage="Failed to delete the API client."
          onOpenChange={(open) => {
            if (!open) setDeleting(null);
          }}
          onConfirm={async () => {
            await deleteApiClient(deleting.id);
            removeClient(deleting.id);
          }}
        />
      )}
      {created && (
        <ApiClientSecretDialog
          client={created}
+6 −1
Changes for portal-gui/src/hooks/useApiClients.ts: 6 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -41,5 +41,10 @@ export function useApiClients() {
    setReloadKey((key) => key + 1);
  }, []);

  return { clients, loading, error, unavailable, retry };
  const removeClient = useCallback(
    (id: string) => setClients((rows) => rows.filter((row) => row.id !== id)),
    [],
  );

  return { clients, loading, error, unavailable, retry, removeClient };
}
+7 −1
Changes for portal-gui/src/services/api-clients/api-clients.ts: 7 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -27,7 +27,8 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
      problem?.detail ?? `Request failed (${response.status})`,
    );
  }
  return (await response.json()) as T;
  // 204: nothing to read.
  return (response.status === 204 ? undefined : await response.json()) as T;
}

export const listApiClients = () => request<ApiClient[]>('/api/api-clients');
@@ -37,6 +38,11 @@ export const listApiProducts = () => request<ApiProduct[]>('/api/api-products');
export const getConnection = () =>
  request<Connection>('/api/api-clients/connection');

export const deleteApiClient = (id: string) =>
  request<void>(`/api/api-clients/${encodeURIComponent(id)}`, {
    method: 'DELETE',
  });

export const createApiClient = (body: CreateApiClientRequest) =>
  request<CreatedApiClient>('/api/api-clients', {
    method: 'POST',