Feat/tabs (#31071)
* 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>
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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'>[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 761 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 879 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||