SecretRow.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import dayjs from 'dayjs'
  4. import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react'
  5. import { parseAsString, useQueryState } from 'nuqs'
  6. import { useState } from 'react'
  7. import {
  8. Button,
  9. DropdownMenu,
  10. DropdownMenuContent,
  11. DropdownMenuSeparator,
  12. DropdownMenuTrigger,
  13. } from 'ui'
  14. import { Input } from 'ui-patterns/DataInputs/Input'
  15. import { SecretTableColumn } from './Secrets.types'
  16. import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
  17. import { useVaultSecretDecryptedValueQuery } from '@/data/vault/vault-secret-decrypted-value-query'
  18. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  19. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  20. import type { VaultSecret } from '@/types'
  21. interface SecretRowProps {
  22. row: VaultSecret
  23. col: SecretTableColumn
  24. }
  25. export const SecretRow = ({ row, col }: SecretRowProps) => {
  26. const { ref } = useParams()
  27. const { data: project } = useSelectedProjectQuery()
  28. const [revealSecret, setRevealSecret] = useState(false)
  29. const name = row?.name ?? 'No name provided'
  30. const [, setSelectedSecretToEdit] = useQueryState('edit', parseAsString)
  31. const [, setSelectedSecretToDelete] = useQueryState('delete', parseAsString)
  32. const { can: canManageSecrets } = useAsyncCheckPermissions(
  33. PermissionAction.TENANT_SQL_ADMIN_WRITE,
  34. 'tables'
  35. )
  36. const { data: revealedValue, isFetching } = useVaultSecretDecryptedValueQuery(
  37. {
  38. projectRef: ref!,
  39. connectionString: project?.connectionString,
  40. id: row.id,
  41. },
  42. {
  43. enabled: !!(ref! && row.id) && revealSecret,
  44. }
  45. )
  46. if (col.id === 'actions') {
  47. return (
  48. <div className="flex items-center justify-end w-full" onClick={(e) => e.stopPropagation()}>
  49. <DropdownMenu>
  50. <DropdownMenuTrigger asChild>
  51. <Button title="Manage Secret" type="text" className="px-1" icon={<MoreVertical />} />
  52. </DropdownMenuTrigger>
  53. <DropdownMenuContent side="bottom" align="end" className="w-40">
  54. <DropdownMenuItemTooltip
  55. className="gap-x-2"
  56. disabled={!canManageSecrets}
  57. onClick={() => setSelectedSecretToEdit(row.id)}
  58. tooltip={{
  59. content: { side: 'left', text: 'You need additional permissions to edit secrets' },
  60. }}
  61. >
  62. <Edit3 size={12} />
  63. <p>Edit</p>
  64. </DropdownMenuItemTooltip>
  65. <DropdownMenuSeparator />
  66. <DropdownMenuItemTooltip
  67. className="gap-x-2"
  68. disabled={!canManageSecrets}
  69. onClick={() => setSelectedSecretToDelete(row.id)}
  70. tooltip={{
  71. content: {
  72. side: 'left',
  73. text: 'You need additional permissions to delete secrets',
  74. },
  75. }}
  76. >
  77. <Trash size={12} />
  78. <p className="text-foreground-light">Delete</p>
  79. </DropdownMenuItemTooltip>
  80. </DropdownMenuContent>
  81. </DropdownMenu>
  82. </div>
  83. )
  84. }
  85. if (col.id === 'secret_value') {
  86. return (
  87. <div className="flex items-center gap-2 w-full" onClick={(e) => e.stopPropagation()}>
  88. <Button
  89. type="text"
  90. className="px-1.5"
  91. icon={
  92. isFetching && revealedValue === undefined ? (
  93. <Loader className="animate-spin" size={16} strokeWidth={1.5} />
  94. ) : !revealSecret ? (
  95. <Eye size={16} strokeWidth={1.5} />
  96. ) : (
  97. <EyeOff size={16} strokeWidth={1.5} />
  98. )
  99. }
  100. onClick={() => setRevealSecret(!revealSecret)}
  101. />
  102. <div className="grow min-w-0">
  103. {revealSecret && revealedValue !== undefined ? (
  104. <Input copy readOnly size="tiny" className="font-mono" value={revealedValue} />
  105. ) : (
  106. <p className="text-sm font-mono text-foreground">••••••••••••••••••</p>
  107. )}
  108. </div>
  109. </div>
  110. )
  111. }
  112. if (col.id === 'updated_at') {
  113. return (
  114. <div className="w-full flex items-center justify-start">
  115. <p className="text-xs text-foreground-light">
  116. {row.updated_at === row.created_at ? 'Added' : 'Updated'} on{' '}
  117. {dayjs(row.updated_at).format('MMM D, YYYY')}
  118. </p>
  119. </div>
  120. )
  121. }
  122. if (col.id === 'id') {
  123. return (
  124. <div className="w-full flex items-center">
  125. <Key size={12} strokeWidth={2} className="text-foreground-light mr-2" />
  126. <p className="text-foreground-light text-xs font-mono truncate" title={row.id}>
  127. {row.id}
  128. </p>
  129. </div>
  130. )
  131. }
  132. return (
  133. <div className="w-full flex flex-col justify-center">
  134. <p className="text-xs text-foreground truncate select-text" title={name}>
  135. {name}
  136. </p>
  137. {row.description !== undefined && row.description !== '' && (
  138. <div>
  139. <p className="text-xs text-foreground-lighter w-full truncate select-text">
  140. {row.description}
  141. </p>
  142. </div>
  143. )}
  144. </div>
  145. )
  146. }