mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
chore: paginate user content (#30675)
* chore: paginate user content * progress * loading states * add load more buttons to private snippets * working pagination * fix some types * always show snippet counts * support new api parameters * favorite snippets * progress * searching * paginate root folder * fix renaming snippets * fix ts * removed unused prop * Shift sharing/unsharing query logic outside of valtio to leverage on RQ only * Fix invalidation on an unsaved snippet * Clean up * Fix * Clean up * Update API type * Update API * fix duplicate snippets error after moving a snippet * add currently selected snippet * Fix unsharing a snippet that has yet to be opened * i'm dumb * fix sharing a snippet * fix sharing and unsharing * show favorite or shared snippet in list even if it's in another page * Fix wrong import for debounce * Fix false positive toast error when creating custom report * Update API type * Change create new snippet CTA to link back to /new with skip flag * Fix saving logs explorer query * Bump page number --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
f25ef1e4d5
commit
f6d9617413
42 files changed
+1277
-1009
No files matched your search
@@ -1,4 +1,3 @@
|
||||
import { toString as CronToString } from 'cronstrue'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { UseFormReturn } from 'react-hook-form'
|
||||
@@ -26,7 +25,7 @@ import {
|
||||
} from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { CreateCronJobForm } from './CreateCronJobSheet'
|
||||
import { formatScheduleString, getScheduleMessage, secondsPattern } from './CronJobs.utils'
|
||||
import { formatScheduleString, getScheduleMessage } from './CronJobs.utils'
|
||||
import CronSyntaxChart from './CronSyntaxChart'
|
||||
|
||||
interface CronJobScheduleSectionProps {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const CreateReportModal = ({ visible, onCancel, afterSubmit }: CreateRepo
|
||||
const { mutate: insertReport, isLoading: isCreating } = useContentInsertMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success('Successfully created new report')
|
||||
const newReportId = data[0].id
|
||||
const newReportId = data.id
|
||||
router.push(`/project/${ref}/reports/${newReportId}`)
|
||||
afterSubmit()
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ export const UpdateCustomReportModal = ({
|
||||
projectRef: ref,
|
||||
payload: {
|
||||
...selectedReport,
|
||||
owner_id: selectedReport.owner_id!,
|
||||
project_id: selectedReport.project_id,
|
||||
id: selectedReport.id,
|
||||
name: newVals.name,
|
||||
|
||||
@@ -34,7 +34,10 @@ const Reports = () => {
|
||||
const [endDate, setEndDate] = useState<any>(null)
|
||||
const [hasEdits, setHasEdits] = useState<any>(false)
|
||||
|
||||
const { data: userContents, isLoading } = useContentQuery(ref)
|
||||
const { data: userContents, isLoading } = useContentQuery({
|
||||
projectRef: ref,
|
||||
type: 'report',
|
||||
})
|
||||
const { mutate: saveReport, isLoading: isSaving } = useContentUpdateMutation({
|
||||
onSuccess: () => {
|
||||
setHasEdits(false)
|
||||
|
||||
@@ -190,7 +190,7 @@ const MonacoEditor = ({
|
||||
onMount={handleEditorOnMount}
|
||||
onChange={handleEditorChange}
|
||||
defaultLanguage="pgsql"
|
||||
defaultValue={snippet?.snippet.content.sql}
|
||||
defaultValue={snippet?.snippet.content?.sql}
|
||||
path={id}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
|
||||
@@ -9,8 +9,12 @@ import { useParams } from 'common'
|
||||
import { getContentById } from 'data/content/content-id-query'
|
||||
import { useContentUpsertV2Mutation } from 'data/content/content-upsert-v2-mutation'
|
||||
import { useSQLSnippetFolderCreateMutation } from 'data/content/sql-folder-create-mutation'
|
||||
import { Snippet, SnippetDetail } from 'data/content/sql-folders-query'
|
||||
import { useSnippetFolders, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
||||
import { Snippet } from 'data/content/sql-folders-query'
|
||||
import {
|
||||
SnippetWithContent,
|
||||
useSnippetFolders,
|
||||
useSqlEditorV2StateSnapshot,
|
||||
} from 'state/sql-editor-v2'
|
||||
import {
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
@@ -125,16 +129,18 @@ export const MoveQueryModal = ({ visible, snippets = [], onClose }: MoveQueryMod
|
||||
|
||||
await Promise.all(
|
||||
snippets.map(async (snippet) => {
|
||||
let snippetContent = (snippet as SnippetDetail)?.content
|
||||
let snippetContent = (snippet as SnippetWithContent)?.content
|
||||
if (snippetContent === undefined) {
|
||||
const { content } = await getContentById({ projectRef: ref, id: snippet.id })
|
||||
snippetContent = content
|
||||
if ('sql' in content) {
|
||||
snippetContent = content
|
||||
}
|
||||
}
|
||||
|
||||
if (snippetContent === undefined) {
|
||||
return toast.error('Failed to save snippet: Unable to retrieve snippet contents')
|
||||
} else {
|
||||
moveSnippetAsync({
|
||||
await moveSnippetAsync({
|
||||
projectRef: ref,
|
||||
payload: {
|
||||
id: snippet.id,
|
||||
@@ -144,7 +150,7 @@ export const MoveQueryModal = ({ visible, snippets = [], onClose }: MoveQueryMod
|
||||
visibility: snippet.visibility,
|
||||
project_id: snippet.project_id,
|
||||
owner_id: snippet.owner_id,
|
||||
folder_id: selectedId === 'root' ? (null as any) : folderId,
|
||||
folder_id: selectedId === 'root' ? null : folderId,
|
||||
content: snippetContent as any,
|
||||
},
|
||||
})
|
||||
@@ -158,7 +164,7 @@ export const MoveQueryModal = ({ visible, snippets = [], onClose }: MoveQueryMod
|
||||
snippets.forEach((snippet) => {
|
||||
snapV2.updateSnippet({
|
||||
id: snippet.id,
|
||||
snippet: { ...snippet, folder_id: selectedId === 'root' ? (null as any) : selectedId },
|
||||
snippet: { ...snippet, folder_id: selectedId === 'root' ? null : selectedId },
|
||||
skipSave: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { toast } from 'sonner'
|
||||
import { useParams } from 'common'
|
||||
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
|
||||
import { getContentById } from 'data/content/content-id-query'
|
||||
import { useContentUpdateMutation } from 'data/content/content-update-mutation'
|
||||
import { Snippet } from 'data/content/sql-folders-query'
|
||||
import type { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
@@ -57,7 +58,9 @@ const RenameQueryModal = ({
|
||||
} else {
|
||||
try {
|
||||
const { content } = await getContentById({ projectRef: ref, id: snippet.id })
|
||||
titleSql({ sql: content.sql })
|
||||
if ('sql' in content) {
|
||||
titleSql({ sql: content.sql })
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Unable to generate title based on query contents')
|
||||
}
|
||||
@@ -70,24 +73,44 @@ const RenameQueryModal = ({
|
||||
return errors
|
||||
}
|
||||
|
||||
const { mutateAsync: updateContent } = useContentUpdateMutation()
|
||||
|
||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
if (!id) return console.error('Snippet ID is required')
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
// [Joshen] For SQL V2 - content is loaded on demand so we need to fetch the data if its not already loaded in the valtio state
|
||||
if (!('content' in snippet)) {
|
||||
// [Joshen] I feel like there's definitely some optimization we can do here but will involve changes to API
|
||||
const snippet = await getContentById({ projectRef: ref, id })
|
||||
snapV2.addSnippet({ projectRef: ref, snippet })
|
||||
}
|
||||
snapV2.renameSnippet({ id, name: nameInput, description: descriptionInput })
|
||||
let localSnippet = snippet
|
||||
|
||||
// [Joshen] For SQL V2 - content is loaded on demand so we need to fetch the data if its not already loaded in the valtio state
|
||||
if (!('content' in localSnippet)) {
|
||||
localSnippet = await getContentById({ projectRef: ref, id })
|
||||
|
||||
snapV2.addSnippet({ projectRef: ref, snippet: localSnippet })
|
||||
}
|
||||
|
||||
const updatedSnippet = await updateContent({
|
||||
projectRef: ref,
|
||||
id,
|
||||
type: localSnippet.type,
|
||||
content: (localSnippet as any).content,
|
||||
name: nameInput,
|
||||
description: descriptionInput,
|
||||
})
|
||||
|
||||
snapV2.renameSnippet({
|
||||
id,
|
||||
name: updatedSnippet.name,
|
||||
description: updatedSnippet.description,
|
||||
})
|
||||
|
||||
toast.success('Successfully renamed snippet!')
|
||||
if (onComplete) onComplete()
|
||||
} catch (error: any) {
|
||||
setSubmitting(false)
|
||||
// [Joshen] We probably need some rollback cause all the saving is async
|
||||
toast.error(`Failed to rename query: ${error.message}`)
|
||||
toast.error(`Failed to rename snippet: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ export const SQLEditor = () => {
|
||||
const selection = editor.getSelection()
|
||||
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : undefined
|
||||
const sql = snippet
|
||||
? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content.sql
|
||||
? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content?.sql
|
||||
: selectedValue || editorRef.current?.getValue()
|
||||
formatQuery(
|
||||
{
|
||||
@@ -267,7 +267,7 @@ export const SQLEditor = () => {
|
||||
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : undefined
|
||||
|
||||
const sql = snippet
|
||||
? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content.sql
|
||||
? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content?.sql
|
||||
: selectedValue || editorRef.current?.getValue()
|
||||
|
||||
let queryHasIssues = false
|
||||
@@ -373,7 +373,9 @@ export const SQLEditor = () => {
|
||||
const result = snapV2.results[id]?.[0]
|
||||
appSnap.setAiAssistantPanel({
|
||||
open: true,
|
||||
sqlSnippets: [snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim()],
|
||||
sqlSnippets: [
|
||||
(snippet.snippet.content?.sql ?? '').replace(sqlAiDisclaimerComment, '').trim(),
|
||||
],
|
||||
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${result.error.message}`,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SnippetDetail } from 'data/content/sql-folders-query'
|
||||
import { removeCommentsFromSql } from 'lib/helpers'
|
||||
import type { SnippetWithContent } from 'state/sql-editor-v2'
|
||||
import type { SqlSnippets, UserContent } from 'types'
|
||||
import {
|
||||
NEW_SQL_SNIPPET_SKELETON,
|
||||
@@ -52,7 +52,7 @@ export const createSqlSnippetSkeletonV2 = ({
|
||||
owner_id: number
|
||||
project_id: number
|
||||
folder_id?: string
|
||||
}): SnippetDetail => {
|
||||
}): SnippetWithContent => {
|
||||
return {
|
||||
...NEW_SQL_SNIPPET_SKELETON,
|
||||
id,
|
||||
|
||||
@@ -14,8 +14,6 @@ import { toast } from 'sonner'
|
||||
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import { Content, ContentData } from 'data/content/content-query'
|
||||
import { contentKeys } from 'data/content/keys'
|
||||
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { detectOS } from 'lib/helpers'
|
||||
@@ -74,50 +72,10 @@ const UtilityActions = ({
|
||||
|
||||
const addFavorite = async () => {
|
||||
snapV2.addFavorite(id)
|
||||
|
||||
client.setQueryData<ContentData>(
|
||||
contentKeys.list(project?.ref),
|
||||
(oldData: ContentData | undefined) => {
|
||||
if (!oldData) return
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
content: oldData.content.map((content: Content) => {
|
||||
if (content.type === 'sql' && content.id === id) {
|
||||
return {
|
||||
...content,
|
||||
content: { ...content.content, favorite: true },
|
||||
}
|
||||
}
|
||||
return content
|
||||
}),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const removeFavorite = async () => {
|
||||
snapV2.removeFavorite(id)
|
||||
|
||||
client.setQueryData<ContentData>(
|
||||
contentKeys.list(project?.ref),
|
||||
(oldData: ContentData | undefined) => {
|
||||
if (!oldData) return
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
content: oldData.content.map((content: Content) => {
|
||||
if (content.type === 'sql' && content.id === id) {
|
||||
return {
|
||||
...content,
|
||||
content: { ...content.content, favorite: false },
|
||||
}
|
||||
}
|
||||
return content
|
||||
}),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -47,7 +47,6 @@ const UtilityPanel = ({
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
|
||||
const snippet = snapV2.snippets[id]?.snippet
|
||||
const queryKeys = contentKeys.list(ref)
|
||||
const result = snapV2.results[id]?.[0]
|
||||
|
||||
const { mutate: upsertContent } = useContentUpsertMutation({
|
||||
@@ -60,9 +59,6 @@ const UtilityPanel = ({
|
||||
if (payload.type !== 'sql') return
|
||||
if (!('chart' in payload.content)) return
|
||||
|
||||
// Cancel any existing queries so that the new content is fetched
|
||||
await queryClient.cancelQueries(queryKeys)
|
||||
|
||||
const newSnippet = {
|
||||
...snippet,
|
||||
content: {
|
||||
@@ -83,7 +79,7 @@ const UtilityPanel = ({
|
||||
return DEFAULT_CHART_CONFIG
|
||||
}
|
||||
|
||||
if (!snippet.content.chart) {
|
||||
if (!snippet.content?.chart) {
|
||||
return DEFAULT_CHART_CONFIG
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ interface SavedQueriesItemProps {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
owner_id: number
|
||||
content: {
|
||||
sql: string
|
||||
}
|
||||
|
||||
@@ -96,7 +96,10 @@ export function LogsSidebarMenuV2() {
|
||||
{ enabled: IS_PLATFORM && warehouseEnabled && !!tenantData }
|
||||
)
|
||||
|
||||
const { data: savedQueriesRes, isLoading: savedQueriesLoading } = useContentQuery(ref)
|
||||
const { data: savedQueriesRes, isLoading: savedQueriesLoading } = useContentQuery({
|
||||
projectRef: ref,
|
||||
type: 'log_sql',
|
||||
})
|
||||
|
||||
const savedQueries = [...(savedQueriesRes?.content ?? [])]
|
||||
.filter((c) => c.type === 'log_sql')
|
||||
|
||||
@@ -31,7 +31,10 @@ const ReportsMenu = () => {
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
const { data: content, isLoading } = useContentQuery(ref)
|
||||
const { data: content, isLoading } = useContentQuery({
|
||||
projectRef: ref,
|
||||
type: 'report',
|
||||
})
|
||||
const { mutate: deleteReport, isLoading: isDeleting } = useContentDeleteMutation({
|
||||
onSuccess: () => {
|
||||
setDeleteModalOpen(false)
|
||||
@@ -154,7 +157,7 @@ const ReportsMenu = () => {
|
||||
{reportMenuItems.map((item) => (
|
||||
<ReportMenuItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
item={item as any}
|
||||
pageKey={pageKey}
|
||||
onSelectEdit={() => {
|
||||
setSelectedReportToUpdate(item.report)
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
InnerSideMenuItem,
|
||||
} from 'ui-patterns/InnerSideMenu'
|
||||
import { SQLEditorNav as SQLEditorNavV2 } from './SQLEditorNavV2/SQLEditorNav'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
|
||||
interface SQLEditorMenuProps {
|
||||
onViewOngoingQueries: () => void
|
||||
@@ -40,6 +41,8 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => {
|
||||
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [sort, setSort] = useLocalStorage<'name' | 'inserted_at'>('sql-editor-sort', 'inserted_at')
|
||||
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
@@ -47,11 +50,8 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => {
|
||||
})
|
||||
|
||||
const createNewFolder = () => {
|
||||
// [Joshen] LEFT OFF: We need to figure out a good UX for creating folders
|
||||
// - Modal? Directly chuck into the tree view like storage explorer?
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
snapV2.addNewFolder({ projectRef: ref })
|
||||
// createFolder({ projectRef: ref, name: 'test' })
|
||||
}
|
||||
|
||||
const handleNewQuery = async () => {
|
||||
@@ -61,17 +61,8 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => {
|
||||
if (!canCreateSQLSnippet) {
|
||||
return toast('Your queries will not be saved as you do not have sufficient permissions')
|
||||
}
|
||||
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeletonV2({
|
||||
id: uuidv4(),
|
||||
name: untitledSnippetTitle,
|
||||
owner_id: profile.id,
|
||||
project_id: project.id,
|
||||
sql: '',
|
||||
})
|
||||
snapV2.addSnippet({ projectRef: ref, snippet })
|
||||
router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
router.push(`/project/${ref}/sql/new?skip=true`)
|
||||
setSearchText('')
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to create new query: ${error.message}`)
|
||||
@@ -89,10 +80,11 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => {
|
||||
aria-labelledby="Search queries"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
isLoading={isSearching}
|
||||
>
|
||||
<InnerSideBarFilterSortDropdown
|
||||
value={snapV2.order}
|
||||
onValueChange={(value: any) => snapV2.setOrder(value)}
|
||||
value={sort}
|
||||
onValueChange={(value: any) => setSort(value)}
|
||||
>
|
||||
<InnerSideBarFilterSortDropdownItem key="name" value="name">
|
||||
Alphabetical
|
||||
@@ -141,7 +133,7 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => {
|
||||
</InnerSideMenuItem>
|
||||
</div>
|
||||
|
||||
<SQLEditorNavV2 searchText={searchText} />
|
||||
<SQLEditorNavV2 searchText={searchText} sort={sort} setIsSearching={setIsSearching} />
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t sticky bottom-0 bg-studio">
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Skeleton } from 'ui'
|
||||
|
||||
const SQLEditorLoadingSnippets = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-40 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-32 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3 opacity-75">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-20 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3 opacity-50">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-40 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3 opacity-25">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-20 h-4" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SQLEditorLoadingSnippets
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDebounce } from '@uidotdev/usehooks'
|
||||
import { Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
@@ -13,25 +14,17 @@ import { useContentCountQuery } from 'data/content/content-count-query'
|
||||
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
|
||||
import { getContentById } from 'data/content/content-id-query'
|
||||
import { useSQLSnippetFoldersDeleteMutation } from 'data/content/sql-folders-delete-mutation'
|
||||
import {
|
||||
Snippet,
|
||||
SnippetDetail,
|
||||
SnippetFolder,
|
||||
getSQLSnippetFolders,
|
||||
useSQLSnippetFoldersQuery,
|
||||
} from 'data/content/sql-folders-query'
|
||||
import { Snippet, SnippetFolder, useSQLSnippetFoldersQuery } from 'data/content/sql-folders-query'
|
||||
import { useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import uuidv4 from 'lib/uuid'
|
||||
import {
|
||||
useFavoriteSnippets,
|
||||
SnippetWithContent,
|
||||
useSnippetFolders,
|
||||
useSnippets,
|
||||
useSqlEditorV2StateSnapshot,
|
||||
} from 'state/sql-editor-v2'
|
||||
import { Separator, Skeleton, TreeView } from 'ui'
|
||||
import { cn, Separator, TreeView } from 'ui'
|
||||
import {
|
||||
InnerSideBarEmptyPanel,
|
||||
InnerSideMenuCollapsible,
|
||||
@@ -40,27 +33,38 @@ import {
|
||||
InnerSideMenuSeparator,
|
||||
} from 'ui-patterns'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { ROOT_NODE, formatFolderResponseForTreeView } from './SQLEditorNav.utils'
|
||||
import SQLEditorLoadingSnippets from './SQLEditorLoadingSnippets'
|
||||
import { ROOT_NODE, formatFolderResponseForTreeView, getLastItemIds } from './SQLEditorNav.utils'
|
||||
import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem'
|
||||
import { useContentUpsertV2Mutation } from 'data/content/content-upsert-v2-mutation'
|
||||
import { SqlSnippets } from 'types'
|
||||
|
||||
interface SQLEditorNavProps {
|
||||
searchText: string
|
||||
sort?: 'inserted_at' | 'name'
|
||||
setIsSearching?: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) => {
|
||||
export const SQLEditorNav = ({
|
||||
searchText: _searchText,
|
||||
sort = 'inserted_at',
|
||||
setIsSearching,
|
||||
}: SQLEditorNavProps) => {
|
||||
const searchText = _searchText.trim()
|
||||
const debouncedSearchText = useDebounce(searchText, 250)
|
||||
const isSearching = searchText.length > 0
|
||||
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
const project = useSelectedProject()
|
||||
const { ref: projectRef, id } = useParams()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const [sort] = useLocalStorage<'name' | 'inserted_at'>('sql-editor-sort', 'inserted_at')
|
||||
|
||||
const [mountedId, setMountedId] = useState(false)
|
||||
const [showMoveModal, setShowMoveModal] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [showFavouriteSnippets, setShowFavouriteSnippets] = useState(false)
|
||||
const [showFavoriteSnippets, setShowFavoriteSnippets] = useState(false)
|
||||
const [showSharedSnippets, setShowSharedSnippets] = useState(false)
|
||||
const [showPrivateSnippets, setShowPrivateSnippets] = useState(true)
|
||||
|
||||
@@ -72,76 +76,267 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
const [selectedSnippetToDownload, setSelectedSnippetToDownload] = useState<Snippet>()
|
||||
const [selectedFolderToDelete, setSelectedFolderToDelete] = useState<SnippetFolder>()
|
||||
|
||||
// =======================================================
|
||||
// [Joshen] Set up favorites, shared, and private snippets
|
||||
// =======================================================
|
||||
const snippets = useSnippets(projectRef as string)
|
||||
const folders = useSnippetFolders(projectRef as string)
|
||||
const contents = snippets.filter((x) =>
|
||||
searchText.length > 0 ? x.name.toLowerCase().includes(searchText.toLowerCase()) : true
|
||||
)
|
||||
const snippet = snapV2.snippets[id as string]?.snippet
|
||||
|
||||
const privateSnippets = contents.filter((snippet) => snippet.visibility === 'user')
|
||||
const numPrivateSnippets = snapV2.privateSnippetCount[projectRef as string]
|
||||
const privateSnippetsTreeState =
|
||||
folders.length === 0 && snippets.length === 0
|
||||
? [ROOT_NODE]
|
||||
: formatFolderResponseForTreeView({ folders, contents: privateSnippets })
|
||||
|
||||
const favoriteSnippets = useFavoriteSnippets(projectRef as string).filter((x) =>
|
||||
searchText.length > 0 ? x.name.toLowerCase().includes(searchText.toLowerCase()) : true
|
||||
// ==========================
|
||||
// Private snippets & folders
|
||||
// ==========================
|
||||
const {
|
||||
data: privateSnippetsPages,
|
||||
isLoading,
|
||||
isPreviousData,
|
||||
isFetching,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useSQLSnippetFoldersQuery(
|
||||
{ projectRef, name: debouncedSearchText, sort },
|
||||
{ keepPreviousData: true }
|
||||
)
|
||||
const numFavoriteSnippets = favoriteSnippets.length
|
||||
const favoritesTreeState =
|
||||
numFavoriteSnippets === 0
|
||||
? [ROOT_NODE]
|
||||
: formatFolderResponseForTreeView({ contents: favoriteSnippets as any })
|
||||
|
||||
const projectSnippets = contents.filter((snippet) => snippet.visibility === 'project')
|
||||
const numProjectSnippets = projectSnippets.length
|
||||
const projectSnippetsTreeState =
|
||||
numProjectSnippets === 0
|
||||
? [ROOT_NODE]
|
||||
: formatFolderResponseForTreeView({ contents: projectSnippets })
|
||||
useEffect(() => {
|
||||
if (projectRef && privateSnippetsPages) {
|
||||
privateSnippetsPages.pages.forEach((page) => {
|
||||
page.contents?.forEach((snippet) => {
|
||||
snapV2.addSnippet({
|
||||
projectRef,
|
||||
snippet,
|
||||
})
|
||||
})
|
||||
|
||||
// =================================
|
||||
// [Joshen] React Queries
|
||||
// =================================
|
||||
page.folders?.forEach((folder) => {
|
||||
snapV2.addFolder({ projectRef, folder })
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [projectRef, privateSnippetsPages?.pages])
|
||||
|
||||
useSQLSnippetFoldersQuery(
|
||||
{ projectRef },
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
onSuccess: (data) => {
|
||||
if (projectRef !== undefined) {
|
||||
snapV2.initializeRemoteSnippets({ projectRef, data, sort })
|
||||
const [subResults, setSubResults] = useState<{
|
||||
[id: string]: { snippets?: Snippet[]; isLoading: boolean }
|
||||
}>({})
|
||||
|
||||
const filteredSnippets = useMemo(() => {
|
||||
const rootSnippets = privateSnippetsPages?.pages.flatMap((page) => page.contents ?? []) ?? []
|
||||
|
||||
let snippetInfo = Object.values(subResults).reduce(
|
||||
(
|
||||
acc: {
|
||||
snippets: Snippet[]
|
||||
isLoading: boolean
|
||||
snippetIds: Set<string>
|
||||
},
|
||||
curr
|
||||
) => {
|
||||
// filter out snippets that already exist
|
||||
const newSnippets = (curr.snippets ?? []).filter(
|
||||
(snippet) => !acc.snippetIds.has(snippet.id)
|
||||
)
|
||||
const newSnippetIds = new Set(newSnippets.map((snippet) => snippet.id))
|
||||
|
||||
return {
|
||||
snippets: [...acc.snippets, ...newSnippets],
|
||||
isLoading: acc.isLoading || curr.isLoading,
|
||||
snippetIds: new Set<string>([...acc.snippetIds, ...newSnippetIds]),
|
||||
}
|
||||
},
|
||||
{
|
||||
snippets: rootSnippets,
|
||||
isLoading: isLoading || (isPreviousData && isFetching),
|
||||
snippetIds: new Set<string>(rootSnippets.map((snippet) => snippet.id)),
|
||||
}
|
||||
)
|
||||
|
||||
if (snippet && snippet.visibility === 'user' && !snippetInfo.snippetIds.has(snippet.id)) {
|
||||
snippetInfo.snippetIds.add(snippet.id)
|
||||
snippetInfo.snippets = [...snippetInfo.snippets, snippet]
|
||||
}
|
||||
|
||||
return snippetInfo
|
||||
}, [privateSnippetsPages?.pages, subResults, isLoading, isPreviousData, isFetching, snippet])
|
||||
|
||||
const privateSnippets = useMemo(
|
||||
() =>
|
||||
filteredSnippets.snippets
|
||||
?.filter((snippet) => snippet.visibility === 'user')
|
||||
.sort((a, b) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
}) ?? [],
|
||||
[filteredSnippets.snippets, sort]
|
||||
)
|
||||
const folders = useSnippetFolders(projectRef as string)
|
||||
|
||||
const { data: privateSnippetCountData } = useContentCountQuery({
|
||||
projectRef,
|
||||
type: 'sql',
|
||||
visibility: 'user',
|
||||
name: debouncedSearchText,
|
||||
})
|
||||
const numPrivateSnippets = privateSnippetCountData?.count ?? 0
|
||||
|
||||
const privateSnippetsTreeState = useMemo(
|
||||
() =>
|
||||
folders.length === 0 && privateSnippets.length === 0
|
||||
? [ROOT_NODE]
|
||||
: formatFolderResponseForTreeView({ folders, contents: privateSnippets }),
|
||||
[folders, privateSnippets]
|
||||
)
|
||||
|
||||
useSqlSnippetsQuery(projectRef, {
|
||||
onSuccess(data) {
|
||||
if (projectRef !== undefined) {
|
||||
const favoriteSnippets = data.snippets.filter((snippet) => snippet.content.favorite)
|
||||
snapV2.initializeFavoriteSnippets({ projectRef, snippets: favoriteSnippets })
|
||||
}
|
||||
const privateSnippetsLastItemIds = useMemo(
|
||||
() => getLastItemIds(privateSnippetsTreeState),
|
||||
[privateSnippetsTreeState]
|
||||
)
|
||||
|
||||
// =================
|
||||
// Favorite snippets
|
||||
// =================
|
||||
const {
|
||||
data: favoriteSqlSnippetsData,
|
||||
isLoading: isLoadingFavoriteSqlSnippets,
|
||||
hasNextPage: hasMoreFavoriteSqlSnippets,
|
||||
fetchNextPage: fetchNextFavoriteSqlSnippets,
|
||||
isFetchingNextPage: isFetchingMoreFavoriteSqlSnippets,
|
||||
isSuccess: isFavoriteSnippetsSuccess,
|
||||
} = useSqlSnippetsQuery(
|
||||
{
|
||||
projectRef,
|
||||
favorite: true,
|
||||
name: debouncedSearchText,
|
||||
sort,
|
||||
},
|
||||
{ enabled: showFavoriteSnippets, keepPreviousData: true }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (projectRef === undefined || !isFavoriteSnippetsSuccess) return
|
||||
|
||||
favoriteSqlSnippetsData.pages.forEach((page) => {
|
||||
page.contents?.forEach((snippet) => {
|
||||
snapV2.addSnippet({
|
||||
projectRef,
|
||||
snippet,
|
||||
})
|
||||
})
|
||||
})
|
||||
}, [projectRef, privateSnippetsPages?.pages])
|
||||
|
||||
const favoriteSnippets = useMemo(() => {
|
||||
let snippets = favoriteSqlSnippetsData?.pages.flatMap((page) => page.contents ?? []) ?? []
|
||||
|
||||
if (snippet && snippet.favorite && !snippets.find((x) => x.id === snippet.id)) {
|
||||
snippets.push(snippet as any)
|
||||
}
|
||||
|
||||
return (
|
||||
snippets
|
||||
.map((snippet) => ({ ...snippet, folder_id: undefined }))
|
||||
.sort((a, b) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
}) ?? []
|
||||
)
|
||||
}, [favoriteSqlSnippetsData?.pages, snippet, sort])
|
||||
|
||||
const { data: favoritedSnippetCountData } = useContentCountQuery({
|
||||
projectRef,
|
||||
type: 'sql',
|
||||
favorite: true,
|
||||
name: debouncedSearchText,
|
||||
})
|
||||
const numFavoriteSnippets = favoritedSnippetCountData?.count ?? 0
|
||||
|
||||
const favoritesTreeState = useMemo(
|
||||
() =>
|
||||
numFavoriteSnippets === 0
|
||||
? [ROOT_NODE]
|
||||
: formatFolderResponseForTreeView({ contents: favoriteSnippets }),
|
||||
[favoriteSnippets, numFavoriteSnippets]
|
||||
)
|
||||
|
||||
const favoriteSnippetsLastItemIds = useMemo(
|
||||
() => getLastItemIds(favoritesTreeState),
|
||||
[favoritesTreeState]
|
||||
)
|
||||
|
||||
// =================
|
||||
// Shared snippets
|
||||
// =================
|
||||
const {
|
||||
data: sharedSqlSnippetsData,
|
||||
isLoading: isLoadingSharedSqlSnippets,
|
||||
hasNextPage: hasMoreSharedSqlSnippets,
|
||||
fetchNextPage: fetchNextSharedSqlSnippets,
|
||||
isFetchingNextPage: isFetchingMoreSharedSqlSnippets,
|
||||
isSuccess: isSharedSqlSnippetsSuccess,
|
||||
} = useSqlSnippetsQuery(
|
||||
{
|
||||
projectRef,
|
||||
visibility: 'project',
|
||||
name: debouncedSearchText,
|
||||
sort,
|
||||
},
|
||||
{ enabled: showSharedSnippets, keepPreviousData: true }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (projectRef === undefined || !isSharedSqlSnippetsSuccess) return
|
||||
|
||||
sharedSqlSnippetsData.pages.forEach((page) => {
|
||||
page.contents?.forEach((snippet) => {
|
||||
snapV2.addSnippet({
|
||||
projectRef,
|
||||
snippet,
|
||||
})
|
||||
})
|
||||
})
|
||||
}, [projectRef, privateSnippetsPages?.pages])
|
||||
|
||||
const sharedSnippets = useMemo(() => {
|
||||
let snippets = sharedSqlSnippetsData?.pages.flatMap((page) => page.contents ?? []) ?? []
|
||||
|
||||
if (snippet && snippet.visibility === 'project' && !snippets.find((x) => x.id === snippet.id)) {
|
||||
snippets.push(snippet as any)
|
||||
}
|
||||
|
||||
return (
|
||||
snippets.sort((a, b) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
}) ?? []
|
||||
)
|
||||
}, [sharedSqlSnippetsData?.pages, snippet, sort])
|
||||
|
||||
const { data: sharedSnippetCountData } = useContentCountQuery({
|
||||
projectRef,
|
||||
type: 'sql',
|
||||
visibility: 'project',
|
||||
name: debouncedSearchText,
|
||||
})
|
||||
const numProjectSnippets = sharedSnippetCountData?.count ?? 0
|
||||
|
||||
const projectSnippetsTreeState = useMemo(
|
||||
() =>
|
||||
numProjectSnippets === 0
|
||||
? [ROOT_NODE]
|
||||
: formatFolderResponseForTreeView({ contents: sharedSnippets }),
|
||||
[sharedSnippets, numProjectSnippets]
|
||||
)
|
||||
|
||||
const projectSnippetsLastItemIds = useMemo(
|
||||
() => getLastItemIds(projectSnippetsTreeState),
|
||||
[projectSnippetsTreeState]
|
||||
)
|
||||
|
||||
// ==========================
|
||||
// Snippet mutations from RQ
|
||||
// ==========================
|
||||
|
||||
const { mutate: upsertContent, isLoading: isUpserting } = useContentUpsertV2Mutation({
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to update query: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
useContentCountQuery(
|
||||
{ projectRef, type: 'sql' },
|
||||
{
|
||||
onSuccess(data) {
|
||||
if (projectRef !== undefined) {
|
||||
snapV2.setPrivateSnippetCount({ projectRef, value: data.count })
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { mutate: deleteContent, isLoading: isDeleting } = useContentDeleteMutation({
|
||||
onError: (error, data) => {
|
||||
if (error.message.includes('Contents not found')) {
|
||||
@@ -161,11 +356,13 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
},
|
||||
})
|
||||
|
||||
// =================================
|
||||
// [Joshen] UI functions
|
||||
// =================================
|
||||
// ===============
|
||||
// UI functions
|
||||
// ===============
|
||||
|
||||
const postDeleteCleanup = (ids: string[]) => {
|
||||
// [Refactor] To investigate - deleting a snippet while it's open, will have it in the side nav
|
||||
// for a bit, before it gets removed (assumingly invalidated)
|
||||
setShowDeleteModal(false)
|
||||
setSelectedSnippets([])
|
||||
const existingSnippetIds = Object.keys(snapV2.snippets).filter((x) => !ids.includes(x))
|
||||
@@ -194,47 +391,71 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
)
|
||||
}
|
||||
|
||||
const onConfirmShare = () => {
|
||||
if (!selectedSnippetToShare) return console.error('Snippet ID is required')
|
||||
snapV2.shareSnippet(selectedSnippetToShare.id, 'project')
|
||||
setSelectedSnippetToShare(undefined)
|
||||
setShowSharedSnippets(true)
|
||||
const onUpdateVisibility = async (action: 'share' | 'unshare') => {
|
||||
const snippet = action === 'share' ? selectedSnippetToShare : selectedSnippetToUnshare
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
if (!snippet) return console.error('Snippet ID is required')
|
||||
|
||||
if (projectRef !== undefined) {
|
||||
snapV2.setPrivateSnippetCount({
|
||||
projectRef,
|
||||
value: snapV2.privateSnippetCount[projectRef] - 1,
|
||||
})
|
||||
const storeSnippet = snapV2.snippets[snippet.id]
|
||||
let snippetContent = storeSnippet?.snippet?.content
|
||||
|
||||
if (snippetContent === undefined) {
|
||||
const { content } = await getContentById({ projectRef, id: snippet.id })
|
||||
snippetContent = content as unknown as SqlSnippets.Content
|
||||
}
|
||||
|
||||
// [Joshen] Just as a final check - to ensure that the content is minimally there (empty string is fine)
|
||||
if (snippetContent === undefined) {
|
||||
return toast.error('Unable to update snippet visibility: Content is missing')
|
||||
}
|
||||
|
||||
const visibility = action === 'share' ? 'project' : 'user'
|
||||
|
||||
upsertContent(
|
||||
{
|
||||
projectRef,
|
||||
payload: {
|
||||
...snippet,
|
||||
visibility,
|
||||
folder_id: null,
|
||||
content: snippetContent,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSelectedSnippetToShare(undefined)
|
||||
setSelectedSnippetToUnshare(undefined)
|
||||
setShowSharedSnippets(true)
|
||||
snapV2.updateSnippet({
|
||||
id: snippet.id,
|
||||
snippet: { visibility, folder_id: null },
|
||||
skipSave: true,
|
||||
})
|
||||
toast.success(
|
||||
action === 'share'
|
||||
? 'Snippet is now shared to the project'
|
||||
: 'Snippet is now unshared from the project'
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const onConfirmUnshare = () => {
|
||||
if (!selectedSnippetToUnshare) return console.error('Snippet ID is required')
|
||||
snapV2.shareSnippet(selectedSnippetToUnshare.id, 'user')
|
||||
setSelectedSnippetToUnshare(undefined)
|
||||
setShowPrivateSnippets(true)
|
||||
|
||||
if (projectRef !== undefined) {
|
||||
snapV2.setPrivateSnippetCount({
|
||||
projectRef,
|
||||
value: snapV2.privateSnippetCount[projectRef] + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectCopyPersonal = async (snippet: Snippet) => {
|
||||
const onSelectCopyPersonal = async (snippet: SnippetWithContent) => {
|
||||
if (!profile) return console.error('Profile is required')
|
||||
if (!project) return console.error('Project is required')
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
if (!id) return console.error('Snippet ID is required')
|
||||
|
||||
let sql: string = ''
|
||||
if (!('content' in snippet)) {
|
||||
if (snippet.content && snippet.content.sql) {
|
||||
sql = snippet.content.sql
|
||||
} else {
|
||||
// Fetch the content first
|
||||
const { content } = await getContentById({ projectRef, id: snippet.id })
|
||||
sql = content.sql
|
||||
} else {
|
||||
sql = (snippet as SnippetDetail).content.sql
|
||||
if ('sql' in content) {
|
||||
sql = content.sql
|
||||
}
|
||||
}
|
||||
|
||||
const snippetCopy = createSqlSnippetSkeletonV2({
|
||||
@@ -254,7 +475,7 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
if (selectedFolderToDelete === undefined) return console.error('No folder is selected')
|
||||
|
||||
const folderSnippets = contents.filter(
|
||||
const folderSnippets = privateSnippets.filter(
|
||||
(content) => content.folder_id === selectedFolderToDelete.id
|
||||
)
|
||||
if (folderSnippets.length > 0) {
|
||||
@@ -280,7 +501,7 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
// We're only supporting shift clicks (not cmd/control click) - this is even with the react tree view component itself
|
||||
const onMultiSelect = (selectedId: string) => {
|
||||
// The base is always the current query thats selected
|
||||
const contentIds = contents.map((x) => x.id)
|
||||
const contentIds = privateSnippets.map((x) => x.id)
|
||||
const baseIndex = contentIds.indexOf(id as string)
|
||||
const targetIndex = contentIds.indexOf(selectedId)
|
||||
|
||||
@@ -288,11 +509,12 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
const ceiling = Math.max(baseIndex, targetIndex)
|
||||
|
||||
const _selectedSnippets = []
|
||||
const sameFolder = contents[floor].folder_id === contents[ceiling].folder_id
|
||||
const sameFolder = privateSnippets[floor].folder_id === privateSnippets[ceiling].folder_id
|
||||
|
||||
for (let i = floor; i <= ceiling; i++) {
|
||||
if (sameFolder) {
|
||||
if (contents[i].folder_id === contents[floor].folder_id) _selectedSnippets.push(contents[i])
|
||||
if (privateSnippets[i].folder_id === privateSnippets[floor].folder_id)
|
||||
_selectedSnippets.push(privateSnippets[i])
|
||||
} else {
|
||||
// [Joshen] Temp don't allow selecting across folders for now
|
||||
// _selectedSnippets.push(contents[i])
|
||||
@@ -302,29 +524,25 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
setSelectedSnippets(_selectedSnippets)
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// [Joshen] useEffects kept at the bottom
|
||||
// ======================================
|
||||
// ===============
|
||||
// useEffects
|
||||
// ===============
|
||||
|
||||
useEffect(() => {
|
||||
const loadFolderContents = async (folderId: string) => {
|
||||
const { contents } = await getSQLSnippetFolders({ projectRef, folderId })
|
||||
if (projectRef) {
|
||||
contents?.forEach((snippet) => snapV2.addSnippet({ projectRef, snippet }))
|
||||
}
|
||||
}
|
||||
setIsSearching?.(filteredSnippets.isLoading)
|
||||
}, [filteredSnippets.isLoading, setIsSearching])
|
||||
|
||||
useEffect(() => {
|
||||
if (snippet !== undefined && !mountedId) {
|
||||
if (snippet.visibility === 'project') setShowSharedSnippets(true)
|
||||
if (snippet.folder_id) {
|
||||
setDefaultExpandedFolderIds([snippet.folder_id])
|
||||
loadFolderContents(snippet.folder_id)
|
||||
}
|
||||
|
||||
// Only want to run this once when loading sql/[id] route
|
||||
setMountedId(true)
|
||||
}
|
||||
}, [snippet, mountedId])
|
||||
}, [snippet, mountedId, sort, debouncedSearchText])
|
||||
|
||||
useEffect(() => {
|
||||
// Unselect all snippets whenever opening another snippet
|
||||
@@ -334,116 +552,126 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
return (
|
||||
<>
|
||||
<InnerSideMenuSeparator />
|
||||
{((numProjectSnippets === 0 && searchText.length === 0) || numProjectSnippets > 0) && (
|
||||
<>
|
||||
<InnerSideMenuCollapsible
|
||||
open={showSharedSnippets}
|
||||
onOpenChange={setShowSharedSnippets}
|
||||
className="px-0"
|
||||
>
|
||||
<InnerSideMenuCollapsibleTrigger
|
||||
title={`Shared ${numProjectSnippets > 0 ? ` (${numProjectSnippets})` : ''}`}
|
||||
<InnerSideMenuCollapsible
|
||||
open={showSharedSnippets}
|
||||
onOpenChange={setShowSharedSnippets}
|
||||
className="px-0"
|
||||
>
|
||||
<InnerSideMenuCollapsibleTrigger
|
||||
title={`Shared ${numProjectSnippets > 0 ? ` (${numProjectSnippets})` : ''}`}
|
||||
/>
|
||||
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
|
||||
{isLoadingSharedSqlSnippets ? (
|
||||
<SQLEditorLoadingSnippets />
|
||||
) : numProjectSnippets === 0 ? (
|
||||
<InnerSideBarEmptyPanel
|
||||
className={cn('mx-2 px-3', isSearching ? '[&>div>p]:text-foreground-lighter' : '')}
|
||||
title={isSearching ? 'No results found based on your search' : 'No shared queries'}
|
||||
description={
|
||||
isSearching
|
||||
? undefined
|
||||
: 'Share queries with your team by right-clicking on the query'
|
||||
}
|
||||
/>
|
||||
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
|
||||
{numProjectSnippets === 0 ? (
|
||||
<InnerSideBarEmptyPanel
|
||||
className="mx-2"
|
||||
title="No shared queries"
|
||||
description="Share queries with your team by right-clicking on the query."
|
||||
/>
|
||||
) : (
|
||||
<TreeView
|
||||
data={projectSnippetsTreeState}
|
||||
aria-label="project-level-snippets"
|
||||
nodeRenderer={({ element, ...props }) => (
|
||||
<SQLEditorTreeViewItem
|
||||
{...props}
|
||||
element={element}
|
||||
onSelectDelete={() => {
|
||||
setShowDeleteModal(true)
|
||||
setSelectedSnippets([element.metadata as unknown as Snippet])
|
||||
}}
|
||||
onSelectRename={() => {
|
||||
setShowRenameModal(true)
|
||||
setSelectedSnippetToRename(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectDownload={() => {
|
||||
setSelectedSnippetToDownload(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectCopyPersonal={() => {
|
||||
onSelectCopyPersonal(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectUnshare={() => {
|
||||
setSelectedSnippetToUnshare(element.metadata as Snippet)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : (
|
||||
<TreeView
|
||||
data={projectSnippetsTreeState}
|
||||
aria-label="project-level-snippets"
|
||||
nodeRenderer={({ element, ...props }) => (
|
||||
<SQLEditorTreeViewItem
|
||||
{...props}
|
||||
element={element}
|
||||
onSelectDelete={() => {
|
||||
setShowDeleteModal(true)
|
||||
setSelectedSnippets([element.metadata as unknown as Snippet])
|
||||
}}
|
||||
onSelectRename={() => {
|
||||
setShowRenameModal(true)
|
||||
setSelectedSnippetToRename(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectDownload={() => {
|
||||
setSelectedSnippetToDownload(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectCopyPersonal={() => {
|
||||
onSelectCopyPersonal(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectUnshare={() => {
|
||||
setSelectedSnippetToUnshare(element.metadata as Snippet)
|
||||
}}
|
||||
isLastItem={projectSnippetsLastItemIds.has(element.id as string)}
|
||||
hasNextPage={hasMoreSharedSqlSnippets}
|
||||
fetchNextPage={fetchNextSharedSqlSnippets}
|
||||
isFetchingNextPage={isFetchingMoreSharedSqlSnippets}
|
||||
/>
|
||||
)}
|
||||
</InnerSideMenuCollapsibleContent>
|
||||
</InnerSideMenuCollapsible>
|
||||
<InnerSideMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</InnerSideMenuCollapsibleContent>
|
||||
</InnerSideMenuCollapsible>
|
||||
<InnerSideMenuSeparator />
|
||||
|
||||
{((numFavoriteSnippets === 0 && searchText.length === 0) || numFavoriteSnippets > 0) && (
|
||||
<>
|
||||
<InnerSideMenuCollapsible
|
||||
className="px-0"
|
||||
open={showFavouriteSnippets}
|
||||
onOpenChange={setShowFavouriteSnippets}
|
||||
>
|
||||
<InnerSideMenuCollapsibleTrigger
|
||||
title={`Favorites ${numFavoriteSnippets > 0 ? ` (${numFavoriteSnippets})` : ''}`}
|
||||
<InnerSideMenuCollapsible
|
||||
className="px-0"
|
||||
open={showFavoriteSnippets}
|
||||
onOpenChange={setShowFavoriteSnippets}
|
||||
>
|
||||
<InnerSideMenuCollapsibleTrigger
|
||||
title={`Favorites ${numFavoriteSnippets > 0 ? ` (${numFavoriteSnippets})` : ''}`}
|
||||
/>
|
||||
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
|
||||
{isLoadingFavoriteSqlSnippets ? (
|
||||
<SQLEditorLoadingSnippets />
|
||||
) : numFavoriteSnippets === 0 ? (
|
||||
<InnerSideBarEmptyPanel
|
||||
title={isSearching ? 'No results found based on your search' : 'No favorite queries'}
|
||||
className={cn('mx-2 px-3', isSearching ? '[&>div>p]:text-foreground-lighter' : '')}
|
||||
description={
|
||||
isSearching ? null : (
|
||||
<>
|
||||
Save a query to favorites for easy accessibility by clicking the{' '}
|
||||
<Heart size={12} className="inline-block relative align-center -top-[1px]" />{' '}
|
||||
icon.
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
|
||||
{numFavoriteSnippets === 0 ? (
|
||||
<InnerSideBarEmptyPanel
|
||||
title="No favorite queries"
|
||||
className="mx-2"
|
||||
description={
|
||||
<>
|
||||
Save a query to favorites for easy accessbility by clicking the{' '}
|
||||
<Heart size={12} className="inline-block relative align-center -top-[1px]" />{' '}
|
||||
icon.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TreeView
|
||||
data={favoritesTreeState}
|
||||
aria-label="favorite-snippets"
|
||||
nodeRenderer={({ element, ...props }) => (
|
||||
<SQLEditorTreeViewItem
|
||||
{...props}
|
||||
element={element}
|
||||
onSelectDelete={() => {
|
||||
setShowDeleteModal(true)
|
||||
setSelectedSnippets([element.metadata as unknown as Snippet])
|
||||
}}
|
||||
onSelectRename={() => {
|
||||
setShowRenameModal(true)
|
||||
setSelectedSnippetToRename(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectDownload={() => {
|
||||
setSelectedSnippetToDownload(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectCopyPersonal={() => {
|
||||
onSelectCopyPersonal(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectShare={() => setSelectedSnippetToShare(element.metadata as Snippet)}
|
||||
onSelectUnshare={() => {
|
||||
setSelectedSnippetToUnshare(element.metadata as Snippet)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : (
|
||||
<TreeView
|
||||
data={favoritesTreeState}
|
||||
aria-label="favorite-snippets"
|
||||
nodeRenderer={({ element, ...props }) => (
|
||||
<SQLEditorTreeViewItem
|
||||
{...props}
|
||||
element={element}
|
||||
onSelectDelete={() => {
|
||||
setShowDeleteModal(true)
|
||||
setSelectedSnippets([element.metadata as unknown as Snippet])
|
||||
}}
|
||||
onSelectRename={() => {
|
||||
setShowRenameModal(true)
|
||||
setSelectedSnippetToRename(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectDownload={() => {
|
||||
setSelectedSnippetToDownload(element.metadata as Snippet)
|
||||
}}
|
||||
onSelectCopyPersonal={() => {
|
||||
onSelectCopyPersonal(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}
|
||||
/>
|
||||
)}
|
||||
</InnerSideMenuCollapsibleContent>
|
||||
</InnerSideMenuCollapsible>
|
||||
<InnerSideMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</InnerSideMenuCollapsibleContent>
|
||||
</InnerSideMenuCollapsible>
|
||||
<InnerSideMenuSeparator />
|
||||
|
||||
<InnerSideMenuCollapsible
|
||||
open={showPrivateSnippets}
|
||||
@@ -455,29 +683,8 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
${numPrivateSnippets > 0 ? ` (${numPrivateSnippets})` : ''}`}
|
||||
/>
|
||||
<InnerSideMenuCollapsibleContent className="group-data-[state=open]:pt-2">
|
||||
{!snapV2.loaded[projectRef as string] ? (
|
||||
<>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-40 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-32 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3 opacity-75">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-20 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3 opacity-50">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-40 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-row h-6 px-3 items-center gap-3 opacity-25">
|
||||
<Skeleton className="h-4 w-5" />
|
||||
<Skeleton className="w-20 h-4" />
|
||||
</div>
|
||||
</>
|
||||
{isLoading ? (
|
||||
<SQLEditorLoadingSnippets />
|
||||
) : folders.length === 0 && numPrivateSnippets === 0 ? (
|
||||
<InnerSideBarEmptyPanel
|
||||
className="mx-3 px-4"
|
||||
@@ -498,6 +705,7 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
{...props}
|
||||
element={element}
|
||||
isMultiSelected={selectedSnippets.length > 1}
|
||||
isLastItem={privateSnippetsLastItemIds.has(element.id as string)}
|
||||
status={props.isBranch ? snapV2.folders[element.id].status : 'idle'}
|
||||
onMultiSelect={onMultiSelect}
|
||||
onSelectCreate={() => {
|
||||
@@ -548,6 +756,17 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
snapV2.saveFolder({ id: element.id as string, name })
|
||||
}
|
||||
}}
|
||||
hasNextPage={hasNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
sort={sort}
|
||||
name={debouncedSearchText}
|
||||
onFolderContentsChange={({ isLoading, snippets }) => {
|
||||
setSubResults((prev) => ({
|
||||
...prev,
|
||||
[element.id as string]: { snippets, isLoading },
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -581,12 +800,13 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
|
||||
<ConfirmationModal
|
||||
size="medium"
|
||||
loading={isUpserting}
|
||||
title={`Confirm to share query: ${selectedSnippetToShare?.name}`}
|
||||
confirmLabel="Share query"
|
||||
confirmLabelLoading="Sharing query"
|
||||
visible={selectedSnippetToShare !== undefined}
|
||||
onCancel={() => setSelectedSnippetToShare(undefined)}
|
||||
onConfirm={onConfirmShare}
|
||||
onConfirm={() => onUpdateVisibility('share')}
|
||||
alert={{
|
||||
title: 'This SQL query will become public to all team members',
|
||||
description: 'Anyone with access to the project can view it',
|
||||
@@ -611,7 +831,7 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
confirmLabelLoading="Unsharing query"
|
||||
visible={selectedSnippetToUnshare !== undefined}
|
||||
onCancel={() => setSelectedSnippetToUnshare(undefined)}
|
||||
onConfirm={onConfirmUnshare}
|
||||
onConfirm={() => onUpdateVisibility('unshare')}
|
||||
alert={{
|
||||
title: 'This SQL query will no longer be public to all team members',
|
||||
description: 'Only you will have access to this query',
|
||||
|
||||
+26
-1
@@ -8,7 +8,7 @@ export interface TreeViewItemProps {
|
||||
metadata?: any
|
||||
}
|
||||
|
||||
export const ROOT_NODE = { id: 0, name: '', parent: null, children: [] }
|
||||
export const ROOT_NODE: TreeViewItemProps = { id: 0, name: '', parent: null, children: [] }
|
||||
|
||||
// [Joshen] At the moment this is only tuned for single level folders
|
||||
// Will need to relook at this for multi level folders,
|
||||
@@ -52,3 +52,28 @@ export const formatFolderResponseForTreeView = (
|
||||
|
||||
return [root, ...formattedFolders, ...formattedContents]
|
||||
}
|
||||
|
||||
export function getLastItemIds(items: TreeViewItemProps[]) {
|
||||
let lastItemIds = new Set<string>()
|
||||
|
||||
const topLevelItems = items.filter((item) => item.parent === 0)
|
||||
|
||||
if (topLevelItems.length > 0) {
|
||||
const lastItem = topLevelItems[topLevelItems.length - 1]
|
||||
if (typeof lastItem.id === 'string') {
|
||||
lastItemIds.add(lastItem.id)
|
||||
}
|
||||
|
||||
topLevelItems.forEach((item) => {
|
||||
if (item.children.length > 0) {
|
||||
const childrenLastItem = item.children[item.children.length - 1]
|
||||
|
||||
if (typeof childrenLastItem === 'string') {
|
||||
lastItemIds.add(childrenLastItem)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return lastItemIds
|
||||
}
|
||||
+106
-23
@@ -2,16 +2,18 @@ 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 { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { useParams } from 'common/hooks/useParams'
|
||||
import { getSQLSnippetFolders } from 'data/content/sql-folders-query'
|
||||
import { useSQLSnippetFolderContentsQuery } from 'data/content/sql-folder-contents-query'
|
||||
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 { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
||||
import {
|
||||
Button,
|
||||
ContextMenuContent_Shadcn_,
|
||||
ContextMenuItem_Shadcn_,
|
||||
ContextMenuSeparator_Shadcn_,
|
||||
@@ -40,6 +42,15 @@ interface SQLEditorTreeViewItemProps {
|
||||
onSelectDeleteFolder?: () => void
|
||||
onEditSave?: (name: string) => void
|
||||
onMultiSelect?: (id: string) => void
|
||||
|
||||
// Pagination/filtering options
|
||||
isLastItem: boolean
|
||||
hasNextPage?: boolean
|
||||
fetchNextPage?: () => void
|
||||
isFetchingNextPage?: boolean
|
||||
sort?: 'inserted_at' | 'name'
|
||||
name?: string
|
||||
onFolderContentsChange?: (info: { isLoading: boolean; snippets?: Snippet[] }) => void
|
||||
}
|
||||
|
||||
export const SQLEditorTreeViewItem = ({
|
||||
@@ -61,15 +72,20 @@ export const SQLEditorTreeViewItem = ({
|
||||
onSelectCopyPersonal,
|
||||
onEditSave,
|
||||
onMultiSelect,
|
||||
isLastItem,
|
||||
hasNextPage: _hasNextPage,
|
||||
fetchNextPage: _fetchNextPage,
|
||||
isFetchingNextPage: _isFetchingNextPage,
|
||||
sort,
|
||||
name,
|
||||
onFolderContentsChange,
|
||||
}: SQLEditorTreeViewItemProps) => {
|
||||
const router = useRouter()
|
||||
const { id, ref } = useParams()
|
||||
const { id, ref: projectRef } = useParams()
|
||||
const { profile } = useProfile()
|
||||
const { className, onClick } = getNodeProps()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
|
||||
const isOwner = profile?.id === element?.metadata.owner_id
|
||||
const isSharedSnippet = element.metadata.visibility === 'project'
|
||||
|
||||
@@ -81,20 +97,63 @@ export const SQLEditorTreeViewItem = ({
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
// [Joshen] Folder contents are loaded on demand too
|
||||
const onOpenFolder = async (id: string) => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
const parentId = element.parent === 0 ? undefined : element.parent
|
||||
|
||||
try {
|
||||
setIsFetching(true)
|
||||
const { contents } = await getSQLSnippetFolders({ projectRef: ref, folderId: id })
|
||||
contents?.forEach((snippet) => {
|
||||
snapV2.addSnippet({ projectRef: ref, snippet })
|
||||
const isEnabled = isBranch && isExpanded
|
||||
|
||||
const {
|
||||
data,
|
||||
isSuccess,
|
||||
isLoading,
|
||||
isFetchingNextPage: isFetchingNextPageInFolder,
|
||||
hasNextPage: hasNextPageInFolder,
|
||||
fetchNextPage: fetchNestPageInFolder,
|
||||
isPreviousData,
|
||||
isFetching,
|
||||
} = useSQLSnippetFolderContentsQuery(
|
||||
{
|
||||
projectRef,
|
||||
folderId: parentId ?? element.id,
|
||||
name,
|
||||
sort,
|
||||
},
|
||||
{
|
||||
enabled: isEnabled,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
useEffect(() => {
|
||||
if (projectRef && isSuccess) {
|
||||
data.pages.forEach((page) => {
|
||||
page.contents?.forEach((snippet) => {
|
||||
snapV2.addSnippet({
|
||||
projectRef,
|
||||
snippet,
|
||||
})
|
||||
})
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to retrieve folder contents: ${error.message}`)
|
||||
} finally {
|
||||
setIsFetching(false)
|
||||
}
|
||||
}, [projectRef, data?.pages])
|
||||
|
||||
const onFolderContentsChangeRef = useLatest(onFolderContentsChange)
|
||||
useEffect(() => {
|
||||
if (isEnabled) {
|
||||
onFolderContentsChangeRef.current?.({
|
||||
isLoading: isLoading || (isPreviousData && isFetching),
|
||||
snippets: data?.pages.flatMap((page) => page.contents ?? []),
|
||||
})
|
||||
}
|
||||
}, [data?.pages, isFetching, isLoading, isPreviousData, isEnabled])
|
||||
|
||||
const isInFolder = parentId !== undefined
|
||||
|
||||
const hasNextPage = isInFolder ? hasNextPageInFolder : _hasNextPage
|
||||
|
||||
function fetchNextPage() {
|
||||
if (isInFolder) {
|
||||
fetchNestPageInFolder()
|
||||
} else if (typeof _fetchNextPage === 'function') {
|
||||
_fetchNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,18 +170,18 @@ export const SQLEditorTreeViewItem = ({
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected || id === element.id}
|
||||
isEditing={isEditing}
|
||||
isLoading={isFetching || isSaving}
|
||||
isLoading={(isEnabled && isLoading) || isSaving}
|
||||
onEditSubmit={(value) => {
|
||||
if (onEditSave !== undefined) onEditSave(value)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (!isBranch) {
|
||||
if (!e.shiftKey) {
|
||||
router.push(`/project/${ref}/sql/${element.id}`)
|
||||
router.push(`/project/${projectRef}/sql/${element.id}`)
|
||||
} else if (id !== 'new') {
|
||||
onMultiSelect?.(element.id)
|
||||
} else {
|
||||
router.push(`/project/${ref}/sql/${element.id}`)
|
||||
router.push(`/project/${projectRef}/sql/${element.id}`)
|
||||
}
|
||||
} else {
|
||||
// Prevent expanding folder while editing text
|
||||
@@ -132,7 +191,6 @@ export const SQLEditorTreeViewItem = ({
|
||||
}
|
||||
|
||||
onClick(e)
|
||||
if (!isExpanded) onOpenFolder(element.id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -206,7 +264,11 @@ export const SQLEditorTreeViewItem = ({
|
||||
onSelect={() => {}}
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Link href={`/project/${ref}/sql/${element.id}`} target="_blank" rel="noreferrer">
|
||||
<Link
|
||||
href={`/project/${projectRef}/sql/${element.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
Open in new tab
|
||||
</Link>
|
||||
@@ -288,6 +350,27 @@ export const SQLEditorTreeViewItem = ({
|
||||
)}
|
||||
</ContextMenuContent_Shadcn_>
|
||||
</ContextMenu_Shadcn_>
|
||||
|
||||
{hasNextPage && typeof element.id === 'string' && isLastItem && (
|
||||
<div
|
||||
className="px-4 py-1"
|
||||
style={{
|
||||
paddingLeft:
|
||||
!element.isBranch && element.level > 1 ? 48 * (element.level - 1) : undefined,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="outline"
|
||||
size="tiny"
|
||||
block
|
||||
loading={isInFolder ? isFetchingNextPageInFolder : _isFetchingNextPage}
|
||||
disabled={isInFolder ? isFetchingNextPageInFolder : _isFetchingNextPage}
|
||||
onClick={fetchNextPage}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -91,7 +91,16 @@ export function useSnippetCommands() {
|
||||
|
||||
function RunSnippetPage() {
|
||||
const { ref } = useParams()
|
||||
const { data: snippets, isLoading, isError, isSuccess } = useSqlSnippetsQuery(ref)
|
||||
const {
|
||||
data: snippetPages,
|
||||
isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useSqlSnippetsQuery({
|
||||
projectRef: ref,
|
||||
})
|
||||
|
||||
const snippets = snippetPages?.pages.flatMap((page) => page.contents)
|
||||
|
||||
const { profile } = useProfile()
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
@@ -109,15 +118,11 @@ function RunSnippetPage() {
|
||||
</CommandHeader>
|
||||
{isLoading && <LoadingState />}
|
||||
{isError && <ErrorState />}
|
||||
{isSuccess && (!snippets || snippets.snippets.length === 0) && (
|
||||
{isSuccess && (!snippets || snippets.length === 0) && (
|
||||
<EmptyState projectRef={ref} canCreateNew={canCreateSQLSnippet} />
|
||||
)}
|
||||
{isSuccess && !!snippets && snippets.snippets.length > 0 && (
|
||||
<SnippetSelector
|
||||
projectRef={ref}
|
||||
canCreateNew={canCreateSQLSnippet}
|
||||
snippets={snippets.snippets}
|
||||
/>
|
||||
{isSuccess && !!snippets && snippets.length > 0 && (
|
||||
<SnippetSelector projectRef={ref} canCreateNew={canCreateSQLSnippet} snippets={snippets} />
|
||||
)}
|
||||
</CommandWrapper>
|
||||
)
|
||||
@@ -239,7 +244,7 @@ function SnippetSelector({
|
||||
function snippetValue(snippet: SqlSnippet) {
|
||||
if (snippet.type !== 'sql') return ''
|
||||
return escapeAttributeSelector(
|
||||
`${snippet.id}-${snippet.name}-${snippet.content.sql.slice(0, 30)}`
|
||||
`${snippet.id}-${snippet.name}-${snippet?.content?.sql.slice(0, 30)}`
|
||||
).toLowerCase()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,32 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
import { SqlSnippet } from './sql-snippets-query'
|
||||
|
||||
interface getContentCountVariables {
|
||||
projectRef?: string
|
||||
type GetContentFilters = {
|
||||
type: 'sql' | 'report' | 'log_sql'
|
||||
visibility?: SqlSnippet['visibility']
|
||||
favorite?: boolean
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface getContentCountVariables extends GetContentFilters {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export async function getContentCount(
|
||||
{ projectRef, type }: getContentCountVariables,
|
||||
{ projectRef, type, visibility, favorite, name }: getContentCountVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (typeof projectRef === 'undefined') throw new Error('projectRef is required')
|
||||
|
||||
const query: GetContentFilters = { type }
|
||||
if (visibility) query.visibility = visibility
|
||||
if (favorite) query.favorite = favorite
|
||||
if (name) query.name = name
|
||||
|
||||
const { data, error } = await get('/platform/projects/{ref}/content/count', {
|
||||
params: { path: { ref: projectRef }, query: { type } },
|
||||
params: { path: { ref: projectRef }, query: { type, visibility, favorite, name } },
|
||||
signal,
|
||||
})
|
||||
|
||||
@@ -28,12 +40,16 @@ export type ContentIdData = Awaited<ReturnType<typeof getContentCount>>
|
||||
export type ContentIdError = ResponseError
|
||||
|
||||
export const useContentCountQuery = <TData = ContentIdData>(
|
||||
{ projectRef, type }: getContentCountVariables,
|
||||
{ projectRef, type, visibility, favorite, name }: getContentCountVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<ContentIdData, ContentIdError, TData> = {}
|
||||
) =>
|
||||
useQuery<ContentIdData, ContentIdError, TData>(
|
||||
contentKeys.count(projectRef, type),
|
||||
({ signal }) => getContentCount({ projectRef, type }, signal),
|
||||
contentKeys.count(projectRef, type, {
|
||||
visibility,
|
||||
favorite,
|
||||
name,
|
||||
}),
|
||||
({ signal }) => getContentCount({ projectRef, type, visibility, favorite, name }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const useContentDeleteMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(contentKeys.list(projectRef))
|
||||
await queryClient.invalidateQueries(contentKeys.allContentLists(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import type { ResponseError } from 'types'
|
||||
import type { Content } from './content-query'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
export type GetUserContentByIdResponse = Omit<
|
||||
components['schemas']['GetUserContentByIdResponse'],
|
||||
'content'
|
||||
> & {
|
||||
content: Content['content']
|
||||
}
|
||||
|
||||
export async function getContentById(
|
||||
{ projectRef, id }: { projectRef?: string; id?: string },
|
||||
signal?: AbortSignal
|
||||
@@ -17,7 +26,8 @@ export async function getContentById(
|
||||
})
|
||||
|
||||
if (error) throw handleError(error)
|
||||
return data
|
||||
// override content type
|
||||
return data as unknown as GetUserContentByIdResponse
|
||||
}
|
||||
|
||||
export type ContentIdData = Awaited<ReturnType<typeof getContentById>>
|
||||
|
||||
@@ -7,7 +7,10 @@ import type { ResponseError } from 'types'
|
||||
import type { Content } from './content-query'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
export type InsertContentPayload = Omit<components['schemas']['CreateContentBody'], 'content'> & {
|
||||
export type InsertContentPayload = Omit<
|
||||
components['schemas']['CreateContentBodyDto'],
|
||||
'content'
|
||||
> & {
|
||||
content: Content['content']
|
||||
}
|
||||
|
||||
@@ -32,13 +35,12 @@ export async function insertContent(
|
||||
type: payload.type,
|
||||
visibility: payload.visibility,
|
||||
content: payload.content as any,
|
||||
folder_id: payload.folder_id,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
|
||||
// [Joshen] There's an issue with the API codegen due to content endpoint having 2 versions
|
||||
return data as unknown as InsertContentResponse[]
|
||||
return data
|
||||
}
|
||||
|
||||
export type InsertContentData = Awaited<ReturnType<typeof insertContent>>
|
||||
@@ -58,7 +60,7 @@ export const useContentInsertMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(contentKeys.list(projectRef))
|
||||
await queryClient.invalidateQueries(contentKeys.allContentLists(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import type { Dashboards, LogSqlSnippets, Owner, SqlSnippets } from 'types'
|
||||
import { components } from 'api-types'
|
||||
import { get } from 'data/fetchers'
|
||||
import type { Dashboards, LogSqlSnippets, SqlSnippets } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
export type ContentBase = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
owner_id?: number // user id
|
||||
last_updated_by?: number // user id
|
||||
inserted_at?: string // '2021-08-26T08:24:52.040695+00:00'
|
||||
owner?: Owner
|
||||
project_id?: number
|
||||
updated_at?: string // '2021-08-26T08:24:52.040695+00:00'
|
||||
updated_by?: Owner
|
||||
}
|
||||
export type ContentBase = components['schemas']['GetUserContentObject']
|
||||
|
||||
export type Content = ContentBase &
|
||||
export type Content = Omit<ContentBase, 'content' | 'type'> &
|
||||
(
|
||||
| {
|
||||
type: 'sql'
|
||||
@@ -35,30 +23,30 @@ export type Content = ContentBase &
|
||||
}
|
||||
)
|
||||
|
||||
export type ContentType = 'sql' | 'report' | 'log_sql'
|
||||
export type ContentType = Content['type']
|
||||
|
||||
export async function getContent(
|
||||
projectRef: string | undefined,
|
||||
signal?: AbortSignal
|
||||
): Promise<{
|
||||
content: Content[]
|
||||
}> {
|
||||
interface GetContentVariables {
|
||||
projectRef?: string
|
||||
type: ContentType
|
||||
}
|
||||
|
||||
export async function getContent({ projectRef, type }: GetContentVariables, signal?: AbortSignal) {
|
||||
if (typeof projectRef === 'undefined') {
|
||||
throw new Error('projectRef is required for getContent')
|
||||
}
|
||||
|
||||
let response = await get(`${API_URL}/projects/${projectRef}/content`, { signal })
|
||||
const { data, error } = await get('/platform/projects/{ref}/content', {
|
||||
params: { path: { ref: projectRef }, query: { type } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Content not found')
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return {
|
||||
content: response.data,
|
||||
cursor: data.cursor,
|
||||
content: data.data as unknown as Content[],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +54,11 @@ export type ContentData = Awaited<ReturnType<typeof getContent>>
|
||||
export type ContentError = unknown
|
||||
|
||||
export const useContentQuery = <TData = ContentData>(
|
||||
projectRef: string | undefined,
|
||||
{ projectRef, type }: GetContentVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<ContentData, ContentError, TData> = {}
|
||||
) =>
|
||||
useQuery<ContentData, ContentError, TData>(
|
||||
contentKeys.list(projectRef),
|
||||
({ signal }) => getContent(projectRef, signal),
|
||||
contentKeys.list(projectRef, type),
|
||||
({ signal }) => getContent({ projectRef, type }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
@@ -6,24 +6,30 @@ import type { ResponseError } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
import { ContentType } from './content-query'
|
||||
|
||||
type UpdateContentVariables = { projectRef: string; id: string; type: ContentType; content: any }
|
||||
type UpdateContentVariables = {
|
||||
projectRef: string
|
||||
id: string
|
||||
type: ContentType
|
||||
content: any
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export async function updateContent(
|
||||
{ projectRef, id, type, content }: UpdateContentVariables,
|
||||
{ projectRef, id, type, content, name, description }: UpdateContentVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const { data, error } = await patch('/platform/projects/{ref}/content', {
|
||||
params: {
|
||||
// @ts-ignore API codegen issue
|
||||
path: { ref: projectRef },
|
||||
query: { id },
|
||||
},
|
||||
body: { id, type, content },
|
||||
body: { id, type, content, name, description },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
return data[0]
|
||||
}
|
||||
|
||||
type UpdateContentData = Awaited<ReturnType<typeof updateContent>>
|
||||
@@ -43,7 +49,7 @@ export const useContentUpdateMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(contentKeys.list(projectRef))
|
||||
await queryClient.invalidateQueries(contentKeys.allContentLists(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -7,7 +7,10 @@ import type { ResponseError } from 'types'
|
||||
import type { Content } from './content-query'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
export type UpsertContentPayload = Omit<components['schemas']['UpsertContentBody'], 'content'> & {
|
||||
export type UpsertContentPayload = Omit<
|
||||
components['schemas']['UpsertContentBodyDto'],
|
||||
'content'
|
||||
> & {
|
||||
content: Partial<Content['content']>
|
||||
}
|
||||
|
||||
@@ -31,6 +34,7 @@ export async function upsertContent(
|
||||
type: payload.type,
|
||||
visibility: payload.visibility,
|
||||
content: payload.content as any,
|
||||
folder_id: payload.folder_id,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
@@ -60,7 +64,7 @@ export const useContentUpsertMutation = ({
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
if (invalidateQueriesOnSuccess) {
|
||||
await queryClient.invalidateQueries(contentKeys.list(projectRef))
|
||||
await queryClient.invalidateQueries(contentKeys.allContentLists(projectRef))
|
||||
}
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Content } from './content-query'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
export type UpsertContentPayloadV2 = Omit<
|
||||
components['schemas']['UpsertContentBodyV2'],
|
||||
components['schemas']['UpsertContentBodyDto'],
|
||||
'content'
|
||||
> & {
|
||||
content: Content['content']
|
||||
@@ -24,7 +24,6 @@ export async function upsertContent(
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const { data, error } = await put('/platform/projects/{ref}/content', {
|
||||
// @ts-ignore API codegen is wrong, any is also cause of API codegen being unable to handle versioning
|
||||
params: { path: { ref: projectRef } },
|
||||
body: payload as any,
|
||||
headers: { Version: '2' },
|
||||
@@ -56,13 +55,13 @@ export const useContentUpsertV2Mutation = ({
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
if (invalidateQueriesOnSuccess) {
|
||||
await queryClient.invalidateQueries(contentKeys.list(projectRef))
|
||||
await queryClient.invalidateQueries(contentKeys.allContentLists(projectRef))
|
||||
}
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to insert content: ${data.message}`)
|
||||
toast.error(`Failed to upsert content: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import type { ContentType } from './content-query'
|
||||
import type { SqlSnippet } from './sql-snippets-query'
|
||||
|
||||
export const contentKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'content'] as const,
|
||||
folders: (projectRef: string | undefined, id?: string) =>
|
||||
['projects', projectRef, 'content', 'folders', id].filter(Boolean),
|
||||
allContentLists: (projectRef: string | undefined) => ['projects', projectRef, 'content'] as const,
|
||||
list: (projectRef: string | undefined, type: ContentType | undefined) =>
|
||||
['projects', projectRef, 'content', type] as const,
|
||||
sqlSnippets: (
|
||||
projectRef: string | undefined,
|
||||
options?: {
|
||||
sort?: 'inserted_at' | 'name'
|
||||
name?: string
|
||||
visibility?: SqlSnippet['visibility']
|
||||
favorite?: boolean
|
||||
}
|
||||
) => ['projects', projectRef, 'content', 'sql', options].filter(Boolean),
|
||||
folders: (
|
||||
projectRef: string | undefined,
|
||||
options?: { sort?: 'inserted_at' | 'name'; name?: string }
|
||||
) => ['projects', projectRef, 'content', 'folders', options].filter(Boolean),
|
||||
folderContents: (
|
||||
projectRef: string | undefined,
|
||||
id?: string,
|
||||
options?: { sort?: 'inserted_at' | 'name'; name?: string }
|
||||
) => ['projects', projectRef, 'content', 'folders', id, options].filter(Boolean),
|
||||
resource: (projectRef: string | undefined, id?: string) =>
|
||||
['projects', projectRef, 'content', id] as const,
|
||||
count: (projectRef: string | undefined, type?: string) =>
|
||||
['projects', projectRef, 'content', 'count', type].filter(Boolean),
|
||||
count: (
|
||||
projectRef: string | undefined,
|
||||
type?: string,
|
||||
options?: { visibility?: SqlSnippet['visibility']; favorite?: boolean; name?: string }
|
||||
) => ['projects', projectRef, 'content', 'count', type, options].filter(Boolean),
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
|
||||
export type SQLSnippetFolderContentsVariables = {
|
||||
projectRef?: string
|
||||
folderId?: string
|
||||
cursor?: string
|
||||
name?: string
|
||||
sort?: 'name' | 'inserted_at'
|
||||
}
|
||||
|
||||
export async function getSQLSnippetFolderContents(
|
||||
{ projectRef, folderId, cursor, sort, name }: SQLSnippetFolderContentsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (typeof projectRef === 'undefined') throw new Error('projectRef is required')
|
||||
if (typeof folderId === 'undefined') throw new Error('folderId is required')
|
||||
|
||||
const sortOrder = sort === 'name' ? 'asc' : 'desc'
|
||||
|
||||
const { data, error } = await get('/platform/projects/{ref}/content/folders/{id}', {
|
||||
params: {
|
||||
path: { ref: projectRef, id: folderId },
|
||||
query: { cursor, limit: '3', sort_by: sort, sort_order: sortOrder, name },
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw handleError(error)
|
||||
return {
|
||||
...data.data,
|
||||
cursor: data.cursor,
|
||||
}
|
||||
}
|
||||
|
||||
export type SQLSnippetFolderContentsData = Awaited<ReturnType<typeof getSQLSnippetFolderContents>>
|
||||
export type SQLSnippetFolderContentsError = ResponseError
|
||||
|
||||
export const useSQLSnippetFolderContentsQuery = <TData = SQLSnippetFolderContentsData>(
|
||||
{ projectRef, folderId, name, sort }: Omit<SQLSnippetFolderContentsVariables, 'cursor'>,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseInfiniteQueryOptions<
|
||||
SQLSnippetFolderContentsData,
|
||||
SQLSnippetFolderContentsError,
|
||||
TData
|
||||
> = {}
|
||||
) =>
|
||||
useInfiniteQuery<SQLSnippetFolderContentsData, SQLSnippetFolderContentsError, TData>(
|
||||
contentKeys.folderContents(projectRef, folderId, { name, sort }),
|
||||
({ signal, pageParam }) =>
|
||||
getSQLSnippetFolderContents({ projectRef, folderId, cursor: pageParam, name, sort }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof folderId !== 'undefined',
|
||||
getNextPageParam(lastPage) {
|
||||
return lastPage.cursor
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,37 +1,52 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
import { components } from 'api-types'
|
||||
|
||||
export type SnippetFolderResponse = components['schemas']['GetUserContentFolderResponse']['data']
|
||||
export type SnippetFolder = components['schemas']['UserContentFolder']
|
||||
export type Snippet = components['schemas']['UserContentObjectMeta']
|
||||
export type SnippetDetail = components['schemas']['UserContentObjectV2']
|
||||
|
||||
export type SQLSnippetFolderVariables = {
|
||||
projectRef?: string
|
||||
cursor?: string
|
||||
name?: string
|
||||
sort?: 'name' | 'inserted_at'
|
||||
}
|
||||
|
||||
export const SNIPPET_PAGE_LIMIT = 100
|
||||
|
||||
export async function getSQLSnippetFolders(
|
||||
{ projectRef, folderId }: { projectRef?: string; folderId?: string },
|
||||
{ projectRef, cursor, sort, name }: SQLSnippetFolderVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (typeof projectRef === 'undefined') throw new Error('projectRef is required')
|
||||
|
||||
if (folderId) {
|
||||
const { data, error } = await get('/platform/projects/{ref}/content/folders/{id}', {
|
||||
params: { path: { ref: projectRef, id: folderId } },
|
||||
signal,
|
||||
})
|
||||
const sortOrder = sort === 'name' ? 'asc' : 'desc'
|
||||
|
||||
if (error) throw handleError(error)
|
||||
return data.data
|
||||
} else {
|
||||
const { data, error } = await get('/platform/projects/{ref}/content/folders', {
|
||||
params: { path: { ref: projectRef }, query: { type: 'sql' } },
|
||||
signal,
|
||||
})
|
||||
const { data, error } = await get('/platform/projects/{ref}/content/folders', {
|
||||
params: {
|
||||
path: { ref: projectRef },
|
||||
query: {
|
||||
type: 'sql',
|
||||
cursor,
|
||||
limit: SNIPPET_PAGE_LIMIT.toString(),
|
||||
sort_by: sort,
|
||||
sort_order: sortOrder,
|
||||
name,
|
||||
// [Alaister] Hard coding visibility to 'user' as folders are only supported for user content
|
||||
visibility: 'user',
|
||||
},
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw handleError(error)
|
||||
return data.data
|
||||
if (error) throw handleError(error)
|
||||
return {
|
||||
...data.data,
|
||||
cursor: data.cursor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +54,21 @@ export type SQLSnippetFoldersData = Awaited<ReturnType<typeof getSQLSnippetFolde
|
||||
export type SQLSnippetFoldersError = ResponseError
|
||||
|
||||
export const useSQLSnippetFoldersQuery = <TData = SQLSnippetFoldersData>(
|
||||
{ projectRef, folderId }: { projectRef?: string; folderId?: string },
|
||||
{ projectRef, name, sort }: Omit<SQLSnippetFolderVariables, 'cursor'>,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<SQLSnippetFoldersData, SQLSnippetFoldersError, TData> = {}
|
||||
}: UseInfiniteQueryOptions<SQLSnippetFoldersData, SQLSnippetFoldersError, TData> = {}
|
||||
) =>
|
||||
useQuery<SQLSnippetFoldersData, SQLSnippetFoldersError, TData>(
|
||||
contentKeys.folders(projectRef, folderId),
|
||||
({ signal }) => getSQLSnippetFolders({ projectRef, folderId }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
useInfiniteQuery<SQLSnippetFoldersData, SQLSnippetFoldersError, TData>(
|
||||
contentKeys.folders(projectRef, { name, sort }),
|
||||
({ signal, pageParam }) =>
|
||||
getSQLSnippetFolders({ projectRef, cursor: pageParam, name, sort }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
getNextPageParam(lastPage) {
|
||||
return lastPage.cursor
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,25 +1,77 @@
|
||||
import { UseQueryOptions } from '@tanstack/react-query'
|
||||
import { Content, ContentData, ContentError, useContentQuery } from 'data/content/content-query'
|
||||
import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
export type SqlSnippet = Extract<Content, { type: 'sql' | 'log_sql' | 'report' }>
|
||||
import { get } from 'data/fetchers'
|
||||
import { Content } from './content-query'
|
||||
import { contentKeys } from './keys'
|
||||
import { SNIPPET_PAGE_LIMIT } from './sql-folders-query'
|
||||
|
||||
export type SqlSnippets = {
|
||||
snippets: SqlSnippet[]
|
||||
export type SqlSnippet = Extract<Content, { type: 'sql' }>
|
||||
|
||||
interface GetSqlSnippetsVariables {
|
||||
projectRef?: string
|
||||
cursor?: string
|
||||
visibility?: SqlSnippet['visibility']
|
||||
favorite?: boolean
|
||||
name?: string
|
||||
sort?: 'name' | 'inserted_at'
|
||||
}
|
||||
|
||||
function filterSqlContent(content: Content): content is Extract<Content, { type: 'sql' }> {
|
||||
return content.type === 'sql'
|
||||
}
|
||||
export async function getSqlSnippets(
|
||||
{ projectRef, cursor, visibility, favorite, name, sort }: GetSqlSnippetsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (typeof projectRef === 'undefined') {
|
||||
throw new Error('projectRef is required for getSqlSnippets')
|
||||
}
|
||||
|
||||
export const useSqlSnippetsQuery = (
|
||||
projectRef: string | undefined,
|
||||
options: UseQueryOptions<ContentData, ContentError, SqlSnippets> = {}
|
||||
) =>
|
||||
useContentQuery<SqlSnippets>(projectRef, {
|
||||
select: (data) => {
|
||||
return {
|
||||
snippets: data.content.filter(filterSqlContent),
|
||||
}
|
||||
const sortOrder = sort === 'name' ? 'asc' : 'desc'
|
||||
|
||||
const { data, error } = await get('/platform/projects/{ref}/content', {
|
||||
params: {
|
||||
path: { ref: projectRef },
|
||||
query: {
|
||||
type: 'sql',
|
||||
cursor,
|
||||
visibility,
|
||||
favorite,
|
||||
name,
|
||||
limit: SNIPPET_PAGE_LIMIT.toString(),
|
||||
sort_by: sort,
|
||||
sort_order: sortOrder,
|
||||
},
|
||||
},
|
||||
...options,
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return {
|
||||
cursor: data.cursor,
|
||||
contents: data.data as unknown as SqlSnippet[],
|
||||
}
|
||||
}
|
||||
|
||||
export type SqlSnippetsData = Awaited<ReturnType<typeof getSqlSnippets>>
|
||||
export type SqlSnippetsError = unknown
|
||||
|
||||
export const useSqlSnippetsQuery = <TData = SqlSnippetsData>(
|
||||
{ projectRef, sort, name, visibility, favorite }: Omit<GetSqlSnippetsVariables, 'cursor'>,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseInfiniteQueryOptions<SqlSnippetsData, SqlSnippetsError, TData> = {}
|
||||
) =>
|
||||
useInfiniteQuery<SqlSnippetsData, SqlSnippetsError, TData>(
|
||||
contentKeys.sqlSnippets(projectRef, { sort, name, visibility, favorite }),
|
||||
({ signal, pageParam: cursor }) =>
|
||||
getSqlSnippets({ projectRef, cursor, sort, name, visibility, favorite }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
getNextPageParam(lastPage) {
|
||||
return lastPage.cursor
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -39,10 +39,10 @@ export async function getNotifications(options: NotificationVariables, signal?:
|
||||
const { status, filters, page = 0, limit = NOTIFICATIONS_PAGE_LIMIT } = options
|
||||
const { data, error } = await get('/platform/notifications', {
|
||||
params: {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error maybe the types from the API aren't quite right?
|
||||
query: {
|
||||
offset: page * limit,
|
||||
limit,
|
||||
offset: String(page * limit),
|
||||
limit: String(limit),
|
||||
...(status !== undefined ? { status } : { status: ['new', 'seen'] }),
|
||||
...(filters.priority.length > 0 ? { priority: filters.priority } : {}),
|
||||
...(filters.organizations.length > 0 ? { org_slug: filters.organizations } : {}),
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
"tus-js-client": "^4.1.0",
|
||||
"ui": "*",
|
||||
"ui-patterns": "*",
|
||||
"use-debounce": "^7.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"valtio": "^1.12.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
|
||||
@@ -307,7 +307,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
},
|
||||
onSuccess: (values) => {
|
||||
setSaveModalOpen(false)
|
||||
toast.success(`Saved "${values[0].name}" log query`)
|
||||
toast.success(`Saved "${values.name}" log query`)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -320,7 +320,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
|
||||
},
|
||||
onSuccess: (values) => {
|
||||
setSaveModalOpen(false)
|
||||
toast.success(`Updated "${values[0].name}" log query`)
|
||||
toast.success(`Updated "${values.name}" log query`)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ import { Loading } from 'ui'
|
||||
|
||||
export const LogsSavedPage: NextPageWithLayout = () => {
|
||||
const { ref } = useParams()
|
||||
const { data, isLoading } = useContentQuery(ref)
|
||||
const { data, isLoading } = useContentQuery({
|
||||
projectRef: ref,
|
||||
type: 'log_sql',
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading active={true}>{null}</Loading>
|
||||
|
||||
@@ -19,15 +19,21 @@ export const UserReportPage: NextPageWithLayout = () => {
|
||||
const { profile } = useProfile()
|
||||
const [showCreateReportModal, setShowCreateReportModal] = useState(false)
|
||||
|
||||
const { isLoading } = useContentQuery(ref, {
|
||||
onSuccess: (data) => {
|
||||
const reports = data.content
|
||||
.filter((x) => x.type === 'report')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
if (reports.length >= 1) router.push(`/project/${ref}/reports/${reports[0].id}`)
|
||||
if (reports.length === 0) router.push(`/project/${ref}/reports/api-overview`)
|
||||
const { isLoading } = useContentQuery(
|
||||
{
|
||||
projectRef: ref,
|
||||
type: 'report',
|
||||
},
|
||||
})
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
const reports = data.content
|
||||
.filter((x) => x.type === 'report')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
if (reports.length >= 1) router.push(`/project/${ref}/reports/${reports[0].id}`)
|
||||
if (reports.length === 0) router.push(`/project/${ref}/reports/api-overview`)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const canCreateReport = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'report', owner_id: profile?.id },
|
||||
|
||||
@@ -17,19 +17,19 @@ import { useFormatQueryMutation } from 'data/sql/format-sql-query'
|
||||
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useSnippets, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
||||
import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const SqlEditor: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const monaco = useMonaco()
|
||||
const { id, ref, content } = useParams()
|
||||
const { id, ref, content, skip } = useParams()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const appSnap = useAppStateSnapshot()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
|
||||
const snippets = useSnippets(ref!)
|
||||
const allSnippets = useSnippets(ref!)
|
||||
const { mutateAsync: formatQuery } = useFormatQueryMutation()
|
||||
|
||||
const [intellisenseEnabled] = useLocalStorageQuery(
|
||||
@@ -37,22 +37,24 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
true
|
||||
)
|
||||
|
||||
useContentIdQuery(
|
||||
// [Refactor] There's an unnecessary request getting triggered when we start typing while on /new
|
||||
// the URL ID gets updated and we attempt to fetch content for a snippet that's not been created yet
|
||||
const { data } = useContentIdQuery(
|
||||
{ projectRef: ref, id },
|
||||
{
|
||||
// [Joshen] May need to investigate separately, but occasionally addSnippet doesnt exist in
|
||||
// the snapV2 valtio store for some reason hence why the added typeof check here
|
||||
retry: false,
|
||||
enabled: Boolean(id !== 'new' && typeof snapV2.addSnippet === 'function'),
|
||||
onSuccess: (data) => {
|
||||
snapV2.addSnippet({ projectRef: ref as string, snippet: data })
|
||||
},
|
||||
onError: () => {
|
||||
// [Joshen] Thinking if we need some error handler - it'll error out here when a new snippet is created from quickstart/templates
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && data) {
|
||||
snapV2.setSnippet(ref, data as unknown as SnippetWithContent)
|
||||
}
|
||||
}, [ref, data])
|
||||
|
||||
async function formatPgsql(value: string) {
|
||||
try {
|
||||
if (!project) throw new Error('No project')
|
||||
@@ -118,12 +120,19 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
pgInfoRef.current.functions = functions
|
||||
}
|
||||
|
||||
// Load the last visited snippet when landing on /new
|
||||
useEffect(() => {
|
||||
if (id === 'new' && appSnap.dashboardHistory.sql !== undefined && content === undefined) {
|
||||
const snippet = snippets.find((snippet) => snippet.id === appSnap.dashboardHistory.sql)
|
||||
if (
|
||||
id === 'new' &&
|
||||
skip !== 'true' && // [Joshen] Skip flag implies to skip loading the last visited snippet
|
||||
appSnap.dashboardHistory.sql !== undefined &&
|
||||
content === undefined
|
||||
) {
|
||||
const snippet = allSnippets.find((snippet) => snippet.id === appSnap.dashboardHistory.sql)
|
||||
if (snippet !== undefined) router.push(`/project/${ref}/sql/${appSnap.dashboardHistory.sql}`)
|
||||
}
|
||||
}, [id, snippets, content])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, allSnippets, content])
|
||||
|
||||
// Enable pgsql format
|
||||
useEffect(() => {
|
||||
@@ -138,6 +147,7 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
})
|
||||
return () => formatProvider.dispose()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [monaco])
|
||||
|
||||
// Register auto completion item provider for pgsql
|
||||
@@ -162,6 +172,7 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
signatureHelpProvider?.dispose()
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isPgInfoReady])
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,15 +3,16 @@ import { toast } from 'sonner'
|
||||
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
|
||||
import { devtools, proxySet } from 'valtio/utils'
|
||||
|
||||
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { getContentById } from 'data/content/content-id-query'
|
||||
import { UpsertContentPayloadV2, upsertContent } from 'data/content/content-upsert-v2-mutation'
|
||||
import { contentKeys } from 'data/content/keys'
|
||||
import { createSQLSnippetFolder } from 'data/content/sql-folder-create-mutation'
|
||||
import { updateSQLSnippetFolder } from 'data/content/sql-folder-update-mutation'
|
||||
import { Snippet, SnippetFolder, SnippetFolderResponse } from 'data/content/sql-folders-query'
|
||||
import { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { Snippet, SnippetFolder } from 'data/content/sql-folders-query'
|
||||
import { getQueryClient } from 'data/query-client'
|
||||
import { getContentById } from 'data/content/content-id-query'
|
||||
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { SqlSnippets } from 'types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export type StateSnippetFolder = {
|
||||
projectRef: string
|
||||
@@ -20,14 +21,14 @@ export type StateSnippetFolder = {
|
||||
}
|
||||
|
||||
// [Joshen] API codegen is somehow missing the content property
|
||||
export interface SnippetContent extends Snippet {
|
||||
content?: any
|
||||
export interface SnippetWithContent extends Snippet {
|
||||
content?: SqlSnippets.Content
|
||||
}
|
||||
|
||||
export type StateSnippet = {
|
||||
projectRef: string
|
||||
splitSizes: number[]
|
||||
snippet: SnippetContent
|
||||
snippet: SnippetWithContent
|
||||
}
|
||||
|
||||
const NEW_FOLDER_ID = 'new-folder'
|
||||
@@ -43,14 +44,7 @@ export const sqlEditorState = proxy({
|
||||
snippets: {} as {
|
||||
[snippetId: string]: StateSnippet
|
||||
},
|
||||
// We're storing favorites separately as they need to be a flat list and hence
|
||||
// cannot be derived from snippets as folder contents are loaded on demand
|
||||
favoriteSnippets: {} as {
|
||||
[snippetId: string]: {
|
||||
projectRef: string
|
||||
snippet: SqlSnippet
|
||||
}
|
||||
},
|
||||
|
||||
// Query results, if any, for a snippet
|
||||
results: {} as {
|
||||
[snippetId: string]: {
|
||||
@@ -59,13 +53,6 @@ export const sqlEditorState = proxy({
|
||||
autoLimit?: number
|
||||
}[]
|
||||
},
|
||||
// Project ref as the key, marks which project already has snippets loaded
|
||||
loaded: {} as {
|
||||
[projectRef: string]: boolean
|
||||
},
|
||||
privateSnippetCount: {} as {
|
||||
[projectRef: string]: number
|
||||
},
|
||||
// Synchronous saving of folders and snippets (debounce behavior)
|
||||
needsSaving: proxySet<string>([]),
|
||||
// Stores the state of each snippet
|
||||
@@ -73,7 +60,6 @@ export const sqlEditorState = proxy({
|
||||
[snippetId: string]: 'IDLE' | 'UPDATING' | 'UPDATING_FAILED'
|
||||
},
|
||||
limit: 100,
|
||||
order: 'inserted_at' as 'name' | 'inserted_at',
|
||||
// For handling renaming folder failed
|
||||
lastUpdatedFolderName: '',
|
||||
|
||||
@@ -88,58 +74,15 @@ export const sqlEditorState = proxy({
|
||||
// ## Methods to interact the store with
|
||||
// ========================================================================
|
||||
|
||||
// Initial loading of data into UI, only called once when first loading data
|
||||
// Note that snippets here do not have the content property, and will need to be
|
||||
// further loaded on demand instead. Entry point from SQLEditorNav.tsx
|
||||
initializeRemoteSnippets: ({
|
||||
projectRef,
|
||||
data,
|
||||
sort,
|
||||
}: {
|
||||
projectRef: string
|
||||
data: SnippetFolderResponse
|
||||
sort: 'name' | 'inserted_at'
|
||||
}) => {
|
||||
const { folders, contents } = data
|
||||
folders?.forEach((folder) => {
|
||||
sqlEditorState.folders[folder.id] = { projectRef, folder }
|
||||
})
|
||||
contents?.forEach((snippet) => {
|
||||
sqlEditorState.addSnippet({ projectRef, snippet })
|
||||
})
|
||||
sqlEditorState.loaded[projectRef] = true
|
||||
sqlEditorState.order = sort
|
||||
},
|
||||
|
||||
initializeFavoriteSnippets: ({
|
||||
projectRef,
|
||||
snippets,
|
||||
}: {
|
||||
projectRef: string
|
||||
snippets: SqlSnippet[]
|
||||
}) => {
|
||||
snippets.forEach((snippet) => {
|
||||
if (snippet.id && sqlEditorState.favoriteSnippets[snippet.id]?.snippet === undefined) {
|
||||
sqlEditorState.favoriteSnippets[snippet.id] = { projectRef, snippet }
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
setDiffContent: (sql: string, diffType: DiffType) =>
|
||||
(sqlEditorState.diffContent = { sql, diffType }),
|
||||
|
||||
setOrder: (value: 'name' | 'inserted_at') => (sqlEditorState.order = value),
|
||||
addSnippet: ({ projectRef, snippet }: { projectRef: string; snippet: SnippetWithContent }) => {
|
||||
if (sqlEditorState.snippets[snippet.id]) return
|
||||
|
||||
setPrivateSnippetCount: ({ projectRef, value }: { projectRef: string; value: number }) => {
|
||||
sqlEditorState.privateSnippetCount[projectRef] = value
|
||||
},
|
||||
|
||||
addSnippet: ({ projectRef, snippet }: { projectRef: string; snippet: Snippet }) => {
|
||||
if (snippet.id && sqlEditorState.snippets[snippet.id]?.snippet?.content === undefined) {
|
||||
sqlEditorState.snippets[snippet.id] = { projectRef, splitSizes: [50, 50], snippet }
|
||||
sqlEditorState.results[snippet.id] = []
|
||||
sqlEditorState.savingStates[snippet.id] = 'IDLE'
|
||||
}
|
||||
sqlEditorState.snippets[snippet.id] = { projectRef, splitSizes: [50, 50], snippet }
|
||||
sqlEditorState.results[snippet.id] = []
|
||||
sqlEditorState.savingStates[snippet.id] = 'IDLE'
|
||||
},
|
||||
|
||||
updateSnippet: ({
|
||||
@@ -148,7 +91,7 @@ export const sqlEditorState = proxy({
|
||||
skipSave = false,
|
||||
}: {
|
||||
id: string
|
||||
snippet: Snippet
|
||||
snippet: Partial<Snippet>
|
||||
skipSave?: boolean
|
||||
}) => {
|
||||
if (sqlEditorState.snippets[id]) {
|
||||
@@ -160,9 +103,22 @@ export const sqlEditorState = proxy({
|
||||
}
|
||||
},
|
||||
|
||||
setSnippet: (projectRef: string, snippet: SnippetWithContent) => {
|
||||
let storedSnippet = sqlEditorState.snippets[snippet.id]
|
||||
if (storedSnippet) {
|
||||
if (!storedSnippet.snippet.content) {
|
||||
storedSnippet.snippet.content = snippet.content
|
||||
sqlEditorState.needsSaving.add(storedSnippet.snippet.id)
|
||||
}
|
||||
} else {
|
||||
sqlEditorState.addSnippet({ projectRef: projectRef, snippet })
|
||||
}
|
||||
},
|
||||
|
||||
setSql: (id: string, sql: string) => {
|
||||
if (sqlEditorState.snippets[id]) {
|
||||
sqlEditorState.snippets[id].snippet.content.sql = sql
|
||||
let snippet = sqlEditorState.snippets[id]?.snippet
|
||||
if (snippet?.content) {
|
||||
snippet.content.sql = sql
|
||||
sqlEditorState.needsSaving.add(id)
|
||||
}
|
||||
},
|
||||
@@ -176,12 +132,10 @@ export const sqlEditorState = proxy({
|
||||
name: string
|
||||
description?: string
|
||||
}) => {
|
||||
if (sqlEditorState.snippets[id]) {
|
||||
sqlEditorState.snippets[id] = {
|
||||
...sqlEditorState.snippets[id],
|
||||
snippet: { ...sqlEditorState.snippets[id].snippet, name, description },
|
||||
}
|
||||
sqlEditorState.needsSaving.add(id)
|
||||
let snippet = sqlEditorState.snippets[id]?.snippet
|
||||
if (snippet) {
|
||||
snippet.name = name
|
||||
snippet.description = description
|
||||
}
|
||||
},
|
||||
|
||||
@@ -195,6 +149,12 @@ export const sqlEditorState = proxy({
|
||||
sqlEditorState.needsSaving.delete(id)
|
||||
},
|
||||
|
||||
addFolder: ({ projectRef, folder }: { projectRef: string; folder: SnippetFolder }) => {
|
||||
if (sqlEditorState.folders[folder.id]) return
|
||||
|
||||
sqlEditorState.folders[folder.id] = { projectRef, folder }
|
||||
},
|
||||
|
||||
addNewFolder: ({ projectRef }: { projectRef: string }) => {
|
||||
// [Joshen] Use this to identify new folders that have yet to be saved
|
||||
const id = NEW_FOLDER_ID
|
||||
@@ -212,31 +172,27 @@ export const sqlEditorState = proxy({
|
||||
},
|
||||
|
||||
editFolder: (id: string) => {
|
||||
sqlEditorState.folders[id] = { ...sqlEditorState.folders[id], status: 'editing' }
|
||||
sqlEditorState.folders[id].status = 'editing'
|
||||
},
|
||||
|
||||
saveFolder: ({ id, name }: { id: string; name: string }) => {
|
||||
const hasChanges = sqlEditorState.folders[id].folder.name !== name
|
||||
let storeFolder = sqlEditorState.folders[id]
|
||||
const hasChanges = storeFolder.folder.name !== name
|
||||
|
||||
if (id === 'new-folder' && sqlEditorState.allFolderNames.includes(name)) {
|
||||
sqlEditorState.removeFolder(id)
|
||||
return toast.error('This folder name already exists')
|
||||
} else if (hasChanges && sqlEditorState.allFolderNames.includes(name)) {
|
||||
sqlEditorState.folders[id] = { ...sqlEditorState.folders[id], status: 'idle' }
|
||||
storeFolder.status = 'idle'
|
||||
return toast.error('This folder name already exists')
|
||||
}
|
||||
|
||||
const originalFolderName = sqlEditorState.folders[id].folder.name.slice()
|
||||
const originalFolderName = storeFolder.folder.name.slice()
|
||||
|
||||
storeFolder.status = hasChanges ? 'saving' : 'idle'
|
||||
storeFolder.folder.id = id
|
||||
storeFolder.folder.name = name
|
||||
|
||||
sqlEditorState.folders[id] = {
|
||||
projectRef: sqlEditorState.folders[id].projectRef,
|
||||
status: hasChanges ? 'saving' : 'idle',
|
||||
folder: {
|
||||
...sqlEditorState.folders[id].folder,
|
||||
id,
|
||||
name,
|
||||
},
|
||||
}
|
||||
if (hasChanges) {
|
||||
sqlEditorState.lastUpdatedFolderName = originalFolderName
|
||||
sqlEditorState.needsSaving.add(id)
|
||||
@@ -253,78 +209,18 @@ export const sqlEditorState = proxy({
|
||||
addNeedsSaving: (id: string) => sqlEditorState.needsSaving.add(id),
|
||||
|
||||
addFavorite: (id: string) => {
|
||||
if (sqlEditorState.snippets[id]) {
|
||||
sqlEditorState.snippets[id] = {
|
||||
...sqlEditorState.snippets[id],
|
||||
snippet: {
|
||||
...sqlEditorState.snippets[id].snippet,
|
||||
favorite: true,
|
||||
},
|
||||
}
|
||||
|
||||
sqlEditorState.favoriteSnippets[id] = {
|
||||
projectRef: sqlEditorState.snippets[id].projectRef,
|
||||
snippet: {
|
||||
...sqlEditorState.snippets[id].snippet,
|
||||
folder_id: undefined,
|
||||
} as unknown as SqlSnippet,
|
||||
}
|
||||
const storeSnippet = sqlEditorState.snippets[id]
|
||||
if (storeSnippet) {
|
||||
storeSnippet.snippet.favorite = true
|
||||
|
||||
sqlEditorState.needsSaving.add(id)
|
||||
}
|
||||
},
|
||||
|
||||
removeFavorite: (id: string) => {
|
||||
if (sqlEditorState.snippets[id]) {
|
||||
sqlEditorState.snippets[id] = {
|
||||
...sqlEditorState.snippets[id],
|
||||
snippet: {
|
||||
...sqlEditorState.snippets[id].snippet,
|
||||
favorite: false,
|
||||
},
|
||||
}
|
||||
|
||||
const { [id]: snippet, ...otherSnippets } = sqlEditorState.favoriteSnippets
|
||||
sqlEditorState.favoriteSnippets = otherSnippets
|
||||
|
||||
sqlEditorState.needsSaving.add(id)
|
||||
}
|
||||
},
|
||||
|
||||
shareSnippet: async (id: string, visibility: 'user' | 'project' | 'org' | 'public') => {
|
||||
const storeSnippet = sqlEditorState.snippets[id]
|
||||
|
||||
if (storeSnippet) {
|
||||
let snippetContent = storeSnippet.snippet.content
|
||||
if (snippetContent === undefined) {
|
||||
const { content } = await getContentById({ projectRef: storeSnippet.projectRef, id })
|
||||
snippetContent = content
|
||||
}
|
||||
|
||||
if (snippetContent === undefined) {
|
||||
// [Joshen] Just as a final check - to ensure that the content is minimally there (empty string is fine)
|
||||
return toast.error('Unable to share snippet: Content is missing')
|
||||
}
|
||||
|
||||
sqlEditorState.snippets[id] = {
|
||||
...storeSnippet,
|
||||
snippet: {
|
||||
...storeSnippet.snippet,
|
||||
content: snippetContent,
|
||||
visibility,
|
||||
folder_id: null as any,
|
||||
},
|
||||
}
|
||||
|
||||
if (sqlEditorState.favoriteSnippets[id] !== undefined) {
|
||||
sqlEditorState.favoriteSnippets[id] = {
|
||||
projectRef: sqlEditorState.favoriteSnippets[id].projectRef,
|
||||
snippet: {
|
||||
...sqlEditorState.favoriteSnippets[id].snippet,
|
||||
visibility,
|
||||
} as unknown as SqlSnippet,
|
||||
}
|
||||
}
|
||||
if (storeSnippet.snippet) {
|
||||
storeSnippet.snippet.favorite = false
|
||||
|
||||
sqlEditorState.needsSaving.add(id)
|
||||
}
|
||||
@@ -361,33 +257,28 @@ export const useSqlEditorV2StateSnapshot = (options?: Parameters<typeof useSnaps
|
||||
export const useSnippetFolders = (projectRef: string) => {
|
||||
const snapshot = useSqlEditorV2StateSnapshot()
|
||||
|
||||
return Object.values(snapshot.folders)
|
||||
.filter((x) => x.projectRef === projectRef)
|
||||
.map((x) => x.folder)
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.values(snapshot.folders)
|
||||
.filter((x) => x.projectRef === projectRef)
|
||||
.map((x) => x.folder),
|
||||
[projectRef, snapshot.folders]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ALL snippets for a project
|
||||
*/
|
||||
export const useSnippets = (projectRef: string) => {
|
||||
const snapshot = useSqlEditorV2StateSnapshot()
|
||||
|
||||
return Object.values(snapshot.snippets)
|
||||
.filter((x) => x.projectRef === projectRef)
|
||||
.map((x) => x.snippet)
|
||||
.sort((a, b) => {
|
||||
if (snapshot.order === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
export const useFavoriteSnippets = (projectRef: string) => {
|
||||
const snapshot = useSqlEditorV2StateSnapshot()
|
||||
|
||||
return Object.values(snapshot.favoriteSnippets)
|
||||
.filter((x) => x.projectRef === projectRef)
|
||||
.map((x) => x.snippet)
|
||||
.sort((a, b) => {
|
||||
if (snapshot.order === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at ?? '').valueOf() - new Date(a.inserted_at ?? '').valueOf()
|
||||
})
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.values(snapshot.snippets)
|
||||
.filter((storeSnippet) => storeSnippet.projectRef === projectRef)
|
||||
.map((storeSnippet) => storeSnippet.snippet),
|
||||
[projectRef, snapshot.snippets]
|
||||
)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -400,7 +291,11 @@ async function upsertSnippet(id: string, projectRef: string, payload: UpsertCont
|
||||
await upsertContent({ projectRef, payload })
|
||||
|
||||
const queryClient = getQueryClient()
|
||||
await queryClient.invalidateQueries(contentKeys.count(projectRef, 'sql'))
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(contentKeys.count(projectRef, 'sql')),
|
||||
queryClient.invalidateQueries(contentKeys.sqlSnippets(projectRef)),
|
||||
queryClient.invalidateQueries(contentKeys.folders(projectRef)),
|
||||
])
|
||||
|
||||
sqlEditorState.savingStates[id] = 'IDLE'
|
||||
} catch (error) {
|
||||
@@ -423,7 +318,7 @@ async function upsertFolder(id: string, projectRef: string, name: string) {
|
||||
} else {
|
||||
await updateSQLSnippetFolder({ projectRef, id, name })
|
||||
toast.success('Successfully updated folder')
|
||||
sqlEditorState.folders[id] = { ...sqlEditorState.folders[id], status: 'idle' }
|
||||
sqlEditorState.folders[id].status = 'idle'
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to save folder: ${error.message}`)
|
||||
@@ -433,14 +328,10 @@ async function upsertFolder(id: string, projectRef: string, name: string) {
|
||||
error.message.includes('update') &&
|
||||
sqlEditorState.lastUpdatedFolderName.length > 0
|
||||
) {
|
||||
sqlEditorState.folders[id] = {
|
||||
...sqlEditorState.folders[id],
|
||||
status: 'idle',
|
||||
folder: {
|
||||
...sqlEditorState.folders[id].folder,
|
||||
name: sqlEditorState.lastUpdatedFolderName,
|
||||
},
|
||||
}
|
||||
let storeFolder = sqlEditorState.folders[id]
|
||||
|
||||
storeFolder.status = 'idle'
|
||||
storeFolder.folder.name = sqlEditorState.lastUpdatedFolderName
|
||||
}
|
||||
} finally {
|
||||
sqlEditorState.lastUpdatedFolderName = ''
|
||||
@@ -484,11 +375,11 @@ if (typeof window !== 'undefined') {
|
||||
visibility: visibility ?? 'user',
|
||||
project_id: project_id ?? 0,
|
||||
owner_id: owner_id,
|
||||
folder_id: folder_id,
|
||||
folder_id: folder_id ?? undefined,
|
||||
content: {
|
||||
...content,
|
||||
...content!,
|
||||
content_id: id,
|
||||
favorite: favorite,
|
||||
favorite: favorite ?? false,
|
||||
},
|
||||
})
|
||||
sqlEditorState.needsSaving.delete(id)
|
||||
|
||||
Generated
-1
@@ -1752,7 +1752,6 @@
|
||||
"tus-js-client": "^4.1.0",
|
||||
"ui": "*",
|
||||
"ui-patterns": "*",
|
||||
"use-debounce": "^7.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"valtio": "^1.12.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
|
||||
Vendored
+106
-301
@@ -619,14 +619,6 @@ export interface paths {
|
||||
/** Searches project pg.tables. Return maximum 50 results. */
|
||||
post: operations['SearchController_searchTables']
|
||||
}
|
||||
'/platform/pg-meta/{ref}/table-privileges': {
|
||||
/** Retrieve table privileges */
|
||||
get: operations['TablePrivilegesController_getTablePrivileges']
|
||||
/** Grant table privileges */
|
||||
post: operations['TablePrivilegesController_grantTablePrivileges']
|
||||
/** Revoke table privileges */
|
||||
delete: operations['TablePrivilegesController_revokeTablePrivileges']
|
||||
}
|
||||
'/platform/pg-meta/{ref}/tables': {
|
||||
/** Gets project pg.tables or pg.table with the given ID */
|
||||
get: operations['TablesController_getTables']
|
||||
@@ -693,10 +685,6 @@ export interface paths {
|
||||
/** Search profiles by username, email with the given keywords */
|
||||
post: operations['SearchProfileController_searchProfile']
|
||||
}
|
||||
'/platform/profile/subscriptions': {
|
||||
/** Gets the user's subscription statistics */
|
||||
get: operations['SubscriptionsController_getSubscriptionsStatistics']
|
||||
}
|
||||
'/platform/projects': {
|
||||
/**
|
||||
* Gets all projects that belong to the authenticated user
|
||||
@@ -866,15 +854,12 @@ export interface paths {
|
||||
patch: operations['v1-update-supavisor-config']
|
||||
}
|
||||
'/platform/projects/{ref}/content': {
|
||||
/**
|
||||
* Gets project's content
|
||||
* @deprecated
|
||||
*/
|
||||
/** Gets project's content */
|
||||
get: operations['ContentController_getContent']
|
||||
/** Updates project's content */
|
||||
put: operations['ContentController_updateWholeContentV2']
|
||||
put: operations['ContentController_updateWholeContent']
|
||||
/** Creates project's content */
|
||||
post: operations['ContentController_createContentV2']
|
||||
post: operations['ContentController_createContent']
|
||||
/** Deletes project's contents */
|
||||
delete: operations['ContentController_deleteContents']
|
||||
/**
|
||||
@@ -884,7 +869,7 @@ export interface paths {
|
||||
patch: operations['ContentController_updateContent']
|
||||
}
|
||||
'/platform/projects/{ref}/content/count': {
|
||||
/** Gets the count of a user's content by type */
|
||||
/** Gets the count of a user's content */
|
||||
get: operations['ContentController_getContentCount']
|
||||
}
|
||||
'/platform/projects/{ref}/content/folders': {
|
||||
@@ -1685,14 +1670,6 @@ export interface paths {
|
||||
/** Searches project pg.tables. Return maximum 50 results. */
|
||||
post: operations['SearchController_searchTables']
|
||||
}
|
||||
'/v0/pg-meta/{ref}/table-privileges': {
|
||||
/** Retrieve table privileges */
|
||||
get: operations['TablePrivilegesController_getTablePrivileges']
|
||||
/** Grant table privileges */
|
||||
post: operations['TablePrivilegesController_grantTablePrivileges']
|
||||
/** Revoke table privileges */
|
||||
delete: operations['TablePrivilegesController_revokeTablePrivileges']
|
||||
}
|
||||
'/v0/pg-meta/{ref}/tables': {
|
||||
/** Gets project pg.tables or pg.table with the given ID */
|
||||
get: operations['TablesController_getTables']
|
||||
@@ -1883,15 +1860,12 @@ export interface paths {
|
||||
patch: operations['v1-update-supavisor-config']
|
||||
}
|
||||
'/v0/projects/{ref}/content': {
|
||||
/**
|
||||
* Gets project's content
|
||||
* @deprecated
|
||||
*/
|
||||
/** Gets project's content */
|
||||
get: operations['ContentController_getContent']
|
||||
/** Updates project's content */
|
||||
put: operations['ContentController_updateWholeContentV2']
|
||||
put: operations['ContentController_updateWholeContent']
|
||||
/** Creates project's content */
|
||||
post: operations['ContentController_createContentV2']
|
||||
post: operations['ContentController_createContent']
|
||||
/** Deletes project's contents */
|
||||
delete: operations['ContentController_deleteContents']
|
||||
/**
|
||||
@@ -1901,7 +1875,7 @@ export interface paths {
|
||||
patch: operations['ContentController_updateContent']
|
||||
}
|
||||
'/v0/projects/{ref}/content/count': {
|
||||
/** Gets the count of a user's content by type */
|
||||
/** Gets the count of a user's content */
|
||||
get: operations['ContentController_getContentCount']
|
||||
}
|
||||
'/v0/projects/{ref}/content/item/{id}': {
|
||||
@@ -2956,9 +2930,13 @@ export interface components {
|
||||
tableId: number
|
||||
type: string
|
||||
}
|
||||
CreateContentBody: {
|
||||
content?: Record<string, never>
|
||||
CreateContentBodyDto: {
|
||||
content?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
description?: string
|
||||
/** Format: uuid */
|
||||
folder_id?: null | string
|
||||
id?: string
|
||||
name: string
|
||||
owner_id?: number
|
||||
@@ -2967,20 +2945,9 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
CreateContentBodyV2: {
|
||||
content?: Record<string, never>
|
||||
description?: string
|
||||
folder_id?: string
|
||||
id?: string
|
||||
name: string
|
||||
owner_id?: number
|
||||
/** @enum {string} */
|
||||
type: 'sql' | 'report' | 'log_sql'
|
||||
/** @enum {string} */
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
CreateContentFolderBody: {
|
||||
CreateContentFolderBodyDto: {
|
||||
name: string
|
||||
/** Format: uuid */
|
||||
parent_id?: string
|
||||
}
|
||||
CreateExtensionBody: {
|
||||
@@ -3794,7 +3761,7 @@ export interface components {
|
||||
content: Record<string, never>
|
||||
description?: string
|
||||
favorite: boolean | null
|
||||
folder_id?: string
|
||||
folder_id?: string | null
|
||||
id: string
|
||||
inserted_at: string
|
||||
last_updated_by?: number
|
||||
@@ -3808,6 +3775,7 @@ export interface components {
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
GetUserContentFolderResponse: {
|
||||
cursor?: string
|
||||
data: {
|
||||
contents?: components['schemas']['UserContentObjectMeta'][]
|
||||
folders?: components['schemas']['UserContentFolder'][]
|
||||
@@ -3816,6 +3784,8 @@ export interface components {
|
||||
GetUserContentObject: {
|
||||
content: Record<string, never>
|
||||
description?: string
|
||||
favorite?: boolean | null
|
||||
folder_id?: string | null
|
||||
id: string
|
||||
inserted_at: string
|
||||
last_updated_by?: number
|
||||
@@ -3837,6 +3807,7 @@ export interface components {
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
GetUserContentResponse: {
|
||||
cursor?: string
|
||||
data: components['schemas']['GetUserContentObject'][]
|
||||
}
|
||||
GetUserOrganizationIntegrationResponse: {
|
||||
@@ -4056,21 +4027,6 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES'
|
||||
}
|
||||
GrantTablePrivilegesBody: {
|
||||
grantee: string
|
||||
is_grantable?: boolean
|
||||
/** @enum {string} */
|
||||
privilege_type:
|
||||
| 'ALL'
|
||||
| 'SELECT'
|
||||
| 'INSERT'
|
||||
| 'UPDATE'
|
||||
| 'DELETE'
|
||||
| 'TRUNCATE'
|
||||
| 'REFERENCES'
|
||||
| 'TRIGGER'
|
||||
relation_id: number
|
||||
}
|
||||
HCaptchaBody: {
|
||||
hcaptchaToken: string
|
||||
}
|
||||
@@ -4813,12 +4769,6 @@ export interface components {
|
||||
schema: string
|
||||
size: string
|
||||
}
|
||||
PostgresTablePrivileges: {
|
||||
kind: string
|
||||
name: string
|
||||
privileges: components['schemas']['TablePrivilege'][]
|
||||
schema: string
|
||||
}
|
||||
PostgrestConfigResponse: {
|
||||
db_anon_role: string
|
||||
db_extra_search_path: string
|
||||
@@ -5177,12 +5127,8 @@ export interface components {
|
||||
region: string
|
||||
service_api_keys?: components['schemas']['ProjectServiceApiKeyResponse'][]
|
||||
ssl_enforced: boolean
|
||||
is_sensitive?: boolean
|
||||
status: string
|
||||
}
|
||||
ProjectSensitivitySettingResponse: {
|
||||
is_sensitive: boolean
|
||||
}
|
||||
/** @enum {string} */
|
||||
ProjectStatus:
|
||||
| 'ACTIVE_HEALTHY'
|
||||
@@ -5458,20 +5404,6 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES'
|
||||
}
|
||||
RevokeTablePrivilegesBody: {
|
||||
grantee: string
|
||||
/** @enum {string} */
|
||||
privilege_type:
|
||||
| 'ALL'
|
||||
| 'SELECT'
|
||||
| 'INSERT'
|
||||
| 'UPDATE'
|
||||
| 'DELETE'
|
||||
| 'TRUNCATE'
|
||||
| 'REFERENCES'
|
||||
| 'TRIGGER'
|
||||
relation_id: number
|
||||
}
|
||||
RunQueryBody: {
|
||||
query: string
|
||||
}
|
||||
@@ -5674,6 +5606,7 @@ export interface components {
|
||||
sql: string
|
||||
}
|
||||
SnippetList: {
|
||||
cursor?: string
|
||||
data: components['schemas']['SnippetMeta'][]
|
||||
}
|
||||
SnippetMeta: {
|
||||
@@ -5782,16 +5715,6 @@ export interface components {
|
||||
SubdomainAvailabilityResponse: {
|
||||
available: boolean
|
||||
}
|
||||
SubscriptionStatisticsResponse: {
|
||||
total_active_free_projects: number
|
||||
total_enterprise_projects: number
|
||||
total_free_projects: number
|
||||
total_paid_projects: number
|
||||
total_paused_free_projects: number
|
||||
total_payg_projects: number
|
||||
total_pro_projects: number
|
||||
total_team_projects: number
|
||||
}
|
||||
SupavisorConfigResponse: {
|
||||
connectionString: string
|
||||
/** @enum {string} */
|
||||
@@ -5883,22 +5806,8 @@ export interface components {
|
||||
name: string
|
||||
schema: string
|
||||
}
|
||||
TablePrivilege: {
|
||||
grantee: string
|
||||
grantor: string
|
||||
is_grantable: boolean
|
||||
/** @enum {string} */
|
||||
privilege_type:
|
||||
| 'ALL'
|
||||
| 'SELECT'
|
||||
| 'INSERT'
|
||||
| 'UPDATE'
|
||||
| 'DELETE'
|
||||
| 'TRUNCATE'
|
||||
| 'REFERENCES'
|
||||
| 'TRIGGER'
|
||||
}
|
||||
TargetClonedProject: {
|
||||
name: string
|
||||
ref: string
|
||||
}
|
||||
TargetCloneStatus: {
|
||||
@@ -6241,9 +6150,13 @@ export interface components {
|
||||
name?: string
|
||||
type?: string
|
||||
}
|
||||
UpdateContentBody: {
|
||||
content?: Record<string, never>
|
||||
UpdateContentBodyDto: {
|
||||
content?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
description?: string
|
||||
/** Format: uuid */
|
||||
folder_id?: null | string
|
||||
id?: string
|
||||
name?: string
|
||||
owner_id?: number
|
||||
@@ -6252,7 +6165,7 @@ export interface components {
|
||||
/** @enum {string} */
|
||||
visibility?: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
UpdateContentFolderBody: {
|
||||
UpdateContentFolderBodyDto: {
|
||||
name: string
|
||||
}
|
||||
UpdateCustomHostnameBody: {
|
||||
@@ -6598,9 +6511,6 @@ export interface components {
|
||||
UpdateProjectBody: {
|
||||
name: string
|
||||
}
|
||||
UpdateProjectSensitivityBody: {
|
||||
is_sensitive: boolean
|
||||
}
|
||||
UpdateProviderBody: {
|
||||
attribute_mapping?: components['schemas']['AttributeMapping']
|
||||
domains?: string[]
|
||||
@@ -6782,25 +6692,16 @@ export interface components {
|
||||
release_channel: components['schemas']['ReleaseChannel']
|
||||
target_version: string
|
||||
}
|
||||
UpsertContentBody: {
|
||||
content?: Record<string, never>
|
||||
UpsertContentBodyDto: {
|
||||
content?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
description?: string
|
||||
id?: string
|
||||
/** Format: uuid */
|
||||
folder_id?: null | string
|
||||
id: string
|
||||
name: string
|
||||
owner_id?: number
|
||||
project_id?: number
|
||||
/** @enum {string} */
|
||||
type: 'sql' | 'report' | 'log_sql'
|
||||
/** @enum {string} */
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
UpsertContentBodyV2: {
|
||||
content?: Record<string, never>
|
||||
description?: string
|
||||
folder_id?: string
|
||||
id?: string
|
||||
name: string
|
||||
owner_id?: number
|
||||
owner_id: number
|
||||
project_id?: number
|
||||
/** @enum {string} */
|
||||
type: 'sql' | 'report' | 'log_sql'
|
||||
@@ -6862,6 +6763,8 @@ export interface components {
|
||||
UserContentObject: {
|
||||
content: Record<string, never>
|
||||
description?: string
|
||||
favorite?: boolean | null
|
||||
folder_id?: string | null
|
||||
id: string
|
||||
inserted_at: string
|
||||
last_updated_by?: number
|
||||
@@ -6876,25 +6779,8 @@ export interface components {
|
||||
}
|
||||
UserContentObjectMeta: {
|
||||
description?: string
|
||||
favorite: boolean | null
|
||||
folder_id?: string
|
||||
id: string
|
||||
inserted_at: string
|
||||
last_updated_by?: number
|
||||
name: string
|
||||
owner_id: number
|
||||
project_id: number
|
||||
/** @enum {string} */
|
||||
type: 'sql' | 'report' | 'log_sql'
|
||||
updated_at: string
|
||||
/** @enum {string} */
|
||||
visibility: 'user' | 'project' | 'org' | 'public'
|
||||
}
|
||||
UserContentObjectV2: {
|
||||
content: Record<string, never>
|
||||
description?: string
|
||||
favorite: boolean | null
|
||||
folder_id?: string
|
||||
favorite?: boolean | null
|
||||
folder_id?: string | null
|
||||
id: string
|
||||
inserted_at: string
|
||||
last_updated_by?: number
|
||||
@@ -8650,13 +8536,13 @@ export interface operations {
|
||||
/** Get notifications */
|
||||
NotificationsController_getNotificationsV2: {
|
||||
parameters: {
|
||||
query: {
|
||||
status: 'new' | 'seen' | 'archived'
|
||||
priority: 'Critical' | 'Warning' | 'Info'
|
||||
org_slug?: string[]
|
||||
project_ref?: string[]
|
||||
offset: number
|
||||
limit: number
|
||||
query?: {
|
||||
limit?: string
|
||||
offset?: string
|
||||
priority?: 'Critical' | 'Warning' | 'Info'
|
||||
status?: 'new' | 'seen' | 'archived'
|
||||
org_slug?: string
|
||||
project_ref?: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -11189,94 +11075,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Retrieve table privileges */
|
||||
TablePrivilegesController_getTablePrivileges: {
|
||||
parameters: {
|
||||
header: {
|
||||
'x-connection-encrypted': string
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['PostgresTablePrivileges'][]
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to retrieve table privileges */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Grant table privileges */
|
||||
TablePrivilegesController_grantTablePrivileges: {
|
||||
parameters: {
|
||||
header: {
|
||||
'x-connection-encrypted': string
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GrantTablePrivilegesBody'][]
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['PostgresTablePrivileges'][]
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to grant table privileges */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Revoke table privileges */
|
||||
TablePrivilegesController_revokeTablePrivileges: {
|
||||
parameters: {
|
||||
header: {
|
||||
'x-connection-encrypted': string
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['RevokeTablePrivilegesBody'][]
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['PostgresTablePrivileges'][]
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to revoke table privileges */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets project pg.tables or pg.table with the given ID */
|
||||
TablesController_getTables: {
|
||||
parameters: {
|
||||
@@ -11811,20 +11609,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets the user's subscription statistics */
|
||||
SubscriptionsController_getSubscriptionsStatistics: {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['SubscriptionStatisticsResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to retrieve user's subscription statistics */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets all projects that belong to the authenticated user
|
||||
* @description Only returns the minimal project info
|
||||
@@ -12910,14 +12694,20 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gets project's content
|
||||
* @deprecated
|
||||
*/
|
||||
/** Gets project's content */
|
||||
ContentController_getContent: {
|
||||
parameters: {
|
||||
query?: {
|
||||
cursor?: string
|
||||
limit?: string
|
||||
sort_by?: 'name' | 'inserted_at'
|
||||
sort_order?: 'asc' | 'desc'
|
||||
visibility?: string
|
||||
type?: 'sql' | 'report' | 'log_sql'
|
||||
name?: string
|
||||
favorite?: boolean
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
@@ -12934,16 +12724,15 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
/** Updates project's content */
|
||||
ContentController_updateWholeContentV2: {
|
||||
ContentController_updateWholeContent: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UpsertContentBodyV2']
|
||||
'application/json': components['schemas']['UpsertContentBodyDto']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -12957,22 +12746,21 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
/** Creates project's content */
|
||||
ContentController_createContentV2: {
|
||||
ContentController_createContent: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateContentBodyV2']
|
||||
'application/json': components['schemas']['CreateContentBodyDto']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UserContentObjectV2']
|
||||
'application/json': components['schemas']['UserContentObject']
|
||||
}
|
||||
}
|
||||
/** @description Failed to create project's content */
|
||||
@@ -12988,7 +12776,6 @@ export interface operations {
|
||||
ids: string[]
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
@@ -13013,10 +12800,13 @@ export interface operations {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
path: {
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UpdateContentBody']
|
||||
'application/json': components['schemas']['UpdateContentBodyDto']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -13031,14 +12821,16 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets the count of a user's content by type */
|
||||
/** Gets the count of a user's content */
|
||||
ContentController_getContentCount: {
|
||||
parameters: {
|
||||
query?: {
|
||||
type?: string
|
||||
visibility?: string
|
||||
type?: 'sql' | 'report' | 'log_sql'
|
||||
favorite?: boolean
|
||||
name?: string
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
@@ -13058,10 +12850,15 @@ export interface operations {
|
||||
ContentFoldersController_getRootFolder: {
|
||||
parameters: {
|
||||
query?: {
|
||||
cursor?: string
|
||||
limit?: string
|
||||
sort_by?: 'name' | 'inserted_at'
|
||||
sort_order?: 'asc' | 'desc'
|
||||
visibility?: string
|
||||
type?: 'sql' | 'report' | 'log_sql'
|
||||
name?: string
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
@@ -13081,13 +12878,12 @@ export interface operations {
|
||||
ContentFoldersController_createFolder: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateContentFolderBody']
|
||||
'application/json': components['schemas']['CreateContentFolderBodyDto']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -13108,6 +12904,9 @@ export interface operations {
|
||||
query: {
|
||||
ids: string[]
|
||||
}
|
||||
path: {
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
@@ -13122,11 +12921,17 @@ export interface operations {
|
||||
/** Gets project's content folder */
|
||||
ContentFoldersController_getFolder: {
|
||||
parameters: {
|
||||
query?: {
|
||||
cursor?: string
|
||||
limit?: string
|
||||
sort_by?: 'name' | 'inserted_at'
|
||||
sort_order?: 'asc' | 'desc'
|
||||
name?: string
|
||||
}
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
/** @description Content folder id */
|
||||
id: string
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -13145,15 +12950,14 @@ export interface operations {
|
||||
ContentFoldersController_updateFolder: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
/** @description Content folder id */
|
||||
id: string
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UpdateContentFolderBody']
|
||||
'application/json': components['schemas']['UpdateContentFolderBodyDto']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -13170,10 +12974,8 @@ export interface operations {
|
||||
ContentController_getContentById: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
/** @description Content id */
|
||||
id: string
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -13749,30 +13551,29 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Update project's sensitivity settings */
|
||||
SettingsController_patchProjectSensitivity: {
|
||||
/** Updates the given project sensitivity */
|
||||
SensitivityController_updateProjectSensitivity: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UpdateProjectSensitivityBody']
|
||||
'application/json': components['schemas']['MarkSensitiveBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ProjectSensitivitySettingResponse']
|
||||
'application/json': components['schemas']['ProjectSensitivityResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to update project's sensitivity setting */
|
||||
404: {
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to update project's sensitivity setting */
|
||||
/** @description Failed to update project */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
@@ -17619,6 +17420,10 @@ export interface operations {
|
||||
'v1-list-all-snippets': {
|
||||
parameters: {
|
||||
query?: {
|
||||
cursor?: string
|
||||
limit?: string
|
||||
sort_by?: 'name' | 'inserted_at'
|
||||
sort_order?: 'asc' | 'desc'
|
||||
project_ref?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronRight, ChevronsDown, Search } from 'lucide-react'
|
||||
import { ChevronRight, ChevronsDown, Loader2, Search } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ElementRef, forwardRef } from 'react'
|
||||
import {
|
||||
@@ -136,8 +136,12 @@ const InnerSideBarFilters = forwardRef<HTMLDivElement, React.ComponentPropsWitho
|
||||
|
||||
const InnerSideBarFilterSearchInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
React.ComponentPropsWithoutRef<typeof Input_Shadcn_> & { 'aria-labelledby': string; name: string }
|
||||
>(({ children, ...props }, ref) => {
|
||||
React.ComponentPropsWithoutRef<typeof Input_Shadcn_> & {
|
||||
'aria-labelledby': string
|
||||
name: string
|
||||
isLoading?: boolean
|
||||
}
|
||||
>(({ children, isLoading = false, ...props }, ref) => {
|
||||
return (
|
||||
<label htmlFor={props.name} className="relative w-full">
|
||||
<span className="sr-only">{props['aria-labelledby']}</span>
|
||||
@@ -159,7 +163,20 @@ const InnerSideBarFilterSearchInput = forwardRef<
|
||||
{...props}
|
||||
/>
|
||||
{children}
|
||||
<Search className="absolute left-2 top-2 text-foreground-muted" size={14} strokeWidth={1.5} />
|
||||
{isLoading ? (
|
||||
<Loader2
|
||||
className="animate-spin absolute left-2 text-foreground-muted"
|
||||
style={{ top: 7 }}
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
) : (
|
||||
<Search
|
||||
className="absolute left-2 top-2 text-foreground-muted"
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
)
|
||||
})
|
||||
@@ -251,10 +268,10 @@ const InnerSideBarEmptyPanel = forwardRef<
|
||||
|
||||
export {
|
||||
InnerSideBarEmptyPanel,
|
||||
InnerSideBarFilters,
|
||||
InnerSideBarFilterSearchInput,
|
||||
InnerSideBarFilterSortDropdown,
|
||||
InnerSideBarFilterSortDropdownItem,
|
||||
InnerSideBarFilters,
|
||||
InnerSideBarShimmeringLoaders,
|
||||
InnerSideBarTitle,
|
||||
InnerSideMenuCollapsible,
|
||||
|
||||
Reference in new issue
Block a user