index.tsx634 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import { Monaco } from '@monaco-editor/react' |
| 3 | import { |
| 4 | acceptUntrustedSql, |
| 5 | ident, |
| 6 | joinSqlFragments, |
| 7 | safeSql, |
| 8 | untrustedSql, |
| 9 | type DisplayableSqlFragment, |
| 10 | type SafeSqlFragment, |
| 11 | } from '@supabase/pg-meta/src/pg-format' |
| 12 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 13 | import { useQueryClient } from '@tanstack/react-query' |
| 14 | import { useParams } from 'common' |
| 15 | import { isEqual } from 'lodash' |
| 16 | import { memo, useCallback, useEffect, useRef, useState } from 'react' |
| 17 | import { useForm } from 'react-hook-form' |
| 18 | import { toast } from 'sonner' |
| 19 | import { |
| 20 | Button, |
| 21 | Checkbox, |
| 22 | cn, |
| 23 | Form, |
| 24 | Label, |
| 25 | ScrollArea, |
| 26 | Sheet, |
| 27 | SheetContent, |
| 28 | SheetFooter, |
| 29 | Tabs_Shadcn_, |
| 30 | TabsContent_Shadcn_, |
| 31 | TabsList_Shadcn_, |
| 32 | TabsTrigger_Shadcn_, |
| 33 | } from 'ui' |
| 34 | import * as z from 'zod' |
| 35 | |
| 36 | import { LockedCreateQuerySection, LockedRenameQuerySection } from './LockedQuerySection' |
| 37 | import { PolicyDetailsV2 } from './PolicyDetailsV2' |
| 38 | import { checkIfPolicyHasChanged, generateCreatePolicyQuery } from './PolicyEditorPanel.utils' |
| 39 | import { PolicyEditorPanelHeader } from './PolicyEditorPanelHeader' |
| 40 | import { PolicyTemplates } from './PolicyTemplates' |
| 41 | import { QueryError } from './QueryError' |
| 42 | import { RLSCodeEditor } from './RLSCodeEditor' |
| 43 | import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils' |
| 44 | import { IStandaloneCodeEditor } from '@/components/interfaces/SQLEditor/SQLEditor.types' |
| 45 | import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' |
| 46 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 47 | import { useDatabasePolicyUpdateMutation } from '@/data/database-policies/database-policy-update-mutation' |
| 48 | import { databasePoliciesKeys } from '@/data/database-policies/keys' |
| 49 | import { QueryResponseError, useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' |
| 50 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 51 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 52 | import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' |
| 53 | import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' |
| 54 | |
| 55 | interface PolicyEditorPanelProps { |
| 56 | visible: boolean |
| 57 | schema: string |
| 58 | searchString?: string |
| 59 | selectedTable?: string |
| 60 | selectedPolicy?: Policy |
| 61 | onSelectCancel: () => void |
| 62 | authContext: 'database' | 'realtime' |
| 63 | } |
| 64 | |
| 65 | const FORM_ID = 'rls-editor' |
| 66 | const FormSchema = z.object({ |
| 67 | name: z.string().min(1, 'Please provide a name'), |
| 68 | table: z.string(), |
| 69 | behavior: z.string(), |
| 70 | command: z.string(), |
| 71 | roles: z.string(), |
| 72 | }) |
| 73 | const defaultValues = { |
| 74 | name: '', |
| 75 | table: '', |
| 76 | behavior: 'permissive', |
| 77 | command: 'select', |
| 78 | roles: '', |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Using memo for this component because everything rerenders on window focus because of outside fetches |
| 83 | */ |
| 84 | export const PolicyEditorPanel = memo(function ({ |
| 85 | visible, |
| 86 | schema, |
| 87 | searchString, |
| 88 | selectedTable, |
| 89 | selectedPolicy, |
| 90 | onSelectCancel, |
| 91 | authContext, |
| 92 | }: PolicyEditorPanelProps) { |
| 93 | const { ref } = useParams() |
| 94 | const queryClient = useQueryClient() |
| 95 | const { data: selectedProject } = useSelectedProjectQuery() |
| 96 | |
| 97 | const { can: canUpdatePolicies } = useAsyncCheckPermissions( |
| 98 | PermissionAction.TENANT_SQL_ADMIN_WRITE, |
| 99 | 'tables' |
| 100 | ) |
| 101 | |
| 102 | // [Joshen] Hyrid form fields, just spit balling to get a decent POC out |
| 103 | const [using, setUsing] = useState<DisplayableSqlFragment | undefined>(undefined) |
| 104 | const [check, setCheck] = useState<DisplayableSqlFragment | undefined>(undefined) |
| 105 | const [rolesFragment, setRolesFragment] = useState<SafeSqlFragment>(safeSql`public`) |
| 106 | const [fieldError, setFieldError] = useState<string>() |
| 107 | const [showCheckBlock, setShowCheckBlock] = useState(true) |
| 108 | |
| 109 | const monacoOneRef = useRef<Monaco | null>(null) |
| 110 | const editorOneRef = useRef<IStandaloneCodeEditor | null>(null) |
| 111 | const [expOneLineCount, setExpOneLineCount] = useState(1) |
| 112 | const [expOneContentHeight, setExpOneContentHeight] = useState(0) |
| 113 | |
| 114 | const monacoTwoRef = useRef<Monaco | null>(null) |
| 115 | const editorTwoRef = useRef<IStandaloneCodeEditor | null>(null) |
| 116 | const [expTwoLineCount, setExpTwoLineCount] = useState(1) |
| 117 | const [expTwoContentHeight, setExpTwoContentHeight] = useState(0) |
| 118 | |
| 119 | const [error, setError] = useState<QueryResponseError>() |
| 120 | const [errorPanelOpen, setErrorPanelOpen] = useState<boolean>(true) |
| 121 | const [showDetails, setShowDetails] = useState<boolean>(false) |
| 122 | const [selectedDiff, setSelectedDiff] = useState<string>() |
| 123 | |
| 124 | const [showTools, setShowTools] = useState<boolean>(false) |
| 125 | |
| 126 | const form = useForm<z.infer<typeof FormSchema>>({ |
| 127 | mode: 'onBlur', |
| 128 | reValidateMode: 'onBlur', |
| 129 | resolver: zodResolver(FormSchema as any), |
| 130 | defaultValues, |
| 131 | }) |
| 132 | |
| 133 | const { name, table, behavior, command, roles } = form.watch() |
| 134 | const supportWithCheck = ['update', 'all'].includes(command) |
| 135 | const isRenamingPolicy = selectedPolicy !== undefined && name !== selectedPolicy.name |
| 136 | |
| 137 | const { mutate: executeMutation, isPending: isExecuting } = useExecuteSqlMutation({ |
| 138 | onSuccess: async () => { |
| 139 | // refresh all policies |
| 140 | await queryClient.invalidateQueries({ queryKey: databasePoliciesKeys.list(ref) }) |
| 141 | toast.success('Successfully created new policy') |
| 142 | onSelectCancel() |
| 143 | }, |
| 144 | onError: (error) => setError(error), |
| 145 | }) |
| 146 | |
| 147 | const { mutate: updatePolicy, isPending: isUpdating } = useDatabasePolicyUpdateMutation({ |
| 148 | onSuccess: () => { |
| 149 | toast.success('Successfully updated policy') |
| 150 | onSelectCancel() |
| 151 | }, |
| 152 | }) |
| 153 | |
| 154 | const hasUnsavedChanges = useCallback(() => { |
| 155 | const editorOneValue = editorOneRef.current?.getValue().trim() ?? null |
| 156 | const editorOneFormattedValue = !editorOneValue ? null : editorOneValue |
| 157 | const editorTwoValue = editorTwoRef.current?.getValue().trim() ?? null |
| 158 | const editorTwoFormattedValue = !editorTwoValue ? null : editorTwoValue |
| 159 | |
| 160 | const policyCreateUnsaved = |
| 161 | selectedPolicy === undefined && |
| 162 | (name.length > 0 || |
| 163 | roles.length > 0 || |
| 164 | !!editorOneFormattedValue || |
| 165 | !!editorTwoFormattedValue) |
| 166 | const policyUpdateUnsaved = |
| 167 | selectedPolicy !== undefined |
| 168 | ? checkIfPolicyHasChanged(selectedPolicy, { |
| 169 | name, |
| 170 | roles: roles.length === 0 ? ['public'] : roles.split(', '), |
| 171 | definition: editorOneFormattedValue, |
| 172 | check: command === 'INSERT' ? editorOneFormattedValue : editorTwoFormattedValue, |
| 173 | }) |
| 174 | : false |
| 175 | |
| 176 | return policyCreateUnsaved || policyUpdateUnsaved |
| 177 | }, [command, name, roles, selectedPolicy]) |
| 178 | |
| 179 | const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ |
| 180 | checkIsDirty: hasUnsavedChanges, |
| 181 | onClose: onSelectCancel, |
| 182 | }) |
| 183 | |
| 184 | const onSubmit = (data: z.infer<typeof FormSchema>) => { |
| 185 | const { name, table, behavior, command, roles } = data |
| 186 | |
| 187 | // For INSERT: editor one holds the check expression (not using) |
| 188 | // For others: editor one = using, editor two = optional check |
| 189 | const usingExpr = command !== 'insert' ? using : undefined |
| 190 | const checkExpr = command === 'insert' ? using : check |
| 191 | |
| 192 | if (command === 'insert' && !checkExpr?.trim()) { |
| 193 | return setFieldError('Please provide a SQL expression for the WITH CHECK statement') |
| 194 | } else if (command !== 'insert' && !usingExpr?.trim()) { |
| 195 | return setFieldError('Please provide a SQL expression for the USING statement') |
| 196 | } else { |
| 197 | setFieldError(undefined) |
| 198 | } |
| 199 | |
| 200 | if (selectedPolicy === undefined) { |
| 201 | const sql = generateCreatePolicyQuery({ |
| 202 | name, |
| 203 | schema, |
| 204 | table, |
| 205 | behavior, |
| 206 | command, |
| 207 | roles: rolesFragment, |
| 208 | using: usingExpr ? acceptUntrustedSql(usingExpr) : undefined, |
| 209 | check: checkExpr ? acceptUntrustedSql(checkExpr) : undefined, |
| 210 | }) |
| 211 | |
| 212 | setError(undefined) |
| 213 | executeMutation({ |
| 214 | sql, |
| 215 | projectRef: selectedProject?.ref, |
| 216 | connectionString: selectedProject?.connectionString, |
| 217 | handleError: (error) => { |
| 218 | throw error |
| 219 | }, |
| 220 | }) |
| 221 | } else if (selectedProject !== undefined) { |
| 222 | const payload: { |
| 223 | name?: string |
| 224 | definition?: SafeSqlFragment |
| 225 | check?: SafeSqlFragment |
| 226 | roles?: Array<string> |
| 227 | } = {} |
| 228 | const updatedRoles = roles.length === 0 ? ['public'] : roles.split(', ') |
| 229 | // Trim for string comparison against the stored policy values. The Save click is the |
| 230 | // explicit user gesture that promotes editor content to executable SQL. |
| 231 | const usingVal = using?.trim() |
| 232 | const checkVal = check?.trim() |
| 233 | |
| 234 | if (name !== selectedPolicy.name) payload.name = name |
| 235 | if (!isEqual(selectedPolicy.roles, updatedRoles)) payload.roles = updatedRoles |
| 236 | if (selectedPolicy.definition !== null && selectedPolicy.definition !== usingVal) |
| 237 | payload.definition = |
| 238 | usingVal === undefined ? undefined : acceptUntrustedSql(untrustedSql(usingVal)) |
| 239 | |
| 240 | if (selectedPolicy.command === 'INSERT') { |
| 241 | // [Joshen] Cause editor one will be the check statement in this scenario |
| 242 | if (selectedPolicy.check !== usingVal) |
| 243 | payload.check = |
| 244 | usingVal === undefined ? undefined : acceptUntrustedSql(untrustedSql(usingVal)) |
| 245 | } else { |
| 246 | if (selectedPolicy.check !== checkVal) |
| 247 | payload.check = |
| 248 | checkVal === undefined ? undefined : acceptUntrustedSql(untrustedSql(checkVal)) |
| 249 | } |
| 250 | |
| 251 | if (Object.keys(payload).length === 0) return onSelectCancel() |
| 252 | |
| 253 | updatePolicy({ |
| 254 | projectRef: selectedProject.ref, |
| 255 | connectionString: selectedProject?.connectionString, |
| 256 | originalPolicy: selectedPolicy, |
| 257 | payload, |
| 258 | }) |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | const resetState = useStaticEffectEvent(() => { |
| 263 | if (!visible) { |
| 264 | editorOneRef.current?.setValue('') |
| 265 | editorTwoRef.current?.setValue('') |
| 266 | setShowTools(false) |
| 267 | setError(undefined) |
| 268 | setShowDetails(false) |
| 269 | setSelectedDiff(undefined) |
| 270 | |
| 271 | setUsing(undefined) |
| 272 | setCheck(undefined) |
| 273 | setRolesFragment(safeSql`public`) |
| 274 | setShowCheckBlock(false) |
| 275 | setFieldError(undefined) |
| 276 | |
| 277 | form.reset(defaultValues) |
| 278 | } else { |
| 279 | if (canUpdatePolicies) setShowTools(true) |
| 280 | if (selectedPolicy !== undefined) { |
| 281 | const { name, action, table, command, roles } = selectedPolicy |
| 282 | form.reset({ |
| 283 | name, |
| 284 | table, |
| 285 | behavior: action.toLowerCase(), |
| 286 | command: command.toLowerCase(), |
| 287 | roles: roles.length === 1 && roles[0] === 'public' ? '' : roles.join(', '), |
| 288 | }) |
| 289 | if (selectedPolicy.definition) setUsing(safeSql` ${selectedPolicy.definition}`) |
| 290 | if (selectedPolicy.check && selectedPolicy.command === 'INSERT') |
| 291 | setUsing(safeSql` ${selectedPolicy.check}`) |
| 292 | if (selectedPolicy.check && selectedPolicy.command !== 'INSERT') { |
| 293 | setCheck(safeSql` ${selectedPolicy.check}`) |
| 294 | setShowCheckBlock(true) |
| 295 | } |
| 296 | setRolesFragment( |
| 297 | roles.length === 1 && roles[0] === 'public' |
| 298 | ? safeSql`public` |
| 299 | : joinSqlFragments( |
| 300 | roles.map((r) => ident(r)), |
| 301 | ', ' |
| 302 | ) |
| 303 | ) |
| 304 | } else if (selectedTable !== undefined) { |
| 305 | form.reset({ ...defaultValues, table: selectedTable }) |
| 306 | } |
| 307 | } |
| 308 | }) |
| 309 | |
| 310 | // when the panel is closed, reset all values |
| 311 | useEffect(resetState, [visible, resetState]) |
| 312 | |
| 313 | // whenever the deps (current policy details, new error or error panel opens) change, recalculate |
| 314 | // the height of the editor |
| 315 | useEffect(() => { |
| 316 | editorOneRef.current?.layout({ width: 0, height: 0 }) |
| 317 | window.requestAnimationFrame(() => { |
| 318 | editorOneRef.current?.layout() |
| 319 | }) |
| 320 | }, [showDetails, error, errorPanelOpen]) |
| 321 | |
| 322 | return ( |
| 323 | <> |
| 324 | <Form {...form}> |
| 325 | <form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}> |
| 326 | <Sheet open={visible} onOpenChange={handleOpenChange}> |
| 327 | <SheetContent |
| 328 | showClose={false} |
| 329 | size={showTools ? 'lg' : 'default'} |
| 330 | className={cn( |
| 331 | 'bg-surface-200 p-0 flex flex-row gap-0', |
| 332 | showTools ? 'min-w-screen! lg:min-w-[1000px]!' : 'min-w-screen! lg:min-w-[600px]!' |
| 333 | )} |
| 334 | > |
| 335 | <div className={cn('flex flex-col grow w-full', showTools && 'w-[60%]')}> |
| 336 | <PolicyEditorPanelHeader |
| 337 | selectedPolicy={selectedPolicy} |
| 338 | showTools={showTools} |
| 339 | setShowTools={setShowTools} |
| 340 | /> |
| 341 | |
| 342 | <div className="flex flex-col h-full w-full justify-between overflow-y-auto"> |
| 343 | <PolicyDetailsV2 |
| 344 | schema={schema} |
| 345 | searchString={searchString} |
| 346 | selectedTable={selectedTable} |
| 347 | isEditing={selectedPolicy !== undefined} |
| 348 | form={form} |
| 349 | onUpdateCommand={(command: string) => { |
| 350 | setFieldError(undefined) |
| 351 | if (!['update', 'all'].includes(command)) { |
| 352 | setShowCheckBlock(false) |
| 353 | } else { |
| 354 | setShowCheckBlock(true) |
| 355 | } |
| 356 | }} |
| 357 | onRolesChange={(frag) => setRolesFragment(frag)} |
| 358 | authContext={authContext} |
| 359 | /> |
| 360 | <div className="h-full"> |
| 361 | <LockedCreateQuerySection |
| 362 | schema={schema} |
| 363 | selectedPolicy={selectedPolicy} |
| 364 | isRenamingPolicy={isRenamingPolicy} |
| 365 | formFields={{ name, table, behavior, command, roles }} |
| 366 | /> |
| 367 | |
| 368 | <div |
| 369 | className="mt-1 relative block" |
| 370 | style={{ |
| 371 | height: |
| 372 | expOneContentHeight <= 100 ? `${8 + expOneContentHeight}px` : '108px', |
| 373 | }} |
| 374 | > |
| 375 | <RLSCodeEditor |
| 376 | disableTabToUsePlaceholder |
| 377 | readOnly={!canUpdatePolicies} |
| 378 | id="rls-exp-one-editor" |
| 379 | placeholder={ |
| 380 | command === 'insert' |
| 381 | ? '-- Provide a SQL expression for the with check statement' |
| 382 | : '-- Provide a SQL expression for the using statement' |
| 383 | } |
| 384 | defaultValue={using} |
| 385 | value={using} |
| 386 | editorRef={editorOneRef} |
| 387 | monacoRef={monacoOneRef as any} |
| 388 | lineNumberStart={6} |
| 389 | onInputChange={(value) => setUsing(untrustedSql(value ?? ''))} |
| 390 | onChange={() => { |
| 391 | setExpOneContentHeight(editorOneRef.current?.getContentHeight() ?? 0) |
| 392 | setExpOneLineCount(editorOneRef.current?.getModel()?.getLineCount() ?? 1) |
| 393 | }} |
| 394 | onMount={() => { |
| 395 | setTimeout(() => { |
| 396 | setExpOneContentHeight(editorOneRef.current?.getContentHeight() ?? 0) |
| 397 | setExpOneLineCount( |
| 398 | editorOneRef.current?.getModel()?.getLineCount() ?? 1 |
| 399 | ) |
| 400 | }, 200) |
| 401 | }} |
| 402 | /> |
| 403 | </div> |
| 404 | |
| 405 | <div className="bg-surface-300 py-1"> |
| 406 | <div className="flex items-center" style={{ fontSize: '14px' }}> |
| 407 | <div className="w-[57px]"> |
| 408 | <p className="w-[31px] flex justify-end font-mono text-sm text-foreground-light select-none"> |
| 409 | {7 + expOneLineCount} |
| 410 | </p> |
| 411 | </div> |
| 412 | <p className="font-mono tracking-tighter"> |
| 413 | {showCheckBlock ? ( |
| 414 | <> |
| 415 | {supportWithCheck && showCheckBlock && ( |
| 416 | <span className="text-[#ffd700]">) </span> |
| 417 | )} |
| 418 | <span className="text-[#569cd6]">with check</span>{' '} |
| 419 | <span className="text-[#ffd700]">(</span> |
| 420 | </> |
| 421 | ) : ( |
| 422 | <> |
| 423 | <span className="text-[#ffd700]">)</span>; |
| 424 | </> |
| 425 | )} |
| 426 | </p> |
| 427 | </div> |
| 428 | </div> |
| 429 | |
| 430 | {showCheckBlock && ( |
| 431 | <> |
| 432 | <div |
| 433 | className="mt-1 min-h-[28px] relative block" |
| 434 | style={{ |
| 435 | height: |
| 436 | expTwoContentHeight <= 100 ? `${8 + expTwoContentHeight}px` : '108px', |
| 437 | }} |
| 438 | > |
| 439 | <RLSCodeEditor |
| 440 | disableTabToUsePlaceholder |
| 441 | readOnly={!canUpdatePolicies} |
| 442 | id="rls-exp-two-editor" |
| 443 | placeholder="-- Provide a SQL expression for the with check statement" |
| 444 | defaultValue={check} |
| 445 | value={check} |
| 446 | editorRef={editorTwoRef} |
| 447 | monacoRef={monacoTwoRef as any} |
| 448 | lineNumberStart={7 + expOneLineCount} |
| 449 | onInputChange={(value) => setCheck(untrustedSql(value ?? ''))} |
| 450 | onChange={() => { |
| 451 | setExpTwoContentHeight(editorTwoRef.current?.getContentHeight() ?? 0) |
| 452 | setExpTwoLineCount( |
| 453 | editorTwoRef.current?.getModel()?.getLineCount() ?? 1 |
| 454 | ) |
| 455 | }} |
| 456 | onMount={() => { |
| 457 | setTimeout(() => { |
| 458 | setExpTwoContentHeight( |
| 459 | editorTwoRef.current?.getContentHeight() ?? 0 |
| 460 | ) |
| 461 | setExpTwoLineCount( |
| 462 | editorTwoRef.current?.getModel()?.getLineCount() ?? 1 |
| 463 | ) |
| 464 | }, 200) |
| 465 | }} |
| 466 | /> |
| 467 | </div> |
| 468 | <div className="bg-surface-300 py-1"> |
| 469 | <div className="flex items-center" style={{ fontSize: '14px' }}> |
| 470 | <div className="w-[57px]"> |
| 471 | <p className="w-[31px] flex justify-end font-mono text-sm text-foreground-light select-none"> |
| 472 | {8 + expOneLineCount + expTwoLineCount} |
| 473 | </p> |
| 474 | </div> |
| 475 | <p className="font-mono tracking-tighter"> |
| 476 | <span className="text-[#ffd700]">)</span>; |
| 477 | </p> |
| 478 | </div> |
| 479 | </div> |
| 480 | </> |
| 481 | )} |
| 482 | |
| 483 | {isRenamingPolicy && ( |
| 484 | <LockedRenameQuerySection |
| 485 | oldName={selectedPolicy.name} |
| 486 | newName={name} |
| 487 | schema={schema} |
| 488 | table={table} |
| 489 | lineNumber={8 + expOneLineCount + (showCheckBlock ? expTwoLineCount : 0)} |
| 490 | /> |
| 491 | )} |
| 492 | |
| 493 | {fieldError !== undefined && ( |
| 494 | <p className="px-5 py-2 pb-0 text-sm text-red-900">{fieldError}</p> |
| 495 | )} |
| 496 | |
| 497 | {supportWithCheck && ( |
| 498 | <div className="px-5 py-3 flex items-center gap-x-2"> |
| 499 | <Checkbox |
| 500 | id="use-check" |
| 501 | name="use-check" |
| 502 | checked={showCheckBlock} |
| 503 | onCheckedChange={() => { |
| 504 | setFieldError(undefined) |
| 505 | setShowCheckBlock(!showCheckBlock) |
| 506 | }} |
| 507 | /> |
| 508 | <Label className="text-xs cursor-pointer" htmlFor="use-check"> |
| 509 | Use check expression |
| 510 | </Label> |
| 511 | </div> |
| 512 | )} |
| 513 | </div> |
| 514 | |
| 515 | <div className="flex flex-col"> |
| 516 | {error !== undefined && ( |
| 517 | <QueryError error={error} open={errorPanelOpen} setOpen={setErrorPanelOpen} /> |
| 518 | )} |
| 519 | <SheetFooter className="flex items-center justify-end! px-5 py-4 w-full border-t"> |
| 520 | <Button |
| 521 | type="default" |
| 522 | disabled={isExecuting || isUpdating} |
| 523 | onClick={confirmOnClose} |
| 524 | > |
| 525 | Cancel |
| 526 | </Button> |
| 527 | |
| 528 | <ButtonTooltip |
| 529 | form={FORM_ID} |
| 530 | htmlType="submit" |
| 531 | loading={isExecuting || isUpdating} |
| 532 | disabled={!canUpdatePolicies || isExecuting || isUpdating} |
| 533 | tooltip={{ |
| 534 | content: { |
| 535 | side: 'top', |
| 536 | text: !canUpdatePolicies |
| 537 | ? 'You need additional permissions to update policies' |
| 538 | : undefined, |
| 539 | }, |
| 540 | }} |
| 541 | > |
| 542 | Save policy |
| 543 | </ButtonTooltip> |
| 544 | </SheetFooter> |
| 545 | </div> |
| 546 | </div> |
| 547 | </div> |
| 548 | {showTools && ( |
| 549 | <div |
| 550 | className={cn( |
| 551 | 'border-l shadow-[rgba(0,0,0,0.13)_-4px_0px_6px_0px] z-10', |
| 552 | showTools && 'w-[50%]', |
| 553 | 'bg-studio overflow-auto' |
| 554 | )} |
| 555 | > |
| 556 | <Tabs_Shadcn_ defaultValue="templates" className="flex flex-col h-full w-full"> |
| 557 | <TabsList_Shadcn_ className="flex gap-4 px-content pt-2"> |
| 558 | <TabsTrigger_Shadcn_ |
| 559 | key="templates" |
| 560 | value="templates" |
| 561 | className="px-0 data-[state=active]:bg-transparent" |
| 562 | > |
| 563 | Templates |
| 564 | </TabsTrigger_Shadcn_> |
| 565 | </TabsList_Shadcn_> |
| 566 | |
| 567 | <TabsContent_Shadcn_ |
| 568 | value="templates" |
| 569 | className={cn( |
| 570 | 'mt-0! overflow-y-auto', |
| 571 | 'data-[state=active]:flex data-[state=active]:grow' |
| 572 | )} |
| 573 | > |
| 574 | <ScrollArea className="h-full w-full"> |
| 575 | <PolicyTemplates |
| 576 | schema={schema} |
| 577 | table={table} |
| 578 | selectedPolicy={selectedPolicy} |
| 579 | selectedTemplate={selectedDiff} |
| 580 | onSelectTemplate={(value) => { |
| 581 | form.setValue('name', value.name) |
| 582 | form.setValue('behavior', 'permissive') |
| 583 | form.setValue('command', value.command.toLowerCase()) |
| 584 | form.setValue('roles', value.roles.join(', ') ?? '') |
| 585 | |
| 586 | setUsing(safeSql` ${value.definition}`) |
| 587 | if (value.check) { |
| 588 | if (value.command === 'INSERT') { |
| 589 | setUsing(safeSql` ${value.check}`) |
| 590 | } else { |
| 591 | setCheck(safeSql` ${value.check}`) |
| 592 | } |
| 593 | } |
| 594 | setRolesFragment( |
| 595 | value.roles.length === 0 || |
| 596 | (value.roles.length === 1 && value.roles[0] === 'public') |
| 597 | ? safeSql`public` |
| 598 | : joinSqlFragments( |
| 599 | value.roles.map((r: string) => ident(r)), |
| 600 | ', ' |
| 601 | ) |
| 602 | ) |
| 603 | setExpOneLineCount(1) |
| 604 | setExpTwoLineCount(1) |
| 605 | setFieldError(undefined) |
| 606 | |
| 607 | if (!['update', 'all'].includes(value.command.toLowerCase())) { |
| 608 | setShowCheckBlock(false) |
| 609 | } else if (value.check.length > 0) { |
| 610 | setShowCheckBlock(true) |
| 611 | } else { |
| 612 | setShowCheckBlock(false) |
| 613 | } |
| 614 | }} |
| 615 | /> |
| 616 | </ScrollArea> |
| 617 | </TabsContent_Shadcn_> |
| 618 | </Tabs_Shadcn_> |
| 619 | </div> |
| 620 | )} |
| 621 | </SheetContent> |
| 622 | </Sheet> |
| 623 | </form> |
| 624 | </Form> |
| 625 | |
| 626 | <DiscardChangesConfirmationDialog |
| 627 | {...modalProps} |
| 628 | description="Are you sure you want to close the editor? Any unsaved changes on your policy and conversations with the Assistant will be lost." |
| 629 | /> |
| 630 | </> |
| 631 | ) |
| 632 | }) |
| 633 | |
| 634 | PolicyEditorPanel.displayName = 'PolicyEditorPanel' |