Chore/refactor dashboard history logic (#38586)

* Shift dashboard history logic outside of app state, into its own hook

* Self remediate malformed history for sql editor

* Update apps/studio/hooks/misc/useDashboardHistory.ts

Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com>

* Update apps/studio/hooks/misc/useDashboardHistory.ts

Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com>

---------

Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com>
This commit is contained in:
Joshen LimandJordi Enric authored and GitHub committed 2025-09-10 22:19:08 +08:00
1 parent 47842c7068
commit deda982074
9 files changed
+110 -99

No files matched your search

@@ -5,28 +5,22 @@ import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import useLatest from 'hooks/misc/useLatest'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
// Ideally these could all be within a _middleware when we use Next 12
const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
const router = useRouter()
const { ref, slug, id } = useParams()
const isLoggedIn = useIsLoggedIn()
const snap = useAppStateSnapshot()
const isUserMFAEnabled = useIsMFAEnabled()
const { data: organization } = useSelectedOrganizationQuery()
const [dashboardHistory, _, { isSuccess: isSuccessStorage }] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''),
{ editor: undefined, sql: undefined }
)
const isLoggedIn = useIsLoggedIn()
const isUserMFAEnabled = useIsMFAEnabled()
const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()
const [__, setLastVisitedOrganization] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
@@ -106,22 +100,13 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
useEffect(() => {
if (ref !== undefined && id !== undefined) {
if (router.pathname.endsWith('/sql/[id]') && id !== 'new') {
snap.setDashboardHistory(ref, 'sql', id)
setLastVisitedSnippet(id)
} else if (router.pathname.endsWith('/editor/[id]')) {
setLastVisitedTable(id)
}
if (router.pathname.endsWith('/editor/[id]')) {
snap.setDashboardHistory(ref, 'editor', id)
}
}
}, [ref, id])
useEffect(() => {
// Load dashboard history into app state
if (isSuccessStorage && ref) {
snap.setDashboardHistory(ref, 'editor', dashboardHistory.editor)
snap.setDashboardHistory(ref, 'sql', dashboardHistory.sql)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccessStorage, ref])
}, [ref, id])
useEffect(() => {
if (organization) {
@@ -13,9 +13,9 @@ import {
isView,
} from 'data/table-editor/table-editor-types'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAppStateSnapshot } from 'state/app-state'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import { Button } from 'ui'
@@ -34,8 +34,8 @@ export const TableGridEditor = ({
selectedTable,
}: TableGridEditorProps) => {
const router = useRouter()
const appSnap = useAppStateSnapshot()
const { ref: projectRef, id } = useParams()
const { setLastVisitedTable } = useDashboardHistory()
const tabs = useTabsStateSnapshot()
@@ -58,10 +58,6 @@ export const TableGridEditor = ({
const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
const openTabs = tabs.openTabs.filter((x) => !x.startsWith('sql'))
const onClearDashboardHistory = useCallback(() => {
if (projectRef) appSnap.setDashboardHistory(projectRef, 'editor', undefined)
}, [appSnap, projectRef])
const onTableCreated = useCallback(
(table: { id: number }) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
@@ -74,9 +70,14 @@ export const TableGridEditor = ({
if (selectedTable) {
// Close tab
const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
tabs.handleTabClose({ id: tabId, router, editor: 'table', onClearDashboardHistory })
tabs.handleTabClose({
id: tabId,
router,
editor: 'table',
onClearDashboardHistory: () => setLastVisitedTable(undefined),
})
}
}, [onClearDashboardHistory, router, selectedTable, tabs])
}, [router, selectedTable, tabs])
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
@@ -111,7 +112,7 @@ export const TableGridEditor = ({
id: tabId,
router,
editor: 'table',
onClearDashboardHistory,
onClearDashboardHistory: () => setLastVisitedTable(undefined),
})
}}
>
@@ -122,7 +123,7 @@ export const TableGridEditor = ({
asChild
type="default"
className="mt-2"
onClick={() => appSnap.setDashboardHistory(projectRef, 'editor', undefined)}
onClick={() => setLastVisitedTable(undefined)}
>
<Link href={`/project/${projectRef}/editor/${openTabs[0].split('-')[1]}`}>
Close tab
@@ -133,7 +134,7 @@ export const TableGridEditor = ({
asChild
type="default"
className="mt-2"
onClick={() => appSnap.setDashboardHistory(projectRef, 'editor', undefined)}
onClick={() => setLastVisitedTable(undefined)}
>
<Link href={`/project/${projectRef}/editor`}>Head back</Link>
</Button>
+6 -4
View File
@@ -12,7 +12,7 @@ import { Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useAppStateSnapshot } from 'state/app-state'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { editorEntityTypes, useTabsStateSnapshot, type Tab } from 'state/tabs'
import {
cn,
@@ -32,7 +32,7 @@ import { TabPreview } from './TabPreview'
export const EditorTabs = () => {
const { ref, id } = useParams()
const router = useRouter()
const appSnap = useAppStateSnapshot()
const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()
const editor = useEditorType()
const tabs = useTabsStateSnapshot()
@@ -68,8 +68,10 @@ export const EditorTabs = () => {
}
const onClearDashboardHistory = () => {
if (ref && editor) {
appSnap.setDashboardHistory(ref, editor === 'table' ? 'editor' : editor, undefined)
if (editor === 'table') {
setLastVisitedTable(undefined)
} else if (editor === 'sql') {
setLastVisitedSnippet(undefined)
}
}
@@ -0,0 +1,30 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useLocalStorageQuery } from './useLocalStorage'
type DashboardHistory = { editor?: string; sql?: string }
const DEFAULT_HISTORY = { editor: undefined, sql: undefined }
export const useDashboardHistory = () => {
// [Joshen] History should always refer to the project that the user is currently on
const { ref } = useParams()
const [history, setHistory, { isSuccess }] = useLocalStorageQuery<DashboardHistory>(
LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''),
DEFAULT_HISTORY
)
const setLastVisitedTable = (id?: string) => {
setHistory({ ...history, editor: id })
}
const setLastVisitedSnippet = (id?: string) => {
setHistory({ ...history, sql: id })
}
return {
history,
setLastVisitedTable,
setLastVisitedSnippet,
isHistoryLoaded: isSuccess,
}
}
@@ -8,7 +8,7 @@ import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
import { TableEditorMenu } from 'components/layouts/TableEditorLayout/TableEditorMenu'
import { NewTab } from 'components/layouts/Tabs/NewTab'
import { useAppStateSnapshot } from 'state/app-state'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { editorEntityTypes, useTabsStateSnapshot } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
@@ -16,26 +16,28 @@ const TableEditorPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const tabStore = useTabsStateSnapshot()
const appSnap = useAppStateSnapshot()
const { history, isHistoryLoaded } = useDashboardHistory()
const onTableCreated = (table: { id: number }) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
}
useEffect(() => {
const lastOpenedTab = appSnap.dashboardHistory.editor
if (isHistoryLoaded) {
const lastOpenedTable = history.editor
const lastTabId = tabStore.openTabs.find((id) =>
editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type)
)
const lastTabId = tabStore.openTabs.find((id) =>
editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type)
)
if (lastOpenedTab !== undefined) {
router.push(`/project/${projectRef}/editor/${appSnap.dashboardHistory.editor}`)
} else if (lastTabId) {
// Handle redirect to last opened table tab, or last table tab
const lastTab = tabStore.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`)
if (lastOpenedTable !== undefined) {
router.push(`/project/${projectRef}/editor/${history.editor}`)
} else if (lastTabId) {
const lastTab = tabStore.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`)
}
}
}, [])
}, [isHistoryLoaded])
return (
<>
@@ -7,6 +7,7 @@ import type { NextPageWithLayout } from 'types'
const HomePage: NextPageWithLayout = () => {
const isHomeNew = useFlag('homeNew')
if (isHomeNew) {
return <HomeV2 />
}
+18 -12
View File
@@ -1,3 +1,4 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
@@ -9,8 +10,8 @@ import { useEditorType } from 'components/layouts/editors/EditorsLayout.hooks'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { useContentIdQuery } from 'data/content/content-id-query'
import Link from 'next/link'
import { useAppStateSnapshot } from 'state/app-state'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
@@ -20,11 +21,12 @@ import { Admonition } from 'ui-patterns'
const SqlEditor: NextPageWithLayout = () => {
const router = useRouter()
const { id, ref, content, skip } = useParams()
const { data: project } = useSelectedProjectQuery()
const editor = useEditorType()
const tabs = useTabsStateSnapshot()
const appSnap = useAppStateSnapshot()
const snapV2 = useSqlEditorV2StateSnapshot()
const { history, setLastVisitedSnippet } = useDashboardHistory()
const allSnippets = useSnippets(ref!)
const snippet = allSnippets.find((x) => x.id === id)
@@ -49,21 +51,27 @@ const SqlEditor: NextPageWithLayout = () => {
const invalidId = isError && error.code === 400 && error.message.includes('Invalid uuid')
useEffect(() => {
if (ref && data) {
snapV2.setSnippet(ref, data as unknown as SnippetWithContent)
if (ref && data && project) {
// [Joshen] Check if snippet belongs to the current project
if (data.project_id === project.id) {
snapV2.setSnippet(ref, data as unknown as SnippetWithContent)
} else {
setLastVisitedSnippet(undefined)
router.push(`/project/${ref}/sql/new`)
}
}
}, [ref, data])
}, [ref, data, project])
// Load the last visited snippet when landing on /new
useEffect(() => {
if (
id === 'new' &&
skip !== 'true' && // [Joshen] Skip flag implies to skip loading the last visited snippet
appSnap.dashboardHistory.sql !== undefined &&
history.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}`)
const snippet = allSnippets.find((snippet) => snippet.id === history.sql)
if (snippet !== undefined) router.push(`/project/${ref}/sql/${history.sql}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, allSnippets, content])
@@ -104,9 +112,7 @@ const SqlEditor: NextPageWithLayout = () => {
id: tabId,
router,
editor,
onClearDashboardHistory: () => {
if (ref) appSnap.setDashboardHistory(ref, 'sql', undefined)
},
onClearDashboardHistory: () => setLastVisitedSnippet(undefined),
})
}}
>
+20 -16
View File
@@ -6,35 +6,39 @@ import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { useAppStateSnapshot } from 'state/app-state'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { useTabsStateSnapshot } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const SQLEditorIndexPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const store = useTabsStateSnapshot()
const appSnap = useAppStateSnapshot()
const { history, isHistoryLoaded } = useDashboardHistory()
useEffect(() => {
// Handle redirect to last opened snippet tab, or last snippet tab
const lastOpenedTab = appSnap.dashboardHistory.sql
const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql')
if (lastOpenedTab !== undefined) {
router.push(`/project/${projectRef}/sql/${appSnap.dashboardHistory.sql}`)
} else if (lastTabId) {
const lastTab = store.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`)
} else {
router.push(`/project/${projectRef}/sql/new`)
if (isHistoryLoaded) {
// Handle redirect to last opened snippet tab, or last snippet tab
const lastOpenedTab = history.sql
const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql')
if (lastOpenedTab !== undefined) {
router.push(`/project/${projectRef}/sql/${history.sql}`)
} else if (lastTabId) {
const lastTab = store.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`)
} else {
router.push(`/project/${projectRef}/sql/new`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [isHistoryLoaded])
return null
}
TableEditorPage.getLayout = (page) => (
SQLEditorIndexPage.getLayout = (page) => (
<DefaultLayout>
<EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
<SQLEditorLayout>{page}</SQLEditorLayout>
@@ -42,4 +46,4 @@ TableEditorPage.getLayout = (page) => (
</DefaultLayout>
)
export default TableEditorPage
export default SQLEditorIndexPage
+1 -21
View File
@@ -1,19 +1,9 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS, LOCAL_STORAGE_KEYS } from 'common'
type DashboardHistoryType = {
sql?: string
editor?: string
}
const EMPTY_DASHBOARD_HISTORY: DashboardHistoryType = {
sql: undefined,
editor: undefined,
}
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
const getInitialState = () => {
return {
dashboardHistory: EMPTY_DASHBOARD_HISTORY,
activeDocsSection: ['introduction'],
docsLanguage: 'js',
showProjectApiDocs: false,
@@ -31,16 +21,6 @@ const getInitialState = () => {
export const appState = proxy({
...getInitialState(),
setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string | undefined) => {
if (appState.dashboardHistory[key] !== id) {
appState.dashboardHistory[key] = id
localStorage.setItem(
LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref),
JSON.stringify(appState.dashboardHistory)
)
}
},
activeDocsSection: ['introduction'],
docsLanguage: 'js' as 'js' | 'bash',
showProjectApiDocs: false,