* init

* Update inner-side-menu.mdx

* chore: update SQL sidebar to use ui pattern components

* mor

* Update

* Update index.tsx

* init: merge table editor and sql editor and schema visualization together

* more

* move to valtio

* fix issue with Command+B shortcut

* now shows in treeviews if item is opened in tab

* Update ProjectLayout.tsx

* fix sidebar

* fix schema selector for non explorer version

* show schema name in tabs

* added schema names to tabs

* tabs have been updated to support preview tabs

* fix URL issue

* add empty state stuff

* Update SQLEditorNav.tsx

* preview tab works now

* more tabs stuff. 'new' tab also added

* new tab concept

* updates

* fix type errors

* remove unused files

* update test

* move back button, fix width issues on sidebar

* update sidebar logic

* Update ProjectLayout.tsx

* lots of updates. layouts now streamlined. localstorage for tabs in use

* moar

* bunch of new tab logic

* fix empty tab issue

* Update tabs.ts

* layouts switched

* new pages now have fixed layouts

* fix tabs

* fix code bg

* add tabs support for multiple project refs

* intialization issue

* update ID handling

* fixed isOpened state for SQL snippets

* remove old assistant because its bugging up panels

* preview style works in sql editor

* fix border

* removes preview tab if there is one

* fix background of loading skeleton

* lots of issues with types/icons/redirect

* new tab cards

* snippets in empty state now work

* moar stuff

* tabs now in feature flags

* Update tabs.ts

* Update tabs.ts

* moar

* add feature previews

* remove code not needed

* Update next-env.d.ts

* Delete FeaturePreviewModal.tsx

* fix typescript errors. remove more explorer stuff

* remove explorer files

* fixed issues with templates and quickstarts tab

* fixed active state when tabs are not opted in

* logic error

* fix open/highlight issue when opted out of tabs

* templates/quickstarts now displayed with new cards

* Update recent-items.tsx

* Update new-tab.tsx

* add icon back in

* add old empty state back in

* recent items updated to respect project ref

* localstorage cleanup on deletion

* moar

* overflow tabs now working

* correct tab names used for new sql templates/quickstarts

* ongoing queries fix

* cleanup

* update images

* Update RouteValidationWrapper.tsx

* Update AppLayout.tsx

* Update NavigationBar.tsx

* add headers back into side panels

* improve writing

* tabs now drag and drop a billion times better

* Update tabs.tsx

* Update tabs.tsx

* init issues on stores, which caused a race condition.

* fix hydration error

* fix new tab issue in sql

* Update ProjectLayout.tsx

* Update pnpm-lock.yaml

* Update new-tab.tsx

* move EditorMenuListSkeleton

* Fix type issues

* fixes: DESIGN-87

fixes: DESIGN-87

* refactor sort/filter components

* Update rules-set-button-text.tsx

* remove discussions for now

* small styling fixes

* Update FeaturePreviewModal.tsx

* Update FeaturePreviewModal.tsx

* Update RouteValidationWrapper.tsx

* revert

* revert

* revert

* revert

* revert

* more revert

* Update collapse-button.tsx

* Update SQLEditorTreeViewItem.tsx

* revert

* Update SchemaGraph.tsx

* Delete new-upcoming.tsx

* revert

* Update ProjectLayout.tsx

* fix home link

* Update table-editor.spec.ts

* test update

* Update table-editor.spec.ts

* Fix the playwright tests.

* layout fixes

* layout fix

* revert sort/filter

* Update LastSignInWrapper.tsx

* revert

* revert

* remove

* update file names

* revert

* revert

* revert

* Fix TreeView console error props

* Add guards in SQL Editor to ensure that feature preview tabs changes do not affect existing UI when flag is off

* Fix missing DefaultLayout in SQL editor templates + fix New tab

* Remove console log

* Remove DatabaseSelector for SQL editor on local

* Fix SQL editor shared favorites for local

* Fix test

* Ensure NewTab doesn't show up if flag is not toggled for SQL editor

* Decouple UI state changes from content-query and entity-types-infinite-query

* Fix tab closing unnecessary rerouting

* Beef up feature previews

* Fix create new table from table editor new tab

* Fix tabs getting incorrectly reset when going between table and SQL editors

* Fix last visited SQL snippet for both tabs and not tabs

* Fix last visited table for table editor tabs

* Clear dashboard history when closing last tab

* Fix loading dashboard history

* Add comment to refactor stores

* Ensure we only save up to 8 items for recent items for each type

* Remove unneccesary logic in tabs

* Smol style fix for DeleteAccountButton

* Smol fix

* Fix inability to close New tab

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Alaister Young <a@alaisteryoung.com>
Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-03-27 17:46:57 +08:00
1 parent e8698432bc
commit 143f49414b
68 files changed
+2406 -425

No files matched your search

@@ -68,7 +68,7 @@ const Header = ({ sorts, filters, customHeader }: HeaderProps) => {
return (
<div>
<div className="flex h-10 items-center justify-between bg-dash-sidebar px-1.5 py-1.5 gap-2 overflow-x-auto">
<div className="flex h-10 items-center justify-between bg-dash-sidebar dark:bg-surface-100 px-1.5 py-1.5 gap-2 overflow-x-auto ">
{customHeader ? (
<>{customHeader}</>
) : (
@@ -25,6 +25,7 @@ import {
FormLabel_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
Separator,
} from 'ui'
const setDeletionRequestFlag = () => {
@@ -108,8 +109,8 @@ export const DeleteAccountButton = () => {
Request to delete account
</Button>
</DialogTrigger>
<DialogContent className="!w-[450px]">
<DialogHeader className="border-b">
<DialogContent className="!w-[500px]">
<DialogHeader>
{(organizations ?? []).length > 0 ? (
<>
<DialogTitle>Leave all organizations before requesting account deletion</DialogTitle>
@@ -128,25 +129,25 @@ export const DeleteAccountButton = () => {
)}
</DialogHeader>
<Separator />
{isSuccess && (
<>
{organizations.length > 0 ? (
<DialogSection className="!pt-0">
<span className="text-sm text-foreground flex flex-col gap-y-2">
Before submitting an account deletion request, please ensure that your account is
not part of any organization. This can be done by leaving or deleting the
organizations that you are a part of.
</span>
<Button
block
type="primary"
size="medium"
className="mt-6"
onClick={() => setIsOpen(false)}
>
Understood
</Button>
</DialogSection>
<>
<DialogSection>
<span className="text-sm text-foreground flex flex-col gap-y-2">
Before submitting an account deletion request, please ensure that your account
is not part of any organization. This can be done by leaving or deleting the
organizations that you are a part of.
</span>
</DialogSection>
<DialogFooter>
<Button block type="primary" size="medium" onClick={() => setIsOpen(false)}>
Understood
</Button>
</DialogFooter>
</>
) : (
<Form_Shadcn_ {...form}>
<form
@@ -1,5 +1,5 @@
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { InlineLink } from 'components/ui/InlineLink'
import { BASE_PATH } from 'lib/constants'
import Image from 'next/image'
@@ -28,10 +28,8 @@ export const APISidePanelPreview = () => {
that can be accessed anywhere while in a project.
</li>
<li>
<Markdown
className="text-foreground-light"
content={`Move the GraphiQL interface the Database section of the dashboard [here](/project/${ref}/database/graphiql).`}
/>
Move the GraphiQL interface the Database section of the dashboard{' '}
<InlineLink href={`/project/${ref}/database/graphiql`}>here</InlineLink>.
</li>
</ul>
</div>
@@ -2,6 +2,7 @@ import Image from 'next/image'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { InlineLink } from 'components/ui/InlineLink'
import { BASE_PATH } from 'lib/constants'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, WarningIcon } from 'ui'
@@ -43,10 +44,8 @@ export const CLSPreview = () => {
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
<Markdown
className="text-foreground-light"
content={`Grant access to a new UI for granting and/or revoking column-level privileges [here](${BASE_PATH}/project/${ref}/database/column-privileges).`}
/>
Grant access to a new UI for granting and/or revoking column-level privileges{' '}
<InlineLink href={`/project/${ref}/database/column-privileges`}>here</InlineLink>.
</li>
</ul>
</div>
@@ -1,13 +1,34 @@
import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { APISidePanelPreview } from './APISidePanelPreview'
import { CLSPreview } from './CLSPreview'
import InlineEditorPreview from './InlineEditorPreview'
import { InlineEditorPreview } from './InlineEditorPreview'
import { SqlEditorTabsPreview } from './SqlEditorTabs'
import { TableEditorTabsPreview } from './TableEditorTabs'
export const FEATURE_PREVIEWS = [
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
name: 'Inline SQL Editor',
content: <InlineEditorPreview />,
discussionsUrl: 'https://github.com/orgs/supabase/discussions/33690',
isNew: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
name: 'Table Editor Tabs',
content: <TableEditorTabsPreview />,
isNew: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
name: 'SQL Editor Tabs',
content: <SqlEditorTabsPreview />,
isNew: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
name: 'Project API documentation',
@@ -22,13 +43,6 @@ export const FEATURE_PREVIEWS = [
discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295',
isNew: false,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
name: 'Inline SQL Editor',
content: <InlineEditorPreview />,
discussionsUrl: 'https://github.com/orgs/supabase/discussions/33690',
isNew: true,
},
]
type FeaturePreviewContextType = {
@@ -90,3 +104,13 @@ export const useIsInlineEditorEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR]
}
export const useIsTableEditorTabsEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS]
}
export const useIsSQLEditorTabsEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS]
}
@@ -2,28 +2,30 @@ import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useParams } from 'common'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { removeTabsByEditor } from 'state/tabs'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext'
import { useParams } from 'common'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
const FeaturePreviewModal = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const org = useSelectedOrganization()
const featurePreviewContext = useFeaturePreviewContext()
const { mutate: sendEvent } = useSendEventMutation()
const { ref: projectRef } = useParams()
const org = useSelectedOrganization()
const enableFunctionsAssistant = useFlag('functionsAssistantV2')
const isFeaturePreviewTabsTableEditorFlag = useFlag('featurePreviewTabsTableEditor')
const isFeaturePreviewTabsSqlEditorFlag = useFlag('featurePreviewSqlEditorTabs')
const selectedFeaturePreview =
snap.selectedFeaturePreview === '' ? FEATURE_PREVIEWS[0].key : snap.selectedFeaturePreview
const [selectedFeatureKey, setSelectedFeatureKey] = useState<string>(selectedFeaturePreview)
const isNotReleased =
selectedFeatureKey === 'supabase-ui-functions-assistant' && !enableFunctionsAssistant
const { flags, onUpdateFlag } = featurePreviewContext
const selectedFeature = FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey)
@@ -34,8 +36,15 @@ const FeaturePreviewModal = () => {
sendEvent({
action: isSelectedFeatureEnabled ? 'feature_preview_disabled' : 'feature_preview_enabled',
properties: { feature: selectedFeatureKey },
groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
if (selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS) {
removeTabsByEditor(ref as string | undefined, 'table')
}
if (selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS) {
removeTabsByEditor(ref as string | undefined, 'sql')
}
}
function handleCloseFeaturePreviewModal() {
@@ -43,8 +52,19 @@ const FeaturePreviewModal = () => {
snap.setSelectedFeaturePreview(FEATURE_PREVIEWS[0].key)
}
function isReleasedToPublic(feature: (typeof FEATURE_PREVIEWS)[number]) {
switch (feature.key) {
case LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS:
return isFeaturePreviewTabsTableEditorFlag
case LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS:
return isFeaturePreviewTabsSqlEditorFlag
default:
return true
}
}
// this modal can be triggered on other pages
// Update local state when valtio state changes
useEffect(() => {
if (snap.selectedFeaturePreview !== '') {
setSelectedFeatureKey(snap.selectedFeaturePreview)
@@ -55,7 +75,7 @@ const FeaturePreviewModal = () => {
if (snap.showFeaturePreviewModal) {
sendEvent({
action: 'feature_previews_clicked',
groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}
}, [snap.showFeaturePreviewModal])
@@ -65,7 +85,7 @@ const FeaturePreviewModal = () => {
hideFooter
showCloseButton
size="xlarge"
className="max-w-4xl"
className="!max-w-4xl"
header="Dashboard feature previews"
visible={snap.showFeaturePreviewModal}
onCancel={handleCloseFeaturePreviewModal}
@@ -74,7 +94,10 @@ const FeaturePreviewModal = () => {
<div className="flex">
<div>
<ScrollArea className="h-[550px] w-[280px] border-r">
{FEATURE_PREVIEWS.map((feature) => {
{FEATURE_PREVIEWS.filter((feature) => {
// filter out preview features that are not released to the public
return isReleasedToPublic(feature)
}).map((feature) => {
const isEnabled = flags[feature.key] ?? false
return (
@@ -113,15 +136,9 @@ const FeaturePreviewModal = () => {
</Link>
</Button>
)}
{isNotReleased ? (
<Button disabled type="default">
Coming soon
</Button>
) : (
<Button type="default" onClick={() => toggleFeature()}>
{isSelectedFeatureEnabled ? 'Disable' : 'Enable'} feature
</Button>
)}
<Button type="default" onClick={() => toggleFeature()}>
{isSelectedFeatureEnabled ? 'Disable' : 'Enable'} feature
</Button>
</div>
</div>
{selectedFeature?.content}
@@ -3,7 +3,7 @@ import Image from 'next/image'
import { BASE_PATH } from 'lib/constants'
import { Admonition } from 'ui-patterns'
const InlineEditorPreview = () => {
export const InlineEditorPreview = () => {
return (
<div>
<Image
@@ -29,5 +29,3 @@ const InlineEditorPreview = () => {
</div>
)
}
export default InlineEditorPreview
@@ -0,0 +1,28 @@
import { BASE_PATH } from 'lib/constants'
import Image from 'next/image'
export const SqlEditorTabsPreview = () => {
return (
<div className="space-y-2">
<Image
src={`${BASE_PATH}/img/previews/tabs-sql.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm">
The SQL Editor now features tabs for improved navigation and organization. Conveniently go
between the queries that you're focused on, and collapse the sidebar for a bigger real
estate while writing your queries.
</p>
<div className="space-y-2 !mt-4">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>Enable opening of queries in the SQL Editor as tabs</li>
<li>Support closing of the navigation sidebar for a larger code editor space</li>
</ul>
</div>
</div>
)
}
@@ -0,0 +1,28 @@
import { BASE_PATH } from 'lib/constants'
import Image from 'next/image'
export const TableEditorTabsPreview = () => {
return (
<div className="space-y-2">
<Image
src={`${BASE_PATH}/img/previews/tabs-editor.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm">
The Table Editor now features tabs for improved navigation and organization. Have multiple
tables opened across schemas and conveniently go across them without having to switch
schemas. Collapse the sidebar for a bigger real estate while browsing your data.
</p>
<div className="space-y-2 !mt-4">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>Enable opening of tables in the Table Editor as tabs</li>
<li>Support closing of the navigation sidebar for a larger data grid space</li>
</ul>
</div>
</div>
)
}
@@ -6,7 +6,8 @@ import { useIsLoggedIn, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import useLatest from 'hooks/misc/useLatest'
import { DEFAULT_HOME, IS_PLATFORM } from 'lib/constants'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { DEFAULT_HOME, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
// Ideally these could all be within a _middleware when we use Next 12
@@ -17,6 +18,11 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
const isLoggedIn = useIsLoggedIn()
const snap = useAppStateSnapshot()
const [dashboardHistory, _, { isSuccess: isSuccessStorage }] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''),
{ editor: undefined, sql: undefined }
)
/**
* Array of urls/routes that should be ignored
*/
@@ -96,6 +102,14 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
}
}, [ref, id])
useEffect(() => {
// Load dashboard history into app state
if (isSuccessStorage && ref) {
snap.setDashboardHistory(ref, 'editor', dashboardHistory.editor)
snap.setDashboardHistory(ref, 'sql', dashboardHistory.sql)
}
}, [isSuccessStorage, ref])
return <>{children}</>
}
@@ -15,6 +15,7 @@ import { Admonition } from 'ui-patterns'
import { untitledSnippetTitle } from './SQLEditor.constants'
import type { IStandaloneCodeEditor } from './SQLEditor.types'
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
import { makeActiveTabPermanent } from 'state/tabs'
export type MonacoEditorProps = {
id: string
@@ -146,6 +147,9 @@ const MonacoEditor = ({
const debouncedSetSql = debounce((id, value) => snapV2.setSql(id, value), 1000)
function handleEditorChange(value: string | undefined) {
// make active tab permanent
makeActiveTabPermanent(ref)
const snippetCheck = snapV2.snippets[id]
if (id && value) {
@@ -13,7 +13,9 @@ import type { SqlSnippet } from 'data/content/sql-snippets-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { createTabId, renameTab } from 'state/tabs'
import { AiIconAnimation, Button, Form, Input, Modal } from 'ui'
import { useIsSQLEditorTabsEnabled } from '../App/FeaturePreview/FeaturePreviewContext'
import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils'
export interface RenameQueryModalProps {
@@ -35,6 +37,7 @@ const RenameQueryModal = ({
const snapV2 = useSqlEditorV2StateSnapshot()
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
const isSQLSnippet = snippet.type === 'sql'
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
// Customers on HIPAA plans should not have access to Supabase AI
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
@@ -103,7 +106,10 @@ const RenameQueryModal = ({
})
snapV2.renameSnippet({ id, name: nameInput, description: descriptionInput })
if (isSQLEditorTabsEnabled && ref) {
const tabId = createTabId('sql', { id })
renameTab(ref, tabId, nameInput)
}
toast.success('Successfully renamed snippet!')
if (onComplete) onComplete()
} catch (error: any) {
@@ -9,6 +9,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import ResizableAIWidget from 'components/ui/AIEditor/ResizableAIWidget'
import { GridFooter } from 'components/ui/GridFooter'
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query'
@@ -48,7 +49,6 @@ import {
cn,
} from 'ui'
import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils'
import ResizableAIWidget from 'components/ui/AIEditor/ResizableAIWidget'
import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks'
import { RunQueryWarningModal } from './RunQueryWarningModal'
import {
@@ -1,29 +0,0 @@
import CardButton from 'components/ui/CardButton'
import { useState } from 'react'
export interface SQLCardProps {
title: string
description: string
sql: string
onClick: (sql: string, title: string) => void
}
const SQLCard = ({ title, description, sql, onClick }: SQLCardProps) => {
const [loading, setLoading] = useState(false)
function handleOnClick() {
setLoading(true)
onClick(sql, title)
}
return (
<CardButton
title={title}
loading={loading}
onClick={() => handleOnClick()}
description={description}
className="xl:h-48 p-6"
/>
)
}
export default SQLCard
@@ -5,15 +5,16 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { ActionCard } from 'components/layouts/Tabs/ActionCard'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { cn, SQL_ICON } from 'ui'
import { createSqlSnippetSkeletonV2 } from '../SQLEditor.utils'
import SQLCard from './SQLCard'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
const SQLQuickstarts = () => {
const router = useRouter()
@@ -51,6 +52,7 @@ const SQLQuickstarts = () => {
})
snapV2.addSnippet({ projectRef: ref, snippet })
snapV2.addNeedsSaving(snippet.id)
router.push(`/project/${ref}/sql/${snippet.id}`)
} catch (error: any) {
toast.error(`Failed to create new query: ${error.message}`)
@@ -58,28 +60,29 @@ const SQLQuickstarts = () => {
}
return (
<div className="block h-full space-y-8 overflow-y-auto p-4 md:p-6">
<div className="block h-full space-y-8 overflow-y-auto p-6 px-10 bg-dash-sidebar dark:bg-surface-100">
<div className="mb-8">
<div className="mb-4">
<h1 className="text-foreground mb-3 text-xl">Quickstarts</h1>
<div className="mb-6">
<h1 className="text-foreground mb-1 text-xl">Quickstarts</h1>
<p className="text-foreground-light text-sm">
Click on any script to fill the query box, modify the script, then click
<span className="text-code">Run</span>.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{quickStart.map((x) => (
<SQLCard
key={x.title}
{quickStart.map((x, i) => (
<ActionCard
title={x.title}
description={x.description}
sql={x.sql}
onClick={(sql, title) => {
handleNewQuery(sql, title)
bgColor="bg-alternative border"
key={`action-card-${i}`}
icon={<SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />}
// sql={x.sql}
onClick={() => {
handleNewQuery(x.sql, x.title)
sendEvent({
action: 'sql_editor_quickstart_clicked',
properties: { quickstartName: title },
properties: { quickstartName: x.title },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}}
@@ -6,14 +6,15 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ActionCard } from 'components/layouts/Tabs/ActionCard'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { cn, SQL_ICON } from 'ui'
import { createSqlSnippetSkeletonV2 } from '../SQLEditor.utils'
import SQLCard from './SQLCard'
const SQLTemplates = () => {
const router = useRouter()
@@ -51,6 +52,7 @@ const SQLTemplates = () => {
})
snapV2.addSnippet({ projectRef: ref, snippet })
snapV2.addNeedsSaving(snippet.id)
router.push(`/project/${ref}/sql/${snippet.id}`)
} catch (error: any) {
toast.error(`Failed to create new query: ${error.message}`)
@@ -58,10 +60,10 @@ const SQLTemplates = () => {
}
return (
<div className="block h-full space-y-8 overflow-y-auto p-4 md:p-6">
<div className="block h-full space-y-8 overflow-y-auto p-6 px-10 bg-dash-sidebar dark:bg-surface-100">
<div>
<div className="mb-4">
<h1 className="text-foreground mb-3 text-xl">Scripts</h1>
<div className="mb-6">
<h1 className="text-foreground mb-1 text-xl">Scripts</h1>
<p className="text-foreground-light text-sm">Quick scripts to run on your database.</p>
<p className="text-foreground-light text-sm">
Click on any script to fill the query box, modify the script, then click
@@ -69,17 +71,18 @@ const SQLTemplates = () => {
</p>
</div>
<div className="grid gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 ">
{sql.map((x) => (
<SQLCard
key={x.title}
{sql.map((x, i) => (
<ActionCard
key={`action-card-${i}`}
title={x.title}
description={x.description}
sql={x.sql}
onClick={(sql, title) => {
handleNewQuery(sql, title)
bgColor="bg-alternative border"
icon={<SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />}
onClick={() => {
handleNewQuery(x.sql, x.title)
sendEvent({
action: 'sql_editor_template_clicked',
properties: { templateName: title },
properties: { templateName: x.title },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}}
@@ -176,12 +176,17 @@ const UtilityActions = ({
<div className="flex items-center justify-between gap-x-2">
<div className="flex items-center">
<DatabaseSelector
selectedDatabaseId={lastSelectedDb.length === 0 ? undefined : lastSelectedDb}
variant="connected-on-right"
onSelectId={onSelectDatabase}
{IS_PLATFORM && (
<DatabaseSelector
selectedDatabaseId={lastSelectedDb.length === 0 ? undefined : lastSelectedDb}
variant="connected-on-right"
onSelectId={onSelectDatabase}
/>
)}
<RoleImpersonationPopover
serviceRoleLabel="postgres"
variant={IS_PLATFORM ? 'connected-on-both' : 'connected-on-right'}
/>
<RoleImpersonationPopover serviceRoleLabel="postgres" variant="connected-on-both" />
<SqlRunButton
isDisabled={isDisabled || isExecuting}
isExecuting={isExecuting}
@@ -1,5 +1,6 @@
import { ExternalLink, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { forwardRef } from 'react'
import { useParams } from 'common'
import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils'
@@ -18,158 +19,156 @@ export type UtilityTabResultsProps = {
isDebugging?: boolean
}
const UtilityTabResults = ({
id,
isExecuting,
isDisabled,
isDebugging,
onDebug,
}: UtilityTabResultsProps) => {
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const organization = useSelectedOrganization()
const snapV2 = useSqlEditorV2StateSnapshot()
const UtilityTabResults = forwardRef<HTMLDivElement, UtilityTabResultsProps>(
({ id, isExecuting, isDisabled, isDebugging, onDebug }, htmlRef) => {
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const organization = useSelectedOrganization()
const snapV2 = useSqlEditorV2StateSnapshot()
const result = snapV2.results[id]?.[0]
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
const result = snapV2.results[id]?.[0]
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
// Customers on HIPAA plans should not have access to Supabase AI
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
// Customers on HIPAA plans should not have access to Supabase AI
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
const isTimeout =
result?.error?.message?.includes('canceling statement due to statement timeout') ||
result?.error?.message?.includes('upstream request timeout')
const isTimeout =
result?.error?.message?.includes('canceling statement due to statement timeout') ||
result?.error?.message?.includes('upstream request timeout')
const isNetWorkError = result?.error?.message?.includes('EHOSTUNREACH')
if (isExecuting) {
return (
<div className="flex items-center gap-x-4 px-6 py-4 bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark">
<Loader2 size={14} className="animate-spin" />
<p className="m-0 border-0 font-mono text-sm">Running...</p>
</div>
)
} else if (result?.error) {
const formattedError = (result.error?.formattedError?.split('\n') ?? []).filter(
(x: string) => x.length > 0
)
const readReplicaError =
state.selectedDatabaseId !== ref &&
result.error.message.includes('in a read-only transaction')
const payloadTooLargeError = result.error.message.includes(
'Query is too large to be run via the SQL Editor'
)
if (isExecuting) {
return (
<div className="flex items-center gap-x-4 px-6 py-4 bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark">
<Loader2 size={14} className="animate-spin" />
<p className="m-0 border-0 font-mono text-sm">Running...</p>
</div>
)
} else if (result?.error) {
const formattedError = (result.error?.formattedError?.split('\n') ?? []).filter(
(x: string) => x.length > 0
)
const readReplicaError =
state.selectedDatabaseId !== ref &&
result.error.message.includes('in a read-only transaction')
const payloadTooLargeError = result.error.message.includes(
'Query is too large to be run via the SQL Editor'
)
return (
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark overflow-y-auto">
<div className="flex flex-row justify-between items-start py-4 px-6 gap-x-4">
{isTimeout ? (
<div className="flex flex-col gap-y-1">
<p className="font-mono text-sm">SQL query ran into an upstream timeout</p>
<p className="font-mono text-sm text-foreground-light">
You can either{' '}
<a
target="_blank"
rel="noreferrer"
className="underline transition hover:text-foreground"
href="https://supabase.com/docs/guides/platform/performance#examining-query-performance"
>
optimize your query
</a>
, or{' '}
<a
target="_blank"
rel="noreferrer"
className="underline transition hover:text-foreground"
href="https://supabase.com/docs/guides/database/timeouts"
>
increase the statement timeout
</a>
.
</p>
</div>
) : (
<div className="flex flex-col gap-y-1">
{formattedError.length > 0 ? (
formattedError.map((x: string, i: number) => (
<pre key={`error-${i}`} className="font-mono text-sm text-wrap">
{x}
</pre>
))
) : (
<p className="font-mono text-sm tracking-tight">Error: {result.error?.message}</p>
)}
{!isTimeout && result.autoLimit && (
<p className="text-sm text-foreground-light">
Note: A limit of {result.autoLimit} was applied to your query. If this was the
cause of a syntax error, try selecting "No limit" instead and re-run the query.
</p>
)}
{readReplicaError && (
<p className="text-sm text-foreground-light">
Note: Read replicas are for read only queries. Run write queries on the primary
database instead.
</p>
)}
{payloadTooLargeError && (
<p className="text-sm text-foreground-light flex items-center gap-x-1">
Run this query by{' '}
<Link
return (
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark overflow-y-auto">
<div className="flex flex-row justify-between items-start py-4 px-6 gap-x-4">
{isTimeout ? (
<div className="flex flex-col gap-y-1">
<p className="font-mono text-sm">SQL query ran into an upstream timeout</p>
<p className="font-mono text-sm text-foreground-light">
You can either{' '}
<a
target="_blank"
rel="noreferrer"
href={`/project/${ref}/settings/database`}
className="underline transition hover:text-foreground flex items-center gap-x-1"
className="underline transition hover:text-foreground"
href="https://supabase.com/docs/guides/platform/performance#examining-query-performance"
>
connecting to your database directly
<ExternalLink size={12} />
</Link>
optimize your query
</a>
, or{' '}
<a
target="_blank"
rel="noreferrer"
className="underline transition hover:text-foreground"
href="https://supabase.com/docs/guides/database/timeouts"
>
increase the statement timeout
</a>
.
</p>
</div>
) : (
<div className="flex flex-col gap-y-1">
{formattedError.length > 0 ? (
formattedError.map((x: string, i: number) => (
<pre key={`error-${i}`} className="font-mono text-sm text-wrap">
{x}
</pre>
))
) : (
<p className="font-mono text-sm tracking-tight">Error: {result.error?.message}</p>
)}
{!isTimeout && !isNetWorkError && result.autoLimit && (
<p className="text-sm text-foreground-light">
Note: A limit of {result.autoLimit} was applied to your query. If this was the
cause of a syntax error, try selecting "No limit" instead and re-run the query.
</p>
)}
{readReplicaError && (
<p className="text-sm text-foreground-light">
Note: Read replicas are for read only queries. Run write queries on the primary
database instead.
</p>
)}
{payloadTooLargeError && (
<p className="text-sm text-foreground-light flex items-center gap-x-1">
Run this query by{' '}
<Link
target="_blank"
rel="noreferrer"
href={`/project/${ref}/settings/database`}
className="underline transition hover:text-foreground flex items-center gap-x-1"
>
connecting to your database directly
<ExternalLink size={12} />
</Link>
.
</p>
)}
</div>
)}
<div className="flex items-center gap-x-2">
{readReplicaError && (
<Button
className="py-2"
type="default"
onClick={() => {
state.setSelectedDatabaseId(ref)
snapV2.resetResult(id)
}}
>
Switch to primary database
</Button>
)}
{!hasHipaaAddon && (
<Button
icon={<AiIconAnimation className="scale-75 w-3 h-3" loading={isDebugging} />}
disabled={!!isDisabled || isDebugging}
onClick={onDebug}
>
Debug with Supabase AI
</Button>
)}
</div>
)}
<div className="flex items-center gap-x-2">
{readReplicaError && (
<Button
className="py-2"
type="default"
onClick={() => {
state.setSelectedDatabaseId(ref)
snapV2.resetResult(id)
}}
>
Switch to primary database
</Button>
)}
{!hasHipaaAddon && (
<Button
icon={<AiIconAnimation className="scale-75 w-3 h-3" loading={isDebugging} />}
disabled={!!isDisabled || isDebugging}
onClick={onDebug}
>
Debug with Supabase AI
</Button>
)}
</div>
</div>
</div>
)
} else if (!result) {
return (
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark overflow-y-auto">
<p className="m-0 border-0 px-4 py-4 text-sm text-foreground-light">
Click <code>Run</code> to execute your query.
</p>
</div>
)
} else if (result.rows.length <= 0) {
return (
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark overflow-y-auto">
<p className="m-0 border-0 px-6 py-4 font-mono text-sm">Success. No rows returned</p>
</div>
)
)
} else if (!result) {
return (
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark overflow-y-auto">
<p className="m-0 border-0 px-4 py-4 text-sm text-foreground-light">
Click <code>Run</code> to execute your query.
</p>
</div>
)
} else if (result.rows.length <= 0) {
return (
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark overflow-y-auto">
<p className="m-0 border-0 px-6 py-4 font-mono text-sm">Success. No rows returned</p>
</div>
)
}
return <Results rows={result.rows} />
}
)
return <Results rows={result.rows} />
}
UtilityTabResults.displayName = 'UtilityTabResults'
export default UtilityTabResults
@@ -43,7 +43,10 @@ import {
useSidebar,
} from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { useIsAPIDocsSidePanelEnabled } from './App/FeaturePreview/FeaturePreviewContext'
import {
useIsAPIDocsSidePanelEnabled,
useIsSQLEditorTabsEnabled,
} from './App/FeaturePreview/FeaturePreviewContext'
import { ThemeDropdown } from './ThemeDropdown'
import { UserDropdown } from './UserDropdown'
@@ -264,7 +267,9 @@ function ProjectLinks() {
const snap = useAppStateSnapshot()
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const { securityLints, errorLints } = useLints()
const showWarehouse = useFlag('warehouse')
const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
const activeRoute = router.pathname.split('/')[3]
@@ -280,7 +285,9 @@ function ProjectLinks() {
'realtime:all',
])
const toolRoutes = generateToolRoutes(ref, project)
const toolRoutes = generateToolRoutes(ref, project, {
sqlEditorTabs: isSqlEditorTabsEnabled,
})
const productRoutes = generateProductRoutes(ref, project, {
auth: authEnabled,
edgeFunctions: edgeFunctionsEnabled,
@@ -1,5 +1,6 @@
import { LastSignInType, useLastSignIn } from 'hooks/misc/useLastSignIn'
import { ReactNode } from 'react'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export function LastSignInWrapper({
@@ -1,4 +1,5 @@
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { Button, Skeleton } from 'ui'
export const TableGridInnerLoadingState = () => {
return (
@@ -11,9 +12,8 @@ export const TableGridInnerLoadingState = () => {
export const TableGridSkeletonLoader = () => {
return (
<div className="flex flex-col">
<div className="h-10 bg-dash-sidebar" />
<div className="h-10 bg-dash-sidebar dark:bg-surface-100" />
<div className="h-9 border-y" />
<TableGridInnerLoadingState />
</div>
)
@@ -4,6 +4,8 @@ import { isEmpty, isUndefined, noop } from 'lodash'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabasePublicationCreateMutation } from 'data/database-publications/database-publications-create-mutation'
import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query'
@@ -11,6 +13,7 @@ import { useDatabasePublicationUpdateMutation } from 'data/database-publications
import type { Constraint } from 'data/database/constraints-query'
import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
import { databaseKeys } from 'data/database/keys'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { entityTypeKeys } from 'data/entity-types/keys'
import { tableEditorKeys } from 'data/table-editor/keys'
import { isTableLike } from 'data/table-editor/table-editor-types'
@@ -22,6 +25,7 @@ import { getTables } from 'data/tables/tables-query'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useGetImpersonatedRole } from 'state/role-impersonation-state'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { createTabId, renameTab } from 'state/tabs'
import type { Dictionary } from 'types'
import { SonnerProgress } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -63,7 +67,9 @@ const SidePanelEditor = ({
includeColumns = false,
onTableCreated = noop,
}: SidePanelEditorProps) => {
const { ref } = useParams()
const snap = useTableEditorStateSnapshot()
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const queryClient = useQueryClient()
@@ -478,6 +484,11 @@ const SidePanelEditor = ({
if (hasError) {
toast(`Table ${table.name} has been updated, but there were some errors`, { id: toastId })
} else {
if (isTableEditorTabsEnabled && ref && payload.name) {
// [Joshen] Only table entities can be updated via the dashboard
const tabId = createTabId(ENTITY_TYPE.TABLE, { id: selectedTable.id })
renameTab(ref, tabId, payload.name)
}
toast.success(`Successfully updated ${table.name}!`, { id: toastId })
}
}
@@ -13,9 +13,11 @@ import {
isView,
} from 'data/table-editor/table-editor-types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useUrlState } from 'hooks/ui/useUrlState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import { makeActiveTabPermanent } from 'state/tabs'
import { TableGridSkeletonLoader } from './LoadingState'
import NotFoundState from './NotFoundState'
import SidePanelEditor from './SidePanelEditor/SidePanelEditor'
@@ -31,6 +33,7 @@ const TableGridEditor = ({
selectedTable,
}: TableGridEditorProps) => {
const router = useRouter()
const project = useSelectedProject()
const { ref: projectRef, id } = useParams()
useLoadTableEditorStateFromLocalStorageIntoUrl({
@@ -74,37 +77,40 @@ const TableGridEditor = ({
*/
return (
<TableEditorTableStateContextProvider
key={`table-editor-table-${selectedTable.id}`}
projectRef={projectRef}
table={selectedTable}
editable={editable}
>
<SupabaseGrid
key={gridKey}
gridProps={{ height: '100%' }}
customHeader={
(isViewSelected || isTableSelected) && selectedView === 'definition' ? (
<div className="flex items-center space-x-2">
<p>
SQL Definition of <code className="text-sm">{selectedTable.name}</code>{' '}
</p>
<p className="text-foreground-light text-sm">(Read only)</p>
</div>
) : null
}
>
{(isViewSelected || isTableSelected) && selectedView === 'definition' && (
<TableDefinition entity={selectedTable} />
)}
</SupabaseGrid>
<SidePanelEditor
// When any click happens in a table tab, the tab becomes permanent
<div className="h-full" onClick={() => makeActiveTabPermanent(project?.ref)}>
<TableEditorTableStateContextProvider
key={`table-editor-table-${selectedTable.id}`}
projectRef={projectRef}
table={selectedTable}
editable={editable}
selectedTable={isTableLike(selectedTable) ? selectedTable : undefined}
onTableCreated={onTableCreated}
/>
</TableEditorTableStateContextProvider>
>
<SupabaseGrid
key={gridKey}
gridProps={{ height: '100%' }}
customHeader={
(isViewSelected || isTableSelected) && selectedView === 'definition' ? (
<div className="flex items-center space-x-2">
<p>
SQL Definition of <code className="text-sm">{selectedTable.name}</code>{' '}
</p>
<p className="text-foreground-light text-sm">(Read only)</p>
</div>
) : null
}
>
{(isViewSelected || isTableSelected) && selectedView === 'definition' && (
<TableDefinition entity={selectedTable} />
)}
</SupabaseGrid>
<SidePanelEditor
editable={editable}
selectedTable={isTableLike(selectedTable) ? selectedTable : undefined}
onTableCreated={onTableCreated}
/>
</TableEditorTableStateContextProvider>
</div>
)
}
@@ -1,5 +1,6 @@
import { Blocks, FileText, Lightbulb, List, Settings } from 'lucide-react'
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
import type { Route } from 'components/ui/ui.types'
import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
import type { Project } from 'data/projects/project-detail-query'
@@ -14,9 +15,13 @@ import {
TableEditor,
} from 'icons'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
export const generateToolRoutes = (ref?: string, project?: Project): Route[] => {
export const generateToolRoutes = (
ref?: string,
project?: Project,
features?: { sqlEditorTabs: boolean }
): Route[] => {
const { sqlEditorTabs } = features ?? {}
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
const buildingUrl = `/project/${ref}`
@@ -34,7 +39,8 @@ export const generateToolRoutes = (ref?: string, project?: Project): Route[] =>
icon: <SqlEditor size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: !IS_PLATFORM
? `/project/${ref}/sql/1`
: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/sql/new`),
: ref &&
(isProjectBuilding ? buildingUrl : `/project/${ref}/sql${!!sqlEditorTabs ? '' : '/new'}`),
},
]
}
@@ -1,4 +1,8 @@
import { useParams } from 'common'
import {
useIsSQLEditorTabsEnabled,
useIsTableEditorTabsEnabled,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
import { AIAssistantPanel } from 'components/ui/AIAssistantPanel/AIAssistantPanel'
import AISettingsModal from 'components/ui/AISettingsModal'
@@ -18,6 +22,7 @@ import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
import EnableBranchingModal from '../AppLayout/EnableBranchingButton/EnableBranchingModal'
import { useEditorType } from '../editors/EditorsLayout.hooks'
import BuildingState from './BuildingState'
import ConnectingState from './ConnectingState'
import LoadingState from './LoadingState'
@@ -90,9 +95,20 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
setAiAssistantPanel,
mobileMenuOpen,
setMobileMenuOpen,
showSidebar,
} = useAppStateSnapshot()
const { open } = aiAssistantPanel
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
// For tabs preview flag logic - only conditionally collapse sidebar for table editor and sql editor if feature flags are on
const editor = useEditorType()
const tableEditorTabsEnabled = editor === 'table' && isTableEditorTabsEnabled
const sqlEditorTabsEnabled = editor === 'sql' && isSQLEditorTabsEnabled
const forceShowProductMenu = !tableEditorTabsEnabled && !sqlEditorTabsEnabled
const sideBarIsOpen = forceShowProductMenu || showSidebar
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
@@ -124,8 +140,6 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
const sideBarIsOpen = true // @mildtomato - var for later to use collapsible sidebar
return (
<>
<Head>
@@ -1,28 +1,17 @@
import { OngoingQueriesPanel } from 'components/interfaces/SQLEditor/OngoingQueriesPanel'
import { withAuth } from 'hooks/misc/withAuth'
import { ReactNode, useMemo } from 'react'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { SQLEditorMenu } from './SQLEditorMenu'
import { ReactNode } from 'react'
export interface SQLEditorLayoutProps {
title: string
children: ReactNode
}
const SQLEditorLayout = ({ title, children }: SQLEditorLayoutProps) => {
const productMenu = useMemo(() => <SQLEditorMenu key="sql-editor-menu" />, [])
const SQLEditorLayout = ({ children }: SQLEditorLayoutProps) => {
return (
<ProjectLayout
title={title || 'SQL'}
product="SQL Editor"
productMenu={productMenu}
isBlocking={false}
resizableSidebar
>
<>
{children}
<OngoingQueriesPanel />
</ProjectLayout>
</>
)
}
@@ -1,9 +1,5 @@
import { Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import DownloadSnippetModal from 'components/interfaces/SQLEditor/DownloadSnippetModal'
import { MoveQueryModal } from 'components/interfaces/SQLEditor/MoveQueryModal'
import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
@@ -11,6 +7,7 @@ import { untitledSnippetTitle } from 'components/interfaces/SQLEditor/SQLEditor.
import { createSqlSnippetSkeletonV2 } from 'components/interfaces/SQLEditor/SQLEditor.utils'
import { EmptyPrivateQueriesPanel } from 'components/layouts/SQLEditorLayout/PrivateSqlSnippetEmpty'
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import { sqlEditorTabsCleanup } from 'components/layouts/Tabs/Tabs.utils'
import { useContentCountQuery } from 'data/content/content-count-query'
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
import { getContentById } from 'data/content/content-id-query'
@@ -23,11 +20,16 @@ import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import uuidv4 from 'lib/uuid'
import { Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
SnippetWithContent,
useSnippetFolders,
useSqlEditorV2StateSnapshot,
} from 'state/sql-editor-v2'
import { createTabId, getTabsStore, makeTabPermanent, removeTabs } from 'state/tabs'
import { SqlSnippets } from 'types'
import { Separator, TreeView } from 'ui'
import {
@@ -38,6 +40,7 @@ import {
InnerSideMenuSeparator,
} from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useSnapshot } from 'valtio'
import SQLEditorLoadingSnippets from './SQLEditorLoadingSnippets'
import { formatFolderResponseForTreeView, getLastItemIds, ROOT_NODE } from './SQLEditorNav.utils'
import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem'
@@ -51,11 +54,16 @@ const DEFAULT_SECTION_STATE: SectionState = { shared: false, favorite: false, pr
export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
const router = useRouter()
const { ref: projectRef, id } = useParams()
const { profile } = useProfile()
const project = useSelectedProject()
const { ref: projectRef, id } = useParams()
const snapV2 = useSqlEditorV2StateSnapshot()
// const tabStore = getTabsStore(projectRef)
const tabStore = getTabsStore(projectRef)
const tabs = useSnapshot(tabStore)
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
const [sectionVisibility, setSectionVisibility] = useLocalStorage<SectionState>(
LOCAL_STORAGE_KEYS.SQL_EDITOR_SECTION_STATE(projectRef ?? ''),
DEFAULT_SECTION_STATE
@@ -268,6 +276,11 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
[projectSnippetsTreeState]
)
const allSnippetsInView = [
...(privateSnippetsPages?.pages.flatMap((x) => x.contents) ?? []),
...(sharedSqlSnippetsData?.pages.flatMap((x) => x.contents) ?? []),
]
// ==========================
// Snippet mutations from RQ
// ==========================
@@ -279,6 +292,14 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
})
const { mutate: deleteContent, isLoading: isDeleting } = useContentDeleteMutation({
onSuccess: (data) => {
if (isSQLEditorTabsEnabled) {
// Update Tabs state - currently unknown how to differentiate between sql and non-sql content
// so we're just deleting all tabs for with matching IDs
const tabIds = data.map((id) => createTabId('sql', { id }))
removeTabs(projectRef, tabIds)
}
},
onError: (error, data) => {
if (error.message.includes('Contents not found')) {
postDeleteCleanup(data.ids)
@@ -491,12 +512,8 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
if (projectRef && privateSnippetsPages) {
privateSnippetsPages.pages.forEach((page) => {
page.contents?.forEach((snippet: Snippet) => {
snapV2.addSnippet({
projectRef,
snippet,
})
snapV2.addSnippet({ projectRef, snippet })
})
page.folders?.forEach((folder: SnippetFolder) => snapV2.addFolder({ projectRef, folder }))
})
}
@@ -507,26 +524,27 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
favoriteSqlSnippetsData.pages.forEach((page) => {
page.contents?.forEach((snippet) => {
snapV2.addSnippet({
projectRef,
snippet,
})
snapV2.addSnippet({ projectRef, snippet })
})
})
}, [projectRef, privateSnippetsPages?.pages])
}, [projectRef, favoriteSqlSnippetsData?.pages])
useEffect(() => {
if (projectRef === undefined || !isSharedSqlSnippetsSuccess) return
sharedSqlSnippetsData.pages.forEach((page) => {
page.contents?.forEach((snippet) => {
snapV2.addSnippet({
projectRef,
snippet,
})
snapV2.addSnippet({ projectRef, snippet })
})
})
}, [projectRef, privateSnippetsPages?.pages])
}, [projectRef, sharedSqlSnippetsData?.pages])
useEffect(() => {
if (projectRef && isSuccess && isSharedSqlSnippetsSuccess && isSQLEditorTabsEnabled) {
sqlEditorTabsCleanup({ ref: projectRef, snippets: allSnippetsInView as any })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess, isSharedSqlSnippetsSuccess, allSnippetsInView])
return (
<>
@@ -541,7 +559,9 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
title={`Shared ${numProjectSnippets > 0 ? ` (${numProjectSnippets})` : ''}`}
/>
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
{numProjectSnippets === 0 ? (
{isLoadingSharedSqlSnippets ? (
<SQLEditorLoadingSnippets />
) : numProjectSnippets === 0 ? (
<InnerSideBarEmptyPanel
className="mx-2"
title="No shared queries"
@@ -552,11 +572,27 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
data={projectSnippetsTreeState}
aria-label="project-level-snippets"
nodeRenderer={({ element, ...props }) => {
const isActive = element.metadata?.id === id
const isOpened =
isSQLEditorTabsEnabled &&
Object.values(tabs.tabsMap).some(
(tab) => tab.metadata?.sqlId === element.metadata?.id
)
const tabId = createTabId('sql', {
id: element?.metadata?.id as unknown as Snippet['id'],
})
const isPreview = isSQLEditorTabsEnabled && tabStore.previewTabId === tabId
const isActive = !isPreview && element.metadata?.id === id
return (
<SQLEditorTreeViewItem
{...props}
isOpened={isOpened && !isPreview}
isSelected={isActive}
isPreview={isPreview}
onDoubleClick={(e) => {
e.preventDefault()
makeTabPermanent(projectRef, tabId)
}}
element={element}
onSelectDelete={() => {
setShowDeleteModal(true)
@@ -619,11 +655,26 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
data={favoritesTreeState}
aria-label="favorite-snippets"
nodeRenderer={({ element, ...props }) => {
const isActive = element.metadata?.id === id
const isOpened =
isSQLEditorTabsEnabled &&
Object.values(tabs.tabsMap).some(
(tab) => tab.metadata?.sqlId === element.metadata?.id
)
const tabId = createTabId('sql', {
id: element?.metadata?.id as unknown as Snippet['id'],
})
const isPreview = isSQLEditorTabsEnabled && tabStore.previewTabId === tabId
const isActive = !isPreview && element.metadata?.id === id
return (
<SQLEditorTreeViewItem
{...props}
isSelected={isActive}
isOpened={isOpened && !isPreview}
isPreview={isPreview}
onDoubleClick={(e) => {
e.preventDefault()
makeTabPermanent(projectRef, tabId)
}}
element={element}
onSelectDelete={() => {
setShowDeleteModal(true)
@@ -692,13 +743,24 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
}}
expandedIds={expandedFolderIds}
nodeRenderer={({ element, ...props }) => {
const isActive = element.metadata?.id === id
const isOpened =
isSQLEditorTabsEnabled &&
Object.values(tabs.tabsMap).some(
(tab) => tab.metadata?.sqlId === element.metadata?.id
)
const tabId = createTabId('sql', {
id: element?.metadata?.id as unknown as Snippet['id'],
})
const isPreview = isSQLEditorTabsEnabled && tabStore.previewTabId === tabId
const isActive = !isPreview && element.metadata?.id === id
return (
<SQLEditorTreeViewItem
{...props}
element={element}
isOpened={isOpened && !isPreview}
isSelected={isActive}
isPreview={isPreview}
isMultiSelected={selectedSnippets.length > 1}
isLastItem={privateSnippetsLastItemIds.has(element.id as string)}
status={props.isBranch ? snapV2.folders[element.id].status : 'idle'}
@@ -764,6 +826,10 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
[element.id as string]: { snippets, isLoading },
}))
}}
onDoubleClick={(e) => {
e.preventDefault()
makeTabPermanent(projectRef, tabId)
}}
/>
)
}}
@@ -1,8 +1,3 @@
import { Copy, Download, Edit, ExternalLink, Lock, Move, Plus, Share, Trash } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, useEffect } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { IS_PLATFORM } from 'common'
import { useParams } from 'common/hooks/useParams'
@@ -11,6 +6,10 @@ import { Snippet } from 'data/content/sql-folders-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import useLatest from 'hooks/misc/useLatest'
import { useProfile } from 'lib/profile'
import { Copy, Download, Edit, ExternalLink, Lock, Move, Plus, Share, Trash } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, useEffect } from 'react'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
Button,
@@ -76,6 +75,7 @@ export const SQLEditorTreeViewItem = ({
sort,
name,
onFolderContentsChange,
...props
}: SQLEditorTreeViewItemProps) => {
const router = useRouter()
const { id, ref: projectRef } = useParams()
@@ -164,6 +164,7 @@ export const SQLEditorTreeViewItem = ({
isExpanded={isExpanded}
isBranch={isBranch}
isSelected={isSelected}
isPreview={props.isPreview}
isEditing={isEditing}
isLoading={(isEnabled && isLoading) || isSaving}
onEditSubmit={(value) => {
@@ -184,11 +185,11 @@ export const SQLEditorTreeViewItem = ({
if (isEditing) {
return
}
// When the item is a folder, we want to expand/close it
onClick(e)
}
}}
{...props}
name={element.name}
xPadding={16}
/>
@@ -3,6 +3,7 @@ import { Clipboard, Copy, Download, Edit, Lock, MoreHorizontal, Trash, Unlock }
import Link from 'next/link'
import Papa from 'papaparse'
import { toast } from 'sonner'
import { useSnapshot } from 'valtio'
import { IS_PLATFORM } from 'common'
import {
@@ -10,6 +11,7 @@ import {
MAX_EXPORT_ROW_COUNT_MESSAGE,
} from 'components/grid/components/header/Header'
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
formatTableRowsToSQL,
getEntityLintDetails,
@@ -26,6 +28,7 @@ import { fetchAllTableRows } from 'data/table-rows/table-rows-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { copyToClipboard } from 'lib/helpers'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { createTabId, getTabsStore, makeTabPermanent } from 'state/tabs'
import {
cn,
DropdownMenu,
@@ -42,7 +45,6 @@ import {
TreeViewItemVariant,
} from 'ui'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
export interface EntityListItemProps {
id: number | string
projectRef: string
@@ -61,6 +63,14 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
const snap = useTableEditorStateSnapshot()
const { selectedSchema } = useQuerySchemaState()
// For tabs preview flag logic
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const tabId = createTabId(entity.type, { id: entity.id })
const tabStore = getTabsStore(projectRef)
const isPreview = isTableEditorTabsEnabled ? tabStore.previewTabId === tabId : false
const tabs = useSnapshot(tabStore)
const isOpened = Object.values(tabs.tabsMap).some((tab) => tab.metadata?.tableId === entity.id)
const isActive = Number(id) === entity.id
const canEdit = isActive && !isLocked
@@ -214,10 +224,17 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
aria-label={`View ${entity.name}`}
className={cn(
TreeViewItemVariant({
isSelected: isActive,
isSelected: isActive && !isPreview,
isOpened: isOpened && !isPreview,
isPreview,
}),
'px-4'
)}
onDoubleClick={(e) => {
e.preventDefault()
const tabId = createTabId(entity.type, { id: entity.id })
makeTabPermanent(projectRef, tabId)
}}
>
<>
{isActive && <div className="absolute left-0 h-full w-0.5 bg-foreground" />}
@@ -1,17 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { PropsWithChildren, useMemo } from 'react'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { PropsWithChildren } from 'react'
import { ProjectLayoutWithAuth } from '../ProjectLayout/ProjectLayout'
import TableEditorMenu from './TableEditorMenu'
const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
const isPermissionsLoaded = usePermissionsLoaded()
const canReadTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
const tableEditorMenu = useMemo(() => <TableEditorMenu />, [])
if (isPermissionsLoaded && !canReadTables) {
return (
<ProjectLayoutWithAuth isBlocking={false}>
@@ -20,16 +16,7 @@ const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
)
}
return (
<ProjectLayoutWithAuth
product="Table Editor"
productMenu={tableEditorMenu}
isBlocking={false}
resizableSidebar
>
{children}
</ProjectLayoutWithAuth>
)
return <>{children}</>
}
export default TableEditorLayout
@@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning'
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import AlertError from 'components/ui/AlertError'
@@ -40,16 +41,18 @@ import {
InnerSideBarFilters,
} from 'ui-patterns/InnerSideMenu'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import { tableEditorTabsCleanUp } from '../Tabs/Tabs.utils'
import EntityListItem from './EntityListItem'
import { TableMenuEmptyState } from './TableMenuEmptyState'
const TableEditorMenu = () => {
const { id: _id } = useParams()
const { ref, id: _id } = useParams()
const router = useRouter()
const id = _id ? Number(_id) : undefined
const snap = useTableEditorStateSnapshot()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const isMobile = useBreakpoint()
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const [showModal, setShowModal] = useState(false)
const [searchText, setSearchText] = useState<string>('')
@@ -114,6 +117,13 @@ const TableEditorMenu = () => {
}
}, [selectedTable?.schema])
useEffect(() => {
// Clean up tabs + recent items for any tables that might have been removed outside of the dashboard session
if (isTableEditorTabsEnabled && ref && entityTypes) {
tableEditorTabsCleanUp({ ref, schemas: [selectedSchema], entities: entityTypes })
}
}, [entityTypes])
return (
<>
<div className="flex flex-col flex-grow gap-5 pt-5 h-full">
+324
View File
@@ -0,0 +1,324 @@
import {
DndContext,
DragEndEvent,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import { horizontalListSortingStrategy, SortableContext, useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { AnimatePresence, motion } from 'framer-motion'
import { Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { useSnapshot } from 'valtio'
import { useParams } from 'common'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import { useAppStateSnapshot } from 'state/app-state'
import {
editorEntityTypes,
getTabsStore,
handleTabClose,
handleTabDragEnd,
handleTabNavigation,
makeTabPermanent,
type Tab,
} from 'state/tabs'
import { cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
import { useEditorType } from './editors/EditorsLayout.hooks'
import { CollapseButton } from './Tabs/CollapseButton'
/**
* Individual draggable tab component that handles:
* - Drag
* - Drop functionality
* - Dynamic schema name display
* - Tab label animations
* - Close button interactions
*/
const SortableTab = ({
tab,
index,
openTabs,
onClose,
}: {
tab: Tab
index: number
openTabs: Tab[]
onClose: (id: string) => void
}) => {
const router = useRouter()
const { ref } = useParams()
const currentSchema = (router.query.schema as string) || 'public'
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: tab.id,
})
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 1 : 0,
}
// Update schema visibility check to include URL param comparison
const shouldShowSchema = useMemo(() => {
// For both table and schema tabs, show schema if:
// Any tab has a different schema than the current schema parameter
if (tab.type === 'r') {
const anyTabHasDifferentSchema = openTabs
.filter((t) => t.type === 'r')
.some((t) => t.metadata?.schema !== currentSchema)
return anyTabHasDifferentSchema
}
return false
}, [openTabs, currentSchema, tab.type])
// Create a motion version of TabsTrigger while preserving all functionality
// const MotionTabsTrigger = motion(TabsTrigger_Shadcn_)
return (
<motion.div
ref={setNodeRef}
style={style}
{...attributes}
layoutId={tab.id}
transition={{ duration: 0.045 }}
animate={{ opacity: isDragging ? 0 : 1 }}
className={cn('flex items-center h-10 first-of-type:border-l')}
>
<TabsTrigger_Shadcn_
value={tab.id}
onDoubleClick={() => makeTabPermanent(ref, tab.id)}
className={cn(
'flex items-center gap-2 px-3 text-xs',
'bg-dash-sidebar/50 dark:bg-surface-100/50',
'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
'border-b border-default',
// bottom border active rule
'data-[state=active]:border-b-background-dash-sidebar dark:data-[state=active]:border-b-background-surface-100',
'relative group h-full',
'hover:bg-surface-300 dark:hover:bg-surface-100',
tab.isPreview && 'italic font-light' // Optional: style preview tabs differently
)}
{...listeners}
>
<EntityTypeIcon type={tab.type} />
<div className="flex items-center gap-0">
<AnimatePresence mode="popLayout" initial>
{shouldShowSchema && (
<motion.span
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: 'auto' }}
exit={{ opacity: 0, width: 0 }}
transition={{ duration: 0.15 }}
className="text-foreground-muted group-data-[state=active]:text-foreground-lighter"
>
{tab?.metadata?.schema}.
</motion.span>
)}
</AnimatePresence>
<span>{tab.label || 'Untitled'}</span>
</div>
<span
role="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
className="ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-sm cursor-pointer"
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
onClose(tab.id)
}}
>
<X size={12} className="text-foreground-light" />
</span>
<div className="absolute w-full top-0 left-0 right-0 h-px bg-foreground opacity-0 group-data-[state=active]:opacity-100" />
</TabsTrigger_Shadcn_>
{index < openTabs.length && (
<div role="separator" className="h-full w-px bg-border" key={`separator-${tab.id}`} />
)}
</motion.div>
)
}
const TabPreview = ({ tab }: { tab: string }) => {
const { ref } = useParams()
const store = getTabsStore(ref)
const tabs = useSnapshot(store)
const tabData = tabs.tabsMap[tab]
if (!tabData) return null
return (
<motion.div
layoutId={tab}
transition={{ duration: 0.045 }}
animate={{ opacity: 0.7 }}
className="flex relative items-center gap-2 px-3 text-xs bg-dash-sidebar dark:bg-surface-100 shadow-lg rounded-sm h-10"
>
<EntityTypeIcon type={tabData.type} />
<span>{tabData.label || 'Untitled'}</span>
<div className="absolute w-full top-0 left-0 right-0 h-px bg-foreground-muted" />
</motion.div>
)
}
export function Tabs() {
const { ref } = useParams()
const router = useRouter()
const appSnap = useAppStateSnapshot()
const editor = useEditorType()
const store = getTabsStore(ref)
const tabs = useSnapshot(store)
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 1, // Start with a very small distance
},
})
)
const openTabs = tabs.openTabs
.map((id) => tabs.tabsMap[id])
.filter((tab) => tab !== undefined) as Tab[]
const hasNewTab = router.asPath.includes('/new')
// Filter by editor type - only show SQL tabs for SQL editor and table tabs for table editor
const editorTabs = !!editor
? openTabs.filter((tab) => editorEntityTypes[editor]?.includes(tab.type))
: []
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
const oldIndex = tabs.openTabs.indexOf(active.id.toString())
const newIndex = tabs.openTabs.indexOf(over.id.toString())
if (oldIndex !== newIndex) {
handleTabDragEnd(ref, oldIndex, newIndex, active.id.toString(), router)
}
}
const handleClose = (id: string) => {
const onClearDashboardHistory = () => {
if (ref && editor) {
appSnap.setDashboardHistory(ref, editor === 'table' ? 'editor' : editor, undefined)
}
}
handleTabClose({ ref, id, router, editor, onClearDashboardHistory })
}
const handleTabChange = (id: string) => {
handleTabNavigation(ref, id, router)
}
return (
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<Tabs_Shadcn_
className="w-full flex"
value={hasNewTab ? 'new' : tabs.activeTab ?? undefined}
onValueChange={handleTabChange}
>
<CollapseButton hideTabs={false} />
<TabsList_Shadcn_ className="bg-surface-200 dark:bg-alternative rounded-b-none gap-0 h-10 flex items-center w-full z-[1] border-none overflow-clip overflow-x-auto ">
<SortableContext
items={editorTabs.map((tab) => tab.id)}
strategy={horizontalListSortingStrategy}
>
{editorTabs.map((tab, index) => (
<SortableTab
key={tab.id}
tab={tab}
index={index}
openTabs={openTabs}
onClose={() => handleClose(tab.id)}
/>
))}
</SortableContext>
{/* Non-draggable new tab */}
{hasNewTab && (
<>
<TabsTrigger_Shadcn_
value="new"
className={cn(
'flex items-center gap-2 px-3 text-xs',
'bg-dash-sidebar/50 dark:bg-surface-100/50',
'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
'relative group h-full border-t-2 !border-b-0',
'hover:bg-surface-300 dark:hover:bg-surface-100'
)}
>
<Plus size={16} strokeWidth={1.5} className={'text-foreground-lighter'} />
<div className="flex items-center gap-0">
<span>New</span>
</div>
<span
role="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
className="ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-sm cursor-pointer"
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
handleClose('new')
}}
>
<X size={12} className="text-foreground-light" />
</span>{' '}
<div className="absolute w-full -bottom-[1px] left-0 right-0 h-px bg-dash-sidebar dark:bg-surface-100 opacity-0 group-data-[state=active]:opacity-100" />
</TabsTrigger_Shadcn_>
</>
)}
<AnimatePresence initial={false}>
{!hasNewTab && (
<motion.button
className="flex items-center justify-center w-10 h-10 hover:bg-surface-100 shrink-0 border-b"
onClick={() =>
router.push(
`/project/${router.query.ref}/${editor === 'table' ? 'editor' : 'sql'}/new?skip=true`
)
}
initial={{ opacity: 0, scale: 0.8, x: -10 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
transition={{ duration: 0.2 }}
>
<Plus
size={16}
strokeWidth={1.5}
className="text-foreground-lighter hover:text-foreground-light"
/>
</motion.button>
)}
</AnimatePresence>
<div className="grow h-full border-b pr-6" />
</TabsList_Shadcn_>
</Tabs_Shadcn_>
<DragOverlay dropAnimation={null}>
{tabs.activeTab ? <TabPreview tab={tabs.activeTab} /> : null}
</DragOverlay>
</DndContext>
)
}
@@ -0,0 +1,34 @@
import { Badge, Card } from 'ui'
export const ActionCard = (card: {
icon: JSX.Element
title: string
description: string
bgColor: string
isBeta?: boolean
onClick?: () => void
}) => {
return (
<Card
className="grow bg-surface-100 p-3 transition-colors hover:bg-surface-200 border border-light hover:border-default cursor-pointer"
onClick={card.onClick}
>
<div className={`relative flex items-start gap-3`}>
{card.isBeta && (
<Badge className="absolute -right-5 -top-5 bg-surface-300 bg-opacity-100 text-xs text-foreground">
Coming soon
</Badge>
)}
<div
className={`rounded-full ${card.bgColor} w-8 h-8 flex items-center justify-center flex-shrink-0`}
>
{card.icon}
</div>
<div className="flex flex-col gap-0">
<h3 className="text-sm text-foreground mb-0">{card.title}</h3>
<p className="text-xs text-foreground-light">{card.description}</p>
</div>
</div>
</Card>
)
}
@@ -0,0 +1,37 @@
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
import { useAppStateSnapshot } from 'state/app-state'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export function CollapseButton({ hideTabs }: { hideTabs: boolean }) {
const { showSidebar, setShowSidebar } = useAppStateSnapshot()
return (
<Tooltip>
<TooltipTrigger asChild>
<button
className={cn(
'flex items-center justify-center w-10 h-10 hover:bg-surface-100 shrink-0',
!hideTabs && 'border-b border-b-default'
)}
onClick={() => setShowSidebar(!showSidebar)}
>
{showSidebar ? (
<PanelLeftClose
size={16}
strokeWidth={1.5}
className="text-foreground-lighter hover:text-foreground-light"
/>
) : (
<PanelLeftOpen
size={16}
strokeWidth={1.5}
className="text-foreground-lighter hover:text-foreground-light"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">{true ? 'Collapse' : 'Expand'} sidebar</TooltipContent>
</Tooltip>
)
}
@@ -0,0 +1,46 @@
import { motion } from 'framer-motion'
import { BaggageClaim, BookOpen } from 'lucide-react'
import { Button } from 'ui'
// not in use yet
export const MigrateToPostgres = () => (
<motion.div
className="w-full rounded-lg overflow-hidden relative border border-muted"
variants={{
hidden: { opacity: 0, scale: 0.96, y: 15 },
show: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 600,
damping: 50,
staggerChildren: 0.08,
delayChildren: 0.03,
},
},
}}
>
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
<div className="relative z-10 px-5 py-4 h-full flex gap-5 items-center justify-between">
<div className="flex flex-col gap-3">
<div className="flex gap-4">
<div className="inline-flex items-center justify-center w-7 h-7 rounded-lg bg-surface-300 border">
<BaggageClaim size={14} className="text-foreground-light" strokeWidth={1.5} />
</div>
<div className="flex flex-col gap-1">
<h2 className="text-base text-foreground">Migrate your Database to Postgres</h2>
<p className="text-xs text-foreground-light max-w-md">
Already have a database? Migrate any database to Supabase to access backups, RESTful
auto APIs, Authentication and more.
</p>
</div>
</div>
</div>
<Button type="default" className="rounded-full" iconRight={<BookOpen />}>
Migrate to Postgres
</Button>
</div>
</motion.div>
)
@@ -0,0 +1,188 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { Table2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
import { useParams } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { createSqlSnippetSkeletonV2 } from 'components/interfaces/SQLEditor/SQLEditor.utils'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { getTabsStore } from 'state/tabs'
import {
Button,
cn,
SQL_ICON,
Tabs_Shadcn_,
TabsContent_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
} from 'ui'
import { useEditorType } from '../editors/EditorsLayout.hooks'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import { ActionCard } from './ActionCard'
import { RecentItems } from './RecentItems'
export function NewTab() {
const router = useRouter()
const { ref } = useParams()
const editor = useEditorType()
const snap = useTableEditorStateSnapshot()
const { profile } = useProfile()
const [templates] = partition(SQL_TEMPLATES, { type: 'template' })
const [quickstarts] = partition(SQL_TEMPLATES, { type: 'quickstart' })
const { mutate: sendEvent } = useSendEventMutation()
const snapV2 = useSqlEditorV2StateSnapshot()
const { project } = useProjectContext()
const org = useSelectedOrganization()
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'sql', owner_id: profile?.id },
subject: { id: profile?.id },
})
const tableEditorActions = [
{
icon: <Table2 className="h-4 w-4 text-foreground" strokeWidth={1.5} />,
title: 'Create a table',
description: 'Design and create a new database table',
bgColor: 'bg-blue-500',
isBeta: false,
onClick: snap.onAddTable,
},
]
const sqlEditorActions = [
{
icon: <SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />,
title: 'New SQL Snippet',
description: 'Execute SQL queries',
bgColor: 'bg-green-500',
isBeta: false,
onClick: () => router.push(`/project/${ref}/sql/new`),
},
]
const actions = editor === 'sql' ? sqlEditorActions : tableEditorActions
const handleNewQuery = async (sql: string, name: string) => {
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')
if (!canCreateSQLSnippet) {
return toast('Your queries will not be saved as you do not have sufficient permissions')
}
try {
const snippet = createSqlSnippetSkeletonV2({
id: uuidv4(),
name,
sql,
owner_id: profile?.id,
project_id: project?.id,
})
snapV2.addSnippet({ projectRef: ref, snippet })
snapV2.addNeedsSaving(snippet.id)
const store = getTabsStore(ref)
const tabId = `sql-${snippet.id}`
store.openTabs = [...store.openTabs, tabId]
store.tabsMap[tabId] = {
id: tabId,
type: 'sql',
label: name,
metadata: { sqlId: snippet.id },
}
store.activeTab = tabId
router.push(`/project/${ref}/sql/${snippet.id}`)
} catch (error: any) {
toast.error(`Failed to create new query: ${error.message}`)
}
}
return (
<div className="bg-surface-100 h-full overflow-y-auto py-12">
<div className="mx-auto max-w-2xl flex flex-col gap-10 px-10">
<div className="grid grid-cols-2 gap-4">
{actions.map((item, i) => (
<ActionCard key={`action-card-${i}`} {...item} />
))}
</div>
<RecentItems />
</div>
{editor === 'sql' && (
<div className="flex flex-col gap-4 mx-auto py-10">
<Tabs_Shadcn_ defaultValue="templates">
<TabsList_Shadcn_ className="mx-auto justify-center gap-5">
<TabsTrigger_Shadcn_ value="templates">Templates</TabsTrigger_Shadcn_>
<TabsTrigger_Shadcn_ value="quickstarts">Quickstarts</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
<TabsContent_Shadcn_ value="templates" className="max-w-5xl mx-auto py-5">
<div className="grid grid-cols-3 gap-4 px-8">
{templates.slice(0, 9).map((item, i) => (
<ActionCard
onClick={() => {
handleNewQuery(item.sql, item.title)
sendEvent({
action: 'sql_editor_template_clicked',
properties: { templateName: item.title },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}}
bgColor="bg-alternative border"
key={`action-card-${i}`}
{...item}
icon={
<SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />
}
/>
))}
</div>
<div className="flex justify-center mt-5">
<Button asChild type="default">
<Link href={`/project/${ref}/sql/templates`}>View more templates</Link>
</Button>
</div>
</TabsContent_Shadcn_>
<TabsContent_Shadcn_ value="quickstarts" className="max-w-5xl mx-auto py-5">
<div className="grid grid-cols-3 gap-4 px-8">
{quickstarts.map((item, i) => (
<ActionCard
onClick={() => {
handleNewQuery(item.sql, item.title)
sendEvent({
action: 'sql_editor_quickstart_clicked',
properties: { quickstartName: item.title },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}}
bgColor="bg-alternative border"
key={`action-card-${i}`}
{...item}
icon={
<SQL_ICON className={cn('fill-foreground', 'w-4 h-4')} strokeWidth={1.5} />
}
/>
))}
</div>
<div className="flex justify-center mt-5">
<Button asChild type="default">
<Link href={`/project/${ref}/sql/quickstarts`}>View more templates</Link>
</Button>
</div>
</TabsContent_Shadcn_>
</Tabs_Shadcn_>
</div>
)}
</div>
)
}
@@ -0,0 +1,98 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useSnapshot } from 'valtio'
import { useParams } from 'common'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import type { RecentItem } from 'state/recent-items'
import { getRecentItemsStore } from 'state/recent-items'
import { editorEntityTypes } from 'state/tabs'
import { useEditorType } from '../editors/EditorsLayout.hooks'
dayjs.extend(relativeTime)
export function RecentItems() {
const { ref } = useParams()
const store = getRecentItemsStore(ref)
const recentItemsSnap = useSnapshot(store)
const editor = useEditorType()
if (!recentItemsSnap?.items || !ref) return null
// Filter items based on editor type
const filteredItems = !editor
? [...recentItemsSnap.items]
: recentItemsSnap.items.filter((item) => editorEntityTypes[editor].includes(item.type))
const sortedItems = filteredItems.sort((a, b) => b.timestamp - a.timestamp)
return (
<div className="space-y-4">
<h2 className="text-sm text-foreground">Recent items</h2>
<div className="flex flex-col gap-0">
{sortedItems.length === 0 ? (
<motion.div
layout
initial={false}
className="flex flex-col items-center justify-center p-8 text-center border rounded-md border-muted gap-3"
>
<EntityTypeIcon type={'r' as ENTITY_TYPE} />
<div>
<p className="text-xs text-foreground-light">No recent items yet</p>
<p className="text-xs text-foreground-lighter">
Items will appear here as you browse through your project
</p>
</div>
</motion.div>
) : (
<AnimatePresence>
<div className="grid grid-cols-1 gap-12 gap-y-0">
{sortedItems.map((item: RecentItem, index: number) => (
<motion.div
key={item.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ delay: index * 0.012, duration: 0.15 }}
>
<Link
href={`/project/${ref}/${
item.type === 'sql'
? `sql/${item.metadata?.sqlId}`
: item.type === 'r' ||
item.type === 'v' ||
item.type === 'm' ||
item.type === 'f' ||
item.type === 'p'
? `editor/${item.metadata?.tableId}?schema=${item.metadata?.schema}`
: `explorer/${item.type}/${item.metadata?.schema}/${item.metadata?.name}`
}`}
className="flex items-center gap-4 rounded-lg bg-surface-100 py-2 transition-colors hover:bg-surface-200"
>
<div className="flex h-6 w-6 items-center justify-center rounded bg-surface-100 border">
<EntityTypeIcon type={item.type} />
</div>
<div className="flex flex-1 gap-5 items-center">
<span className="text-sm text-foreground">
<span className="text-foreground-lighter">{item.metadata?.schema}</span>
{item.metadata?.schema && <span className="text-foreground-light">.</span>}
<span className="text-foreground">{item.label || 'Untitled'}</span>
</span>
<div className="bg-border-muted flex grow h-px"></div>
<span className="text-xs text-foreground-lighter">
{dayjs(item.timestamp).fromNow()}
</span>
</div>
</Link>
</motion.div>
))}
</div>
</AnimatePresence>
)}
</div>
</div>
)
}
@@ -0,0 +1,98 @@
import { Entity } from 'data/entity-types/entity-types-infinite-query'
import {
getRecentItemsByType,
getRecentItemsStore,
RecentItem,
removeRecentItems,
} from 'state/recent-items'
import { createTabId, getTabsStore, removeTabs } from 'state/tabs'
export const tableEditorTabsCleanUp = ({
ref,
schemas,
entities,
}: {
ref: string
schemas: string[]
entities: Entity[]
}) => {
// Identity all entities by tab ID
const entitiesById: string[] = entities.map((x: Entity) => createTabId(x.type, { id: x.id }))
// Remove any recent items that no longer exists in the database
const recentItemsStore = getRecentItemsStore(ref)
const recentItemsFilteredToSchemas: string[] = []
for (const schema of schemas) {
recentItemsFilteredToSchemas.push(
...recentItemsStore?.items
?.filter((x: RecentItem) => x.metadata?.schema === schema)
.map((x) => x.id)
)
}
const recentItemsToRemove = [
...recentItemsFilteredToSchemas.filter((entityId) => !entitiesById.includes(entityId)),
]
removeRecentItems(ref, recentItemsToRemove)
// Close any tabs which might have been opened but no longer exists in the database
const tabsStore = getTabsStore(ref)
const tabsFilteredToSchemas: string[] = []
for (const schema of schemas) {
tabsFilteredToSchemas.push(
...tabsStore?.openTabs?.filter((tabId) => {
const tab = tabsStore.tabsMap[tabId]
return tab.metadata?.schema === schema
})
)
}
const tableEditorTabsToBeCleaned = [
...tabsFilteredToSchemas.filter(
(id) => !id.startsWith('sql') && !id.startsWith('schema') && !entitiesById.includes(id)
),
]
// perform tabs cleanup
removeTabs(ref, tableEditorTabsToBeCleaned)
}
export const sqlEditorTabsCleanup = ({
ref,
snippets,
}: {
ref: string
snippets: { id: string; type: string }[]
}) => {
// these are tabs that are static content
// these canot be removed from localstorage based on this query request
const IGNORED_TAB_IDS = ['sql-templates', 'sql-quickstarts']
// Identify all SQL snippets / content by their tab ids
const currentContentIds = [
...snippets
.filter((content) => content.type === 'sql')
.map((content) => createTabId('sql', { id: content.id })),
// append ignored tab IDs
...IGNORED_TAB_IDS,
]
// Remove any snippet tabs that might no longer be existing (removed outside of the dashboard session)
const tabsStore = getTabsStore(ref)
const snippetTabsToBeCleaned = tabsStore.openTabs.filter(
(id: string) => id.startsWith('sql') && !currentContentIds.includes(id)
)
removeTabs(ref, snippetTabsToBeCleaned)
// Remove any recent items that might no longer be existing (removed outside of the dashboard session)
const recentItems = getRecentItemsByType(ref, 'sql')
removeRecentItems(
ref,
recentItems
? recentItems.filter((item) => !currentContentIds.includes(item.id)).map((item) => item.id)
: []
)
}
@@ -0,0 +1,52 @@
import { usePathname } from 'next/navigation'
import { ComponentProps, ReactNode } from 'react'
import { useParams } from 'common'
import {
useIsSQLEditorTabsEnabled,
useIsTableEditorTabsEnabled,
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { cn } from 'ui'
import { ProjectLayoutWithAuth } from '../ProjectLayout/ProjectLayout'
import { Tabs } from '../Tabs'
import { CollapseButton } from '../Tabs/CollapseButton'
import { useEditorType } from './EditorsLayout.hooks'
export interface ExplorerLayoutProps extends ComponentProps<typeof ProjectLayoutWithAuth> {
children: ReactNode
hideTabs?: boolean
title?: string
product?: string
}
export const EditorBaseLayout = ({ children, title, product, ...props }: ExplorerLayoutProps) => {
const { ref } = useParams()
const pathname = usePathname()
const editor = useEditorType()
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
const tableEditorTabsEnabled = editor === 'table' && isTableEditorTabsEnabled
const sqlEditorTabsEnabled = editor === 'sql' && isSQLEditorTabsEnabled
const hideTabs = pathname === `/project/${ref}/editor` || pathname === `/project/${ref}/sql`
return (
<ProjectLayoutWithAuth resizableSidebar={true} title={title} product={product} {...props}>
<div className="flex flex-col h-full">
{tableEditorTabsEnabled || sqlEditorTabsEnabled ? (
<div
className={cn(
'h-10 flex items-center',
!hideTabs ? 'bg-surface-200 dark:bg-alternative' : 'bg-surface-100'
)}
>
{hideTabs && <CollapseButton hideTabs={hideTabs} />}
{!hideTabs && <Tabs />}
</div>
) : null}
<div className="h-full">{children}</div>
</div>
</ProjectLayoutWithAuth>
)
}
@@ -0,0 +1,13 @@
import { useParams } from 'common'
import { usePathname } from 'next/navigation'
export function useEditorType(): 'table' | 'sql' | undefined {
const pathname = usePathname()
const { ref } = useParams()
return pathname?.includes(`/project/${ref}/editor`)
? 'table'
: pathname?.includes(`/project/${ref}/sql`)
? 'sql'
: undefined
}
@@ -1,5 +1,6 @@
import { QueryClient, useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
import { executeSql, ExecuteSqlVariables } from 'data/sql/execute-sql-query'
import { ResponseError } from 'types'
import { ENTITY_TYPE } from './entity-type-constants'
import { entityTypeKeys } from './keys'
@@ -114,7 +115,7 @@ export async function getEntityTypes(
}
export type EntityTypesData = Awaited<ReturnType<typeof getEntityTypes>>
export type EntityTypesError = unknown
export type EntityTypesError = ResponseError
export const useEntityTypesQuery = <TData = EntityTypesData>(
{
@@ -130,8 +131,8 @@ export const useEntityTypesQuery = <TData = EntityTypesData>(
enabled = true,
...options
}: UseInfiniteQueryOptions<EntityTypesData, EntityTypesError, TData> = {}
) =>
useInfiniteQuery<EntityTypesData, EntityTypesError, TData>(
) => {
return useInfiniteQuery<EntityTypesData, EntityTypesError, TData>(
entityTypeKeys.list(projectRef, { schemas, search, sort, limit, filterTypes }),
({ signal, pageParam }) =>
getEntityTypes(
@@ -163,6 +164,7 @@ export const useEntityTypesQuery = <TData = EntityTypesData>(
...options,
}
)
}
export function prefetchEntityTypes(
client: QueryClient,
+1
View File
@@ -59,6 +59,7 @@ export async function getTables(
if (Array.isArray(data) && sortByProperty) {
return sortBy(data, (t) => t[sortByProperty]) as PostgresTable[]
}
return data as Omit<PostgresTable, 'columns'>[]
}
+3
View File
@@ -50,6 +50,9 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs',
UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs',
SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED: 'supabase-sql-scratch-pad-banner-acknowledged',
DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`,
-5
View File
@@ -380,11 +380,6 @@ const nextConfig = {
destination: '/org/_/billing',
permanent: true,
},
{
source: '/project/:ref/sql',
destination: '/project/:ref/sql/new',
permanent: true,
},
{
permanent: true,
source: '/project/:ref/reports/linter',
+3
View File
@@ -25,6 +25,9 @@
"@ai-sdk/openai": "^0.0.72",
"@dagrejs/dagre": "^1.0.4",
"@deno/eszip": "0.83.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electric-sql/pglite": "^0.2.15",
"@graphiql/react": "^0.19.4",
"@graphiql/toolkit": "^0.9.1",
@@ -20,5 +20,5 @@ type ResponseData =
paths['/platform/projects/{ref}/content/count']['get']['responses']['200']['content']['application/json']
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse<ResponseData>) => {
return res.status(200).json({ shared: 1, favorites: 1, private: 1 })
return res.status(200).json({ shared: 0, favorites: 0, private: 1 })
}
@@ -28,6 +28,11 @@ type GetResponseData =
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse<GetResponseData>) => {
// Platform specific endpoint
const { favorite, visibility } = req.query
if (favorite || visibility === 'project') {
return res.status(200).json({ data: [] })
}
const snippets = [
{
id: '1',
@@ -4,7 +4,7 @@ import Link from 'next/link'
import { useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useFeaturePreviewContext } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsColumnLevelPrivilegesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
getDefaultColumnCheckedStates,
getDefaultTableCheckedStates,
@@ -15,6 +15,7 @@ import PrivilegesHead from 'components/interfaces/Database/Privileges/Privileges
import PrivilegesTable from 'components/interfaces/Database/Privileges/PrivilegesTable'
import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
@@ -31,7 +32,6 @@ import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
import DefaultLayout from 'components/layouts/DefaultLayout'
const EDITABLE_ROLES = ['authenticated', 'anon', 'service_role']
@@ -39,10 +39,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
const { ref, table: paramTable } = useParams()
const { project } = useProjectContext()
const snap = useAppStateSnapshot()
const featurePreviewContext = useFeaturePreviewContext()
const { flags } = featurePreviewContext
const isEnabled = flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
const isEnabled = useIsColumnLevelPrivilegesEnabled()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [selectedTable, setSelectedTable] = useState<string | undefined>(paramTable)
@@ -1,19 +1,24 @@
import { useRouter } from 'next/router'
import { useCallback } from 'react'
import { useCallback, useEffect } from 'react'
import { useParams } from 'common/hooks'
import { useParams } from 'common'
import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { TableGridEditor } from 'components/interfaces/TableGridEditor'
import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { TablesData } from 'data/tables/tables-query'
import { addTab, createTabId, getTabsStore } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const { id: _id, ref: projectRef } = useParams()
const id = _id ? Number(_id) : undefined
const store = getTabsStore(projectRef)
const { project } = useProjectContext()
const { data: selectedTable, isLoading } = useTableEditorQuery({
@@ -35,6 +40,36 @@ const TableEditorPage: NextPageWithLayout = () => {
[router, projectRef]
)
/**
* Effect: Creates or updates tab when table is loaded
* Runs when:
* - selectedTable changes (when a new table is loaded)
* - id changes (when URL parameter changes)
*/
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
useEffect(() => {
// tabs preview flag logic
if (isTableEditorTabsEnabled && selectedTable && projectRef) {
const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
if (!store.tabsMap[tabId]) {
addTab(projectRef, {
id: tabId,
type: selectedTable.entity_type,
label: selectedTable.name,
metadata: {
schema: selectedTable.schema,
name: selectedTable.name,
tableId: id,
},
})
} else {
// If tab already exists, just make it active
store.activeTab = tabId
}
}
}, [selectedTable, id, projectRef, isTableEditorTabsEnabled])
return (
<>
<TableGridEditor isLoadingSelectedTable={isLoading} selectedTable={selectedTable} />
@@ -48,7 +83,9 @@ const TableEditorPage: NextPageWithLayout = () => {
TableEditorPage.getLayout = (page) => (
<DefaultLayout>
<TableEditorLayout>{page}</TableEditorLayout>
<EditorBaseLayout productMenu={<TableEditorMenu />} product="Table Editor">
<TableEditorLayout>{page}</TableEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
@@ -1,24 +1,48 @@
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import EmptyState from 'components/interfaces/TableGridEditor/EmptyState'
import SidePanelEditor from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu'
import { NewTab } from 'components/layouts/Tabs/NewTab'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { editorEntityTypes, getTabsStore } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const { ref: projectRef } = useParams()
const router = useRouter()
const { ref: projectRef } = useParams()
const tabStore = getTabsStore(projectRef)
const appSnap = useAppStateSnapshot()
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const onTableCreated = (table: { id: number }) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
}
useEffect(() => {
if (isTableEditorTabsEnabled) {
const lastOpenedTab = appSnap.dashboardHistory.editor
const lastTabId = tabStore.openTabs.find((id) =>
editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type)
)
if (lastOpenedTab !== undefined) {
router.push(`/project/${projectRef}/editor/${appSnap.dashboardHistory.editor}`)
} else if (lastTabId) {
// Handle redirect to last opened table tab, or last table tab
const lastTab = tabStore.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`)
}
}
}, [isTableEditorTabsEnabled])
return (
<>
<EmptyState />
{isTableEditorTabsEnabled ? <NewTab /> : <EmptyState />}
<SidePanelEditor onTableCreated={onTableCreated} />
</>
)
@@ -26,7 +50,9 @@ const TableEditorPage: NextPageWithLayout = () => {
TableEditorPage.getLayout = (page) => (
<DefaultLayout>
<TableEditorLayout>{page}</TableEditorLayout>
<EditorBaseLayout productMenu={<TableEditorMenu />} product="Table Editor">
<TableEditorLayout>{page}</TableEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
@@ -0,0 +1,36 @@
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { SidePanelEditor } from 'components/interfaces/TableGridEditor'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu'
import { NewTab } from 'components/layouts/Tabs/NewTab'
import type { NextPageWithLayout } from 'types'
const EditorNewPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const onTableCreated = (table: { id: number }) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
}
return (
<>
<NewTab />
<SidePanelEditor onTableCreated={onTableCreated} />
</>
)
}
EditorNewPage.getLayout = (page) => (
<DefaultLayout>
<EditorBaseLayout productMenu={<TableEditorMenu />} product="Table Editor">
<TableEditorLayout>{page}</TableEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
export default EditorNewPage
+31 -7
View File
@@ -2,13 +2,17 @@ import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams } from 'common/hooks/useParams'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { SQLEditor } from 'components/interfaces/SQLEditor/SQLEditor'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { useContentIdQuery } from 'data/content/content-id-query'
import { useAppStateSnapshot } from 'state/app-state'
import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { addTab, createTabId } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
import DefaultLayout from 'components/layouts/DefaultLayout'
const SqlEditor: NextPageWithLayout = () => {
const router = useRouter()
@@ -51,16 +55,36 @@ const SqlEditor: NextPageWithLayout = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, allSnippets, content])
return (
<div className="flex-1 overflow-auto">
<SQLEditor />
</div>
)
const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
// Watch for route changes
useEffect(() => {
if (isSqlEditorTabsEnabled) {
if (!router.isReady || !id || id === 'new') return
const tabId = createTabId('sql', { id })
const snippet = allSnippets.find((x) => x.id === id)
addTab(ref, {
id: tabId,
type: 'sql',
label: snippet?.name || 'Untitled Query',
metadata: {
sqlId: id,
name: snippet?.name,
},
})
}
}, [router.isReady, id, isSqlEditorTabsEnabled])
return <SQLEditor />
}
SqlEditor.getLayout = (page) => (
<DefaultLayout>
<SQLEditorLayout title="SQL">{page}</SQLEditorLayout>
<EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
<SQLEditorLayout>{page}</SQLEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
@@ -0,0 +1,49 @@
import { useParams } from 'common'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { NewTab } from 'components/layouts/Tabs/NewTab'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { getTabsStore } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const store = getTabsStore(projectRef)
const appSnap = useAppStateSnapshot()
const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
useEffect(() => {
if (!isSqlEditorTabsEnabled) {
// Redirect to /new if not using tabs
router.push(`/project/${projectRef}/sql/new`)
} else {
// Handle redirect to last opened snippet tab, or last snippet tab
const lastOpenedTab = appSnap.dashboardHistory.sql
const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql')
if (lastOpenedTab !== undefined) {
router.push(`/project/${projectRef}/sql/${appSnap.dashboardHistory.sql}`)
} else if (lastTabId) {
const lastTab = store.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`)
}
}
}, [isSqlEditorTabsEnabled])
return isSqlEditorTabsEnabled ? <NewTab /> : null
}
TableEditorPage.getLayout = (page) => (
<DefaultLayout>
<EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
<SQLEditorLayout>{page}</SQLEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
export default TableEditorPage
@@ -1,16 +1,49 @@
import { useParams } from 'next/navigation'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import SQLQuickstarts from 'components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { addTab, createTabId } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const SqlEditorWelcome: NextPageWithLayout = () => {
const SqlQuickstarts: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = useParams<{ ref: string }>()
const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
useEffect(() => {
if (isSqlEditorTabsEnabled) {
if (!router.isReady) return
const tabId = createTabId('sql', { id: 'quickstarts' })
addTab(ref, {
id: tabId,
type: 'sql',
label: 'Quickstarts',
metadata: {
sqlId: 'quickstarts',
name: 'quickstarts',
},
})
}
}, [router.isReady, isSqlEditorTabsEnabled, ref])
return <SQLQuickstarts />
}
SqlEditorWelcome.getLayout = (page) => (
SqlQuickstarts.getLayout = (page) => (
<DefaultLayout>
<SQLEditorLayout title="Quickstarts">{page}</SQLEditorLayout>
<EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
<SQLEditorLayout>{page}</SQLEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
export default SqlEditorWelcome
export default SqlQuickstarts
@@ -1,16 +1,49 @@
import { useParams } from 'next/navigation'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import SQLTemplates from 'components/interfaces/SQLEditor/SQLTemplates/SQLTemplates'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { addTab, createTabId } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const SqlEditorWelcome: NextPageWithLayout = () => {
const SqlTemplates: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = useParams<{ ref: string }>()
const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
useEffect(() => {
if (isSqlEditorTabsEnabled) {
if (!router.isReady) return
const tabId = createTabId('sql', { id: 'templates' })
addTab(ref, {
id: tabId,
type: 'sql',
label: 'Templates',
metadata: {
sqlId: 'templates',
name: 'templates',
},
})
}
}, [router.isReady, isSqlEditorTabsEnabled, ref])
return <SQLTemplates />
}
SqlEditorWelcome.getLayout = (page) => (
SqlTemplates.getLayout = (page) => (
<DefaultLayout>
<SQLEditorLayout title="SQL">{page}</SQLEditorLayout>
<EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
<SQLEditorLayout>{page}</SQLEditorLayout>
</EditorBaseLayout>
</DefaultLayout>
)
export default SqlEditorWelcome
export default SqlTemplates
Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 879 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+16 -9
View File
@@ -1,9 +1,9 @@
import type { Message as MessageType } from 'ai/react'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
export type CommonDatabaseEntity = {
id: number
@@ -100,6 +100,7 @@ const getInitialState = () => {
showGenerateSqlModal: false,
ongoingQueriesPanelOpen: false,
mobileMenuOpen: false,
showSidebar: true,
}
}
@@ -149,13 +150,14 @@ const getInitialState = () => {
showGenerateSqlModal: false,
ongoingQueriesPanelOpen: false,
mobileMenuOpen: false,
showSidebar: true,
}
}
export const appState = proxy({
...getInitialState(),
setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string) => {
setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string | undefined) => {
if (appState.dashboardHistory[key] !== id) {
appState.dashboardHistory[key] = id
localStorage.setItem(
@@ -211,6 +213,11 @@ export const appState = proxy({
appState.showGenerateSqlModal = value
},
showSidebar: true,
setShowSidebar: (value: boolean) => {
appState.showSidebar = value
},
resetAiAssistantPanel: () => {
appState.aiAssistantPanel = {
...INITIAL_AI_ASSISTANT,
@@ -232,6 +239,11 @@ export const appState = proxy({
}
},
showOngoingQueriesPanelOpen: false,
setOnGoingQueriesPanelOpen: (value: boolean) => {
appState.ongoingQueriesPanelOpen = value
},
saveLatestMessage: (message: any) => {
appState.aiAssistantPanel = {
...appState.aiAssistantPanel,
@@ -239,11 +251,6 @@ export const appState = proxy({
}
},
showOngoingQueriesPanelOpen: false,
setOnGoingQueriesPanelOpen: (value: boolean) => {
appState.ongoingQueriesPanelOpen = value
},
setEditorPanel: (value: Partial<EditorPanelType>) => {
// Close AI Assistant panel if editor panel is being opened
if (value.open && appState.aiAssistantPanel.open) {
+160
View File
@@ -0,0 +1,160 @@
import { partition } from 'lodash'
import { proxy, subscribe } from 'valtio'
import { editorEntityTypes, Tab, TabType } from './tabs'
/**
* [TODO REFACTOR] Joshen: JFYI this differs from the way that we usually write our stores
* where we have contexts and such - ideally we need to refactor this for consistency
* and the main benefit from our usual way of writing stores is that we do not need to pass
* in the project ref to all the functions, as the stores are written to be scoped to the project
*
* So do not use this as reference for writing new valtio stores - refer to database-selector instead
*/
// Define the key for storing recent items in localStorage
const RECENT_ITEMS_KEY = 'supabase_recent_items'
// Define the maximum number of recent items to store
const MAX_RECENT_ITEMS = 8
const getStorageKey = (ref: string) => `${RECENT_ITEMS_KEY}_${ref}`
// Define the structure of a recent item
export interface RecentItem {
id: string // Unique identifier for the item
type: TabType // Type of the tab (e.g., sql, table)
label: string // Label for the item
timestamp: number // Timestamp of when the item was added
metadata?: {
// Optional metadata associated with the item
schema?: string // Schema name (if applicable)
name?: string // Name of the item (if applicable)
tableId?: number // Table ID (if applicable)
sqlId?: string // SQL ID (if applicable)
}
}
// Define the structure of the recent items state
interface RecentItems {
items: RecentItem[] // Each project just has an array of items
}
interface RecentItemsStore {
[key: string]: RecentItems // Removed | undefined since we always initialize it
}
const defaultState: RecentItems = {
items: [],
}
export const recentItemsStore = proxy<RecentItemsStore>({})
export const getRecentItemsStore = (ref: string | undefined): RecentItems => {
if (!ref) return proxy(defaultState)
if (!recentItemsStore[ref]) {
const stored = localStorage.getItem(getStorageKey(ref))
console.debug('[Recent Items] Loading stored items:', stored)
try {
const parsed = stored ? JSON.parse(stored) : defaultState
// Validate the shape of the data
if (!parsed.items || !Array.isArray(parsed.items)) {
console.warn('[Recent Items] Invalid stored data, using default')
recentItemsStore[ref] = proxy({ ...defaultState })
} else {
recentItemsStore[ref] = proxy(parsed)
}
} catch (error) {
console.error('[Recent Items] Failed to parse stored items:', error)
recentItemsStore[ref] = proxy({ ...defaultState })
}
}
return recentItemsStore[ref]
}
// Subscribe to changes for each ref and save to localStorage
if (typeof window !== 'undefined') {
subscribe(recentItemsStore, () => {
Object.entries(recentItemsStore).forEach(([ref, state]) => {
try {
localStorage.setItem(getStorageKey(ref), JSON.stringify(state))
} catch (error) {
console.error('Failed to save recent items to localStorage:', error)
}
})
})
}
// Function to add a recent item
export const addRecentItem = (ref: string | undefined, tab: Tab) => {
if (!ref) return
const store = getRecentItemsStore(ref)
// Check if an item with the same ID already exists
const existingItem = store.items.find((item) => item.id === tab.id)
if (existingItem) {
// If it exists, update its timestamp
existingItem.timestamp = Date.now()
return // Exit the function
}
// If it doesn't exist, create and add a new item
const recentItem: RecentItem = {
id: tab.id, // Set the ID
type: tab.type, // Set the type
label: tab.label || 'Untitled', // Set the label or default to 'Untitled'
timestamp: Date.now(), // Set the current timestamp
metadata: tab.metadata, // Set the metadata
}
// Add the new recent item to the beginning of the list
store.items.unshift(recentItem)
// Ensure that there's only up to max of MAX_RECENT_ITEMS items per tab type
const [itemsOfSameType, itemsOfDifferentType] = partition(store.items, (item) => {
if (editorEntityTypes.table.includes(item.type)) return item
})
store.items = [...itemsOfSameType.slice(0, MAX_RECENT_ITEMS), ...itemsOfDifferentType]
}
// Function to clear all recent items
export const clearRecentItems = (ref: string | undefined) => {
if (!ref) return
const store = getRecentItemsStore(ref)
store.items = [] // Set items to an empty array
}
// Function to remove a specific recent item by its ID
export const removeRecentItem = (ref: string | undefined, itemId: string) => {
if (!ref) return
const store = getRecentItemsStore(ref)
store.items = store.items.filter((item) => item.id !== itemId)
}
// Function to remove multiple recent items by their IDs
export const removeRecentItems = (ref: string | undefined, itemIds: string[]) => {
if (!ref) return
const store = getRecentItemsStore(ref)
store.items = store.items.filter((item) => !itemIds.includes(item.id))
}
// Function to remove recent items by their type
export const removeRecentItemsByType = (ref: string | undefined, type: TabType) => {
if (!ref) return
const store = getRecentItemsStore(ref)
store.items = store.items.filter((item) => item.type !== type)
}
// Function to get all recent items
export const getRecentItems = (ref: string | undefined) => {
if (!ref) return
const store = getRecentItemsStore(ref)
return store.items
}
// Function to get recent items by their type
export const getRecentItemsByType = (ref: string | undefined, type: TabType) => {
if (!ref) return
const store = getRecentItemsStore(ref)
return store.items.filter((item) => item.type === type)
}
+392
View File
@@ -0,0 +1,392 @@
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { NextRouter } from 'next/router'
import { ReactNode } from 'react'
import { proxy, subscribe } from 'valtio'
import { addRecentItem } from './recent-items'
/**
* [TODO REFACTOR] Joshen: JFYI this differs from the way that we usually write our stores
* where we have contexts and such - ideally we need to refactor this for consistency
* and the main benefit from our usual way of writing stores is that we do not need to pass
* in the project ref to all the functions, as the stores are written to be scoped to the project
*
* So do not use this as reference for writing new valtio stores - refer to database-selector instead
*/
// Define the type of tabs available in the application
export type TabType = ENTITY_TYPE | 'sql'
export interface Tab {
id: string // Unique identifier for the tab
type: TabType // Type of the tab, which can be an entity type or specific string values
label?: string // Optional label for the tab, displayed to the user
icon?: ReactNode // Optional icon associated with the tab
metadata?: {
// Optional metadata related to the tab
schema?: string // Optional schema name associated with the tab
name?: string // Optional name of the entity represented by the tab
tableId?: number // Optional ID of the table associated with the tab
sqlId?: string // Optional ID of the SQL query associated with the tab
}
isPreview?: boolean // Optional flag indicating if the tab is in preview mode
createdAt?: Date // Optional timestamp for when the tab was created
updatedAt?: Date // Optional timestamp for when the tab was last updated
}
// Interface representing the state of tabs
interface TabsState {
activeTab: string | null
openTabs: string[]
tabsMap: { [key: string]: Tab }
previewTabId?: string
}
// Map of tab states for different references
interface TabsStateMap {
[ref: string]: TabsState
}
const STORAGE_KEY = 'supabase_studio_tabs'
const getStorageKey = (ref: string) => `${STORAGE_KEY}_${ref}`
const defaultState: TabsState = {
openTabs: [],
activeTab: null,
tabsMap: {},
}
// Store now holds states for multiple refs
export const tabsStore = proxy<TabsStateMap>({})
// Helper to get/create state for a specific ref
export const getTabsStore = (ref: string | undefined): TabsState => {
if (!ref) return proxy(defaultState)
if (!tabsStore[ref]) {
const stored = localStorage.getItem(getStorageKey(ref))
console.debug('[Tabs] Loading stored tabs:', stored)
try {
const parsed = stored ? JSON.parse(stored) : defaultState
if (
!parsed.openTabs ||
!Array.isArray(parsed.openTabs) ||
!parsed.tabsMap ||
typeof parsed.tabsMap !== 'object'
) {
console.warn('[Tabs] Invalid stored data, using default')
tabsStore[ref] = proxy({ ...defaultState })
} else {
tabsStore[ref] = proxy(parsed)
}
} catch (error) {
console.error('[Tabs] Failed to parse stored tabs:', error)
tabsStore[ref] = proxy({ ...defaultState })
}
}
return tabsStore[ref]
}
// Subscribe to changes for each ref and save to localStorage
if (typeof window !== 'undefined') {
subscribe(tabsStore, () => {
Object.entries(tabsStore).forEach(([ref, state]) => {
try {
localStorage.setItem(getStorageKey(ref), JSON.stringify(state))
} catch (error) {
console.error('Failed to save tabs to localStorage:', error)
}
})
})
}
// Function to add a new tab to the store
export const addTab = (ref: string | undefined, tab: Tab) => {
if (!ref) return
const store = getTabsStore(ref)
// If tab exists and is active, don't do anything
if (store.tabsMap[tab.id] && store.activeTab === tab.id) {
return
}
// If tab exists but isn't active, just make it active
if (store.tabsMap[tab.id]) {
store.activeTab = tab.id
if (!tab.isPreview) addRecentItem(ref, tab)
return
}
// If this tab should be permanent, add it normally
if (tab.isPreview === false) {
store.openTabs = [...store.openTabs, tab.id]
store.tabsMap[tab.id] = tab
store.activeTab = tab.id
// Add to recent items when creating permanent tab
addRecentItem(ref, tab)
return
}
// Remove any existing preview tab
if (store.previewTabId) {
store.openTabs = store.openTabs.filter((id) => id !== store.previewTabId)
delete store.tabsMap[store.previewTabId]
}
// Add new preview tab
store.tabsMap[tab.id] = { ...tab, isPreview: true }
store.openTabs = [...store.openTabs, tab.id]
store.previewTabId = tab.id
store.activeTab = tab.id
}
// Function to remove a tab from the store
// this is used for removing tabs from the localstorage state
// for handling a manual tab removal with a close action, use handleTabClose()
export const removeTab = (ref: string | undefined, id: string) => {
console.log('removeTab')
const store = getTabsStore(ref)
const idx = store.openTabs.indexOf(id)
store.openTabs = store.openTabs.filter((tabId) => tabId !== id)
delete store.tabsMap[id]
// Update active tab if the removed tab was active
if (id === store.activeTab) {
store.activeTab = store.openTabs[idx - 1] || store.openTabs[idx + 1] || null
}
}
// Function to remove multiple tabs from the store
// this is used for removing tabs from the localstorage state
// for handling a manual tab removal with a close action, use handleTabClose()
export const removeTabs = (ref: string | undefined, ids: string[]) => {
if (!ref) return
if (!ids.length) return
ids.forEach((id) => removeTab(ref, id))
}
export const renameTab = (ref: string, id: string, name: string) => {
const store = getTabsStore(ref)
if (!!store.tabsMap[id]) {
store.tabsMap[id].label = name
}
}
// Function to reorder tabs in the store
export const reorderTabs = (ref: string | undefined, oldIndex: number, newIndex: number) => {
if (!ref) return
const store = getTabsStore(ref)
const newOpenTabs = [...store.openTabs]
const [removedTab] = newOpenTabs.splice(oldIndex, 1)
newOpenTabs.splice(newIndex, 0, removedTab)
store.openTabs = newOpenTabs
}
// Function to make a tab permanent
export const makeTabPermanent = (ref: string | undefined, tabId: string) => {
if (!ref) return
const store = getTabsStore(ref)
const tab = store.tabsMap[tabId]
if (tab?.isPreview) {
tab.isPreview = false
store.previewTabId = undefined
// Add to recent items when preview tab becomes permanent
addRecentItem(ref, tab)
}
}
// make the active tab permanent if it is a preview
export const makeActiveTabPermanent = (ref?: string) => {
if (!ref) return false
const store = getTabsStore(ref)
if (store.activeTab && store.tabsMap[store.activeTab]?.isPreview) {
makeTabPermanent(ref, store.activeTab)
return true
}
return false
}
// handle navigation to a specific tab
export const handleTabNavigation = (ref: string | undefined, id: string, router: NextRouter) => {
if (!ref) return
const store = getTabsStore(ref)
const tab = store.tabsMap[id]
if (!tab) return
store.activeTab = id
// Add to recent items when navigating to a non-preview, non-new tab
if (!tab.isPreview) addRecentItem(ref, tab)
switch (tab.type) {
case 'sql':
const schema = (router.query.schema as string) || 'public'
router.push(`/project/${router.query.ref}/sql/${tab.metadata?.sqlId}?schema=${schema}`)
break
case 'r':
case 'v':
case 'm':
case 'f':
case 'p':
router.push(
`/project/${router.query.ref}/editor/${tab.metadata?.tableId}?schema=${tab.metadata?.schema}`
)
break
}
}
// Function to handle closing a tab
export const handleTabClose = ({
ref,
id,
router,
editor,
onClose,
onClearDashboardHistory,
}: {
ref: string | undefined
id: string
router: NextRouter
editor?: 'sql' | 'table'
onClose?: (id: string) => void
onClearDashboardHistory: () => void
}) => {
if (!ref) return
const store = getTabsStore(ref)
// tabs without the one we're closing
const currentTab = store.tabsMap[id]
const currentTabs = Object.values(store.tabsMap).filter((tab) => tab.id !== id)
const nextTabId = !editor
? undefined
: currentTabs.filter((tab) => {
return editorEntityTypes[editor]?.includes(tab.type)
})[0]?.id
delete store.tabsMap[id]
if (currentTab) {
// Update store
// If the tab being removed is logged in the store, update the open tabs
store.openTabs = [...currentTabs.map((tab) => tab.id).filter((id) => id !== currentTab.id)]
}
// Check if there is a preview tab and if it matches the tab being closed
if (store.previewTabId) {
if (store.previewTabId === id) {
// remove the preview tab if it matches the tab being closed
store.previewTabId = undefined
}
}
// [Joshen] Only navigate away if we're closing the tab that's currently in focus
if (store.activeTab === id || id === 'new') {
if (nextTabId) {
store.activeTab = nextTabId
handleTabNavigation(ref, nextTabId, router)
} else {
onClearDashboardHistory()
// If no tabs of same type, go to the home of the current section
switch (currentTab?.type) {
case 'sql':
router.push(`/project/${router.query.ref}/sql`)
break
case 'r':
case 'v':
case 'm':
case 'f':
case 'p':
router.push(`/project/${router.query.ref}/editor`)
break
default:
router.push(`/project/${router.query.ref}/${editor}`)
}
}
}
onClose?.(id)
}
// Function to handle the end of a tab drag event
export const handleTabDragEnd = (
ref: string | undefined,
oldIndex: number,
newIndex: number,
tabId: string,
router: any
) => {
if (!ref) return
const store = getTabsStore(ref)
// Make permanent if needed
const draggedTab = store.tabsMap[tabId]
if (draggedTab?.isPreview) {
makeTabPermanent(ref, tabId)
}
// Reorder tabs
const newOpenTabs = [...store.openTabs]
newOpenTabs.splice(oldIndex, 1)
newOpenTabs.splice(newIndex, 0, tabId)
store.openTabs = newOpenTabs
store.activeTab = tabId
// Handle navigation
handleTabNavigation(ref, tabId, router)
}
type CreateTabIdParams = {
r: { id: number }
v: { id: number }
m: { id: number }
f: { id: number }
p: { id: number }
sql: { id: string }
schema: { schema: string }
view: never
function: never
new: never
}
// Function to create a unique tab ID based on type and parameters
export function createTabId<T extends TabType>(type: T, params: CreateTabIdParams[T]): string {
switch (type) {
case 'r':
return `r-${(params as CreateTabIdParams['r']).id}`
case 'v':
return `v-${(params as CreateTabIdParams['v']).id}`
case 'm':
return `m-${(params as CreateTabIdParams['m']).id}`
case 'f':
return `f-${(params as CreateTabIdParams['f']).id}`
case 'p':
return `p-${(params as CreateTabIdParams['p']).id}`
case 'sql':
return `sql-${(params as CreateTabIdParams['sql']).id}`
default:
return ''
}
}
// Object mapping editor types to their corresponding tab types
export const editorEntityTypes = {
table: ['r', 'v', 'm', 'f', 'p'],
sql: ['sql'],
}
// Function to remove tabs based on their editor type
export function removeTabsByEditor(ref: string | undefined, type: 'table' | 'sql') {
if (!ref) return
const store = getTabsStore(ref)
// Get tab IDs to remove
const tabIdsToRemove = store.openTabs.filter((id) => {
return editorEntityTypes[type].some((entityType) => id.startsWith(`${entityType}-`))
})
removeTabs(ref, tabIdsToRemove)
}
+8 -2
View File
@@ -13,10 +13,16 @@
border: 0 !important;
}
[data-theme='dark'] .monaco-editor,
[data-theme='dark'] .monaco-diff-editor {
--vscode-editor-background: hsl(var(--background-surface-100)) !important;
--vscode-editorGutter-background: hsl(var(--background-surface-100)) !important;
color-scheme: dark;
}
.monaco-editor,
.monaco-diff-editor {
--vscode-editor-background: hsl(var(--background-surface-200)) !important;
--vscode-editorGutter-background: hsl(var(--background-surface-200)) !important;
--vscode-editor-background: hsl(var(--background-dash-sidebar)) !important;
--vscode-editorGutter-background: hsl(var(--background-dash-sidebar)) !important;
}
.gutter {
@@ -133,23 +133,35 @@ const TreeViewItem = forwardRef<
}
}, [isLoading])
const handleBlur = () => {
onEditSubmit?.(localValueState)
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
onEditSubmit?.(localValueState)
}
const handleBlur = () => {
onEditSubmit?.(localValueState)
}
// [Joshen] These properties were causing console errors as they were getting passed as props to the parent div
const {
isDisabled,
isHalfSelected,
handleSelect,
handleExpand,
treeState,
dispatch,
...divProps
} = props as any
return (
<div
ref={ref}
{...divProps}
aria-selected={isSelected}
aria-expanded={!isEditing && isExpanded}
onDoubleClick={onDoubleClick}
{...props}
className={cn(TreeViewItemVariant({ isSelected, isOpened, isPreview }))}
className={cn(props.className, TreeViewItemVariant({ isSelected, isOpened, isPreview }))}
style={{
paddingLeft:
level === 1 && !isBranch
+64 -8
View File
@@ -535,6 +535,15 @@ importers:
'@deno/eszip':
specifier: 0.83.0
version: 0.83.0
'@dnd-kit/core':
specifier: ^6.1.0
version: 6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@dnd-kit/sortable':
specifier: ^8.0.0
version: 8.0.0(@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@18.2.0)
'@electric-sql/pglite':
specifier: ^0.2.15
version: 0.2.15
@@ -2719,6 +2728,28 @@ packages:
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies:
react: '>=16.8.0'
'@dnd-kit/core@6.3.1':
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@dnd-kit/sortable@8.0.0':
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
peerDependencies:
'@dnd-kit/core': ^6.1.0
react: '>=16.8.0'
'@dnd-kit/utilities@3.2.2':
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
peerDependencies:
react: '>=16.8.0'
'@effect-ts/core@0.60.5':
resolution: {integrity: sha512-qi1WrtJA90XLMnj2hnUszW9Sx4dXP03ZJtCc5DiUBIOhF4Vw7plfb65/bdBySPoC9s7zy995TdUX1XBSxUkl5w==}
@@ -16370,6 +16401,31 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {}
'@dnd-kit/accessibility@3.1.1(react@18.2.0)':
dependencies:
react: 18.2.0
tslib: 2.6.2
'@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@dnd-kit/accessibility': 3.1.1(react@18.2.0)
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2
'@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)':
dependencies:
'@dnd-kit/core': 6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@dnd-kit/utilities': 3.2.2(react@18.2.0)
react: 18.2.0
tslib: 2.6.2
'@dnd-kit/utilities@3.2.2(react@18.2.0)':
dependencies:
react: 18.2.0
tslib: 2.6.2
'@effect-ts/core@0.60.5':
dependencies:
'@effect-ts/system': 0.57.5
@@ -17364,7 +17420,7 @@ snapshots:
'@motionone/easing@10.16.3':
dependencies:
'@motionone/utils': 10.16.3
tslib: 2.6.2
tslib: 2.8.1
'@motionone/generators@10.16.4':
dependencies:
@@ -26452,7 +26508,7 @@ snapshots:
lower-case@2.0.2:
dependencies:
tslib: 2.6.2
tslib: 2.8.1
lowlight@1.20.0:
dependencies:
@@ -29334,7 +29390,7 @@ snapshots:
dependencies:
react: 18.2.0
react-style-singleton: 2.2.3(@types/react@18.3.3)(react@18.2.0)
tslib: 2.6.2
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -29415,7 +29471,7 @@ snapshots:
dependencies:
get-nonce: 1.0.1
react: 18.2.0
tslib: 2.6.2
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -31577,11 +31633,11 @@ snapshots:
upper-case-first@2.0.2:
dependencies:
tslib: 2.6.2
tslib: 2.8.1
upper-case@2.0.2:
dependencies:
tslib: 2.6.2
tslib: 2.8.1
uqr@0.1.2: {}
@@ -31608,7 +31664,7 @@ snapshots:
use-callback-ref@1.3.3(@types/react@18.3.3)(react@18.2.0):
dependencies:
react: 18.2.0
tslib: 2.6.2
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -31632,7 +31688,7 @@ snapshots:
dependencies:
detect-node-es: 1.1.0
react: 18.2.0
tslib: 2.6.2
tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -30,6 +30,7 @@ test.describe('Table Editor', () => {
// The page has been loaded with the table data, we can now interact with the page
await page.getByRole('button', { name: 'New table', exact: true }).click()
await page.getByTestId('table-name-input').waitFor({ state: 'visible' })
await page.getByTestId('table-name-input').click()
await page.getByTestId('table-name-input').fill(tableName)