Description.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { ident, literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { noop } from 'lodash'
  4. import { Loader } from 'lucide-react'
  5. import { useState } from 'react'
  6. import { toast } from 'sonner'
  7. import { Button, ExpandingTextArea } from 'ui'
  8. import { executeSql } from '@/data/sql/execute-sql-query'
  9. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  10. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  11. import { timeout } from '@/lib/helpers'
  12. // Removes some auto-generated Postgrest text
  13. // Ideally PostgREST wouldn't add this if there is already a comment
  14. const temp_removePostgrestText = (content: string) => {
  15. const postgrestTextPk = `Note:\nThis is a Primary Key.<pk/>`
  16. const postgrestTextFk = `Note:\nThis is a Foreign Key to`
  17. const pkTextPos = content.lastIndexOf(postgrestTextPk)
  18. const fkTextPos = content.lastIndexOf(postgrestTextFk)
  19. let cleansed = content
  20. if (pkTextPos >= 0) cleansed = cleansed.substring(0, pkTextPos)
  21. if (fkTextPos >= 0) cleansed = cleansed.substring(0, fkTextPos)
  22. return cleansed
  23. }
  24. interface DescrptionProps {
  25. content: string
  26. metadata: { table?: string; column?: string; rpc?: string }
  27. onChange: (value: string) => void
  28. }
  29. const Description = ({ content, metadata, onChange = noop }: DescrptionProps) => {
  30. const contentText = temp_removePostgrestText(content || '').trim()
  31. const [value, setValue] = useState(contentText)
  32. const [isUpdating, setIsUpdating] = useState(false)
  33. const { data: project } = useSelectedProjectQuery()
  34. const { table, column, rpc } = metadata
  35. const hasChanged = value != contentText
  36. const animateCss = `transition duration-150`
  37. const { can: canUpdateDescription } = useAsyncCheckPermissions(
  38. PermissionAction.TENANT_SQL_QUERY,
  39. '*'
  40. )
  41. const updateDescription = async () => {
  42. if (isUpdating || !canUpdateDescription) return false
  43. setIsUpdating(true)
  44. let query: SafeSqlFragment | undefined
  45. if (table && column)
  46. query = safeSql`comment on column ${ident('public')}.${ident(table)}.${ident(column)} is ${literal(value)};`
  47. if (table && !column)
  48. query = safeSql`comment on table ${ident('public')}.${ident(table)} is ${literal(value)};`
  49. if (rpc) query = safeSql`comment on function ${ident(rpc)} is ${literal(value)};`
  50. if (query) {
  51. try {
  52. await executeSql({
  53. projectRef: project?.ref,
  54. connectionString: project?.connectionString,
  55. sql: query,
  56. })
  57. // [Joshen] Temp fix, immediately refreshing the docs fetches stale state
  58. await timeout(500)
  59. toast.success(`Successfully updated description`)
  60. } catch (error: any) {
  61. toast.error(`Failed to update description: ${error.message}`)
  62. }
  63. }
  64. onChange(value)
  65. setIsUpdating(false)
  66. }
  67. if (!canUpdateDescription) {
  68. return (
  69. <span className={`block text-sm ${value ? 'text-foreground' : ''}`}>
  70. {value || 'No description'}
  71. </span>
  72. )
  73. }
  74. return (
  75. <div className="space-y-2 px-0.5">
  76. <ExpandingTextArea
  77. className="w-full min-h-auto"
  78. placeholder="Click to edit."
  79. value={value}
  80. onChange={(e: any) => setValue(e.target.value)}
  81. />
  82. <div
  83. className={`flex items-center gap-2 ${
  84. hasChanged ? 'opacity-100' : 'h-0 cursor-default opacity-0'
  85. } ${animateCss}`}
  86. >
  87. <Button
  88. type="default"
  89. disabled={!hasChanged}
  90. onClick={() => {
  91. setValue(contentText)
  92. setIsUpdating(false)
  93. }}
  94. >
  95. Cancel
  96. </Button>
  97. <Button disabled={!hasChanged} onClick={updateDescription}>
  98. {isUpdating ? (
  99. <Loader className="mx-auto animate-spin" size={14} strokeWidth={2} />
  100. ) : (
  101. <span>Save</span>
  102. )}
  103. </Button>
  104. </div>
  105. </div>
  106. )
  107. }
  108. export default Description