| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { sortBy } from 'lodash'
- import { RefreshCw, Search, X } from 'lucide-react'
- import { parseAsBoolean, useQueryState } from 'nuqs'
- import { useEffect, useMemo, useState } from 'react'
- import DataGrid, { Row } from 'react-data-grid'
- import {
- Button,
- cn,
- LoadingLine,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- } from 'ui'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import { AddNewSecretModal } from './AddNewSecretModal'
- import { DeleteSecretModal } from './DeleteSecretModal'
- import { EditSecretModal } from './EditSecretModal'
- import { formatSecretColumns } from './Secrets.utils'
- import AlertError from '@/components/ui/AlertError'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { DocsButton } from '@/components/ui/DocsButton'
- import { useVaultSecretsQuery } from '@/data/vault/vault-secrets-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL } from '@/lib/constants'
- import type { VaultSecret } from '@/types'
- export const SecretsManagement = () => {
- const { search } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const [searchValue, setSearchValue] = useState<string>('')
- const [, setShowAddSecretModal] = useQueryState('new', parseAsBoolean.withDefault(false))
- const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at')
- const { can: canManageSecrets } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'tables'
- )
- const {
- data,
- error,
- isError,
- isPending: isLoading,
- isRefetching,
- refetch,
- } = useVaultSecretsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const allSecrets = useMemo(() => data || [], [data])
- const secrets = useMemo(() => {
- const filtered =
- searchValue.length > 0
- ? allSecrets.filter(
- (secret) =>
- (secret?.name ?? '').toLowerCase().includes(searchValue.trim().toLowerCase()) ||
- (secret?.id ?? '').toLowerCase().includes(searchValue.trim().toLowerCase())
- )
- : allSecrets
- if (selectedSort === 'updated_at') {
- return sortBy(filtered, (s) => Number(new Date(s.updated_at))).reverse()
- }
- return sortBy(filtered, (s) => (s.name || '').toLowerCase())
- }, [allSecrets, searchValue, selectedSort])
- const columns = useMemo(() => formatSecretColumns(), [])
- useEffect(() => {
- if (search !== undefined) setSearchValue(search)
- }, [search])
- return (
- <>
- <div className="h-full w-full space-y-4">
- <div className="h-full w-full flex flex-col relative">
- <div className="bg-surface-200 py-3 px-10 flex items-center justify-between flex-wrap">
- <div className="flex items-center gap-2">
- <Input
- size="tiny"
- className="w-52"
- placeholder="Search by name or key ID"
- icon={<Search />}
- value={searchValue ?? ''}
- onChange={(e) => setSearchValue(e.target.value)}
- actions={[
- searchValue && (
- <Button
- key="clear"
- size="tiny"
- type="text"
- icon={<X />}
- onClick={() => setSearchValue('')}
- className="p-0 h-5 w-5"
- />
- ),
- ]}
- />
- <Select value={selectedSort} onValueChange={(v) => setSelectedSort(v as any)}>
- <SelectTrigger size="tiny" className="w-44">
- <SelectValue asChild>
- <>Sort by {selectedSort}</>
- </SelectValue>
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="updated_at" className="text-xs">
- Updated at
- </SelectItem>
- <SelectItem value="name" className="text-xs">
- Name
- </SelectItem>
- </SelectContent>
- </Select>
- </div>
- <div className="flex items-center gap-x-2">
- <Button
- type="default"
- icon={<RefreshCw />}
- loading={isRefetching}
- onClick={() => refetch()}
- >
- Refresh
- </Button>
- <DocsButton href={`${DOCS_URL}/guides/database/vault`} />
- <ButtonTooltip
- type="primary"
- disabled={!canManageSecrets}
- onClick={() => setShowAddSecretModal(true)}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canManageSecrets
- ? 'You need additional permissions to add secrets'
- : undefined,
- },
- }}
- >
- Add new secret
- </ButtonTooltip>
- </div>
- </div>
- <LoadingLine loading={isLoading || isRefetching} />
- {isError ? (
- <div className="grow p-4">
- <AlertError error={error} subject="Failed to load secrets" />
- </div>
- ) : (
- <DataGrid
- className="grow border-t-0"
- rowHeight={52}
- headerRowHeight={36}
- columns={columns}
- rows={secrets}
- rowKeyGetter={(row: VaultSecret) => row.id}
- rowClass={() => {
- return cn(
- 'cursor-pointer',
- '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none',
- '[&>.rdg-cell:first-child>div]:pl-8'
- )
- }}
- renderers={{
- renderRow(_, props) {
- return <Row key={(props.row as VaultSecret).id} {...props} />
- },
- }}
- />
- )}
- {secrets.length === 0 && !isLoading && !isError ? (
- <div className="absolute top-32 px-6 w-full">
- <div className="text-center text-sm flex flex-col gap-y-1">
- <p className="text-foreground">
- {searchValue ? 'No secrets found' : 'No secrets added yet'}
- </p>
- <p className="text-foreground-light">
- {searchValue
- ? `There are currently no secrets based on the search "${searchValue}"`
- : 'The Vault allows you to store sensitive information like API keys'}
- </p>
- </div>
- </div>
- ) : null}
- </div>
- </div>
- <AddNewSecretModal />
- <EditSecretModal />
- <DeleteSecretModal />
- </>
- )
- }
|