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 && ( - )} @@ -95,14 +101,15 @@ const SchemaSelector = ({ size={size} disabled={disabled} type="default" - className={`w-full [&>span]:w-full`} + data-testid="schema-selector" + className={`w-full [&>span]:w-full !pr-1 space-x-1`} iconRight={ } > {selectedSchemaName ? (
-

schema:

+

schema

{selectedSchemaName === '*' ? 'All schemas' : selectedSchemaName}

@@ -114,7 +121,12 @@ const SchemaSelector = ({ )} - + diff --git a/packages/ui-patterns/InnerSideMenu/index.tsx b/packages/ui-patterns/InnerSideMenu/index.tsx index 8dc98935f07..961f3b43fa6 100644 --- a/packages/ui-patterns/InnerSideMenu/index.tsx +++ b/packages/ui-patterns/InnerSideMenu/index.tsx @@ -15,6 +15,7 @@ import { Tooltip, TooltipContent, TooltipTrigger, + TreeViewItemVariant, cn, } from 'ui' import ShimmeringLoader from '../ShimmeringLoader' @@ -115,6 +116,37 @@ const InnerSideMenuItem = forwardRef< ) }) +const InnerSideMenuDataItem = forwardRef< + ElementRef, + React.ComponentPropsWithoutRef & { + isActive?: boolean + forceHoverState?: boolean | null + isPreview?: boolean + isOpened?: boolean + } +>(({ isActive = true, forceHoverState, isPreview, isOpened = true, ...props }, ref) => { + return ( + + {!isPreview && isActive &&
} + {props.children} + + ) +}) + function InnerSideMenuItemLoading({ className, ...props @@ -250,17 +282,18 @@ const InnerSideBarEmptyPanel = forwardRef< ref={ref} {...props} className={cn( - 'border bg-surface-100/50 flex flex-col gap-y-3 items-center justify-center rounded-md px-5 py-4', + 'border border-muted bg-surface-100 dark:bg-surface-75 flex flex-col gap-y-3 items-center justify-center rounded-md px-5 py-4', props.className )} > -
+
{illustration} {title &&

{title}

} {description && (

{description}

)} {actions &&
{actions}
} + {props.children}
) @@ -277,6 +310,7 @@ export { InnerSideMenuCollapsible, InnerSideMenuCollapsibleContent, InnerSideMenuCollapsibleTrigger, + InnerSideMenuDataItem, InnerSideMenuItem, InnerSideMenuItemLoading, InnerSideMenuSeparator, diff --git a/packages/ui/src/components/TreeView/TreeView.tsx b/packages/ui/src/components/TreeView/TreeView.tsx index 8673e308b13..96a8cf30def 100644 --- a/packages/ui/src/components/TreeView/TreeView.tsx +++ b/packages/ui/src/components/TreeView/TreeView.tsx @@ -5,9 +5,33 @@ import { ComponentPropsWithoutRef, ReactNode, forwardRef, useEffect, useRef, use import TreeViewPrimitive, { flattenTree } from 'react-accessible-treeview' import { cn } from '../../lib/utils' import { Input } from '../shadcn/ui/input' +import { cva, VariantProps } from 'class-variance-authority' const TreeView = TreeViewPrimitive +export type TreeViewItemVariantProps = VariantProps +export const TreeViewItemVariant = cva( + // [Joshen Temp]: aria-selected:text-foreground not working as aria-selected property not rendered in DOM, + // [Joshen Temp]: aria-selected:!bg-selection not working as aria-selected property not rendered in DOM + 'group relative transition-colors h-[28px] flex items-center gap-3 text-sm cursor-pointer select-none text-foreground-light hover:bg-control aria-expanded:bg-control data-[state=open]:bg-control', // data-[state=open]:bg-control bg state for context menu open + { + variants: { + isSelected: { + true: 'text-foreground !bg-selection', // bg state for context menu open + false: '', + }, + isOpened: { + true: 'bg-control', + false: '', + }, + isPreview: { + true: 'bg-control text-foreground', + false: '', + }, + }, + } +) + const TreeViewItem = forwardRef< HTMLDivElement, ComponentPropsWithoutRef<'div'> & { @@ -15,6 +39,10 @@ const TreeViewItem = forwardRef< level: number /** Specifies if the item is expanded */ isExpanded?: boolean + /** Specifies if the item is opened somewhere */ + isOpened?: boolean + /** Specifies if the item is a preview */ + isPreview?: boolean /** Specifies if the item is a branch */ isBranch?: boolean /** The padding for each level of the item */ @@ -40,8 +68,10 @@ const TreeViewItem = forwardRef< level = 1, levelPadding = 56, isExpanded = false, + isOpened = false, isBranch = false, isSelected = false, + isPreview = false, isLoading = false, xPadding = 16, name = '', @@ -92,20 +122,7 @@ const TreeViewItem = forwardRef< aria-selected={isSelected} aria-expanded={!isEditing && isExpanded} {...props} - className={cn( - 'group relative', - 'transition-colors', - 'h-[28px]', - 'flex items-center gap-3', - 'text-sm', - 'cursor-pointer select-none', - 'text-foreground-light', - 'hover:bg-control', - 'aria-expanded:bg-control', - isSelected ? 'text-foreground' : '', // [Joshen Temp]: aria-selected:text-foreground not working as aria-selected property not rendered in DOM, - isSelected ? '!bg-selection' : '', // [Joshen Temp]: aria-selected:!bg-selection not working as aria-selected property not rendered in DOM - 'data-[state=open]:bg-control' // bg state for context menu open - )} + className={cn(TreeViewItemVariant({ isSelected, isOpened, isPreview }))} style={{ paddingLeft: level === 1 && !isBranch diff --git a/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts b/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts index 55196008d28..6323716141f 100644 --- a/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts +++ b/tests/local-studio-tests/tests/snapshot/spec/table-editor.spec.ts @@ -108,7 +108,7 @@ test.describe('Table Editor page', () => { { timeout: 0 } ) - await page.getByRole('button', { name: 'schema: public' }).click() + await page.getByTestId('schema-selector').click() await page.getByRole('option', { name: 'auth' }).click() // wait for the table data to load for the auth schema