diff --git a/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx b/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx
index 6136b0d5eab..aaf067caba0 100644
--- a/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/OngoingQueriesPanel.tsx
@@ -30,10 +30,6 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useAppStateSnapshot } from 'state/app-state'
import { useParams } from 'common'
-interface OngoingQueriesPanel {
- onClose: () => void
-}
-
export const OngoingQueriesPanel = () => {
const [_, setParams] = useUrlState({ replace: true })
const { viewOngoingQueries } = useParams()
diff --git a/apps/studio/components/layouts/SQLEditorLayout/PrivateSqlSnippetEmpty.tsx b/apps/studio/components/layouts/SQLEditorLayout/PrivateSqlSnippetEmpty.tsx
new file mode 100644
index 00000000000..09d1d0906b6
--- /dev/null
+++ b/apps/studio/components/layouts/SQLEditorLayout/PrivateSqlSnippetEmpty.tsx
@@ -0,0 +1,34 @@
+import { Pointer } from 'lucide-react'
+import { TreeViewItem } from 'ui'
+import { InnerSideBarEmptyPanel } from 'ui-patterns'
+
+export const EmptyPrivateQueriesPanel = () => (
+
+
+
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+ ))}
+
+
+
+)
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx
index cf36ca5afb0..931727c85b2 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx
@@ -1,10 +1,8 @@
-import { ReactNode, useEffect, useMemo, useState } from 'react'
-
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 { useParams } from 'common'
export interface SQLEditorLayoutProps {
title: string
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
index 202b0c29de2..52b458484e9 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
-import { useRouter } from 'next/router'
-import { useEffect, useState } from 'react'
-import { toast } from 'sonner'
-
import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -11,6 +6,11 @@ import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
+import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
+import { getAppStateSnapshot } from 'state/app-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
Button,
@@ -27,11 +27,10 @@ import {
InnerSideBarFilterSearchInput,
InnerSideBarFilterSortDropdown,
InnerSideBarFilterSortDropdownItem,
- InnerSideMenuItem,
} from 'ui-patterns/InnerSideMenu'
-import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
+import { SqlEditorMenuStaticLinks } from './SqlEditorMenuStaticLinks'
import { SearchList } from './SQLEditorNavV2/SearchList'
-import { getAppStateSnapshot } from 'state/app-state'
+import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav'
export const SQLEditorMenu = () => {
const router = useRouter()
@@ -157,23 +156,7 @@ export const SQLEditorMenu = () => {
) : (
<>
-
-
- Templates
-
-
- Quickstarts
-
-
-
+
>
)}
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx
index fad9e81ac60..60ec2b9c887 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx
@@ -9,6 +9,7 @@ import { MoveQueryModal } from 'components/interfaces/SQLEditor/MoveQueryModal'
import RenameQueryModal from 'components/interfaces/SQLEditor/RenameQueryModal'
import { untitledSnippetTitle } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import { createSqlSnippetSkeletonV2 } from 'components/interfaces/SQLEditor/SQLEditor.utils'
+import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import { useContentCountQuery } from 'data/content/content-count-query'
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
import { getContentById } from 'data/content/content-id-query'
@@ -36,6 +37,7 @@ import {
InnerSideMenuSeparator,
} from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { EmptyPrivateQueriesPanel } from 'components/layouts/SQLEditorLayout/PrivateSqlSnippetEmpty'
import SQLEditorLoadingSnippets from './SQLEditorLoadingSnippets'
import { formatFolderResponseForTreeView, getLastItemIds, ROOT_NODE } from './SQLEditorNav.utils'
import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem'
@@ -53,6 +55,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
const project = useSelectedProject()
const { ref: projectRef, id } = useParams()
const snapV2 = useSqlEditorV2StateSnapshot()
+ // const tabStore = getTabsStore(projectRef)
const [sectionVisibility, setSectionVisibility] = useLocalStorage(
LOCAL_STORAGE_KEYS.SQL_EDITOR_SECTION_STATE(projectRef ?? ''),
DEFAULT_SECTION_STATE
@@ -487,14 +490,14 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
useEffect(() => {
if (projectRef && privateSnippetsPages) {
privateSnippetsPages.pages.forEach((page) => {
- page.contents?.forEach((snippet) => {
+ page.contents?.forEach((snippet: Snippet) => {
snapV2.addSnippet({
projectRef,
snippet,
})
})
- page.folders?.forEach((folder) => snapV2.addFolder({ projectRef, folder }))
+ page.folders?.forEach((folder: SnippetFolder) => snapV2.addFolder({ projectRef, folder }))
})
}
}, [projectRef, privateSnippetsPages?.pages])
@@ -527,58 +530,58 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
return (
<>
-
-
{
setSectionVisibility({ ...(sectionVisibility ?? DEFAULT_SECTION_STATE), shared: value })
}}
- className="px-0"
>
0 ? ` (${numProjectSnippets})` : ''}`}
/>
- {isLoadingSharedSqlSnippets ? (
-
- ) : numProjectSnippets === 0 ? (
+ {numProjectSnippets === 0 ? (
) : (
(
- {
- setShowDeleteModal(true)
- setSelectedSnippets([element.metadata as unknown as Snippet])
- }}
- onSelectRename={() => {
- setShowRenameModal(true)
- setSelectedSnippetToRename(element.metadata as Snippet)
- }}
- onSelectDownload={() => {
- setSelectedSnippetToDownload(element.metadata as Snippet)
- }}
- onSelectDuplicate={() => {
- onSelectDuplicate(element.metadata as Snippet)
- }}
- onSelectUnshare={() => {
- setSelectedSnippetToUnshare(element.metadata as Snippet)
- }}
- isLastItem={projectSnippetsLastItemIds.has(element.id as string)}
- hasNextPage={hasMoreSharedSqlSnippets}
- fetchNextPage={fetchNextSharedSqlSnippets}
- isFetchingNextPage={isFetchingMoreSharedSqlSnippets}
- />
- )}
+ nodeRenderer={({ element, ...props }) => {
+ const isActive = element.metadata?.id === id
+ return (
+ {
+ setShowDeleteModal(true)
+ setSelectedSnippets([element.metadata as unknown as Snippet])
+ }}
+ onSelectRename={() => {
+ setShowRenameModal(true)
+ setSelectedSnippetToRename(element.metadata as Snippet)
+ }}
+ onSelectDownload={() => {
+ setSelectedSnippetToDownload(element.metadata as Snippet)
+ }}
+ onSelectDuplicate={() => {
+ onSelectDuplicate(element.metadata as Snippet)
+ }}
+ onSelectUnshare={() => {
+ setSelectedSnippetToUnshare(element.metadata as Snippet)
+ }}
+ isLastItem={projectSnippetsLastItemIds.has(element.id as string)}
+ hasNextPage={hasMoreSharedSqlSnippets}
+ fetchNextPage={fetchNextSharedSqlSnippets}
+ isFetchingNextPage={isFetchingMoreSharedSqlSnippets}
+ />
+ )
+ }}
/>
)}
@@ -615,34 +618,38 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
(
- {
- setShowDeleteModal(true)
- setSelectedSnippets([element.metadata as unknown as Snippet])
- }}
- onSelectRename={() => {
- setShowRenameModal(true)
- setSelectedSnippetToRename(element.metadata as Snippet)
- }}
- onSelectDownload={() => {
- setSelectedSnippetToDownload(element.metadata as Snippet)
- }}
- onSelectDuplicate={() => {
- onSelectDuplicate(element.metadata as Snippet)
- }}
- onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
- onSelectUnshare={() => {
- setSelectedSnippetToUnshare(element.metadata as Snippet)
- }}
- isLastItem={favoriteSnippetsLastItemIds.has(element.id as string)}
- hasNextPage={hasMoreFavoriteSqlSnippets}
- fetchNextPage={fetchNextFavoriteSqlSnippets}
- isFetchingNextPage={isFetchingMoreFavoriteSqlSnippets}
- />
- )}
+ nodeRenderer={({ element, ...props }) => {
+ const isActive = element.metadata?.id === id
+ return (
+ {
+ setShowDeleteModal(true)
+ setSelectedSnippets([element.metadata as unknown as Snippet])
+ }}
+ onSelectRename={() => {
+ setShowRenameModal(true)
+ setSelectedSnippetToRename(element.metadata as Snippet)
+ }}
+ onSelectDownload={() => {
+ setSelectedSnippetToDownload(element.metadata as Snippet)
+ }}
+ onSelectDuplicate={() => {
+ onSelectDuplicate(element.metadata as Snippet)
+ }}
+ onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
+ onSelectUnshare={() => {
+ setSelectedSnippetToUnshare(element.metadata as Snippet)
+ }}
+ isLastItem={favoriteSnippetsLastItemIds.has(element.id as string)}
+ hasNextPage={hasMoreFavoriteSqlSnippets}
+ fetchNextPage={fetchNextFavoriteSqlSnippets}
+ isFetchingNextPage={isFetchingMoreFavoriteSqlSnippets}
+ />
+ )
+ }}
/>
)}
@@ -663,13 +670,9 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
/>
{isLoading ? (
-
+
) : folders.length === 0 && numPrivateSnippets === 0 ? (
-
+
) : (
{
}
}}
expandedIds={expandedFolderIds}
- nodeRenderer={({ element, ...props }) => (
- 1}
- isLastItem={privateSnippetsLastItemIds.has(element.id as string)}
- status={props.isBranch ? snapV2.folders[element.id].status : 'idle'}
- onMultiSelect={onMultiSelect}
- onSelectCreate={() => {
- if (profile && project) {
- const snippet = createSqlSnippetSkeletonV2({
- id: uuidv4(),
- name: untitledSnippetTitle,
- owner_id: profile?.id,
- project_id: project?.id,
- folder_id: element.id as string,
- sql: '',
- })
- snapV2.addSnippet({ projectRef: project.ref, snippet })
- router.push(`/project/${projectRef}/sql/${snippet.id}`)
- }
- }}
- onSelectDelete={() => {
- if (props.isBranch) {
- setSelectedFolderToDelete(element.metadata as SnippetFolder)
- } else {
- setShowDeleteModal(true)
- if (selectedSnippets.length === 0) {
- setSelectedSnippets([element.metadata as unknown as Snippet])
+ nodeRenderer={({ element, ...props }) => {
+ const isActive = element.metadata?.id === id
+
+ return (
+ 1}
+ isLastItem={privateSnippetsLastItemIds.has(element.id as string)}
+ status={props.isBranch ? snapV2.folders[element.id].status : 'idle'}
+ onMultiSelect={onMultiSelect}
+ onSelectCreate={() => {
+ if (profile && project) {
+ const snippet = createSqlSnippetSkeletonV2({
+ id: uuidv4(),
+ name: untitledSnippetTitle,
+ owner_id: profile?.id,
+ project_id: project?.id,
+ folder_id: element.id as string,
+ sql: '',
+ })
+ snapV2.addSnippet({ projectRef: project.ref, snippet })
+ router.push(`/project/${projectRef}/sql/${snippet.id}`)
}
+ }}
+ onSelectDelete={() => {
+ if (props.isBranch) {
+ setSelectedFolderToDelete(element.metadata as SnippetFolder)
+ } else {
+ setShowDeleteModal(true)
+ if (selectedSnippets.length === 0) {
+ setSelectedSnippets([element.metadata as unknown as Snippet])
+ }
+ }
+ }}
+ onSelectRename={() => {
+ if (props.isBranch) {
+ snapV2.editFolder(element.id as string)
+ } else {
+ setShowRenameModal(true)
+ setSelectedSnippetToRename(element.metadata as Snippet)
+ }
+ }}
+ onSelectMove={() => {
+ setShowMoveModal(true)
+ if (selectedSnippets.length === 0) {
+ setSelectedSnippets([element.metadata as Snippet])
+ }
+ }}
+ onSelectDownload={() =>
+ setSelectedSnippetToDownload(element.metadata as Snippet)
}
- }}
- onSelectRename={() => {
- if (props.isBranch) {
- snapV2.editFolder(element.id as string)
- } else {
- setShowRenameModal(true)
- setSelectedSnippetToRename(element.metadata as Snippet)
- }
- }}
- onSelectMove={() => {
- setShowMoveModal(true)
- if (selectedSnippets.length === 0) {
- setSelectedSnippets([element.metadata as Snippet])
- }
- }}
- onSelectDownload={() => setSelectedSnippetToDownload(element.metadata as Snippet)}
- onSelectDuplicate={() => onSelectDuplicate(element.metadata as Snippet)}
- onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
- onEditSave={(name: string) => {
- // [Joshen] Inline editing only for folders for now
- if (name.length === 0 && element.id === 'new-folder') {
- snapV2.removeFolder(element.id as string)
- } else if (name.length > 0) {
- snapV2.saveFolder({ id: element.id as string, name })
- }
- }}
- hasNextPage={hasNextPage}
- fetchNextPage={fetchNextPage}
- isFetchingNextPage={isFetchingNextPage}
- sort={sort}
- onFolderContentsChange={({ isLoading, snippets }) => {
- setSubResults((prev) => ({
- ...prev,
- [element.id as string]: { snippets, isLoading },
- }))
- }}
- />
- )}
+ onSelectDuplicate={() => onSelectDuplicate(element.metadata as Snippet)}
+ onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
+ onEditSave={(name: string) => {
+ // [Joshen] Inline editing only for folders for now
+ if (name.length === 0 && element.id === 'new-folder') {
+ snapV2.removeFolder(element.id as string)
+ } else if (name.length > 0) {
+ snapV2.saveFolder({ id: element.id as string, name })
+ }
+ }}
+ hasNextPage={hasNextPage}
+ fetchNextPage={fetchNextPage}
+ isFetchingNextPage={isFetchingNextPage}
+ sort={sort}
+ onFolderContentsChange={({ isLoading, snippets }) => {
+ setSubResults((prev) => ({
+ ...prev,
+ [element.id as string]: { snippets, isLoading },
+ }))
+ }}
+ />
+ )
+ }}
/>
)}
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx
index 8c71003b1c1..de186bf75ed 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx
@@ -1,9 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Copy, Download, Edit, ExternalLink, Lock, Move, Plus, Share, Trash } from 'lucide-react'
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import { useEffect } from 'react'
-
import { IS_PLATFORM } from 'common'
import { useParams } from 'common/hooks/useParams'
import { useSQLSnippetFolderContentsQuery } from 'data/content/sql-folder-contents-query'
@@ -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,
@@ -22,12 +21,9 @@ import {
TreeViewItem,
} from 'ui'
-interface SQLEditorTreeViewItemProps {
+interface SQLEditorTreeViewItemProps
+ extends Omit, 'name' | 'xPadding'> {
element: any
- level: number
- isBranch: boolean
- isSelected: boolean
- isExpanded: boolean
isMultiSelected?: boolean
status?: 'editing' | 'saving' | 'idle'
getNodeProps: () => any
@@ -79,11 +75,12 @@ export const SQLEditorTreeViewItem = ({
sort,
name,
onFolderContentsChange,
+ ...props
}: SQLEditorTreeViewItemProps) => {
const router = useRouter()
const { id, ref: projectRef } = useParams()
const { profile } = useProfile()
- const { className, onClick } = getNodeProps()
+
const snapV2 = useSqlEditorV2StateSnapshot()
const isOwner = profile?.id === element?.metadata.owner_id
@@ -163,12 +160,9 @@ export const SQLEditorTreeViewItem = ({
{
@@ -189,10 +183,11 @@ export const SQLEditorTreeViewItem = ({
if (isEditing) {
return
}
-
- onClick(e)
}
}}
+ {...props}
+ name={element.name}
+ xPadding={16}
/>
e.stopPropagation()}>
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SqlEditorMenuStaticLinks.tsx b/apps/studio/components/layouts/SQLEditorLayout/SqlEditorMenuStaticLinks.tsx
new file mode 100644
index 00000000000..12b3bbcda81
--- /dev/null
+++ b/apps/studio/components/layouts/SQLEditorLayout/SqlEditorMenuStaticLinks.tsx
@@ -0,0 +1,33 @@
+import { useParams } from 'common'
+import { useRouter } from 'next/router'
+import { InnerSideMenuDataItem } from 'ui-patterns/InnerSideMenu'
+
+const OPTIONS = ['templates', 'quickstarts'] as const
+
+export function SqlEditorMenuStaticLinks() {
+ const { ref } = useParams()
+ const router = useRouter()
+
+ function isPageActive(key: string): boolean {
+ return router.asPath === `/project/${ref}/sql/${key}`
+ }
+
+ return (
+
+ {OPTIONS.map((pageId) => {
+ const active = isPageActive(pageId)
+ return (
+
+ {pageId}
+
+ )
+ })}
+
+ )
+}
diff --git a/apps/studio/components/layouts/TableEditorLayout/EditorMenuListSkeleton.tsx b/apps/studio/components/layouts/TableEditorLayout/EditorMenuListSkeleton.tsx
new file mode 100644
index 00000000000..163d653cdc3
--- /dev/null
+++ b/apps/studio/components/layouts/TableEditorLayout/EditorMenuListSkeleton.tsx
@@ -0,0 +1,25 @@
+import { memo } from 'react'
+import { Skeleton } from 'ui'
+
+const EditorMenuListSkeleton = memo(function EditorMenuListSkeleton() {
+ const items = [
+ { width: 'w-40', opacity: 'opacity-100' },
+ { width: 'w-32', opacity: 'opacity-100' },
+ { width: 'w-20', opacity: 'opacity-75' },
+ { width: 'w-40', opacity: 'opacity-50' },
+ { width: 'w-20', opacity: 'opacity-25' },
+ ]
+
+ return (
+
+ {items.map((item, index) => (
+
+
+
+
+ ))}
+
+ )
+})
+
+export default EditorMenuListSkeleton
diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
index ea2530ffa04..9d59aa0834c 100644
--- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
@@ -1,20 +1,3 @@
-import saveAs from 'file-saver'
-import {
- Clipboard,
- Copy,
- Download,
- Edit,
- Eye,
- Lock,
- MoreHorizontal,
- Table2,
- Trash,
- Unlock,
-} from 'lucide-react'
-import Link from 'next/link'
-import Papa from 'papaparse'
-import { toast } from 'sonner'
-
import { IS_PLATFORM } from 'common'
import {
MAX_EXPORT_ROW_COUNT,
@@ -25,6 +8,7 @@ import {
formatTableRowsToSQL,
getEntityLintDetails,
} from 'components/interfaces/TableGridEditor/TableEntity.utils'
+import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import type { ItemRenderer } from 'components/ui/InfiniteList'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { Entity } from 'data/entity-types/entity-types-infinite-query'
@@ -33,8 +17,14 @@ import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { getTableEditor } from 'data/table-editor/table-editor-query'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { fetchAllTableRows } from 'data/table-rows/table-rows-query'
+import saveAs from 'file-saver'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
+import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { copyToClipboard } from 'lib/helpers'
+import { Clipboard, Copy, Download, Edit, Lock, MoreHorizontal, Trash, Unlock } from 'lucide-react'
+import Link from 'next/link'
+import Papa from 'papaparse'
+import { toast } from 'sonner'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
cn,
@@ -49,13 +39,16 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
+ TreeViewItemVariant,
} from 'ui'
+import { useSnapshot } from 'valtio'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
export interface EntityListItemProps {
- id: number
+ id: number | string
projectRef: string
isLocked: boolean
+ isActive?: boolean
}
const EntityListItem: ItemRenderer = ({
@@ -63,6 +56,7 @@ const EntityListItem: ItemRenderer = ({
projectRef,
item: entity,
isLocked,
+ isActive: _isActive,
}) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
@@ -212,106 +206,25 @@ const EntityListItem: ItemRenderer = ({
}
}
- const EntityTooltipTrigger = ({ entity }: { entity: Entity }) => {
- let tooltipContent = null
-
- switch (entity.type) {
- case ENTITY_TYPE.TABLE:
- if (tableHasLints) {
- tooltipContent = 'RLS disabled'
- }
- break
- case ENTITY_TYPE.VIEW:
- if (viewHasLints) {
- tooltipContent = 'Security definer view'
- }
- break
- case ENTITY_TYPE.MATERIALIZED_VIEW:
- if (materializedViewHasLints) {
- tooltipContent = 'Security definer view'
- }
- break
- case ENTITY_TYPE.FOREIGN_TABLE:
- tooltipContent = 'RLS is not enforced on foreign tables'
- break
- default:
- break
- }
-
- if (tooltipContent) {
- return (
-
-
-
-
- {tooltipContent}
-
- )
- }
-
- return null
- }
-
return (
-
-
+
+ <>
+ {isActive && }
-
- {entity.type === ENTITY_TYPE.TABLE ? (
-
- ) : entity.type === ENTITY_TYPE.VIEW ? (
-
- ) : (
-
- {Object.entries(ENTITY_TYPE)
- .find(([, value]) => value === entity.type)?.[0]?.[0]
- ?.toUpperCase()}
-
- )}
+
+
{formatTooltipText(entity.type)}
@@ -332,12 +245,18 @@ const EntityListItem: ItemRenderer = ({
>
{entity.name}
-
+
{canEdit && (
-
+
@@ -435,9 +354,67 @@ const EntityListItem: ItemRenderer = ({
)}
-
-
+ >
+
)
}
+const EntityTooltipTrigger = ({
+ entity,
+ isActive,
+ tableHasLints,
+ viewHasLints,
+ materializedViewHasLints,
+}: {
+ entity: Entity
+ isActive: boolean
+ tableHasLints: boolean
+ viewHasLints: boolean
+ materializedViewHasLints: boolean
+}) => {
+ let tooltipContent = ''
+
+ switch (entity.type) {
+ case ENTITY_TYPE.TABLE:
+ if (tableHasLints) {
+ tooltipContent = 'RLS disabled'
+ }
+ break
+ case ENTITY_TYPE.VIEW:
+ if (viewHasLints) {
+ tooltipContent = 'Security definer view'
+ }
+ break
+ case ENTITY_TYPE.MATERIALIZED_VIEW:
+ if (materializedViewHasLints) {
+ tooltipContent = 'Security definer view'
+ }
+ break
+ case ENTITY_TYPE.FOREIGN_TABLE:
+ tooltipContent = 'RLS is not enforced on foreign tables'
+ break
+ default:
+ break
+ }
+
+ if (tooltipContent) {
+ return (
+
+
+
+
+
+ {tooltipContent}
+
+
+ )
+ }
+
+ return null
+}
+
export default EntityListItem
diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
index be47257ac42..eda09f71fc2 100644
--- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
@@ -1,11 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { partition } from 'lodash'
-import { Filter, Plus } from 'lucide-react'
-import { useRouter } from 'next/router'
-import { useEffect, useMemo, useState } from 'react'
-
-import { useBreakpoint, useParams } from 'common'
+import { useParams } from 'common'
+import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning'
+import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InfiniteList from 'components/ui/InfiniteList'
@@ -18,6 +15,10 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { partition } from 'lodash'
+import { Filter, Plus } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { useEffect, useMemo, useState } from 'react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
AlertDescription_Shadcn_,
@@ -36,10 +37,10 @@ import {
InnerSideBarFilterSortDropdown,
InnerSideBarFilterSortDropdownItem,
InnerSideBarFilters,
- InnerSideBarShimmeringLoaders,
} from 'ui-patterns/InnerSideMenu'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import EntityListItem from './EntityListItem'
+import { TableMenuEmptyState } from './TableMenuEmptyState'
const TableEditorMenu = () => {
const { id: _id } = useParams()
@@ -114,10 +115,7 @@ const TableEditorMenu = () => {
return (
<>
-
+
-
-
+
+
{
- {isLoading &&
}
+ {isLoading &&
}
{isError && (
-
+
)}
{isSuccess && (
<>
{searchText.length === 0 && (entityTypes?.length ?? 0) <= 0 && (
-
+
)}
{searchText.length > 0 && (entityTypes?.length ?? 0) <= 0 && (
{
/>
)}
{(entityTypes?.length ?? 0) > 0 && (
-
+
{
+ return (
+
+
+
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+ {i === 2 &&
}
+
+ {`postgres_table_${i}`}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/apps/studio/components/ui/EntityTypeIcon.tsx b/apps/studio/components/ui/EntityTypeIcon.tsx
new file mode 100644
index 00000000000..4538f1ffdb3
--- /dev/null
+++ b/apps/studio/components/ui/EntityTypeIcon.tsx
@@ -0,0 +1,80 @@
+import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
+import { Eye, GitBranch, Table2 } from 'lucide-react'
+import { cn, SQL_ICON } from 'ui'
+
+interface EntityTypeIconProps {
+ type: 'sql' | 'schema' | 'new' | 'r' | 'v' | 'm' | 'f' | 'p'
+ size?: number
+ strokeWidth?: number
+ isActive?: boolean
+}
+
+export const EntityTypeIcon = ({
+ type,
+ size = 15,
+ strokeWidth = 1.5,
+ isActive,
+}: EntityTypeIconProps) => {
+ if (type === 'sql') {
+ return (
+
+ )
+ }
+
+ if (type === ENTITY_TYPE.TABLE) {
+ return (
+
+ )
+ }
+
+ if (type === 'schema') {
+ return
+ }
+
+ if (type === ENTITY_TYPE.VIEW) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {Object.entries(ENTITY_TYPE)
+ .find(([, value]) => value === type)?.[0]?.[0]
+ ?.toUpperCase()}
+
+ )
+}
diff --git a/apps/studio/components/ui/InfiniteList.tsx b/apps/studio/components/ui/InfiniteList.tsx
index 5d318a97bcd..2a3ece04e04 100644
--- a/apps/studio/components/ui/InfiniteList.tsx
+++ b/apps/studio/components/ui/InfiniteList.tsx
@@ -1,11 +1,10 @@
+import { propsAreEqual } from 'lib/helpers'
import memoize from 'memoize-one'
import { CSSProperties, ComponentType, MutableRefObject, ReactNode, memo, useRef } from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { VariableSizeList, areEqual } from 'react-window'
import InfiniteLoader from 'react-window-infinite-loader'
-
-import ShimmeringLoader from 'components/ui/ShimmeringLoader'
-import { propsAreEqual } from 'lib/helpers'
+import { Skeleton } from 'ui'
/**
* Note that the loading more logic of this component works best with a cursor-based
@@ -58,9 +57,28 @@ const Item = memo(({ data, index, style }: ItemProps) => {
{LoaderComponent}
) : (
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}, areEqual)
@@ -92,35 +110,34 @@ function InfiniteList({
const itemData = createItemData(items, { itemProps, ItemComponent, LoaderComponent, listRef })
return (
-
-
-
- {({ height, width }: { height: number; width: number }) => (
-
- {({ onItemsRendered, ref }) => (
- {
- ref(refy)
- listRef.current = refy
- }}
- height={height ?? 0}
- width={width ?? 0}
- itemCount={itemCount}
- itemData={itemData}
- itemSize={getItemSize}
- onItemsRendered={onItemsRendered}
- >
- {Item}
-
- )}
-
- )}
-
-
+ <>
+
+ {({ height, width }: { height: number; width: number }) => (
+
+ {({ onItemsRendered, ref }) => (
+ {
+ ref(refy)
+ listRef.current = refy
+ }}
+ height={height ?? 0}
+ width={width ?? 0}
+ itemCount={itemCount}
+ itemData={itemData}
+ itemSize={getItemSize}
+ onItemsRendered={onItemsRendered}
+ >
+ {Item}
+
+ )}
+
+ )}
+
+
({
pointerEvents: 'none', //https://github.com/bvaughn/react-window/issues/455
}}
/>
-
+ >
)
}
diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx
index 8ab840df5f4..5bda5d52592 100644
--- a/apps/studio/components/ui/SchemaSelector.tsx
+++ b/apps/studio/components/ui/SchemaSelector.tsx
@@ -1,10 +1,9 @@
-import { Check, ChevronsUpDown, Plus } from 'lucide-react'
-import { useState } from 'react'
-
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { Check, ChevronsUpDown, Plus } from 'lucide-react'
+import { useState } from 'react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -21,6 +20,7 @@ import {
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
ScrollArea,
+ Skeleton,
} from 'ui'
interface SchemaSelectorProps {
@@ -69,8 +69,14 @@ const SchemaSelector = ({
return (
{isSchemasLoading && (
-