index.tsx 25 KB

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