PolicyTemplates.tsx158 lines · main
1import type { PGPolicy } from '@supabase/pg-meta'
2import { Search } from 'lucide-react'
3import { useState } from 'react'
4import {
5 Badge,
6 cn,
7 HoverCard,
8 HoverCardContent,
9 HoverCardTrigger,
10 InputGroup,
11 InputGroupAddon,
12 InputGroupInput,
13} from 'ui'
14import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock'
15
16import {
17 getGeneralPolicyTemplates,
18 getQueuePolicyTemplates,
19 getRealtimePolicyTemplates,
20} from '../PolicyEditorModal/PolicyEditorModal.constants'
21import { Markdown } from '@/components/interfaces/Markdown'
22import CardButton from '@/components/ui/CardButton'
23import CopyButton from '@/components/ui/CopyButton'
24import { NoSearchResults } from '@/components/ui/NoSearchResults'
25
26interface PolicyTemplatesProps {
27 schema: string
28 table: string
29 selectedPolicy?: PGPolicy
30 selectedTemplate?: string
31 onSelectTemplate: (template: any) => void
32}
33
34export const PolicyTemplates = ({
35 schema,
36 table,
37 selectedPolicy,
38 selectedTemplate,
39 onSelectTemplate,
40}: PolicyTemplatesProps) => {
41 const [search, setSearch] = useState('')
42
43 const templates =
44 schema === 'realtime'
45 ? getRealtimePolicyTemplates()
46 : schema === 'pgmq'
47 ? getQueuePolicyTemplates()
48 : getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name')
49
50 const baseTemplates =
51 selectedPolicy !== undefined
52 ? templates.filter((t) => t.command === selectedPolicy.command)
53 : templates
54 const filteredTemplates =
55 search.length > 0
56 ? baseTemplates.filter(
57 (template) =>
58 template.name.toLowerCase().includes(search.toLowerCase()) ||
59 template.command.toLowerCase().includes(search.toLowerCase())
60 )
61 : baseTemplates
62
63 return (
64 <div className="h-full px-content py-content flex flex-col gap-3">
65 <label className="sr-only" htmlFor="template-search">
66 Search templates
67 </label>
68 <InputGroup>
69 <InputGroupInput
70 id="template-search"
71 placeholder="Search templates"
72 value={search}
73 onChange={(e) => setSearch(e.target.value)}
74 />
75 <InputGroupAddon>
76 <Search />
77 </InputGroupAddon>
78 </InputGroup>
79
80 {search.length > 0 && filteredTemplates.length === 0 && (
81 <NoSearchResults searchString={search} className="min-w-full" />
82 )}
83
84 <div className="flex flex-col gap-1.5">
85 {filteredTemplates.map((template) => {
86 return (
87 <HoverCard key={template.id} openDelay={100} closeDelay={100}>
88 <HoverCardTrigger>
89 <CardButton
90 title={template.name}
91 titleClass="text-sm"
92 className={cn(
93 'transition w-full',
94 template.id === selectedTemplate
95 ? 'border-stronger! bg-surface-200 hover:border-stronger!'
96 : ''
97 )}
98 key={template.id}
99 onClick={() => onSelectTemplate(template)}
100 hideChevron
101 fixedHeight={false}
102 icon={
103 <div className="min-w-16 flex items-start">
104 <Badge
105 className={cn(
106 'rounded-sm! font-mono',
107 template.command === 'UPDATE'
108 ? 'bg-blue-900/50 text-blue-200 border border-blue-800'
109 : ''
110 )}
111 variant={
112 template.command === 'ALL'
113 ? 'default'
114 : template.command === 'SELECT'
115 ? 'success'
116 : template.command === 'UPDATE'
117 ? 'default'
118 : template.command === 'DELETE'
119 ? 'destructive'
120 : 'warning'
121 }
122 >
123 {template.command}
124 </Badge>
125 </div>
126 }
127 >
128 <Markdown content={template.description} className="[&>p]:m-0 space-y-2" />
129 </CardButton>
130 </HoverCardTrigger>
131 <HoverCardContent
132 hideWhenDetached
133 side="left"
134 align="center"
135 className="w-[500px] flex"
136 animate="slide-in"
137 >
138 <SimpleCodeBlock
139 showCopy={false}
140 className="sql"
141 parentClassName="p-0! [&>div>span]:text-xs"
142 >
143 {template.statement}
144 </SimpleCodeBlock>
145 <CopyButton
146 iconOnly
147 type="default"
148 className="px-1 absolute top-1.5 right-1.5"
149 text={template.statement}
150 />
151 </HoverCardContent>
152 </HoverCard>
153 )
154 })}
155 </div>
156 </div>
157 )
158}