From 8456108fb16e7a8e8d337befdbba6da2871f7553 Mon Sep 17 00:00:00 2001 From: Ryan Martin <51780559+rmrt1n@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:19:32 +0700 Subject: [PATCH 001/151] feat(studio): check column types in imported spreadsheet (#17072) * feat(studio): check column types in imported spreadsheet * refactor: more checks on check inferred column type * fix: spreadsheet import type check not working because selectedtable is undefined * rebuild --------- Co-authored-by: Ryan Martin Co-authored-by: Terry Sutton --- .../SpreadsheetImport/SpreadsheetImport.tsx | 36 +++++++++++++++++-- .../SpreadsheetImportPreview.tsx | 34 ++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx index c587a914a41..b51ae3d0179 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx @@ -60,13 +60,39 @@ const SpreadsheetImport = ({ }) const [errors, setErrors] = useState([]) const [selectedHeaders, setSelectedHeaders] = useState([]) + const [selectedColumnTypeMap, setSelectedColumnTypeMap] = useState([]) - const selectedTableColumns = (selectedTable?.columns ?? []).map((column) => column.name) + const selectedTableColumns = (selectedTable?.columns ?? []).map((column) => ({ + name: column.name, + format: column.format, + })) const incompatibleHeaders = selectedHeaders.filter( - (header) => !selectedTableColumns.includes(header) + (header) => !selectedTableColumns.some((column) => column.name === header) ) const isCompatible = selectedTable !== undefined ? incompatibleHeaders.length === 0 : true + const checkInferredColumnTypes = (columnTypeMap: any) => { + // all other types can in theory be casted into a string + // e.g. a column with type of text can still contain the value "1", but inferColumnType() will + // still infer it as an int8 + const stringTypes = ['text', 'varchar'] + const lookup: { [key: string]: string[] } = { + int8: ['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', ...stringTypes], + float8: ['float4', 'float8', 'numeric', ...stringTypes], + jsonb: ['json', 'jsonb', ...stringTypes], + text: ['uuid', ...stringTypes], + timestamptz: ['date', 'time', 'timetz', 'timestamp', 'timestamptz', ...stringTypes], + bool: ['bool', ...stringTypes], + } + return selectedTableColumns + .filter((c) => Object.keys(columnTypeMap).includes(c.name)) + .filter((c) => !lookup[columnTypeMap[c.name]].includes(c.format)) + .map((c) => ({ name: c.name, format: columnTypeMap[c.name], expectedFormat: c.format })) + } + + const incompatibleTypeColumns = checkInferredColumnTypes(selectedColumnTypeMap) + const isTypeCompatible = selectedTable !== undefined ? selectedColumnTypeMap.length === 0 : true + const onProgressUpdate = (progress: number) => { setParseProgress(progress) } @@ -113,6 +139,7 @@ const SpreadsheetImport = ({ setErrors(errors) setSelectedHeaders(headers) + setSelectedColumnTypeMap(columnTypeMap) setSpreadsheetData({ headers, rows: previewRows, rowCount, columnTypeMap }) } event.target.value = '' @@ -134,8 +161,10 @@ const SpreadsheetImport = ({ `Some issues have been detected on ${errors.length} rows. More details below the content preview.` ) } + setErrors(errors) setSelectedHeaders(headers) + setSelectedColumnTypeMap(columnTypeMap) setSpreadsheetData({ headers, rows, rowCount: rows.length, columnTypeMap }) } else { setSpreadsheetData(EMPTY_SPREADSHEET_DATA) @@ -162,7 +191,7 @@ const SpreadsheetImport = ({ } else if (selectedHeaders.length === 0) { toast.error('Please select at least one header from your CSV') resolve() - } else if (!isCompatible) { + } else if (!(isCompatible && isTypeCompatible)) { toast.error( 'The data that you are trying to import is incompatible with your table structure' ) @@ -233,6 +262,7 @@ const SpreadsheetImport = ({ errors={errors} selectedHeaders={selectedHeaders} incompatibleHeaders={incompatibleHeaders} + incompatibeTypeColumns={incompatibleTypeColumns} /> diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx index e275feb3196..b274345020c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx @@ -24,6 +24,7 @@ interface SpreadsheetImportPreviewProps { errors?: any[] selectedHeaders: string[] incompatibleHeaders: string[] + incompatibeTypeColumns: any[] } const SpreadsheetImportPreview = ({ @@ -32,6 +33,7 @@ const SpreadsheetImportPreview = ({ errors = [], selectedHeaders, incompatibleHeaders, + incompatibeTypeColumns, }: SpreadsheetImportPreviewProps) => { const [expandPreview, setExpandPreview] = useState(false) const [expandedErrors, setExpandedErrors] = useState([]) @@ -43,6 +45,7 @@ const SpreadsheetImportPreview = ({ const previewRows = rows.slice(0, MAX_ROWS) const isCompatible = selectedTable !== undefined ? incompatibleHeaders.length === 0 : true + const isTypeCompatible = selectedTable !== undefined ? incompatibeTypeColumns.length === 0 : true useEffect(() => { setExpandPreview(true) @@ -64,6 +67,7 @@ const SpreadsheetImportPreview = ({

Preview data to be imported

{!isCompatible && Data incompatible} + {!isTypeCompatible && Data type incompatible} {errors.length > 0 && {errors.length} issues found}
+ + ) : ( +
+ + {answer} + + + {message.status === MessageStatus.Complete && ( + + )} +
+ )} + + + ) + } + }) +} + +function EmptyState({ + query, + handleSubmit, +}: { + query: string + handleSubmit: (query: string) => void +}) { + const [activeTab, setActiveTab] = useState(SAMPLE_QUERIES[0].category) + + useEffect(() => { + function handleSwitchTab(event: KeyboardEvent) { + if (query || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) { + return + } + + const activeSampleIndex = SAMPLE_QUERIES.findIndex( + (samples) => samples.category === activeTab + )! + const nextIndex = + event.key === 'ArrowRight' + ? Math.min(activeSampleIndex + 1, SAMPLE_QUERIES.length - 1) + : Math.max(0, activeSampleIndex - 1) + setActiveTab(SAMPLE_QUERIES[nextIndex].category) + } + + document.addEventListener('keydown', handleSwitchTab) + return () => document.removeEventListener('keydown', handleSwitchTab) + }, [query, activeTab]) + + return ( + <> +

+ Describe what you need and Supabase AI will try to generate the relevant SQL statements +

+

+ Here are some example prompts to try out: +

+
+ setActiveTab(value)} + className="focus-visible:ring-0" + > + + {SAMPLE_QUERIES.map((samples) => ( + {samples.category} + ))} + + + {SAMPLE_QUERIES.map((samples) => ( + + {samples.queries.map((sampleQuery) => ( + { + if (!query) { + handleSubmit(sampleQuery) + } + }} + > + + {sampleQuery} + + ))} + + ))} + + + + ) +} + +function ErrorMessage({ handleReset }: { handleReset: () => void }) { + return ( +
+ +

+ Sorry, looks like Supabase AI is having a hard time! +

+

Please try again in a bit.

+ +
+ ) +} diff --git a/apps/studio/components/interfaces/SqlGenerator/SqlOutputActions.tsx b/apps/studio/components/interfaces/SqlGenerator/SqlOutputActions.tsx new file mode 100644 index 00000000000..5daf0680a69 --- /dev/null +++ b/apps/studio/components/interfaces/SqlGenerator/SqlOutputActions.tsx @@ -0,0 +1,169 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { codeBlock, stripIndent } from 'common-tags' +import { Check, Clipboard, Save } from 'lucide-react' +import { useCallback, useEffect, useState } from 'react' +import CopyToClipboard from 'react-copy-to-clipboard' +import toast from 'react-hot-toast' + +import { useParams } from 'common' +import { createSqlSnippetSkeleton } from 'components/interfaces/SQLEditor/SQLEditor.utils' +import type { SqlSnippet } from 'data/content/sql-snippets-query' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { uuidv4 } from 'lib/helpers' +import { useProfile } from 'lib/profile' +import { useSqlEditorStateSnapshot } from 'state/sql-editor' +import { Button, cn } from 'ui' +import type { Message } from 'ui-patterns/CommandMenu/prepackaged/ai' +import { MessageRole, MessageStatus, queryAi } from 'ui-patterns/CommandMenu/prepackaged/ai' +import { formatTitle } from './SqlGenerator.utils' + +const useSaveGeneratedSql = () => { + const { ref } = useParams() + const { profile } = useProfile() + const selectedProject = useSelectedProject() + const snap = useSqlEditorStateSnapshot() + const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { + resource: { type: 'sql', owner_id: profile?.id }, + subject: { id: profile?.id }, + }) + + const saveGeneratedSql = useCallback( + (answer: string, title: string) => { + if (!ref) return console.error('Project ref is required') + + if (!canCreateSQLSnippet) { + toast('Unable to save query as you do not have sufficient permissions for this project') + return + } + + // Remove markdown syntax from returned answer + answer = answer.replace(/`/g, '').replace(/sql\n/g, '').trim() + + const formattedSql = codeBlock` + -- Note: This query was generated via Supabase AI, please verify the correctness of the + -- SQL snippet before running it against your database as we are not able to guarantee it + -- will do exactly what you requested the AI. + ${answer} + ` + + try { + const snippet = createSqlSnippetSkeleton({ + id: uuidv4(), + name: title || 'Generated query', + owner_id: profile?.id, + project_id: selectedProject?.id, + sql: formattedSql, + }) + + snap.addSnippet(snippet as SqlSnippet, ref) + toast.success(`Successfully saved snippet!`) + } catch (error: any) { + toast.error(`Failed to create new query: ${error.message}`) + } + }, + [canCreateSQLSnippet, profile?.id, ref, selectedProject?.id, snap] + ) + + return saveGeneratedSql +} + +export interface SQLOutputActionsProps { + answer: string + messages: Message[] + className?: string +} + +export function SQLOutputActions({ answer, messages, className }: SQLOutputActionsProps) { + const { ref } = useParams() + const saveGeneratedSql = useSaveGeneratedSql() + + const [showCopied, setShowCopied] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [isSaved, setIsSaved] = useState(false) + + const onSelectSaveSnippet = async () => { + setIsSaving(true) + + let suggestedTitle + try { + suggestedTitle = await queryAi( + [ + ...messages, + { + role: MessageRole.User, + content: stripIndent` + Generate a title for the above SQL snippet following all of these rules: + - The title is only for the last SQL snippet + - Focus on the main purposes of this snippet + - Use as few words as possible + - Title should be nouns, not verbs + - Do not include word articles (eg. a, the, for, of) + - Do not use words like "SQL" or "snippet" or "title" + - Do not output markdown, quotes, etc + - Do not be too verbose + `, + status: MessageStatus.Complete, + }, + ], + 10000 + ) + } catch (error) { + suggestedTitle = '' + } + + const formattedTitle = formatTitle(suggestedTitle) + await saveGeneratedSql(answer, formattedTitle) + setIsSaved(true) + setIsSaving(false) + } + + useEffect(() => { + if (!showCopied) return + const timer = setTimeout(() => setShowCopied(false), 2000) + return () => clearTimeout(timer) + }, [showCopied]) + + useEffect(() => { + if (!isSaved) return + const timer = setTimeout(() => setIsSaved(false), 2000) + return () => clearTimeout(timer) + }, [isSaved]) + + return ( +
+ + + + {ref !== undefined && ( + + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Support/SupportForm.tsx b/apps/studio/components/interfaces/Support/SupportForm.tsx index 6038548517c..629ebffdf7c 100644 --- a/apps/studio/components/interfaces/Support/SupportForm.tsx +++ b/apps/studio/components/interfaces/Support/SupportForm.tsx @@ -51,8 +51,7 @@ import { Separator, cn, } from 'ui' -import { useCommandMenu } from 'ui-patterns/Cmdk' -import { TextHighlighter } from 'ui-patterns/Cmdk/Command.utils' +import { TextHighlighterBase as TextHighlighter } from 'ui-patterns/CommandMenu' import MultiSelect from 'ui-patterns/MultiSelectDeprecated' import DisabledStateForFreeTier from './DisabledStateForFreeTier' import { CATEGORY_OPTIONS, SERVICE_OPTIONS, SEVERITY_OPTIONS } from './Support.constants' @@ -180,7 +179,6 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) allowSupportAccess: false, } - const { site } = useCommandMenu() const router = useRouter() const onFilesUpload = async (event: ChangeEvent) => { @@ -318,19 +316,9 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) switch (pageType) { case PageType.Markdown: case PageType.Reference: - if (site === 'docs') { - return link - } else if (site === 'website') { - return `/docs${link}` - } else { - return `https://supabase.com/docs${link}` - } + return `https://supabase.com/docs${link}` case PageType.Integration: - if (site === 'website') { - return link - } else { - return `https://supabase.com${link}` - } + return `https://supabase.com${link}` case PageType.GithubDiscussion: return link default: @@ -338,33 +326,6 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) } } - async function handleLinkClick(pageType: PageType, link: string) { - switch (pageType) { - case PageType.Markdown: - case PageType.Reference: - if (site === 'docs') { - await router.push(link) - } else if (site === 'website') { - await router.push(`/docs${link}`) - } else { - window.open(`https://supabase.com/docs${link}`, '_blank') - } - break - case PageType.Integration: - if (site === 'website') { - router.push(link) - } else { - window.open(`https://supabase.com${link}`, '_blank') - } - break - case PageType.GithubDiscussion: - window.open(link, '_blank') - break - default: - throw new Error(`Unknown page type '${pageType}'`) - } - } - function formatSectionUrl(page: Page, section: PageSection): string { switch (page.type) { case PageType.Markdown: diff --git a/apps/studio/components/layouts/AdvisorsLayout/Advisors.Commands.tsx b/apps/studio/components/layouts/AdvisorsLayout/Advisors.Commands.tsx new file mode 100644 index 00000000000..719a387333e --- /dev/null +++ b/apps/studio/components/layouts/AdvisorsLayout/Advisors.Commands.tsx @@ -0,0 +1,28 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useAdvisorsGoToCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-advisors-security', + name: 'Security Advisor', + route: `/project/${ref}/advisors/security`, + defaultHidden: true, + }, + { + id: 'nav-advisors-performance', + name: 'Performance Advisor', + route: `/project/${ref}/advisors/performance`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx b/apps/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx index c4a41710016..410b3c7504d 100644 --- a/apps/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx @@ -1,10 +1,11 @@ +import { User } from 'lucide-react' import Link from 'next/link' +import { useTheme } from 'next-themes' import { useRouter } from 'next/router' import { useState } from 'react' import { useSignOut } from 'lib/auth' import { useProfile } from 'lib/profile' -import { useTheme } from 'next-themes' import { DropdownMenu, DropdownMenuContent, @@ -19,15 +20,14 @@ import { singleThemes, Theme, } from 'ui' -import { useCommandMenu } from 'ui-patterns/Cmdk' -import { User } from 'lucide-react' +import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' const UserSettingsDropdown = () => { const signOut = useSignOut() const router = useRouter() const [open, setOpen] = useState(false) const { profile } = useProfile() - const { setIsOpen: setCommandMenuOpen } = useCommandMenu() + const setCommandMenuOpen = useSetCommandMenuOpen() const { theme, setTheme } = useTheme() const onClickLogout = async () => { diff --git a/apps/studio/components/layouts/AuthLayout/Auth.Commands.tsx b/apps/studio/components/layouts/AuthLayout/Auth.Commands.tsx new file mode 100644 index 00000000000..20f52cd7334 --- /dev/null +++ b/apps/studio/components/layouts/AuthLayout/Auth.Commands.tsx @@ -0,0 +1,73 @@ +import { Lock } from 'lucide-react' + +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' +import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering' +import { useParams } from 'common' + +export function useAuthGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + 'Actions', + [ + { + id: 'create-rls-policy', + name: 'Create RLS policy', + value: 'Create RLS (Row Level Security) policy', + route: `/project/${ref}/auth/policies`, + icon: () => , + }, + ], + { + ...options, + deps: [ref], + orderSection: orderCommandSectionsByPriority, + sectionMeta: { priority: 3 }, + } + ) + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-auth-users', + name: 'Users', + value: 'Auth: Users', + route: `/project/${ref}/auth/users`, + defaultHidden: true, + }, + { + id: 'nav-auth-policies', + name: 'Policies', + value: 'Auth: Policies (RLS)', + route: `/project/${ref}/auth/policies`, + defaultHidden: true, + }, + { + id: 'nav-auth-providers', + name: 'Providers', + value: 'Auth: Providers (Social Login, SSO)', + route: `/project/${ref}/auth/providers`, + defaultHidden: true, + }, + { + id: 'nav-auth-templates', + name: 'Email Templates', + value: 'Auth: Email Templates', + route: `/project/${ref}/auth/templates`, + defaultHidden: true, + }, + { + id: 'nav-auth-url-configuration', + name: 'URL Configuration', + value: 'Auth: URL Configuration (Site URL, Redirect URLs)', + route: `/project/${ref}/auth/url-configuration`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx b/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx new file mode 100644 index 00000000000..891f526f234 --- /dev/null +++ b/apps/studio/components/layouts/DatabaseLayout/Database.Commands.tsx @@ -0,0 +1,107 @@ +import { Code } from 'lucide-react' + +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' +import { useParams } from 'common' +import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering' + +export function useDatabaseGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.QUERY, + [ + { + id: 'run-sql', + name: 'Run SQL', + route: `/project/${ref}/sql/new`, + icon: () => , + }, + ], + { + ...options, + deps: [ref], + orderSection: orderCommandSectionsByPriority, + sectionMeta: { priority: 2 }, + } + ) + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-database-tables', + name: 'Tables', + value: 'Database: Tables', + route: `/project/${ref}/database/tables`, + defaultHidden: true, + }, + { + id: 'nav-database-triggers', + name: 'Triggers', + value: 'Database: Triggers', + route: `/project/${ref}/database/triggers`, + defaultHidden: true, + }, + { + id: 'nav-database-functions', + name: 'Functions', + value: 'Database: Functions', + route: `/project/${ref}/database/functions`, + defaultHidden: true, + }, + { + id: 'nav-database-extensions', + name: 'Extensions', + value: 'Database: Extensions', + route: `/project/${ref}/database/extensions`, + defaultHidden: true, + }, + { + id: 'nav-database-roles', + name: 'Roles', + value: 'Database: Roles', + route: `/project/${ref}/database/roles`, + defaultHidden: true, + }, + { + id: 'nav-database-replication', + name: 'Replication', + value: 'Database: Replication', + route: `/project/${ref}/database/replication`, + defaultHidden: true, + }, + { + id: 'nav-database-hooks', + name: 'Webhooks', + value: 'Database: Webhooks', + route: `/project/${ref}/database/hooks`, + defaultHidden: true, + }, + { + id: 'nav-database-backups', + name: 'Backups', + value: 'Database: Backups', + route: `/project/${ref}/database/backups/scheduled`, + defaultHidden: true, + }, + { + id: 'nav-database-wrappers', + name: 'Wrappers', + value: 'Database: Wrappers', + route: `/project/${ref}/database/wrappers`, + defaultHidden: true, + }, + { + id: 'nav-database-migrations', + name: 'Migrations', + value: 'Database: Migrations', + route: `/project/${ref}/database/migrations`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/FunctionsLayout/Functions.Commands.tsx b/apps/studio/components/layouts/FunctionsLayout/Functions.Commands.tsx new file mode 100644 index 00000000000..1c447f1c19e --- /dev/null +++ b/apps/studio/components/layouts/FunctionsLayout/Functions.Commands.tsx @@ -0,0 +1,22 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useFunctionsGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-functions', + name: 'Edge Functions', + route: `/project/${ref}/functions`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/FunctionsLayout.tsx b/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx similarity index 97% rename from apps/studio/components/layouts/FunctionsLayout.tsx rename to apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx index 768683b7cb7..1d2feb88dca 100644 --- a/apps/studio/components/layouts/FunctionsLayout.tsx +++ b/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx @@ -11,8 +11,8 @@ import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { withAuth } from 'hooks/misc/withAuth' import { Button, IconCode, IconExternalLink } from 'ui' -import FunctionsNav from '../interfaces/Functions/FunctionsNav' -import ProjectLayout from './ProjectLayout/ProjectLayout' +import FunctionsNav from '../../interfaces/Functions/FunctionsNav' +import ProjectLayout from '../ProjectLayout/ProjectLayout' interface FunctionsLayoutProps { title?: string diff --git a/apps/studio/components/layouts/LogsLayout/Logs.Commands.tsx b/apps/studio/components/layouts/LogsLayout/Logs.Commands.tsx new file mode 100644 index 00000000000..e302bb87400 --- /dev/null +++ b/apps/studio/components/layouts/LogsLayout/Logs.Commands.tsx @@ -0,0 +1,58 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useLogsGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-logs-explorer', + name: 'Logs Explorer', + route: `/project/${ref}/logs/explorer`, + defaultHidden: true, + }, + { + id: 'nav-logs-postgres', + name: 'Postgres Logs', + route: `/project/${ref}/logs/postgres-logs`, + defaultHidden: true, + }, + { + id: 'nav-logs-postgrest', + name: 'PostgREST Logs', + route: `/project/${ref}/logs/postgrest-logs`, + defaultHidden: true, + }, + { + id: 'nav-logs-pooler', + name: 'Pooler Logs', + route: `/project/${ref}/logs/pooler-logs`, + defaultHidden: true, + }, + { + id: 'nav-logs-auth', + name: 'Auth Logs', + route: `/project/${ref}/logs/auth-logs`, + defaultHidden: true, + }, + { + id: 'nav-logs-storage', + name: 'Storage Logs', + route: `/project/${ref}/logs/storage-logs`, + defaultHidden: true, + }, + { + id: 'nav-logs-realtime', + name: 'Realtime Logs', + route: `/project/${ref}/logs/realtime-logs`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index cda0a21ff0e..84b983b94b5 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -31,7 +31,7 @@ import { cn, singleThemes, } from 'ui' -import { useCommandMenu } from 'ui-patterns/Cmdk' +import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu' import { useProjectContext } from '../ProjectContext' import { generateOtherRoutes, @@ -53,7 +53,7 @@ const NavigationBar = () => { const { project } = useProjectContext() const { theme, setTheme } = useTheme() const { ref: projectRef } = useParams() - const { setIsOpen } = useCommandMenu() + const setCommandMenuOpen = useSetCommandMenuOpen() const snap = useAppStateSnapshot() const signOut = useSignOut() @@ -235,7 +235,7 @@ const NavigationBar = () => { { - setIsOpen(true) + setCommandMenuOpen(true) snap.setNavigationPanelOpen(false) }} type="text" diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx new file mode 100644 index 00000000000..20b432d04ad --- /dev/null +++ b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx @@ -0,0 +1,118 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useProjectSettingsGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-project-settings-general', + name: 'General Settings', + route: `/project/${ref}/settings/general`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-database', + name: 'Database Settings', + route: `/project/${ref}/settings/database`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-auth', + name: 'Auth Settings', + route: `/project/${ref}/settings/auth`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-api', + name: 'API Settings', + route: `/project/${ref}/settings/api`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-storage', + name: 'Storage Settings', + route: `/project/${ref}/settings/storage`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-custom-domains', + name: 'Custom Domains', + route: `/project/${ref}/settings/general#custom-domains`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-restart-project', + name: 'Restart project', + route: `/project/${ref}/settings/general#restart-project`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-pause-project', + name: 'Pause project', + route: `/project/${ref}/settings/general#pause-project`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-transfer-project', + name: 'Transfer project', + route: `/project/${ref}/settings/general#transfer-project`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-delete-project', + name: 'Delete project', + route: `/project/${ref}/settings/general#delete-project`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-database-password', + name: 'Database password', + route: `/project/${ref}/settings/general#database-password`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-connection-string', + name: 'Connection string', + route: `/project/${ref}/settings/general#connection-string`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-connection-pooling', + name: 'Connection pooling', + route: `/project/${ref}/settings/general#connection-pooling`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-ssl-configuration', + name: 'SSL configuration', + route: `/project/${ref}/settings/general#ssl-configuration`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-network-restrictions', + name: 'Network restrictions', + route: `/project/${ref}/settings/general#network-restrictions`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-banned-ips', + name: 'Banned IPs', + route: `/project/${ref}/settings/general#banned-ips`, + defaultHidden: true, + }, + { + id: 'nav-project-settings-log-drains', + name: 'Log drains', + route: `/project/${ref}/settings/log-drains`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx b/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx new file mode 100644 index 00000000000..22317d162f4 --- /dev/null +++ b/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx @@ -0,0 +1,46 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useReportsGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-reports', + name: 'Reports', + route: `/project/${ref}/reports`, + defaultHidden: true, + }, + { + id: 'nav-reports-api', + name: 'API Reports', + route: `/project/${ref}/reports/api-overview`, + defaultHidden: true, + }, + { + id: 'nav-reports-storage', + name: 'Storage Reports', + route: `/project/${ref}/reports/storage`, + defaultHidden: true, + }, + { + id: 'nav-reports-database', + name: 'Database Reports', + route: `/project/${ref}/reports/database`, + defaultHidden: true, + }, + { + id: 'nav-reports-query-performance', + name: 'Query Performance Reports', + route: `/project/${ref}/reports/query-performance`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx new file mode 100644 index 00000000000..0a287f6588d --- /dev/null +++ b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx @@ -0,0 +1,22 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useSqlEditorGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-sql-editor', + name: 'SQL Editor', + route: `/project/${ref}/sql`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/StorageLayout/Storage.Commands.tsx b/apps/studio/components/layouts/StorageLayout/Storage.Commands.tsx new file mode 100644 index 00000000000..77f14d49216 --- /dev/null +++ b/apps/studio/components/layouts/StorageLayout/Storage.Commands.tsx @@ -0,0 +1,22 @@ +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useStorageGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-storage', + name: 'Storage', + route: `/project/${ref}/storage`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditor.Commands.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditor.Commands.tsx new file mode 100644 index 00000000000..f365ab88d16 --- /dev/null +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditor.Commands.tsx @@ -0,0 +1,43 @@ +import { Table } from 'lucide-react' + +import { useParams } from 'common' +import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils' +import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering' +import type { CommandOptions } from 'ui-patterns/CommandMenu' +import { useRegisterCommands } from 'ui-patterns/CommandMenu' + +export function useTableEditorGotoCommands(options?: CommandOptions) { + let { ref } = useParams() + ref ||= '_' + + useRegisterCommands( + COMMAND_MENU_SECTIONS.ACTIONS, + [ + { + id: 'view-data', + name: 'View your data', + route: `/project/${ref}/editor`, + icon: () => , + }, + ], + { + ...options, + deps: [ref], + orderSection: orderCommandSectionsByPriority, + sectionMeta: { priority: 3 }, + } + ) + + useRegisterCommands( + COMMAND_MENU_SECTIONS.NAVIGATE, + [ + { + id: 'nav-table-editor', + name: 'Table Editor', + route: `/project/${ref}/editor`, + defaultHidden: true, + }, + ], + { ...options, deps: [ref] } + ) +} diff --git a/apps/studio/components/layouts/useLayoutNavCommands.ts b/apps/studio/components/layouts/useLayoutNavCommands.ts new file mode 100644 index 00000000000..d67bbec45f6 --- /dev/null +++ b/apps/studio/components/layouts/useLayoutNavCommands.ts @@ -0,0 +1,28 @@ +import { useApiDocsGotoCommands } from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs.Commands' +import { useAdvisorsGoToCommands } from './AdvisorsLayout/Advisors.Commands' +import { useAuthGotoCommands } from './AuthLayout/Auth.Commands' +import { useDatabaseGotoCommands } from './DatabaseLayout/Database.Commands' +import { useFunctionsGotoCommands } from './FunctionsLayout/Functions.Commands' +import { useLogsGotoCommands } from './LogsLayout/Logs.Commands' +import { useProjectSettingsGotoCommands } from './ProjectSettingsLayout/ProjectSettings.Commands' +import { useReportsGotoCommands } from './ReportsLayout/Reports.Commands' +import { useStorageGotoCommands } from './StorageLayout/Storage.Commands' +import { useSqlEditorGotoCommands } from './SQLEditorLayout/SqlEditor.Commands' +import { useTableEditorGotoCommands } from './TableEditorLayout/TableEditor.Commands' +import { useIsLoggedIn } from 'common' + +export function useLayoutNavCommands() { + const isLoggedIn = useIsLoggedIn() + + useTableEditorGotoCommands({ enabled: isLoggedIn }) + useSqlEditorGotoCommands({ enabled: isLoggedIn }) + useDatabaseGotoCommands({ enabled: isLoggedIn }) + useAuthGotoCommands({ enabled: isLoggedIn }) + useAdvisorsGoToCommands({ enabled: isLoggedIn }) + useStorageGotoCommands({ enabled: isLoggedIn }) + useFunctionsGotoCommands({ enabled: isLoggedIn }) + useLogsGotoCommands({ enabled: isLoggedIn }) + useReportsGotoCommands({ enabled: isLoggedIn }) + useApiDocsGotoCommands({ enabled: isLoggedIn }) + useProjectSettingsGotoCommands({ enabled: isLoggedIn }) +} diff --git a/apps/studio/components/ui/AISettingsModal.tsx b/apps/studio/components/ui/AISettingsModal.tsx index b0b3e9ae82b..fc7303c4daf 100644 --- a/apps/studio/components/ui/AISettingsModal.tsx +++ b/apps/studio/components/ui/AISettingsModal.tsx @@ -2,8 +2,9 @@ import Link from 'next/link' import toast from 'react-hot-toast' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { IS_PLATFORM, LOCAL_STORAGE_KEYS, OPT_IN_TAGS } from 'lib/constants' +import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { AlertDescription_Shadcn_, @@ -18,7 +19,7 @@ import { const AISettingsModal = () => { const snap = useAppStateSnapshot() const selectedOrganization = useSelectedOrganization() - const isOptedInToAI = selectedOrganization?.opt_in_tags?.includes(OPT_IN_TAGS.AI_SQL) ?? false + const isOptedInToAI = useOrgOptedIntoAi() const [hasEnabledAISchema, setHasEnabledAISchema] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_SCHEMA, diff --git a/apps/studio/hooks/misc/useAllowSendAiSchema.ts b/apps/studio/hooks/misc/useAllowSendAiSchema.ts new file mode 100644 index 00000000000..780ca55c269 --- /dev/null +++ b/apps/studio/hooks/misc/useAllowSendAiSchema.ts @@ -0,0 +1,12 @@ +import { IS_PLATFORM } from 'common' +import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' +import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' +import { LOCAL_STORAGE_KEYS } from 'lib/constants' + +export function useAllowSendAiSchema() { + const isOptedIntoAI = useOrgOptedIntoAi() + const [hasEnabledAISchema] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_SCHEMA, true) + const includeSchemaMetadata = (isOptedIntoAI || !IS_PLATFORM) && hasEnabledAISchema + + return includeSchemaMetadata +} diff --git a/apps/studio/hooks/misc/useDisallowHipaa.ts b/apps/studio/hooks/misc/useDisallowHipaa.ts new file mode 100644 index 00000000000..5958b82fb33 --- /dev/null +++ b/apps/studio/hooks/misc/useDisallowHipaa.ts @@ -0,0 +1,20 @@ +import { useCallback } from 'react' + +import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' + +export function useDisallowHipaa() { + const selectedOrganization = useSelectedOrganization() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrganization?.slug }) + const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) + + const disallowHipaa = useCallback( + (allowed: boolean) => { + return hasHipaaAddon ? false : allowed + }, + [hasHipaaAddon] + ) + + return disallowHipaa +} diff --git a/apps/studio/hooks/misc/useOrgOptedIntoAi.ts b/apps/studio/hooks/misc/useOrgOptedIntoAi.ts new file mode 100644 index 00000000000..49f2c1ec937 --- /dev/null +++ b/apps/studio/hooks/misc/useOrgOptedIntoAi.ts @@ -0,0 +1,12 @@ +import { useDisallowHipaa } from 'hooks/misc/useDisallowHipaa' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { OPT_IN_TAGS } from 'lib/constants' + +export function useOrgOptedIntoAi() { + const selectedOrganization = useSelectedOrganization() + const optInTags = selectedOrganization?.opt_in_tags + const isOptedIntoAI = optInTags?.includes(OPT_IN_TAGS.AI_SQL) ?? false + + const disallowHipaa = useDisallowHipaa() + return disallowHipaa(isOptedIntoAI) +} diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 300e65caea1..51c6847bdf1 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -35,17 +35,16 @@ import { ErrorInfo, useEffect, useMemo, useRef, useState } from 'react' import { ErrorBoundary } from 'react-error-boundary' import toast from 'react-hot-toast' import { PortalToast, Toaster } from 'ui' +import { CommandProvider } from 'ui-patterns/CommandMenu' import { ConsentToast } from 'ui-patterns/ConsentToast' import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router' -import { - AppBannerWrapper, - CommandMenuWrapper, - RouteValidationWrapper, -} from 'components/interfaces/App' +import { AppBannerWrapper, RouteValidationWrapper } from 'components/interfaces/App' +import { StudioCommandMenu } from 'components/interfaces/App/CommandMenu' import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal' +import { GenerateSql } from 'components/interfaces/SqlGenerator/SqlGenerator' import { ErrorBoundaryState } from 'components/ui/ErrorBoundaryState' import FlagProvider from 'components/ui/Flag/FlagProvider' import PageTelemetry from 'components/ui/PageTelemetry' @@ -174,14 +173,16 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { disableTransitionOnChange > - + {getLayout()} + + - + diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx index 88e563b343f..893b8efb3ed 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx @@ -1,5 +1,5 @@ import { EdgeFunctionDetails } from 'components/interfaces/Functions' -import FunctionsLayout from 'components/layouts/FunctionsLayout' +import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout' import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx index d305ac3490c..053397213a9 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/router' import { useMemo, useState } from 'react' import ReportWidget from 'components/interfaces/Reports/ReportWidget' -import FunctionsLayout from 'components/layouts/FunctionsLayout' +import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout' import AreaChart from 'components/ui/Charts/AreaChart' import StackedBarChart from 'components/ui/Charts/StackedBarChart' import NoPermission from 'components/ui/NoPermission' diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx index 94d90a18b09..29c2e48d83b 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx @@ -1,7 +1,7 @@ import { useParams } from 'common' import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' -import FunctionsLayout from 'components/layouts/FunctionsLayout' +import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx index cb20fc4d122..c3d5282e6d9 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx @@ -1,6 +1,6 @@ import { useParams } from 'common/hooks' import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' -import FunctionsLayout from 'components/layouts/FunctionsLayout' +import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout' import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 740ac23c1fa..9a819b83a6f 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -17,7 +17,7 @@ import { FunctionsEmptyState, TerminalInstructions, } from 'components/interfaces/Functions' -import FunctionsLayout from 'components/layouts/FunctionsLayout' +import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index 8fc8d0d976c..4aa575e3702 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -58,6 +58,10 @@ export const appState = proxy({ setShowAiSettingsModal: (value: boolean) => { appState.showAiSettingsModal = value }, + showGenerateSqlModal: false, + setShowGenerateSqlModal: (value: boolean) => { + appState.showGenerateSqlModal = value + }, navigationPanelOpen: false, navigationPanelJustClosed: false, diff --git a/packages/ui-patterns/CommandMenu/internal/CommandSection.tsx b/packages/ui-patterns/CommandMenu/internal/CommandSection.tsx index 79749b2f09c..ab52fac28b1 100644 --- a/packages/ui-patterns/CommandMenu/internal/CommandSection.tsx +++ b/packages/ui-patterns/CommandMenu/internal/CommandSection.tsx @@ -5,6 +5,7 @@ type ICommandSection = { name: string commands: Array forceMount?: boolean + meta?: any } const toSectionId = (str: string) => str.toLowerCase().replace(/\s+/g, '-') diff --git a/packages/ui-patterns/CommandMenu/internal/state/commandsState.ts b/packages/ui-patterns/CommandMenu/internal/state/commandsState.ts index ad91bfa268c..9c53e8afa89 100644 --- a/packages/ui-patterns/CommandMenu/internal/state/commandsState.ts +++ b/packages/ui-patterns/CommandMenu/internal/state/commandsState.ts @@ -11,6 +11,7 @@ type CommandOptions = { deps?: any[] enabled?: boolean forceMountSection?: boolean + sectionMeta?: any orderSection?: OrderSectionInstruction orderCommands?: OrderCommandsInstruction } @@ -32,6 +33,14 @@ const initCommandsState = () => { if (editIndex === -1) editIndex = state.commandSections.length state.commandSections[editIndex] ??= section$new(sectionName) + if (options?.sectionMeta) { + const oldMeta = state.commandSections[editIndex].meta + if (!!oldMeta && typeof oldMeta === 'object' && typeof options.sectionMeta === 'object') { + state.commandSections[editIndex].meta = { ...oldMeta, ...options.sectionMeta } + } else { + state.commandSections[editIndex].meta = options.sectionMeta + } + } if (options?.forceMountSection) state.commandSections[editIndex].forceMount = true diff --git a/packages/ui-patterns/CommandMenu/prepackaged/Changelog.tsx b/packages/ui-patterns/CommandMenu/prepackaged/Changelog.tsx index 5b8f56fe44b..2827eccd9f3 100644 --- a/packages/ui-patterns/CommandMenu/prepackaged/Changelog.tsx +++ b/packages/ui-patterns/CommandMenu/prepackaged/Changelog.tsx @@ -3,15 +3,19 @@ import { Inbox } from 'lucide-react' import { useRegisterCommands } from '..' import { BASE_PATH } from './shared/constants' -const useChangelogCommand = () => { - useRegisterCommands('Updates', [ - { - id: 'changelog', - name: 'Changelog', - route: BASE_PATH ? 'https://supabase.com/changelog' : '/changelog', - icon: () => , - }, - ]) +const useChangelogCommand = ({ enabled = true }: { enabled?: boolean } = {}) => { + useRegisterCommands( + 'Updates', + [ + { + id: 'changelog', + name: 'Changelog', + route: BASE_PATH ? 'https://supabase.com/changelog' : '/changelog', + icon: () => , + }, + ], + { enabled } + ) } export { useChangelogCommand } diff --git a/packages/ui-patterns/CommandMenu/prepackaged/DocsAi/index.tsx b/packages/ui-patterns/CommandMenu/prepackaged/DocsAi/index.tsx index 469a9b4d62d..b7215873082 100644 --- a/packages/ui-patterns/CommandMenu/prepackaged/DocsAi/index.tsx +++ b/packages/ui-patterns/CommandMenu/prepackaged/DocsAi/index.tsx @@ -31,7 +31,6 @@ const useDocsAiCommands = ({ setCommandPage(DOCS_AI_COMMANDS.PAGE_NAME, true) }, icon: () => , - className: 'text-brand', }, ].map(modify), options diff --git a/packages/ui-patterns/CommandMenu/prepackaged/Support.tsx b/packages/ui-patterns/CommandMenu/prepackaged/Support.tsx index 119d7ac035c..b7d9bf3530c 100644 --- a/packages/ui-patterns/CommandMenu/prepackaged/Support.tsx +++ b/packages/ui-patterns/CommandMenu/prepackaged/Support.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react' import { useRegisterCommands, useSetCommandMenuOpen, type ICommand } from '..' import { BASE_PATH } from './shared/constants' -const useSupportCommands = () => { +const useSupportCommands = ({ enabled = true }: { enabled?: boolean } = {}) => { const setOpen = useSetCommandMenuOpen() const commands = useMemo( @@ -41,7 +41,7 @@ const useSupportCommands = () => { [setOpen] ) - useRegisterCommands('Support', commands) + useRegisterCommands('Support', commands, { enabled }) } export { useSupportCommands } diff --git a/packages/ui-patterns/CommandMenu/prepackaged/ai/useAiChat.ts b/packages/ui-patterns/CommandMenu/prepackaged/ai/useAiChat.ts index f2af9154855..87d9e92316b 100644 --- a/packages/ui-patterns/CommandMenu/prepackaged/ai/useAiChat.ts +++ b/packages/ui-patterns/CommandMenu/prepackaged/ai/useAiChat.ts @@ -172,6 +172,7 @@ const useAiChat = ({ messageTemplate = (message) => message, setIsLoading }: Use eventSourceRef.current = undefined setIsResponding(false) setHasError(false) + setCurrentMessageIndex(1) dispatchMessage({ type: 'reset', }) From 77bc1a4d8b01c5a42e4f817c71562679b6158e33 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Fri, 9 Aug 2024 13:02:16 +0800 Subject: [PATCH 005/151] Revert "Bug Fix - Nav elements overlap" (#28497) Revert "Bug Fix - Nav elements overlap (#25966)" This reverts commit 77dfed4b7c03781ad853f1713d2b03ac86915a84. --- .../ProjectLayout/NavigationBar/NavigationBar.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 84b983b94b5..57753ac82a9 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -106,21 +106,21 @@ const NavigationBar = () => {