diff --git a/apps/studio/components/interfaces/Account/Preferences/HotkeySettings.tsx b/apps/studio/components/interfaces/Account/Preferences/HotkeySettings.tsx index 4d92c83a137c0..cf89c8a4d1d78 100644 --- a/apps/studio/components/interfaces/Account/Preferences/HotkeySettings.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/HotkeySettings.tsx @@ -6,7 +6,7 @@ import { KeyboardShortcut, Toggle } from 'ui' export const HotkeySettings = () => { const [inlineEditorEnabled, setInlineEditorEnabled] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.HOTKEY_INLINE_EDITOR, + LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL), true ) const [commandMenuEnabled, setCommandMenuEnabled] = useLocalStorageQuery( diff --git a/apps/studio/components/interfaces/Support/SubjectAndSuggestionsInfo.tsx b/apps/studio/components/interfaces/Support/SubjectAndSuggestionsInfo.tsx index 3347129c2e456..5e370b95a04eb 100644 --- a/apps/studio/components/interfaces/Support/SubjectAndSuggestionsInfo.tsx +++ b/apps/studio/components/interfaces/Support/SubjectAndSuggestionsInfo.tsx @@ -54,7 +54,7 @@ interface GitHubDiscussionSuggestionProps { function GitHubDiscussionSuggestion({ subject }: GitHubDiscussionSuggestionProps) { return (

- Check our + Check our - for a quick answer + for a quick answer

) } diff --git a/apps/studio/components/interfaces/Support/SupportFormPage.tsx b/apps/studio/components/interfaces/Support/SupportFormPage.tsx index 064a4c6fe22ab..4043ef900a104 100644 --- a/apps/studio/components/interfaces/Support/SupportFormPage.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormPage.tsx @@ -23,8 +23,8 @@ import type { SupportFormValues } from './SupportForm.schema' import { createInitialSupportFormState, type SupportFormActions, - type SupportFormState, supportFormReducer, + type SupportFormState, } from './SupportForm.state' import { SupportFormV2 } from './SupportFormV2' import { useSupportForm } from './useSupportForm' @@ -125,7 +125,7 @@ function SupportFormHeader() {
- setShowEditorPanel(false)} - isInlineEditorHotkeyEnabled={inlineEditorHotkeyEnabled} - /> ) } diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx index 6ae847d36c1c9..bc946ffbabe76 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider.tsx @@ -2,13 +2,16 @@ import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' import { useRegisterSidebar, useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant' +import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' export const SIDEBAR_KEYS = { AI_ASSISTANT: 'ai-assistant', + EDITOR_PANEL: 'editor-panel', } as const export const LayoutSidebarProvider = ({ children }: PropsWithChildren) => { useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => , {}, 'i') + useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => , {}, 'e') const router = useRouter() const { openSidebar } = useSidebarManagerSnapshot() diff --git a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx index d189ee02f7c37..e18c0b3ebd9de 100644 --- a/apps/studio/components/ui/EditorPanel/EditorPanel.tsx +++ b/apps/studio/components/ui/EditorPanel/EditorPanel.tsx @@ -1,23 +1,24 @@ -import { Book, Save, X } from 'lucide-react' -import { useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' -import { toast } from 'sonner' +import { Book, Maximize2, X } from 'lucide-react' +import { useRouter } from 'next/router' +import { useState } from 'react' -import { useParams } from 'common' +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { createSqlSnippetSkeletonV2, suffixWithLimit, } from 'components/interfaces/SQLEditor/SQLEditor.utils' import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' import { SqlRunButton } from 'components/interfaces/SQLEditor/UtilityPanel/RunButton' -import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation' import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' +import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { Button, cn, @@ -28,89 +29,56 @@ import { CommandInput_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, - Form_Shadcn_, - FormField_Shadcn_, HoverCard_Shadcn_, HoverCardContent_Shadcn_, HoverCardTrigger_Shadcn_, - Input_Shadcn_ as Input, KeyboardShortcut, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, SQL_ICON, } from 'ui' import { Admonition } from 'ui-patterns' import { containsUnknownFunction, isReadOnlySelect } from '../AIAssistantPanel/AIAssistant.utils' import AIEditor from '../AIEditor' import { ButtonTooltip } from '../ButtonTooltip' -import { InlineLink } from '../InlineLink' import { SqlWarningAdmonition } from '../SqlWarningAdmonition' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -type Template = { - name: string - description: string - content: string -} +export const EditorPanel = () => { + const { + value, + templates, + results, + error, + initialPrompt, + onChange, + setValue, + setTemplates, + setResults, + setError, + } = useEditorPanelStateSnapshot() + const { closeSidebar } = useSidebarManagerSnapshot() + const { profile } = useProfile() + const sqlEditorSnap = useSqlEditorV2StateSnapshot() -interface EditorPanelProps { - open: boolean - onClose: () => void - initialValue?: string - label?: string - saveLabel?: string - saveValue?: string - onSave?: (value: string, saveValue: string) => void - onRunSuccess?: (value: any[]) => void - onRunError?: (value: any) => void - functionName?: string - templates?: Template[] - initialPrompt?: string - onChange?: (value: string) => void - isInlineEditorHotkeyEnabled?: boolean -} + const label = 'SQL Editor' + const [isInlineEditorHotkeyEnabled] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL), + true + ) + + const currentValue = value || '' -export const EditorPanel = ({ - open, - onClose, - isInlineEditorHotkeyEnabled = true, - initialValue = '', - label = '', - saveLabel = 'Save', - saveValue = '', - onSave, - onRunSuccess, - onRunError, - templates = [], - initialPrompt = '', - onChange, -}: EditorPanelProps) => { const { ref } = useParams() + const router = useRouter() const { data: project } = useSelectedProjectQuery() - const { profile } = useProfile() - const snapV2 = useSqlEditorV2StateSnapshot() - const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation() const { data: org } = useSelectedOrganizationQuery() - const [isSaving, setIsSaving] = useState(false) - const [error, setError] = useState() - const [results, setResults] = useState(undefined) const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() - const [currentValue, setCurrentValue] = useState(initialValue) const [showResults, setShowResults] = useState(true) const [isTemplatesOpen, setIsTemplatesOpen] = useState(false) - const saveForm = useForm({ - defaultValues: { - saveValue: saveValue || '', - }, - }) - const errorHeader = error?.formattedError?.split('\n')?.filter((x: string) => x.length > 0)?.[0] const errorContent = 'formattedError' in (error || {}) @@ -123,22 +91,18 @@ export const EditorPanel = ({ const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: async (res) => { setResults(res.result) - if (onRunSuccess) { - onRunSuccess(res.result) - } + setError(undefined) }, - onError: (error) => { - setError(error) + onError: (mutationError) => { + setError(mutationError) setResults([]) - if (onRunError) { - onRunError(error) - } }, }) const onExecuteSql = (skipValidation = false) => { setError(undefined) setShowWarning(undefined) + setResults(undefined) if (currentValue.length === 0) return @@ -150,19 +114,20 @@ export const EditorPanel = ({ return } } + executeSql({ sql: suffixWithLimit(currentValue, 100), projectRef: project?.ref, connectionString: project?.connectionString, - handleError: (error) => { - throw error + handleError: (executeError) => { + throw executeError }, contextualInvalidation: true, }) } const handleChange = (value: string) => { - setCurrentValue(value) + setValue(value) onChange?.(value) } @@ -171,286 +136,237 @@ export const EditorPanel = ({ setIsTemplatesOpen(false) } - useEffect(() => { - if (initialValue !== undefined && initialValue !== currentValue) { - setCurrentValue(initialValue) - setResults(undefined) - setError(undefined) - setShowWarning(undefined) - } - }, [initialValue]) - - useEffect(() => { - saveForm.reset({ - saveValue: saveValue || '', - }) - }, [saveValue, saveForm]) + const handleClosePanel = () => { + closeSidebar(SIDEBAR_KEYS.EDITOR_PANEL) + setTemplates([]) + setError(undefined) + setShowWarning(undefined) + setShowResults(true) + } return ( - !open && onClose()}> - - -
- SQL Editor - {label && {label}} -
-
- {templates.length > 0 && ( - - - - - - - - - No templates found. - - {templates.map((template) => ( - - - onSelectTemplate(template.content)} - className="cursor-pointer" - > -
- -
-

{template.name}

-

- {template.description} -

-
+
+
+
{label}
+
+ {templates.length > 0 && ( + + + + + + + + + No templates found. + + {templates.map((template) => ( + + + onSelectTemplate(template.content)} + className="cursor-pointer" + > +
+ +
+

{template.name}

+

+ {template.description} +

- - - - - - - ))} - - - - - - )} - } - onClick={async () => { - if (!ref) return console.error('Project ref is required') - if (!project) return console.error('Project is required') - if (!profile) return console.error('Profile is required') +
+
+
+ + + +
+ ))} +
+
+
+
+
+ )} + } + tooltip={{ + content: { + side: 'bottom', + text: 'Expand to SQL editor', + }, + }} + onClick={() => { + if (!ref) return console.error('Project ref is required') - try { - setIsSaving(true) - const { title: name } = await generateSqlTitle({ - sql: currentValue, - }) - const snippet = createSqlSnippetSkeletonV2({ - id: uuidv4(), - name, - sql: currentValue, - owner_id: profile.id, - project_id: project.id, - }) - snapV2.addSnippet({ projectRef: ref, snippet }) - snapV2.addNeedsSaving(snippet.id) - toast.success( -
- Saved snippet! View it{' '} - here -
- ) - } catch (error: any) { - toast.error(`Failed to create new query: ${error.message}`) - } finally { - setIsSaving(false) - } - }} - /> + if (!project) { + console.error('Project is required') + return + } + if (!profile) { + console.error('Profile is required') + return + } - } - tooltip={{ - content: { - side: 'bottom', - text: ( -
- Close Editor - {isInlineEditorHotkeyEnabled && } -
- ), - }, - }} - /> -
- + const snippet = createSqlSnippetSkeletonV2({ + id: uuidv4(), + name: 'New query', + sql: currentValue, + owner_id: profile.id, + project_id: project.id, + }) -
-
- onClose()} - closeShortcutEnabled={isInlineEditorHotkeyEnabled} - /> -
+ sqlEditorSnap.addSnippet({ projectRef: ref, snippet }) + sqlEditorSnap.addNeedsSaving(snippet.id) - {error !== undefined && ( -
- - {errorContent.length > 0 ? ( - errorContent.map((errorText: string, i: number) => ( -
-                          {errorText}
-                        
- )) - ) : ( -

{error.error}

- )} + router.push(`/project/${ref}/sql/${snippet.id}`) + handleClosePanel() + }} + /> + + } + tooltip={{ + content: { + side: 'bottom', + text: ( +
+ Close Editor + {isInlineEditorHotkeyEnabled && }
- } - /> -
- )} + ), + }, + }} + /> +
+
- {showWarning && ( - setShowWarning(undefined)} - onConfirm={() => { - setShowWarning(undefined) - onExecuteSql(true) - }} - /> - )} +
+
+ +
- {results !== undefined && results.length > 0 && ( -
- {showResults && ( -
- + {error !== undefined && ( +
+ + {errorContent.length > 0 ? ( + errorContent.map((errorText: string, i: number) => ( +
+                        {errorText}
+                      
+ )) + ) : ( +

{error?.error}

+ )}
- )} -

- - {results.length} rows{results.length >= 100 && ` (Limited to only 100 rows)`} - - -

-
- )} - {results !== undefined && results.length === 0 && !error && ( -
-

- Success. No rows returned. -

-
- )} + } + /> +
+ )} -
- {onSave && ( - -
{ - onSave(currentValue, values.saveValue) - })} - className="flex items-center gap-2" - > - {saveValue && ( - ( - - )} - /> - )} - - -
+ {showWarning && ( + setShowWarning(undefined)} + onConfirm={() => { + setShowWarning(undefined) + onExecuteSql(true) + }} + /> + )} + + {results !== undefined && results.length > 0 && ( +
+ {showResults && ( +
+ +
)} - +
+ + {results.length} rows{results.length >= 100 && ` (Limited to only 100 rows)`} + + +
+
+ )} + {results !== undefined && results.length === 0 && !error && ( +
+

+ Success. No rows returned. +

+ )} + +
+
- - +
+
) } diff --git a/apps/studio/hooks/misc/useLegacyInlineEditorHotkeyMigration.ts b/apps/studio/hooks/misc/useLegacyInlineEditorHotkeyMigration.ts new file mode 100644 index 0000000000000..01f359647eb45 --- /dev/null +++ b/apps/studio/hooks/misc/useLegacyInlineEditorHotkeyMigration.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react' + +import { LOCAL_STORAGE_KEYS } from 'common' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { useLocalStorageQuery } from './useLocalStorage' + +const LEGACY_INLINE_EDITOR_HOTKEY_KEY = 'supabase-dashboard-hotkey-inline-editor' + +/** + * Migrates the inline editor hotkey preference to the new sidebar editor panel key. + * Runs when idle (or within 5 seconds) to avoid blocking render. + */ +const useLegacyInlineEditorHotkeyMigration = () => { + const [_inlineEditorEnabled, setInlineEditorEnabled] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.EDITOR_PANEL), + true + ) + + useEffect(() => { + if (typeof window === 'undefined') return + + const migrate = () => { + try { + const legacyValue = window.localStorage.getItem(LEGACY_INLINE_EDITOR_HOTKEY_KEY) + + if (legacyValue !== null) { + setInlineEditorEnabled(legacyValue === 'true') + window.localStorage.removeItem(LEGACY_INLINE_EDITOR_HOTKEY_KEY) + } + } catch (error) { + console.warn('Failed to migrate inline editor hotkey preference', error) + } + } + + if (typeof window.requestIdleCallback === 'function') { + const idleCallbackId = window.requestIdleCallback(migrate, { timeout: 5000 }) + return () => window.cancelIdleCallback?.(idleCallbackId) + } + + const timeoutId = window.setTimeout(migrate, 5000) + return () => window.clearTimeout(timeoutId) + }, [setInlineEditorEnabled]) +} + +export const LegacyInlineEditorHotkeyMigration = () => { + useLegacyInlineEditorHotkeyMigration() + return null +} diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index f6816f5ed7968..be50993ffd82e 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -44,6 +44,7 @@ import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router' import { RouteValidationWrapper } from 'components/interfaces/App' import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' import { StudioCommandMenu } from 'components/interfaces/App/CommandMenu' +import { StudioCommandProvider as CommandProvider } from 'components/interfaces/App/CommandMenu/StudioCommandProvider' import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal' import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider' @@ -51,13 +52,13 @@ import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErro import { useRootQueryClient } from 'data/query-client' import { customFont, sourceCodePro } from 'fonts' import { useCustomContent } from 'hooks/custom-content/useCustomContent' +import { LegacyInlineEditorHotkeyMigration } from 'hooks/misc/useLegacyInlineEditorHotkeyMigration' import { AuthProvider } from 'lib/auth' import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from 'lib/constants' import { ProfileProvider } from 'lib/profile' import { Telemetry } from 'lib/telemetry' import { AppPropsWithLayout } from 'types' import { SonnerToaster, TooltipProvider } from 'ui' -import { StudioCommandProvider as CommandProvider } from 'components/interfaces/App/CommandMenu/StudioCommandProvider' dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -163,6 +164,8 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { + {/* Temporary migration, to be removed by 2025-11-28 */} + {!isTestEnv && ( diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx index 4509f9af6058f..64f9aa45999b9 100644 --- a/apps/studio/pages/project/[ref]/auth/policies.tsx +++ b/apps/studio/pages/project/[ref]/auth/policies.tsx @@ -15,7 +15,6 @@ import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' -import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import NoPermission from 'components/ui/NoPermission' import SchemaSelector from 'components/ui/SchemaSelector' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' @@ -26,6 +25,9 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { DOCS_URL } from 'lib/constants' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { parseAsString, useQueryState } from 'nuqs' import type { NextPageWithLayout } from 'types' import { Input } from 'ui-patterns/DataInputs/Input' @@ -87,14 +89,17 @@ const AuthPoliciesPage: NextPageWithLayout = () => { const { data: project } = useSelectedProjectQuery() const { data: postgrestConfig } = useProjectPostgrestConfigQuery({ projectRef: project?.ref }) const isInlineEditorEnabled = useIsInlineEditorEnabled() + const { openSidebar } = useSidebarManagerSnapshot() + const { + setValue: setEditorPanelValue, + setTemplates: setEditorPanelTemplates, + setInitialPrompt: setEditorPanelInitialPrompt, + } = useEditorPanelStateSnapshot() const [selectedTable, setSelectedTable] = useState() const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false) const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState() - // Local editor panel state - const [editorPanelOpen, setEditorPanelOpen] = useState(false) - const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] }) const { @@ -139,26 +144,49 @@ const AuthPoliciesPage: NextPageWithLayout = () => { (table: string) => { setSelectedTable(table) setSelectedPolicyToEdit(undefined) + if (isInlineEditorEnabled) { - setEditorPanelOpen(true) + const defaultSql = `create policy "replace_with_policy_name" + on ${schema}.${table} + for select + to authenticated + using ( + true -- Write your policy condition here +);` + + setEditorPanelInitialPrompt('Create a new RLS policy that...') + setEditorPanelValue(defaultSql) + setEditorPanelTemplates([]) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setShowPolicyAiEditor(true) } }, - [isInlineEditorEnabled] + [isInlineEditorEnabled, openSidebar, schema] ) const handleSelectEditPolicy = useCallback( (policy: PostgresPolicy) => { setSelectedPolicyToEdit(policy) setSelectedTable(undefined) + if (isInlineEditorEnabled) { - setEditorPanelOpen(true) + setEditorPanelInitialPrompt(`Update the RLS policy with name "${policy.name}" that...`) + setEditorPanelValue(generatePolicyUpdateSQL(policy)) + const templates = getGeneralPolicyTemplates(policy.schema, policy.table).map( + (template) => ({ + name: template.templateName, + description: template.description, + content: template.statement, + }) + ) + setEditorPanelTemplates(templates) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setShowPolicyAiEditor(true) } }, - [isInlineEditorEnabled] + [isInlineEditorEnabled, openSidebar] ) const handleResetSearch = useCallback(() => setSearchString(''), [setSearchString]) @@ -235,53 +263,6 @@ const AuthPoliciesPage: NextPageWithLayout = () => { }} authContext="database" /> - - { - setEditorPanelOpen(false) - setSelectedPolicyToEdit(undefined) - setSelectedTable(undefined) - }} - onRunSuccess={() => { - setEditorPanelOpen(false) - setSelectedPolicyToEdit(undefined) - setSelectedTable(undefined) - }} - initialValue={ - selectedPolicyToEdit - ? generatePolicyUpdateSQL(selectedPolicyToEdit) - : selectedTable - ? `create policy "replace_with_policy_name"\n on ${schema}.${selectedTable}\n for select\n to authenticated\n using (\n true -- Write your policy condition here\n);` - : '' - } - label={ - selectedPolicyToEdit - ? 'RLS policies are just SQL statements that you can alter' - : selectedTable - ? `Create new RLS policy on "${selectedTable}"` - : '' - } - initialPrompt={ - selectedPolicyToEdit - ? `Update the policy with name "${selectedPolicyToEdit.name}" in the ${selectedPolicyToEdit.schema} schema on the ${selectedPolicyToEdit.table} table. It should...` - : selectedTable - ? `Create and name a entirely new RLS policy for the "${selectedTable}" table in the ${schema} schema. The policy should...` - : '' - } - templates={ - selectedPolicyToEdit - ? getGeneralPolicyTemplates( - selectedPolicyToEdit.schema, - selectedPolicyToEdit.table - ).map((template) => ({ - name: template.templateName, - description: template.description, - content: template.statement, - })) - : [] - } - /> ) diff --git a/apps/studio/pages/project/[ref]/database/functions.tsx b/apps/studio/pages/project/[ref]/database/functions.tsx index 09b2fc341865d..de1808bf48d45 100644 --- a/apps/studio/pages/project/[ref]/database/functions.tsx +++ b/apps/studio/pages/project/[ref]/database/functions.tsx @@ -7,13 +7,15 @@ import FunctionsList from 'components/interfaces/Database/Functions/FunctionsLis import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { DatabaseFunction } from 'data/database-functions/database-functions-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' const DatabaseFunctionsPage: NextPageWithLayout = () => { const [selectedFunction, setSelectedFunction] = useState() @@ -21,12 +23,12 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false) const [isDuplicating, setIsDuplicating] = useState(false) const isInlineEditorEnabled = useIsInlineEditorEnabled() - - // Local editor panel state - const [editorPanelOpen, setEditorPanelOpen] = useState(false) - const [selectedFunctionForEditor, setSelectedFunctionForEditor] = useState< - DatabaseFunction | undefined - >() + const { openSidebar } = useSidebarManagerSnapshot() + const { + setValue: setEditorPanelValue, + setTemplates: setEditorPanelTemplates, + setInitialPrompt: setEditorPanelInitialPrompt, + } = useEditorPanelStateSnapshot() const { can: canReadFunctions, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_READ, @@ -34,9 +36,19 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { ) const createFunction = () => { + setIsDuplicating(false) if (isInlineEditorEnabled) { - setSelectedFunctionForEditor(undefined) - setEditorPanelOpen(true) + setEditorPanelInitialPrompt('Create a new database function that...') + setEditorPanelValue(`create function function_name() +returns void +language plpgsql +as $$ +begin + -- Write your function logic here +end; +$$;`) + setEditorPanelTemplates([]) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setSelectedFunction(undefined) setShowCreateFunctionForm(true) @@ -52,8 +64,10 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { } if (isInlineEditorEnabled) { - setSelectedFunctionForEditor(dupFn) - setEditorPanelOpen(true) + setEditorPanelInitialPrompt('Create new database function that...') + setEditorPanelValue(dupFn.complete_statement) + setEditorPanelTemplates([]) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setSelectedFunction(dupFn) setShowCreateFunctionForm(true) @@ -61,9 +75,11 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { } const editFunction = (fn: DatabaseFunction) => { + setIsDuplicating(false) if (isInlineEditorEnabled) { - setSelectedFunctionForEditor(fn) - setEditorPanelOpen(true) + setEditorPanelValue(fn.complete_statement) + setEditorPanelTemplates([]) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setSelectedFunction(fn) setShowCreateFunctionForm(true) @@ -75,12 +91,6 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { setShowDeleteFunctionForm(true) } - const resetEditorPanel = () => { - setIsDuplicating(false) - setEditorPanelOpen(false) - setSelectedFunctionForEditor(undefined) - } - if (isPermissionsLoaded && !canReadFunctions) { return } @@ -117,38 +127,6 @@ const DatabaseFunctionsPage: NextPageWithLayout = () => { visible={showDeleteFunctionForm} setVisible={setShowDeleteFunctionForm} /> - - ) } diff --git a/apps/studio/pages/project/[ref]/database/triggers.tsx b/apps/studio/pages/project/[ref]/database/triggers.tsx index 6830818d9d9e8..50edcb6b84490 100644 --- a/apps/studio/pages/project/[ref]/database/triggers.tsx +++ b/apps/studio/pages/project/[ref]/database/triggers.tsx @@ -10,15 +10,24 @@ import TriggersList from 'components/interfaces/Database/Triggers/TriggersList/T import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import { EditorPanel } from 'components/ui/EditorPanel/EditorPanel' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { DOCS_URL } from 'lib/constants' import type { NextPageWithLayout } from 'types' +import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' +import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' const TriggersPage: NextPageWithLayout = () => { const isInlineEditorEnabled = useIsInlineEditorEnabled() + const { openSidebar } = useSidebarManagerSnapshot() + const { + templates: editorPanelTemplates, + setValue: setEditorPanelValue, + setTemplates: setEditorPanelTemplates, + setInitialPrompt: setEditorPanelInitialPrompt, + } = useEditorPanelStateSnapshot() const [selectedTrigger, setSelectedTrigger] = useState() const [isDuplicatingTrigger, setIsDuplicatingTrigger] = useState(false) @@ -26,19 +35,23 @@ const TriggersPage: NextPageWithLayout = () => { const [showCreateTriggerForm, setShowCreateTriggerForm] = useState(false) const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState(false) - // Local editor panel state - const [editorPanelOpen, setEditorPanelOpen] = useState(false) - const [selectedTriggerForEditor, setSelectedTriggerForEditor] = useState() - const { can: canReadTriggers, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers' ) const createTrigger = () => { + setIsDuplicatingTrigger(false) if (isInlineEditorEnabled) { - setSelectedTriggerForEditor(undefined) - setEditorPanelOpen(true) + setEditorPanelInitialPrompt('Create a new database trigger that...') + setEditorPanelValue(`create trigger trigger_name +after insert or update or delete on table_name +for each row +execute function function_name();`) + if (editorPanelTemplates.length > 0) { + setEditorPanelTemplates([]) + } + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setSelectedTrigger(undefined) setShowCreateTriggerForm(true) @@ -46,9 +59,11 @@ const TriggersPage: NextPageWithLayout = () => { } const editTrigger = (trigger: PostgresTrigger) => { + setIsDuplicatingTrigger(false) if (isInlineEditorEnabled) { - setSelectedTriggerForEditor(trigger) - setEditorPanelOpen(true) + setEditorPanelValue(generateTriggerCreateSQL(trigger)) + setEditorPanelTemplates([]) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setSelectedTrigger(trigger) setShowCreateTriggerForm(true) @@ -64,8 +79,9 @@ const TriggersPage: NextPageWithLayout = () => { } if (isInlineEditorEnabled) { - setSelectedTriggerForEditor(dupTrigger) - setEditorPanelOpen(true) + setEditorPanelValue(generateTriggerCreateSQL(dupTrigger)) + setEditorPanelTemplates([]) + openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setSelectedTrigger(dupTrigger) setShowCreateTriggerForm(true) @@ -77,12 +93,6 @@ const TriggersPage: NextPageWithLayout = () => { setShowDeleteTriggerForm(true) } - const resetEditorPanel = () => { - setIsDuplicatingTrigger(false) - setEditorPanelOpen(false) - setSelectedTriggerForEditor(undefined) - } - if (isPermissionsLoaded && !canReadTriggers) { return } @@ -120,34 +130,6 @@ const TriggersPage: NextPageWithLayout = () => { visible={showDeleteTriggerForm} setVisible={setShowDeleteTriggerForm} /> - - ) } diff --git a/apps/studio/state/editor-panel-state.tsx b/apps/studio/state/editor-panel-state.tsx new file mode 100644 index 0000000000000..167e33ef3b5ad --- /dev/null +++ b/apps/studio/state/editor-panel-state.tsx @@ -0,0 +1,55 @@ +import { proxy, snapshot, useSnapshot } from 'valtio' + +type Template = { + name: string + description: string + content: string +} + +type EditorPanelState = { + value: string + templates: Template[] + results: any[] | undefined + error: any + initialPrompt: string + onChange: ((value: string) => void) | undefined +} + +const initialState: EditorPanelState = { + value: '', + templates: [], + results: undefined, + error: undefined, + initialPrompt: '', + onChange: undefined, +} + +export const editorPanelState = proxy({ + ...initialState, + setValue(value: string) { + editorPanelState.value = value + editorPanelState.onChange?.(value) + editorPanelState.setResults(undefined) + editorPanelState.setError(undefined) + }, + setTemplates(templates: Template[]) { + editorPanelState.templates = templates + }, + setResults(results: any[] | undefined) { + editorPanelState.results = results + }, + setError(error: any) { + editorPanelState.error = error + }, + setInitialPrompt(initialPrompt: string) { + editorPanelState.initialPrompt = initialPrompt + }, + reset() { + Object.assign(editorPanelState, initialState) + }, +}) + +export const getEditorPanelStateSnapshot = () => snapshot(editorPanelState) + +export const useEditorPanelStateSnapshot = (options?: Parameters[1]) => + useSnapshot(editorPanelState, options) diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 1cfda8d9b9512..b9b8bcf29477f 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -76,7 +76,6 @@ export const LOCAL_STORAGE_KEYS = { USER_IMPERSONATION_SELECTOR_PREVIOUS_SEARCHES: (ref: string) => `user-impersonation-selector-previous-searches-${ref}`, - HOTKEY_INLINE_EDITOR: 'supabase-dashboard-hotkey-inline-editor', HOTKEY_COMMAND_MENU: 'supabase-dashboard-hotkey-command-menu', // Project sidebar hotkeys