mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat: New Policy Editor (#19166)
* Add two more sizes to the Panel component. * Add alias for the older openai-api library. The new one is added under the openai name. * Add API routes * Add components for the new AI RLS panel. * Bunch of changes to the AI Policy Editor. * Add a button for opening the new Policy Editor. * Add a feature flag for the new editor. * Add a confirmation modal when closing the panel. * Fix leftover data when closing the panel. * Make the copy button work. * Add the next/swc packages to package-lock.json. * Merge master * Scaffold debug sql in rls editor * Small improvements to policy chat * Hook up debug to ai assistant panel * Improve debug UX * Add debug request badge * Some styling fix * Small styling fix * Another small styling fix * Shift create new policy ai button + fix error stylign with code editor height * Add tooltips to apply changes and copy code from assistant message * Hide assistant button is not platform * Small lint * Add default error handlers to all AI RQ mutations * Small fix * Remove IS PLATFORM check for rls assistant * Add placeholder to RLS code editor * Fix diff + rls code editor * Add placeholder message after sending prompt * Small style * RLSCodeEditor hit tab if empty to populate placeholder text * Light mode nudeges * Update logic for when confirmation close modal should show * Set render overview ruler as false for rls diff editor * improve chat UX to make it smoother (thank you alaister for your help 🙏) * Dynamically do keepPreviousData * Gracefully handle errors for add prompt * Use animated ai icon while message is loading * using Sheet component * Address commernts * Bit more improvements --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
35 files changed
+1467
-78
No files matched your search
@@ -0,0 +1,142 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { compact, last, sortBy } from 'lodash'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import OpenAI from 'openai'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
AiIcon,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
} from 'ui'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { useProfile } from 'lib/profile'
|
||||
import Message from './Message'
|
||||
|
||||
export const AIPolicyChat = ({
|
||||
messages,
|
||||
loading,
|
||||
onSubmit,
|
||||
onDiff,
|
||||
onChange,
|
||||
}: {
|
||||
messages: OpenAI.Beta.Threads.Messages.ThreadMessage[]
|
||||
loading: boolean
|
||||
onSubmit: (s: string) => void
|
||||
onDiff: (s: string) => void
|
||||
onChange: (value: boolean) => void
|
||||
}) => {
|
||||
const { profile } = useProfile()
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const name = compact([profile?.first_name, profile?.last_name]).join(' ')
|
||||
const sorted = useMemo(() => {
|
||||
return sortBy(messages, (m) => m.created_at).filter((m) => {
|
||||
if (m.content[0].type === 'text') {
|
||||
return !m.content[0].text.value.startsWith('Here is my database schema for reference:')
|
||||
}
|
||||
return false
|
||||
})
|
||||
}, [messages])
|
||||
|
||||
const FormSchema = z.object({ chat: z.string() })
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
mode: 'onBlur',
|
||||
reValidateMode: 'onBlur',
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: { chat: '' },
|
||||
})
|
||||
const formChatValue = form.getValues().chat
|
||||
const pendingReply = loading && last(sorted)?.role === 'user'
|
||||
|
||||
useEffect(() => {
|
||||
// 👇️ scroll to bottom every time messages change
|
||||
if (bottomRef.current) {
|
||||
setTimeout(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 500)
|
||||
}
|
||||
}, [messages.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.setValue('chat', '')
|
||||
form.setFocus('chat')
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
useEffect(() => {
|
||||
onChange(formChatValue.length === 0)
|
||||
}, [formChatValue])
|
||||
|
||||
return (
|
||||
<div id={'ai-chat-assistant'} className="flex flex-col h-full">
|
||||
<div className="overflow-auto flex-1">
|
||||
<Message
|
||||
role="assistant"
|
||||
content={`Hi${
|
||||
name ? ' ' + name : ''
|
||||
}, how can I help you? I'm powered by AI, so surprises and mistakes are possible.
|
||||
Make sure to verify any generated code or suggestions, and share feedback so that we can
|
||||
learn and improve.`}
|
||||
/>
|
||||
|
||||
{sorted.map((m, idx) => (
|
||||
<Message
|
||||
key={`message-${idx}`}
|
||||
name={name}
|
||||
role={m.role}
|
||||
content={
|
||||
m.content[0] && m.content[0].type === 'text' ? m.content[0].text.value : undefined
|
||||
}
|
||||
createdAt={m.created_at}
|
||||
isDebug={(m.metadata as any).type === 'debug'}
|
||||
onDiff={onDiff}
|
||||
/>
|
||||
))}
|
||||
|
||||
{pendingReply && <Message role="assistant" content="Thinking..." />}
|
||||
|
||||
<div ref={bottomRef} className="h-1" />
|
||||
</div>
|
||||
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id="rls-chat"
|
||||
className="sticky p-5 flex-0 border-t"
|
||||
onSubmit={form.handleSubmit((data: z.infer<typeof FormSchema>) => {
|
||||
onSubmit(data.chat)
|
||||
})}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="chat"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_>
|
||||
<FormControl_Shadcn_>
|
||||
<div className="relative">
|
||||
<AiIcon className="absolute top-2 left-3 [&>div>div]:border-black dark:[&>div>div]:border-white" />
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
className={`bg-surface-300 dark:bg-black rounded-full pl-10 ${
|
||||
loading ? 'pr-10' : ''
|
||||
}`}
|
||||
placeholder="Ask for some changes to your policy"
|
||||
/>
|
||||
{loading && <Loader2 className="absolute top-2 right-3 animate-spin" />}
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { ThreadMessage } from 'openai/resources/beta/threads/messages/messages'
|
||||
|
||||
export const generateThreadMessage = ({
|
||||
id,
|
||||
threadId,
|
||||
runId,
|
||||
content,
|
||||
metadata = {},
|
||||
}: {
|
||||
id?: string
|
||||
threadId?: string
|
||||
runId?: string
|
||||
content: string
|
||||
metadata?: any
|
||||
}) => {
|
||||
const message: ThreadMessage = {
|
||||
id: id ?? uuidv4(),
|
||||
object: 'thread.message',
|
||||
role: 'assistant',
|
||||
file_ids: [],
|
||||
metadata,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: { value: content, annotations: [] },
|
||||
},
|
||||
],
|
||||
created_at: Math.floor(Number(new Date()) / 1000),
|
||||
assistant_id: null,
|
||||
thread_id: threadId ?? '',
|
||||
run_id: runId ?? '',
|
||||
}
|
||||
return message
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { AiIcon, Button, SheetClose_Shadcn_, SheetHeader_Shadcn_, SheetTitle_Shadcn_, cn } from 'ui'
|
||||
|
||||
import styles from '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
export const AIPolicyHeader = ({
|
||||
assistantVisible,
|
||||
setAssistantVisible,
|
||||
}: {
|
||||
assistantVisible: boolean
|
||||
setAssistantVisible: (v: boolean) => void
|
||||
}) => {
|
||||
return (
|
||||
<SheetHeader_Shadcn_ className="py-3 flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<SheetClose_Shadcn_ className="text-light hover:text ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-3 w-3" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose_Shadcn_>
|
||||
<div className="h-[24px] w-[1px] bg-border"></div>
|
||||
<SheetTitle_Shadcn_>Create a new row level security policy</SheetTitle_Shadcn_>
|
||||
</div>
|
||||
<Button
|
||||
aria-expanded={assistantVisible}
|
||||
aria-controls="ai-chat-assistant"
|
||||
size="tiny"
|
||||
type="outline"
|
||||
className={cn('group', styles['ai-icon__container--allow-hover-effect'], 'px-3 py-0.5')}
|
||||
rounded
|
||||
icon={
|
||||
<AiIcon className="scale-75 [&>div>div]:border-foreground-light [&>div>div]:group-hover:border-foreground -mr-0.5" />
|
||||
}
|
||||
onClick={() => setAssistantVisible(!assistantVisible)}
|
||||
>
|
||||
{assistantVisible ? <>Close Assistant</> : <>Open Assistant</>}
|
||||
</Button>
|
||||
</SheetHeader_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import dayjs from 'dayjs'
|
||||
import { kebabCase, noop, take } from 'lodash'
|
||||
import { Copy, FileDiff } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { memo, useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { format } from 'sql-formatter'
|
||||
import { AiIcon, AiIconAnimation, Badge, Button } from 'ui'
|
||||
|
||||
import CodeEditor from 'components/ui/CodeEditor'
|
||||
import { useProfile } from 'lib/profile'
|
||||
|
||||
const Message = memo(function Message({
|
||||
name,
|
||||
role,
|
||||
content,
|
||||
createdAt,
|
||||
isDebug,
|
||||
onDiff = noop,
|
||||
}: {
|
||||
name?: string
|
||||
role: 'user' | 'assistant'
|
||||
content?: string
|
||||
createdAt?: number
|
||||
isDebug?: boolean
|
||||
onDiff?: (s: string) => void
|
||||
}) {
|
||||
const { profile } = useProfile()
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return role === 'assistant' ? (
|
||||
<AiIconAnimation
|
||||
loading={content === 'Thinking...'}
|
||||
className="[&>div>div]:border-black dark:[&>div>div]:border-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="relative border shadow-lg w-8 h-8 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={`https://github.com/${profile?.username}.png` || ''}
|
||||
width={30}
|
||||
height={30}
|
||||
alt="avatar"
|
||||
className="relative"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}, [role])
|
||||
|
||||
if (!content) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col py-4 gap-4 border-t px-5">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
{icon}
|
||||
<span className="text-sm">{role === 'assistant' ? 'Assistant' : name ? name : 'You'}</span>
|
||||
{createdAt && (
|
||||
<span className="text-xs text-foreground-muted">{dayjs(createdAt * 1000).fromNow()}</span>
|
||||
)}
|
||||
{isDebug && <Badge color="amber">Debug request</Badge>}
|
||||
</div>
|
||||
<ReactMarkdown
|
||||
className="gap-2.5 flex flex-col"
|
||||
components={{
|
||||
p: ({ children }) => <div className="text-foreground-light text-sm">{children}</div>,
|
||||
// intentionally rendering as pre. The other approach would be to render as code element,
|
||||
// but that will render <code> elements which appear in the explanations as Monaco editors.
|
||||
pre: ({ children }) => {
|
||||
const code = (children[0] as any).props.children[0] as string
|
||||
let formatted = code
|
||||
try {
|
||||
formatted = format(code)
|
||||
} catch {}
|
||||
|
||||
// create a key from the name of the generated policy so that we're sure it's unique
|
||||
const key = kebabCase(take(code.split(' '), 3).join(' '))
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-md border border-control overflow-hidden relative group"
|
||||
key={key}
|
||||
>
|
||||
<CodeEditor
|
||||
id={`rls-sql_${key}`}
|
||||
language="pgsql"
|
||||
className="h-48"
|
||||
value={formatted}
|
||||
autofocus={false}
|
||||
options={{
|
||||
scrollBeyondLastLine: false,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 3,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-3 right-3 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button type="text" size="tiny" onClick={() => onDiff(formatted)}>
|
||||
<FileDiff className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">Apply changes</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={() => navigator.clipboard.writeText(formatted).then()}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">Copy code</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Message
|
||||
@@ -0,0 +1,102 @@
|
||||
import styles from '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css'
|
||||
import { QueryResponseError } from 'data/sql/execute-sql-mutation'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
AiIcon,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
CollapsibleContent_Shadcn_,
|
||||
CollapsibleTrigger_Shadcn_,
|
||||
Collapsible_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
const QueryError = ({
|
||||
error,
|
||||
onSelectDebug,
|
||||
}: {
|
||||
error: QueryResponseError
|
||||
onSelectDebug: () => void
|
||||
}) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
const formattedError =
|
||||
(error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3 px-5">
|
||||
<Alert_Shadcn_ variant="destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex flex-col gap-3">
|
||||
<AlertTitle_Shadcn_ className="m-0">Error running SQL query</AlertTitle_Shadcn_>
|
||||
|
||||
<Collapsible_Shadcn_
|
||||
defaultOpen
|
||||
className="flex flex-col gap-3"
|
||||
open={open}
|
||||
onOpenChange={() => setOpen(!open)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<CollapsibleTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
size={'tiny'}
|
||||
type="outline"
|
||||
className={cn('group', styles['ai-icon__container--allow-hover-effect'])}
|
||||
>
|
||||
{open ? 'Hide error details' : 'Show error details'}
|
||||
</Button>
|
||||
</CollapsibleTrigger_Shadcn_>
|
||||
<Button
|
||||
size={'tiny'}
|
||||
type="default"
|
||||
className={cn(
|
||||
'group',
|
||||
styles['ai-icon__container--allow-hover-effect h-[21px] !py-0']
|
||||
)}
|
||||
// icon={
|
||||
// <AiIcon className="scale-50 [&>div>div]:border-warning/50 [&>div>div]:group-hover:border-warning" />
|
||||
// }
|
||||
onClick={() => onSelectDebug()}
|
||||
>
|
||||
Fix with Assistant
|
||||
</Button>
|
||||
</div>
|
||||
<CollapsibleContent_Shadcn_ className="overflow-auto">
|
||||
{formattedError.length > 0 ? (
|
||||
formattedError.map((x: string, i: number) => (
|
||||
<pre key={`error-${i}`} className="font-mono text-xs overflow">
|
||||
{x.split(' ').map((x: string, i: number) => (
|
||||
<span
|
||||
className={cn(x === 'ERROR:' && 'text-destructive')}
|
||||
key={`error-${i}-${x}`}
|
||||
>
|
||||
{x}{' '}
|
||||
</span>
|
||||
))}
|
||||
</pre>
|
||||
))
|
||||
) : (
|
||||
<p className="font-mono text-xs">{error.error}</p>
|
||||
)}
|
||||
</CollapsibleContent_Shadcn_>
|
||||
</Collapsible_Shadcn_>
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
<div className="overflow-x-auto"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryError
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import Editor, { OnChange, OnMount } from '@monaco-editor/react'
|
||||
import { editor } from 'monaco-editor'
|
||||
import { MutableRefObject, useRef } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { alignEditor } from 'components/ui/CodeEditor'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
|
||||
// [Joshen] Is there a way we can just have one single MonacoEditor component that's shared across the dashboard?
|
||||
// Feels like we're creating multiple copies of Editor
|
||||
|
||||
interface RLSCodeEditorProps {
|
||||
id: string
|
||||
defaultValue?: string
|
||||
onInputChange?: (value?: string) => void
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
value?: string
|
||||
editorRef: MutableRefObject<editor.IStandaloneCodeEditor | null>
|
||||
}
|
||||
|
||||
// const placeholderText = `
|
||||
// CREATE POLICY *name* ON *table_name*\n
|
||||
// [ AS { PERMISSIVE | RESTRICTIVE } ]\n
|
||||
// [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]\n
|
||||
// [ TO *role_name* ]\n
|
||||
// [ USING ( *using_expression* ) ]\n
|
||||
// [ WITH CHECK ( *check_expression* ) ];
|
||||
// `.trim()
|
||||
|
||||
const placeholderText = `
|
||||
CREATE POLICY *name* ON *table_name*\n
|
||||
AS PERMISSIVE -- PERMISSIVE | RESTRICTIVE\n
|
||||
FOR ALL -- ALL | SELECT | INSERT | UPDATE | DELETE\n
|
||||
TO *role_name* -- Default: public\n
|
||||
USING ( *using_expression* )\n
|
||||
WITH CHECK ( *check_expression* );\n
|
||||
\n
|
||||
-- Docs: https://www.postgresql.org/docs/current/sql-createpolicy.html
|
||||
`.trim()
|
||||
|
||||
const RLSCodeEditor = ({
|
||||
id,
|
||||
defaultValue,
|
||||
wrapperClassName,
|
||||
className,
|
||||
value,
|
||||
editorRef,
|
||||
}: RLSCodeEditorProps) => {
|
||||
const hasValue = useRef<any>()
|
||||
|
||||
const onMount: OnMount = async (editor, monaco) => {
|
||||
editorRef.current = editor
|
||||
alignEditor(editor)
|
||||
|
||||
hasValue.current = editor.createContextKey('hasValue', false)
|
||||
|
||||
const placeholder = document.querySelector('.monaco-placeholder') as HTMLElement | null
|
||||
if (placeholder) placeholder.style.display = 'block'
|
||||
|
||||
editor.addCommand(
|
||||
monaco.KeyCode.Tab,
|
||||
() => {
|
||||
editor.executeEdits('source', [
|
||||
{
|
||||
// @ts-ignore
|
||||
identifier: 'add-placeholder',
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
text: placeholderText
|
||||
.split('\n\n')
|
||||
.join('\n')
|
||||
.replaceAll('*', '')
|
||||
.replaceAll(' ', ''),
|
||||
},
|
||||
])
|
||||
},
|
||||
'!hasValue'
|
||||
)
|
||||
|
||||
editor.focus()
|
||||
}
|
||||
|
||||
const onChange: OnChange = (value) => {
|
||||
hasValue.current.set((value ?? '').length > 0)
|
||||
|
||||
const placeholder = document.querySelector('.monaco-placeholder') as HTMLElement | null
|
||||
if (placeholder) {
|
||||
if (!value) {
|
||||
placeholder.style.display = 'block'
|
||||
} else {
|
||||
placeholder.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
tabSize: 2,
|
||||
fontSize: 13,
|
||||
readOnly: false,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on' as const,
|
||||
fixedOverflowWidgets: true,
|
||||
contextmenu: true,
|
||||
lineNumbers: undefined,
|
||||
glyphMargin: undefined,
|
||||
lineNumbersMinChars: 4,
|
||||
folding: undefined,
|
||||
scrollBeyondLastLine: false,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
path={id}
|
||||
theme="supabase"
|
||||
wrapperProps={{ className: cn(wrapperClassName) }}
|
||||
className={cn(className, 'monaco-editor')}
|
||||
value={value ?? undefined}
|
||||
defaultLanguage="pgsql"
|
||||
defaultValue={defaultValue ?? undefined}
|
||||
options={options}
|
||||
onMount={onMount}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div
|
||||
className="monaco-placeholder absolute top-[3px] left-[57px] text-sm pointer-events-none font-mono [&>div>p]:text-foreground-lighter [&>div>p]:!m-0 tracking-tighter"
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
<Markdown content={placeholderText} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RLSCodeEditor
|
||||
@@ -0,0 +1,372 @@
|
||||
import { FileDiff } from 'lucide-react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { ThreadMessage } from 'openai/resources/beta/threads/messages/messages'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button, Modal, SheetContent_Shadcn_, SheetFooter_Shadcn_, Sheet_Shadcn_, cn } from 'ui'
|
||||
|
||||
import {
|
||||
IStandaloneCodeEditor,
|
||||
IStandaloneDiffEditor,
|
||||
} from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import { useRlsSuggestMutation } from 'data/ai/rls-suggest-mutation'
|
||||
import { useRlsSuggestQuery } from 'data/ai/rls-suggest-query'
|
||||
import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation'
|
||||
import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query'
|
||||
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { useSelectedProject, useStore } from 'hooks'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { AIPolicyChat } from './AIPolicyChat'
|
||||
import { generateThreadMessage } from './AIPolicyEditorPanel.utils'
|
||||
import { AIPolicyHeader } from './AIPolicyHeader'
|
||||
import QueryError from './QueryError'
|
||||
import RLSCodeEditor from './RLSCodeEditor'
|
||||
|
||||
const DiffEditor = dynamic(
|
||||
() => import('@monaco-editor/react').then(({ DiffEditor }) => DiffEditor),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
interface AIPolicyEditorPanelProps {
|
||||
visible: boolean
|
||||
onSelectCancel: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Using memo for this component because everything rerenders on window focus because of outside fetches
|
||||
*/
|
||||
export const AIPolicyEditorPanel = memo(function ({
|
||||
visible,
|
||||
onSelectCancel,
|
||||
}: AIPolicyEditorPanelProps) {
|
||||
const { meta } = useStore()
|
||||
const selectedProject = useSelectedProject()
|
||||
|
||||
const editorRef = useRef<IStandaloneCodeEditor | null>(null)
|
||||
const diffEditorRef = useRef<IStandaloneDiffEditor | null>(null)
|
||||
|
||||
const [error, setError] = useState<QueryResponseError>()
|
||||
// [Joshen] Separate state here as there's a delay between submitting and the API updating the loading status
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [keepPreviousData, setKeepPreviousData] = useState(false)
|
||||
const [debugThread, setDebugThread] = useState<ThreadMessage[]>([])
|
||||
const [assistantVisible, setAssistantPanel] = useState(false)
|
||||
const [ids, setIds] = useState<{ threadId: string; runId: string } | undefined>(undefined)
|
||||
const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState(true)
|
||||
const [incomingChange, setIncomingChange] = useState<string | undefined>(undefined)
|
||||
// used for confirmation when closing the panel with unsaved changes
|
||||
const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState(false)
|
||||
|
||||
const { data } = useRlsSuggestQuery(
|
||||
{ thread_id: ids?.threadId!, run_id: ids?.runId! },
|
||||
{
|
||||
enabled: !!(ids?.runId && ids.threadId),
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchInterval: (data) => {
|
||||
if (data && data.status === 'completed') {
|
||||
return Infinity
|
||||
}
|
||||
return 5000
|
||||
},
|
||||
keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
const { data: entities } = useEntityDefinitionsQuery(
|
||||
{
|
||||
projectRef: selectedProject?.ref,
|
||||
connectionString: selectedProject?.connectionString,
|
||||
},
|
||||
{ enabled: true, refetchOnWindowFocus: false }
|
||||
)
|
||||
|
||||
const entityDefinitions = entities?.map((def) => def.sql.trim())
|
||||
|
||||
const { mutate: addPromptMutation } = useRlsSuggestMutation({
|
||||
onSuccess: (data) => {
|
||||
setIds({ threadId: data.threadId, runId: data.runId })
|
||||
},
|
||||
onError: (error) => {
|
||||
const threadMessage = generateThreadMessage({
|
||||
threadId: ids?.threadId,
|
||||
runId: ids?.runId,
|
||||
content: error.message.includes('No OPENAI_KEY set')
|
||||
? `Seems like you haven't set an OPENAI_KEY in your environment variables of the dashboard. Update your .env file with that environment variable to use all the AI features of the dashboard!`
|
||||
: error.message,
|
||||
})
|
||||
setDebugThread([...debugThread, threadMessage])
|
||||
setLoading(false)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: executeMutation, isLoading: isExecuting } = useExecuteSqlMutation({
|
||||
onSuccess: () => {
|
||||
// refresh all policies
|
||||
meta.policies.load()
|
||||
toast.success('Successfully created new policy')
|
||||
onSelectCancel()
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation()
|
||||
|
||||
const addPrompt = useCallback(
|
||||
(message: string) => {
|
||||
setLoading(true)
|
||||
if (ids?.threadId) {
|
||||
addPromptMutation({
|
||||
thread_id: ids?.threadId,
|
||||
prompt: message,
|
||||
})
|
||||
} else {
|
||||
addPromptMutation({
|
||||
thread_id: ids?.threadId,
|
||||
entityDefinitions,
|
||||
prompt: message,
|
||||
})
|
||||
}
|
||||
},
|
||||
[addPromptMutation, entityDefinitions, ids?.threadId]
|
||||
)
|
||||
|
||||
const messages = useMemo(
|
||||
() => [...(data?.messages ?? []), ...debugThread],
|
||||
[data?.messages, debugThread]
|
||||
)
|
||||
|
||||
const errorLines =
|
||||
error?.formattedError.split('\n').filter((x: string) => x.length > 0).length ?? 0
|
||||
|
||||
const createNewPolicy = useCallback(() => {
|
||||
// clean up the sql before sending
|
||||
const policy = editorRef.current?.getValue().replaceAll('\n', ' ').replaceAll(' ', ' ')
|
||||
|
||||
if (policy) {
|
||||
setError(undefined)
|
||||
executeMutation({
|
||||
sql: policy,
|
||||
projectRef: selectedProject?.ref,
|
||||
connectionString: selectedProject?.connectionString,
|
||||
})
|
||||
}
|
||||
}, [executeMutation, selectedProject?.connectionString, selectedProject?.ref])
|
||||
|
||||
const acceptChange = useCallback(async () => {
|
||||
if (!incomingChange) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!editorRef.current || !diffEditorRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const editorModel = editorRef.current.getModel()
|
||||
const diffModel = diffEditorRef.current.getModel()
|
||||
|
||||
if (!editorModel || !diffModel) {
|
||||
return
|
||||
}
|
||||
|
||||
const sql = diffModel.modified.getValue()
|
||||
|
||||
// apply the incoming change in the editor directly so that Undo/Redo work properly
|
||||
editorRef.current.executeEdits('apply-ai-edit', [
|
||||
{
|
||||
text: sql,
|
||||
range: editorModel.getFullModelRange(),
|
||||
},
|
||||
])
|
||||
|
||||
// remove the incoming change to revert to the original editor
|
||||
setIncomingChange(undefined)
|
||||
}, [incomingChange])
|
||||
|
||||
const onClosingPanel = useCallback(() => {
|
||||
const policy = editorRef.current?.getValue()
|
||||
if (policy || messages.length > 0 || !isAssistantChatInputEmpty) {
|
||||
setIsClosingPolicyEditorPanel(true)
|
||||
} else {
|
||||
onSelectCancel()
|
||||
}
|
||||
}, [onSelectCancel, messages, isAssistantChatInputEmpty])
|
||||
|
||||
const onSelectDebug = async () => {
|
||||
const policy = editorRef.current?.getValue().replaceAll('\n', ' ').replaceAll(' ', ' ')
|
||||
if (error === undefined || policy === undefined) return
|
||||
|
||||
setAssistantPanel(true)
|
||||
const messageId = uuidv4()
|
||||
|
||||
const assistantMessageBefore = generateThreadMessage({
|
||||
id: messageId,
|
||||
threadId: ids?.threadId,
|
||||
runId: ids?.runId,
|
||||
content: 'Thinking...',
|
||||
metadata: { type: 'debug' },
|
||||
})
|
||||
setDebugThread([...debugThread, assistantMessageBefore])
|
||||
|
||||
const { solution, sql } = await debugSql({
|
||||
sql: policy.trim(),
|
||||
errorMessage: error.message,
|
||||
entityDefinitions,
|
||||
})
|
||||
|
||||
const assistantMessageAfter = generateThreadMessage({
|
||||
id: messageId,
|
||||
threadId: ids?.threadId,
|
||||
runId: ids?.runId,
|
||||
content: `${solution}\n\`\`\`sql\n${sql}\n\`\`\``,
|
||||
metadata: { type: 'debug' },
|
||||
})
|
||||
setDebugThread([...debugThread, assistantMessageAfter])
|
||||
}
|
||||
|
||||
const onDiff = useCallback((v: string) => setIncomingChange(v), [])
|
||||
|
||||
// when the panel is closed, reset all values
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
const policy = editorRef.current?.getValue()
|
||||
if (policy) editorRef.current?.setValue('')
|
||||
if (incomingChange) setIncomingChange(undefined)
|
||||
if (assistantVisible) setAssistantPanel(false)
|
||||
setIsClosingPolicyEditorPanel(false)
|
||||
setIds(undefined)
|
||||
setError(undefined)
|
||||
setDebugThread([])
|
||||
setKeepPreviousData(false)
|
||||
} else {
|
||||
setKeepPreviousData(true)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.status === 'completed') setLoading(false)
|
||||
}, [data?.status])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet_Shadcn_ open={visible} onOpenChange={() => onClosingPanel()}>
|
||||
<SheetContent_Shadcn_
|
||||
size="lg"
|
||||
className={cn('p-0 flex flex-row gap-0', assistantVisible && '!min-w-[1024px]')}
|
||||
>
|
||||
<div className={cn('flex flex-col grow w-full', assistantVisible && 'w-[60%]')}>
|
||||
<AIPolicyHeader
|
||||
assistantVisible={assistantVisible}
|
||||
setAssistantVisible={setAssistantPanel}
|
||||
/>
|
||||
<div className="flex flex-col h-full w-full justify-between">
|
||||
{incomingChange ? (
|
||||
<div className="px-5 py-3 flex justify-between gap-3 bg-muted">
|
||||
<div className="flex gap-2 items-center text-foreground-light">
|
||||
<FileDiff className="h-4 w-4" />
|
||||
<span className="text-sm">Apply changes from assistant</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button type="default" onClick={() => setIncomingChange(undefined)}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => acceptChange()}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{incomingChange ? (
|
||||
<DiffEditor
|
||||
theme="supabase"
|
||||
language="pgsql"
|
||||
className="flex grow"
|
||||
original={editorRef.current?.getValue()}
|
||||
modified={incomingChange}
|
||||
onMount={(editor) => (diffEditorRef.current = editor)}
|
||||
options={{
|
||||
renderSideBySide: false,
|
||||
scrollBeyondLastLine: false,
|
||||
renderOverviewRuler: false,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
// [Joshen] Not the cleanest but its to force the editor to re-render its height
|
||||
// for now, till we can find a better solution
|
||||
className={`relative ${incomingChange ? 'hidden' : 'block'}`}
|
||||
style={{
|
||||
height:
|
||||
error === undefined
|
||||
? 'calc(100vh - 58px - 54px)'
|
||||
: `calc(100vh - 58px - 151px - ${20 * errorLines}px)`,
|
||||
}}
|
||||
>
|
||||
<RLSCodeEditor
|
||||
id="rls-sql-policy"
|
||||
wrapperClassName={incomingChange ? '!hidden' : ''}
|
||||
defaultValue={''}
|
||||
editorRef={editorRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{error !== undefined && <QueryError error={error} onSelectDebug={onSelectDebug} />}
|
||||
<SheetFooter_Shadcn_ className="flex flex-col gap-12 px-5 py-4 w-full">
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<Button type="default" disabled={isExecuting} onClick={() => onSelectCancel()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
loading={isExecuting}
|
||||
htmlType="submit"
|
||||
disabled={isExecuting || incomingChange !== undefined}
|
||||
onClick={() => createNewPolicy()}
|
||||
>
|
||||
Save policy
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{assistantVisible && (
|
||||
<div className={cn('flex border-l grow w-full', assistantVisible && 'w-[40%]')}>
|
||||
<AIPolicyChat
|
||||
messages={messages}
|
||||
onSubmit={(message: string) => addPrompt(message)}
|
||||
onDiff={onDiff}
|
||||
onChange={setIsAssistantChatInputEmpty}
|
||||
loading={loading || data?.status === 'loading'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isClosingPolicyEditorPanel}
|
||||
header="Discard changes"
|
||||
buttonLabel="Discard"
|
||||
onSelectCancel={() => setIsClosingPolicyEditorPanel(false)}
|
||||
onSelectConfirm={() => {
|
||||
onSelectCancel()
|
||||
setIsClosingPolicyEditorPanel(false)
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
Are you sure you want to close the editor? Any unsaved changes on your policy and
|
||||
conversations with the Assistant will be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</SheetContent_Shadcn_>
|
||||
</Sheet_Shadcn_>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
AIPolicyEditorPanel.displayName = 'AIPolicyEditorPanel'
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { PostgresRole, PostgresTable } from '@supabase/postgres-meta'
|
||||
import type { PostgresPolicy, PostgresRole, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { useParams } from 'common/hooks'
|
||||
import { PolicyEditorModal, PolicyTableRow } from 'components/interfaces/Auth/Policies'
|
||||
import { useStore } from 'hooks'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { IconHelpCircle } from 'ui'
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { tableKeys } from 'data/tables/keys'
|
||||
|
||||
interface PoliciesProps {
|
||||
@@ -31,13 +31,17 @@ const Policies = ({ tables, hasTables, isLocked }: PoliciesProps) => {
|
||||
|
||||
const [selectedSchemaAndTable, setSelectedSchemaAndTable] = useState<any>({})
|
||||
const [selectedTableToToggleRLS, setSelectedTableToToggleRLS] = useState<any>({})
|
||||
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<any>({})
|
||||
const [RLSEditorWithAIShown, showRLSEditorWithAI] = useState(false)
|
||||
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<PostgresPolicy | {}>({})
|
||||
const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<any>({})
|
||||
|
||||
const closePolicyEditorModal = () => {
|
||||
const closePolicyEditorModal = useCallback(() => {
|
||||
setSelectedPolicyToEdit({})
|
||||
setSelectedSchemaAndTable({})
|
||||
}
|
||||
if (RLSEditorWithAIShown) {
|
||||
showRLSEditorWithAI(false)
|
||||
}
|
||||
}, [RLSEditorWithAIShown])
|
||||
|
||||
const closeConfirmModal = () => {
|
||||
setSelectedPolicyToDelete({})
|
||||
@@ -61,10 +65,10 @@ const Policies = ({ tables, hasTables, isLocked }: PoliciesProps) => {
|
||||
setSelectedPolicyToDelete(policy)
|
||||
}
|
||||
|
||||
const onSavePolicySuccess = async () => {
|
||||
const onSavePolicySuccess = useCallback(async () => {
|
||||
ui.setNotification({ category: 'success', message: 'Policy successfully saved!' })
|
||||
closePolicyEditorModal()
|
||||
}
|
||||
}, [closePolicyEditorModal])
|
||||
|
||||
// Methods that involve some API
|
||||
const onToggleRLS = async () => {
|
||||
@@ -85,7 +89,7 @@ const Policies = ({ tables, hasTables, isLocked }: PoliciesProps) => {
|
||||
closeConfirmModal()
|
||||
}
|
||||
|
||||
const onCreatePolicy = async (payload: any) => {
|
||||
const onCreatePolicy = useCallback(async (payload: any) => {
|
||||
const res = await meta.policies.create(payload)
|
||||
if (res.error) {
|
||||
ui.setNotification({
|
||||
@@ -95,7 +99,7 @@ const Policies = ({ tables, hasTables, isLocked }: PoliciesProps) => {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onUpdatePolicy = async (payload: any) => {
|
||||
const res = await meta.policies.update(payload.id, payload)
|
||||
@@ -181,9 +185,7 @@ const Policies = ({ tables, hasTables, isLocked }: PoliciesProps) => {
|
||||
table={selectedSchemaAndTable.table}
|
||||
selectedPolicyToEdit={selectedPolicyToEdit}
|
||||
onSelectCancel={closePolicyEditorModal}
|
||||
// @ts-ignore
|
||||
onCreatePolicy={onCreatePolicy}
|
||||
// @ts-ignore
|
||||
onUpdatePolicy={onUpdatePolicy}
|
||||
onSaveSuccess={onSavePolicySuccess}
|
||||
/>
|
||||
|
||||
@@ -31,8 +31,8 @@ interface PolicyEditorModalProps {
|
||||
table: string
|
||||
selectedPolicyToEdit: any
|
||||
onSelectCancel: () => void
|
||||
onCreatePolicy: (payload: PostgresPolicyCreatePayload) => boolean
|
||||
onUpdatePolicy: (payload: PostgresPolicyUpdatePayload) => boolean
|
||||
onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise<boolean>
|
||||
onUpdatePolicy: (payload: PostgresPolicyUpdatePayload) => Promise<boolean>
|
||||
onSaveSuccess: () => void
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ const PolicyEditorModal = ({
|
||||
table = '',
|
||||
selectedPolicyToEdit = {},
|
||||
onSelectCancel = noop,
|
||||
onCreatePolicy = () => false,
|
||||
onUpdatePolicy = () => false,
|
||||
onCreatePolicy,
|
||||
onUpdatePolicy,
|
||||
onSaveSuccess = noop,
|
||||
}: PolicyEditorModalProps) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
@@ -84,7 +84,7 @@ const PolicyRow = ({
|
||||
icon={<IconMoreVertical />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-32">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => onSelectEditPolicy(policy)}>
|
||||
<IconEdit size={14} />
|
||||
<p>Edit</p>
|
||||
|
||||
@@ -22,12 +22,14 @@ const prism = {
|
||||
interface SimpleCodeBlockProps {
|
||||
className?: string
|
||||
metastring?: string
|
||||
showCopy?: boolean
|
||||
}
|
||||
|
||||
const SimpleCodeBlock = ({
|
||||
children,
|
||||
className: languageClassName,
|
||||
metastring,
|
||||
showCopy = true,
|
||||
}: PropsWithChildren<SimpleCodeBlockProps>) => {
|
||||
const [showCopied, setShowCopied] = useState(false)
|
||||
const target = useRef(null)
|
||||
@@ -82,11 +84,13 @@ const SimpleCodeBlock = ({
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
<div className="invisible absolute right-0 top-0 opacity-0 transition-opacity group-hover:visible group-hover:opacity-100">
|
||||
<Button size="tiny" type="default" onClick={() => handleCopyCode(children)}>
|
||||
{showCopied ? 'Copied' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
{showCopy && (
|
||||
<div className="invisible absolute right-0 top-0 opacity-0 transition-opacity group-hover:visible group-hover:opacity-100">
|
||||
<Button size="tiny" type="default" onClick={() => handleCopyCode(children)}>
|
||||
{showCopied ? 'Copied' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Editor, { EditorProps } from '@monaco-editor/react'
|
||||
import Editor, { EditorProps, OnMount } from '@monaco-editor/react'
|
||||
import { merge, noop } from 'lodash'
|
||||
import { useRef } from 'react'
|
||||
|
||||
@@ -36,9 +36,9 @@ const CodeEditor = ({
|
||||
options,
|
||||
value,
|
||||
}: CodeEditorProps) => {
|
||||
const editorRef = useRef()
|
||||
const editorRef = useRef<any>()
|
||||
|
||||
const onMount = async (editor: any, monaco: any) => {
|
||||
const onMount: OnMount = async (editor, monaco) => {
|
||||
editorRef.current = editor
|
||||
alignEditor(editor)
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const aiKeys = {
|
||||
rlsSuggest: (threadId: string, runId: string) => [threadId, runId] as const,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { isResponseOk, post } from 'lib/common/fetch'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type RlsSuggestResponse = {
|
||||
threadId: string
|
||||
runId: string
|
||||
}
|
||||
|
||||
export type RlsSuggestVariables = {
|
||||
thread_id?: string
|
||||
entityDefinitions?: string[]
|
||||
prompt: string
|
||||
}
|
||||
|
||||
export async function rlsSuggest({ thread_id, entityDefinitions, prompt }: RlsSuggestVariables) {
|
||||
const response = await post<RlsSuggestResponse>(BASE_PATH + '/api/ai/sql/suggest', {
|
||||
thread_id,
|
||||
entityDefinitions,
|
||||
prompt,
|
||||
})
|
||||
|
||||
if (!isResponseOk(response)) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
type RlsSuggestData = Awaited<ReturnType<typeof rlsSuggest>>
|
||||
|
||||
export const useRlsSuggestMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<RlsSuggestData, ResponseError, RlsSuggestVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
return useMutation<RlsSuggestData, ResponseError, RlsSuggestVariables>(
|
||||
(vars) => rlsSuggest(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to prompt suggestion: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import OpenAI from 'openai'
|
||||
import { aiKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type RlsSuggestVariables = {
|
||||
thread_id: string
|
||||
run_id: string
|
||||
}
|
||||
|
||||
export type RlsSuggestResponse = {
|
||||
id: string
|
||||
status: 'completed' | 'loading'
|
||||
messages: OpenAI.Beta.Threads.Messages.ThreadMessage[]
|
||||
}
|
||||
|
||||
export async function rlsSuggest({ thread_id, run_id }: RlsSuggestVariables, signal?: AbortSignal) {
|
||||
const response = await get(`${BASE_PATH}/api/ai/sql/suggest/${thread_id}/${run_id}`, { signal })
|
||||
if (response.error) throw response.error
|
||||
|
||||
return response as RlsSuggestResponse
|
||||
}
|
||||
|
||||
export type RlsSuggestData = Awaited<ReturnType<typeof rlsSuggest>>
|
||||
export type RlsSuggestDataError = ResponseError
|
||||
|
||||
export const useRlsSuggestQuery = <TData = RlsSuggestData>(
|
||||
{ thread_id, run_id }: RlsSuggestVariables,
|
||||
options: UseQueryOptions<RlsSuggestData, RlsSuggestDataError, TData> = {}
|
||||
) =>
|
||||
useQuery<RlsSuggestData, RlsSuggestDataError, TData>(
|
||||
aiKeys.rlsSuggest(thread_id, run_id),
|
||||
({ signal }) => rlsSuggest({ thread_id, run_id }, signal),
|
||||
options
|
||||
)
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { isResponseOk, post } from 'lib/common/fetch'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
@@ -32,12 +34,20 @@ type SqlDebugData = Awaited<ReturnType<typeof debugSql>>
|
||||
|
||||
export const useSqlDebugMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<UseMutationOptions<SqlDebugData, ResponseError, SqlDebugVariables>, 'mutationFn'> = {}) => {
|
||||
return useMutation<SqlDebugData, ResponseError, SqlDebugVariables>((vars) => debugSql(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to debug SQL: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { isResponseOk, post } from 'lib/common/fetch'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
@@ -31,12 +33,20 @@ type SqlEditData = Awaited<ReturnType<typeof editSql>>
|
||||
|
||||
export const useSqlEditMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<UseMutationOptions<SqlEditData, ResponseError, SqlEditVariables>, 'mutationFn'> = {}) => {
|
||||
return useMutation<SqlEditData, ResponseError, SqlEditVariables>((vars) => editSql(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to edit SQL: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { isResponseOk, post } from 'lib/common/fetch'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
@@ -30,6 +32,7 @@ type SqlGenerateData = Awaited<ReturnType<typeof generateSql>>
|
||||
|
||||
export const useSqlGenerateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<SqlGenerateData, ResponseError, SqlGenerateVariables>,
|
||||
@@ -41,6 +44,13 @@ export const useSqlGenerateMutation = ({
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to generate SQL: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { isResponseOk, post } from 'lib/common/fetch'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ResponseError } from 'types'
|
||||
@@ -26,6 +28,7 @@ type SqlTitleGenerateData = Awaited<ReturnType<typeof generateSqlTitle>>
|
||||
|
||||
export const useSqlTitleGenerateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<SqlTitleGenerateData, ResponseError, SqlTitleGenerateVariables>,
|
||||
@@ -37,6 +40,13 @@ export const useSqlTitleGenerateMutation = ({
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to generate title: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
|
||||
import { executeSql, ExecuteSqlData, ExecuteSqlVariables } from './execute-sql-query'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
/* Execute Query */
|
||||
export type QueryResponseError = {
|
||||
code: string
|
||||
message: string
|
||||
error: string
|
||||
formattedError: string
|
||||
file: string
|
||||
length: number
|
||||
line: string
|
||||
name: string
|
||||
position: string
|
||||
routine: string
|
||||
severity: string
|
||||
}
|
||||
|
||||
export const useExecuteSqlMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<ExecuteSqlData, ResponseError, ExecuteSqlVariables>,
|
||||
UseMutationOptions<ExecuteSqlData, QueryResponseError, ExecuteSqlVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
return useMutation<ExecuteSqlData, ResponseError, ExecuteSqlVariables>(
|
||||
return useMutation<ExecuteSqlData, QueryResponseError, ExecuteSqlVariables>(
|
||||
(args) => executeSql(args),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
"monaco-editor": "0.33.0",
|
||||
"next": "^13.5.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"openai": "^3.3.0",
|
||||
"openai-old": "npm:openai@^3.3.0",
|
||||
"openai": "^4.0.0",
|
||||
"openapi-fetch": "^0.6.1",
|
||||
"p-queue": "^6.6.2",
|
||||
"papaparse": "^5.3.1",
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
CreateChatCompletionRequest,
|
||||
CreateChatCompletionResponse,
|
||||
ErrorResponse,
|
||||
} from 'openai'
|
||||
} from 'openai-old'
|
||||
|
||||
const openAiKey = process.env.OPENAI_KEY
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
CreateChatCompletionRequest,
|
||||
CreateChatCompletionResponse,
|
||||
ErrorResponse,
|
||||
} from 'openai'
|
||||
} from 'openai-old'
|
||||
|
||||
const openAiKey = process.env.OPENAI_KEY
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
CreateChatCompletionRequest,
|
||||
CreateChatCompletionResponse,
|
||||
ErrorResponse,
|
||||
} from 'openai'
|
||||
} from 'openai-old'
|
||||
|
||||
const openAiKey = process.env.OPENAI_KEY
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
// This API function uses the new Threads API, which is only available on the v4 OpenAI lib.
|
||||
import { OpenAI } from 'openai'
|
||||
|
||||
const openAiKey = process.env.OPENAI_KEY
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!openAiKey) {
|
||||
return res.status(500).json({
|
||||
error: 'No OPENAI_KEY set. Create this environment variable to use AI features.',
|
||||
})
|
||||
}
|
||||
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGet(req, res)
|
||||
default:
|
||||
res.setHeader('Allow', ['GET'])
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleGet(req: NextApiRequest, res: NextApiResponse) {
|
||||
const openai = new OpenAI({ apiKey: openAiKey })
|
||||
|
||||
const { thread_id, run_id } = req.query as { thread_id: string; run_id: string }
|
||||
|
||||
const [run, { data: messages }] = await Promise.all([
|
||||
openai.beta.threads.runs.retrieve(thread_id, run_id),
|
||||
openai.beta.threads.messages.list(thread_id),
|
||||
])
|
||||
|
||||
let status = 'loading'
|
||||
if (run.status === 'completed') {
|
||||
status = 'completed'
|
||||
} else if (run.status === 'failed') {
|
||||
status = 'failed'
|
||||
}
|
||||
|
||||
const result = {
|
||||
id: thread_id,
|
||||
status,
|
||||
messages: messages,
|
||||
}
|
||||
|
||||
return res.json(result)
|
||||
}
|
||||
|
||||
const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
export default wrapper
|
||||
@@ -0,0 +1,68 @@
|
||||
import { codeBlock } from 'common-tags'
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
// This API function uses the new Threads API, which is only available on the v4 OpenAI lib.
|
||||
import { OpenAI } from 'openai'
|
||||
|
||||
const openAiKey = process.env.OPENAI_KEY
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!openAiKey) {
|
||||
return res.status(500).json({
|
||||
error: 'No OPENAI_KEY set. Create this environment variable to use AI features.',
|
||||
})
|
||||
}
|
||||
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return handlePost(req, res)
|
||||
default:
|
||||
res.setHeader('Allow', ['POST'])
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePost(req: NextApiRequest, res: NextApiResponse) {
|
||||
const openai = new OpenAI({ apiKey: openAiKey })
|
||||
|
||||
try {
|
||||
let {
|
||||
body: { thread_id, entityDefinitions, prompt },
|
||||
} = req
|
||||
|
||||
if (!thread_id) {
|
||||
const thread = await openai.beta.threads.create()
|
||||
thread_id = thread.id
|
||||
}
|
||||
|
||||
if (entityDefinitions) {
|
||||
const one = await openai.beta.threads.messages.create(thread_id, {
|
||||
role: 'user',
|
||||
content: codeBlock`
|
||||
Here is my database schema for reference:
|
||||
${entityDefinitions.join('\n\n')}
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
const two = await openai.beta.threads.messages.create(thread_id, {
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
})
|
||||
|
||||
const run = await openai.beta.threads.runs.create(thread_id, {
|
||||
assistant_id: 'asst_O89dyQ2ttPVWfs8YEjKPFqAK',
|
||||
})
|
||||
|
||||
return res.json({ threadId: thread_id, runId: run.id })
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500)
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
export default wrapper
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
CreateChatCompletionRequest,
|
||||
CreateChatCompletionResponse,
|
||||
ErrorResponse,
|
||||
} from 'openai'
|
||||
} from 'openai-old'
|
||||
|
||||
const openAiKey = process.env.OPENAI_KEY
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { partition } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, IconExternalLink, IconSearch, Input } from 'ui'
|
||||
|
||||
import { useParams } from 'common/hooks'
|
||||
import { Policies } from 'components/interfaces/Auth/Policies'
|
||||
import { AIPolicyEditorPanel } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
@@ -14,11 +17,10 @@ import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useCheckPermissions, useStore } from 'hooks'
|
||||
import { useCheckPermissions, useFlag, useStore } from 'hooks'
|
||||
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Button, IconExternalLink, IconSearch, Input } from 'ui'
|
||||
|
||||
/**
|
||||
* Filter tables by table name and policy name
|
||||
@@ -61,6 +63,9 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
const { meta } = useStore()
|
||||
const { search } = useParams()
|
||||
const [searchString, setSearchString] = useState<string>('')
|
||||
const canCreatePolicyWithAi = useFlag('policyEditorWithAi')
|
||||
|
||||
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (search) setSearchString(search)
|
||||
@@ -92,6 +97,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
|
||||
const filteredTables = onFilterTables(tables ?? [], policies, searchString)
|
||||
const canReadPolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies')
|
||||
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
|
||||
|
||||
if (!canReadPolicies) {
|
||||
return <NoPermission isFullPage resourceText="view this project's RLS policies" />
|
||||
@@ -121,22 +127,62 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
|
||||
>
|
||||
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
What is RLS?
|
||||
</Button>
|
||||
</a>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security"
|
||||
>
|
||||
<Button type="default" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
{canCreatePolicyWithAi && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canCreatePolicies}
|
||||
onClick={() => setShowPolicyAiEditor(true)}
|
||||
>
|
||||
Create a new policy
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canCreatePolicies && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">
|
||||
You need additional permissions to create RLS policies
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
|
||||
|
||||
{isSuccess && (
|
||||
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
|
||||
)}
|
||||
|
||||
<AIPolicyEditorPanel
|
||||
visible={showPolicyAiEditor}
|
||||
onSelectCancel={() => setShowPolicyAiEditor(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Generated
+72
-19
@@ -212,7 +212,8 @@
|
||||
},
|
||||
"apps/database-new/node_modules/@tanstack/query-core": {
|
||||
"version": "5.8.3",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.8.3.tgz",
|
||||
"integrity": "sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
@@ -220,7 +221,8 @@
|
||||
},
|
||||
"apps/database-new/node_modules/@tanstack/react-query": {
|
||||
"version": "5.8.4",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.8.4.tgz",
|
||||
"integrity": "sha512-CD+AkXzg8J72JrE6ocmuBEJfGzEzu/bzkD6sFXFDDB5yji9N20JofXZlN6n0+CaPJuIi+e4YLCbGsyPFKkfNQA==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.8.3"
|
||||
},
|
||||
@@ -252,10 +254,13 @@
|
||||
},
|
||||
"apps/database-new/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"license": "Python-2.0"
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"apps/database-new/node_modules/autoprefixer": {
|
||||
"version": "10.4.14",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
||||
"integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -267,7 +272,6 @@
|
||||
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.21.5",
|
||||
"caniuse-lite": "^1.0.30001464",
|
||||
@@ -396,7 +400,8 @@
|
||||
},
|
||||
"apps/database-new/node_modules/lucide-react": {
|
||||
"version": "0.292.0",
|
||||
"license": "ISC",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.292.0.tgz",
|
||||
"integrity": "sha512-rRgUkpEHWpa5VCT66YscInCQmQuPCB1RFRzkkxMxg4b+jaL0V12E3riWWR2Sh5OIiUhCwGW/ZExuEO4Az32E6Q==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
@@ -474,8 +479,9 @@
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/openai": {
|
||||
"version": "4.19.0",
|
||||
"license": "Apache-2.0",
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.20.0.tgz",
|
||||
"integrity": "sha512-VbAYerNZFfIIeESS+OL9vgDkK8Mnri55n+jN0UN/HZeuM0ghGh6nDN6UGRZxslNgyJ7XmY/Ca9DO4YYyvrszGA==",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
@@ -509,7 +515,8 @@
|
||||
},
|
||||
"apps/database-new/node_modules/sql-formatter": {
|
||||
"version": "13.1.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-13.1.0.tgz",
|
||||
"integrity": "sha512-/nZQXuN7KzipFNM20ko+dHY4kOr9rymSfZLUDED8rhx3m8OK5y74jcyN+y1L51ZqHqiB0kp40VdpZP99uWvQdA==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"get-stdin": "=8.0.0",
|
||||
@@ -780,7 +787,8 @@
|
||||
"monaco-editor": "0.33.0",
|
||||
"next": "^13.5.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"openai": "^3.3.0",
|
||||
"openai": "^4.0.0",
|
||||
"openai-old": "npm:openai@^3.3.0",
|
||||
"openapi-fetch": "^0.6.1",
|
||||
"p-queue": "^6.6.2",
|
||||
"papaparse": "^5.3.1",
|
||||
@@ -879,6 +887,13 @@
|
||||
"glob": "7.1.7"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/@types/node": {
|
||||
"version": "18.18.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"license": "MIT",
|
||||
@@ -1009,6 +1024,24 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/studio/node_modules/openai": {
|
||||
"version": "4.19.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"digest-fetch": "^1.3.0",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"web-streams-polyfill": "^3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/resolve": {
|
||||
"version": "2.0.0-next.5",
|
||||
"dev": true,
|
||||
@@ -3907,7 +3940,8 @@
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "0.43.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.43.1.tgz",
|
||||
"integrity": "sha512-Q5sMc4Z4gsD4tlmlyFu+MpNAwpR7Gv2errDhVJ+SOhNjWcx8UTqy+hswb8L31RfC8jBvDgcnT87l3xI2w08rAg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -4134,8 +4168,9 @@
|
||||
},
|
||||
"node_modules/@gregnr/libpg-query": {
|
||||
"version": "13.4.0-dev.12",
|
||||
"resolved": "https://registry.npmjs.org/@gregnr/libpg-query/-/libpg-query-13.4.0-dev.12.tgz",
|
||||
"integrity": "sha512-t3cQXXwTPeYJEIiNl3HioRdkRTq8bAblS3+ZfKBlRYL57uwFsibeBcqO8zdMA7FEwCqDhyBy0I3SMCHc3ZTb2w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^0.43.1",
|
||||
"@mapbox/node-pre-gyp": "^1.0.8",
|
||||
@@ -4145,7 +4180,8 @@
|
||||
},
|
||||
"node_modules/@gregnr/libpg-query/node_modules/node-addon-api": {
|
||||
"version": "7.0.0",
|
||||
"license": "MIT"
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz",
|
||||
"integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA=="
|
||||
},
|
||||
"node_modules/@hcaptcha/react-hcaptcha": {
|
||||
"version": "1.8.1",
|
||||
@@ -10094,8 +10130,7 @@
|
||||
},
|
||||
"node_modules/@supabase/ssr": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.0.10.tgz",
|
||||
"integrity": "sha512-eVs7+bNlff8Fd79x8K3Jbfpmf8P8QRA1Z6rUDN+fi4ReWvRBZyWOFfR6eqlsX6vTjvGgTiEqujFSkv2PYW5kbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^0.5.0",
|
||||
"ramda": "^0.29.0"
|
||||
@@ -15490,8 +15525,7 @@
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.1",
|
||||
@@ -25836,8 +25870,9 @@
|
||||
},
|
||||
"node_modules/nextjs-node-loader": {
|
||||
"version": "1.1.5-alpha.0",
|
||||
"resolved": "https://registry.npmjs.org/nextjs-node-loader/-/nextjs-node-loader-1.1.5-alpha.0.tgz",
|
||||
"integrity": "sha512-EaY047JUWPlD2pOwRdcYPeyJrUTOfNOGcLyPaiped1bFHvHGIAN8596IbFAM4vwaOMpICodC1mEoOXTKvtcrCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.3"
|
||||
},
|
||||
@@ -25850,8 +25885,9 @@
|
||||
},
|
||||
"node_modules/nextjs-node-loader/node_modules/loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
@@ -26570,6 +26606,22 @@
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/openai-old": {
|
||||
"name": "openai",
|
||||
"version": "3.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/openai-old/node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"license": "MIT",
|
||||
@@ -29515,7 +29567,8 @@
|
||||
},
|
||||
"node_modules/react-flow": {
|
||||
"version": "1.0.3",
|
||||
"license": "ISC"
|
||||
"resolved": "https://registry.npmjs.org/react-flow/-/react-flow-1.0.3.tgz",
|
||||
"integrity": "sha512-Rx4Oqqbe9y7NyrUGzCrmtNvuGuMJBVjfIeMFuwbtbMc9f22sUSmxMjIA0c8r6Z7iMtB1zOWoGkTgfpmmpH9ReQ=="
|
||||
},
|
||||
"node_modules/react-from-dom": {
|
||||
"version": "0.6.2",
|
||||
|
||||
@@ -147,17 +147,34 @@ export {
|
||||
} from './src/components/shadcn/ui/accordion'
|
||||
|
||||
export { Input as Input_Shadcn_ } from './src/components/shadcn/ui/input'
|
||||
|
||||
export { TextArea as TextArea_Shadcn_ } from './src/components/shadcn/ui/text-area'
|
||||
|
||||
export { Label as Label_Shadcn_ } from './src/components/shadcn/ui/label'
|
||||
|
||||
export * from './src/components/shadcn/ui/switch'
|
||||
|
||||
export { Checkbox as Checkbox_Shadcn_ } from './src/components/shadcn/ui/checkbox'
|
||||
|
||||
export * from './src/components/shadcn/ui/scroll-area'
|
||||
|
||||
export {
|
||||
Collapsible as Collapsible_Shadcn_,
|
||||
CollapsibleTrigger as CollapsibleTrigger_Shadcn_,
|
||||
CollapsibleContent as CollapsibleContent_Shadcn_,
|
||||
} from './src/components/shadcn/ui/collapsible'
|
||||
|
||||
export {
|
||||
Sheet as Sheet_Shadcn_,
|
||||
SheetTrigger as SheetTrigger_Shadcn_,
|
||||
SheetClose as SheetClose_Shadcn_,
|
||||
SheetContent as SheetContent_Shadcn_,
|
||||
SheetHeader as SheetHeader_Shadcn_,
|
||||
SheetFooter as SheetFooter_Shadcn_,
|
||||
SheetTitle as SheetTitle_Shadcn_,
|
||||
SheetDescription as SheetDescription_Shadcn_,
|
||||
} from './src/components/shadcn/ui/sheet'
|
||||
|
||||
export { ScrollArea, ScrollBar } from './src/components/shadcn/ui/scroll-area'
|
||||
|
||||
export { Separator } from './src/components/shadcn/ui/separator'
|
||||
|
||||
@@ -115,6 +115,9 @@ const buttonVariants = cva(
|
||||
disabled: {
|
||||
true: 'opacity-50 cursor-default',
|
||||
},
|
||||
rounded: {
|
||||
true: 'rounded-full',
|
||||
},
|
||||
defaultVariants: {
|
||||
// variant: 'default',
|
||||
// size: 'default',
|
||||
@@ -146,6 +149,7 @@ export interface ButtonProps
|
||||
icon?: React.ReactNode
|
||||
iconLeft?: React.ReactNode
|
||||
iconRight?: React.ReactNode
|
||||
rounded?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
@@ -161,6 +165,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
iconRight,
|
||||
iconLeft,
|
||||
htmlType = 'button',
|
||||
rounded,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -178,7 +183,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
ref={ref}
|
||||
type={htmlType}
|
||||
{...props}
|
||||
className={cn(buttonVariants({ type, size, disabled, block }), className)}
|
||||
className={cn(buttonVariants({ type, size, disabled, block, rounded }), className)}
|
||||
>
|
||||
{asChild ? (
|
||||
React.isValidElement(children) ? (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import React from 'react'
|
||||
import { Button } from '../../../index'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import styleHandler from '../../lib/theme/styleHandler'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
export type SidePanelProps = RadixProps & CustomProps
|
||||
|
||||
@@ -24,7 +24,7 @@ interface CustomProps {
|
||||
children?: React.ReactNode
|
||||
header?: string | React.ReactNode
|
||||
visible: boolean
|
||||
size?: 'medium' | 'large' | 'xlarge' | 'xxlarge'
|
||||
size?: 'medium' | 'large' | 'xlarge' | 'xxlarge' | 'xxxlarge' | 'xxxxlarge'
|
||||
loading?: boolean
|
||||
align?: 'right' | 'left'
|
||||
hideFooter?: boolean
|
||||
|
||||
@@ -6,6 +6,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { cn } from '@ui/lib/utils'
|
||||
import { DialogCloseProps } from '@radix-ui/react-alert-dialog'
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
@@ -13,7 +14,7 @@ const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const portalVariants = cva('fixed inset-0 z-50 flex', {
|
||||
const portalVariants = cva('fixed inset-0 z-40 flex', {
|
||||
variants: {
|
||||
position: {
|
||||
top: 'items-start',
|
||||
@@ -29,8 +30,8 @@ interface SheetPortalProps
|
||||
extends SheetPrimitive.DialogPortalProps,
|
||||
VariantProps<typeof portalVariants> {}
|
||||
|
||||
const SheetPortal = ({ position, className, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal className={cn(className)} {...props}>
|
||||
const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal {...props}>
|
||||
<div className={portalVariants({ position })}>{children}</div>
|
||||
</SheetPrimitive.Portal>
|
||||
)
|
||||
@@ -42,7 +43,7 @@ const SheetOverlay = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
|
||||
'fixed inset-0 z-40 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -52,7 +53,7 @@ const SheetOverlay = React.forwardRef<
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 scale-100 gap-4 bg-background p-6 opacity-100 shadow-lg border',
|
||||
'fixed z-40 scale-100 gap-4 bg-background p-6 opacity-100 shadow-lg border',
|
||||
{
|
||||
variants: {
|
||||
position: {
|
||||
@@ -67,6 +68,7 @@ const sheetVariants = cva(
|
||||
sm: '',
|
||||
lg: '',
|
||||
xl: '',
|
||||
xxl: '',
|
||||
full: '',
|
||||
},
|
||||
},
|
||||
@@ -124,6 +126,11 @@ const sheetVariants = cva(
|
||||
{
|
||||
position: ['right', 'left'],
|
||||
size: 'xl',
|
||||
class: 'w-4/6',
|
||||
},
|
||||
{
|
||||
position: ['right', 'left'],
|
||||
size: 'xxl',
|
||||
class: 'w-5/6',
|
||||
},
|
||||
{
|
||||
@@ -155,17 +162,17 @@ const SheetContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
{/* <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Close> */}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
<div className={cn('px-5 py-4 text-center sm:text-left', className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
@@ -183,7 +190,7 @@ const SheetTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
className={cn('text-base text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -495,6 +495,7 @@
|
||||
height: 8px;
|
||||
border-radius: 0%;
|
||||
}
|
||||
|
||||
.ai-icon__container--allow-hover-effect:hover .ai-icon__grid__square--static:nth-child(2),
|
||||
[data-selected='true'] .ai-icon__grid__square--static:nth-child(2) {
|
||||
transform: rotate(45deg);
|
||||
@@ -504,6 +505,7 @@
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.ai-icon__container--allow-hover-effect:hover .ai-icon__grid__square--static:nth-child(3),
|
||||
[data-selected='true'] .ai-icon__grid__square--static:nth-child(3) {
|
||||
transform: rotate(45deg);
|
||||
@@ -513,6 +515,7 @@
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.ai-icon__container--allow-hover-effect:hover .ai-icon__grid__square--static:nth-child(4),
|
||||
[data-selected='true'] .ai-icon__grid__square--static:nth-child(4) {
|
||||
transform: rotate(45deg);
|
||||
|
||||
@@ -879,6 +879,8 @@ export default {
|
||||
large: `w-screen max-w-2xl h-full`,
|
||||
xlarge: `w-screen max-w-3xl h-full`,
|
||||
xxlarge: `w-screen max-w-4xl h-full`,
|
||||
xxxlarge: `w-screen max-w-5xl h-full`,
|
||||
xxxxlarge: `w-screen max-w-6xl h-full`,
|
||||
},
|
||||
align: {
|
||||
left: `
|
||||
|
||||
Reference in new issue
Block a user