teableio/teable

View on GitHub
apps/nextjs-app/src/features/app/blocks/db-connection/Panel.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Copy, Database, HelpCircle } from '@teable/icons';
import { deleteDbConnection, getDbConnection, createDbConnection } from '@teable/openapi';
import { useBaseId, useBasePermission } from '@teable/sdk/hooks';
import {
  Button,
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
  Input,
  Label,
  Skeleton,
} from '@teable/ui-lib/shadcn';
import { Trans, useTranslation } from 'next-i18next';
import { tableConfig } from '@/features/i18n/table.config';

const ContentCard = () => {
  const baseId = useBaseId() as string;
  const queryClient = useQueryClient();
  const { t } = useTranslation(tableConfig.i18nNamespaces);
  const { data, isLoading } = useQuery({
    queryKey: ['connection', baseId],
    queryFn: ({ queryKey }) => getDbConnection(queryKey[1]).then((data) => data.data),
  });

  const mutationCreate = useMutation(createDbConnection, {
    onSuccess: () => {
      queryClient.invalidateQueries(['connection', baseId]);
    },
  });

  const mutationDelete = useMutation(deleteDbConnection, {
    onSuccess: () => {
      queryClient.invalidateQueries(['connection', baseId]);
    },
  });
  const dataArray = data?.dsn
    ? Object.entries(data?.dsn).map(([label, value]) => {
        if (label === 'params') {
          return {
            label,
            type: 'text',
            value: Object.entries(value)
              .map((v) => v.join('='))
              .join('&'),
          };
        }
        if (label === 'pass') {
          return {
            label,
            type: 'password',
            value: String(value ?? ''),
          };
        }
        return { label, type: 'text', value: String(value ?? '') };
      })
    : [];

  dataArray.unshift({
    label: 'url',
    type: 'text',
    value: data?.url || '',
  });

  return (
    <div className="flex flex-col gap-4">
      {isLoading ? (
        <div className="space-y-2">
          <Skeleton className="h-6 w-full" />
          <Skeleton className="h-6 w-full" />
          <Skeleton className="h-6 w-full" />
          <Skeleton className="h-6 w-full" />
          <Skeleton className="h-6 w-full" />
        </div>
      ) : (
        <>
          <div className="flex flex-col gap-2">
            {dataArray.map(({ label, type, value }) => (
              <div key={label} className="flex flex-col gap-2">
                {data ? (
                  <div className="flex items-center gap-2">
                    <Label className="w-20" htmlFor="subject">
                      {label}
                    </Label>
                    <Input
                      readOnly
                      data-pass={label === 'pass' ? true : undefined}
                      type={type}
                      value={value}
                      onMouseEnter={(e) => {
                        if ((e.target as HTMLInputElement).type === 'password') {
                          (e.target as HTMLInputElement).type = 'text';
                        }
                      }}
                      onMouseLeave={(e) => {
                        console.log(e.target);
                        if ((e.target as HTMLInputElement).getAttribute('data-pass')) {
                          (e.target as HTMLInputElement).type = 'password';
                        }
                      }}
                    />
                    <Button
                      className="shrink-0"
                      size="icon"
                      variant={'outline'}
                      onClick={() => {
                        navigator.clipboard.writeText(value);
                      }}
                    >
                      <Copy className="size-4" />
                    </Button>
                  </div>
                ) : (
                  <div className="flex h-20 justify-center">
                    <Database className="size-20 text-neutral-600" />
                  </div>
                )}
              </div>
            ))}
          </div>
          {data && (
            <div className="text-sm text-secondary-foreground">
              <Trans
                ns="table"
                i18nKey="connection.connectionCountTip"
                components={{ b: <b /> }}
                values={{
                  max: data.connection.max,
                  current: data.connection.current,
                }}
              />
            </div>
          )}
          <div className="flex justify-end">
            {data ? (
              <Button size="sm" onClick={() => mutationDelete.mutate(baseId)}>
                {t('common:actions.delete')}
              </Button>
            ) : (
              <Button size="sm" onClick={() => mutationCreate.mutate(baseId)}>
                {t('common:actions.create')}
              </Button>
            )}
          </div>
        </>
      )}
    </div>
  );
};

export const DbConnectionPanel = ({ className }: { className?: string }) => {
  const { t } = useTranslation(tableConfig.i18nNamespaces);
  const permissions = useBasePermission();

  return (
    <Card className={className}>
      <CardHeader className="py-4">
        <CardTitle>
          {t('table:connection.title')}
          <Button variant="ghost" size="icon">
            <a href={t('table:connection.helpLink')} target="_blank" rel="noreferrer">
              <HelpCircle className="size-4" />
            </a>
          </Button>
        </CardTitle>
        <CardDescription>{t('table:connection.description')}</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col">
        {permissions?.['base|db_connection'] ? <ContentCard /> : t('table:connection.noPermission')}
      </CardContent>
    </Card>
  );
};