From e8c309c312e53643ea249e42e4c36f23c8efc5ff Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 27 Feb 2026 05:32:01 +0100 Subject: [PATCH 1/5] fix: resolve undefined project ref in TabsStateContextProvider (#43222) This pull request refactors how the `TabsStateContextProvider` receives the project reference and updates related imports for consistency and maintainability. The main change is to pass `projectRef` explicitly as a prop instead of fetching it internally, which improves context control and makes the component easier to test and reuse. Additionally, the PR updates import paths to use absolute aliases and removes an unused function. This fixes an issue which you can replicate by: 1. Go to SQL editor 2. Open any snippet 3. Delete the local storage `supabase_studio_tabs_{project ref}` 4. Refresh the page while still the snippet is open This will make the snippet to enter in a ghost state where the tab name is not visible but you see the content. --------- Co-authored-by: Joshen Lim --- .../layouts/ProjectLayout/ProjectContext.tsx | 11 ++-- apps/studio/state/tabs.tsx | 63 +++++-------------- 2 files changed, 21 insertions(+), 53 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx index 1ac054e7c8ef6..a796c54102eb4 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -1,9 +1,10 @@ import { PropsWithChildren } from 'react' -import { DatabaseSelectorStateContextProvider } from 'state/database-selector' -import { RoleImpersonationStateContextProvider } from 'state/role-impersonation-state' -import { StorageExplorerStateContextProvider } from 'state/storage-explorer' -import { TableEditorStateContextProvider } from 'state/table-editor' -import { TabsStateContextProvider } from 'state/tabs' + +import { DatabaseSelectorStateContextProvider } from '@/state/database-selector' +import { RoleImpersonationStateContextProvider } from '@/state/role-impersonation-state' +import { StorageExplorerStateContextProvider } from '@/state/storage-explorer' +import { TableEditorStateContextProvider } from '@/state/table-editor' +import { TabsStateContextProvider } from '@/state/tabs' type ProjectContextProviderProps = { projectRef: string | undefined diff --git a/apps/studio/state/tabs.tsx b/apps/studio/state/tabs.tsx index 6a136cfbe697b..2ded8c04b081f 100644 --- a/apps/studio/state/tabs.tsx +++ b/apps/studio/state/tabs.tsx @@ -1,11 +1,12 @@ -import { buildTableEditorUrl } from 'components/grid/SupabaseGrid.utils' -import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useParams } from 'common' import { partition } from 'lodash' -import { NextRouter } from 'next/router' +import { type NextRouter } from 'next/router' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import { proxy, subscribe, useSnapshot } from 'valtio' +import { buildTableEditorUrl } from '@/components/grid/SupabaseGrid.utils' +import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants' + export const editorEntityTypes = { table: ['r', 'v', 'm', 'f', 'p'], sql: ['sql'], @@ -387,7 +388,7 @@ function createTabsState(projectRef: string) { onClearDashboardHistory() router.push(`/project/${router.query.ref}/${editor === 'table' ? 'editor' : 'sql'}`) }, - handleTabDragEnd: (oldIndex: number, newIndex: number, tabId: string, router: any) => { + handleTabDragEnd: (oldIndex: number, newIndex: number, tabId: string, router: NextRouter) => { // Make permanent if needed const draggedTab = store.tabsMap[tabId] if (draggedTab?.isPreview) { @@ -415,20 +416,20 @@ export type TabsState = ReturnType export const TabsStateContext = createContext(createTabsState('')) export const TabsStateContextProvider = ({ children }: PropsWithChildren) => { - const { data: project } = useSelectedProjectQuery() - const [state, setState] = useState(createTabsState(project?.ref ?? '')) + const { ref: projectRef } = useParams() + const [state, setState] = useState(createTabsState(projectRef ?? '')) useEffect(() => { - if (typeof window !== 'undefined' && !!project?.ref) { - setState(createTabsState(project?.ref ?? '')) + if (typeof window !== 'undefined' && !!projectRef) { + setState(createTabsState(projectRef ?? '')) } - }, [project?.ref]) + }, [projectRef]) useEffect(() => { - if (typeof window !== 'undefined' && project?.ref) { + if (typeof window !== 'undefined' && projectRef) { return subscribe(state, () => { localStorage.setItem( - getTabsStorageKey(project?.ref), + getTabsStorageKey(projectRef), JSON.stringify({ activeTab: state.activeTab, openTabs: state.openTabs, @@ -437,14 +438,14 @@ export const TabsStateContextProvider = ({ children }: PropsWithChildren) => { }) ) localStorage.setItem( - getRecentItemsStorageKey(project?.ref), + getRecentItemsStorageKey(projectRef), JSON.stringify({ items: state.recentItems, }) ) }) } - }, [project?.ref, state]) + }, [projectRef, state]) return {children} } @@ -472,37 +473,3 @@ export function createTabId(type: T, params: CreateTabIdParam return '' } } - -// Remove from local storage when feature flag is disabled -export function removeTabsByEditor(ref: string, editor: 'table' | 'sql') { - // Recent items - const recentItems = getSavedRecentItems(ref) - const filteredRecentItems = recentItems.filter((item) => - editor === 'sql' ? item.type !== 'sql' : item.type === 'sql' - ) - localStorage.setItem( - getRecentItemsStorageKey(ref), - JSON.stringify({ items: filteredRecentItems }) - ) - - // Tabs - const tabs = getSavedTabs(ref) - const filteredTabsMap = Object.fromEntries( - Object.entries(tabs.tabsMap).filter(([, tab]) => - editor === 'sql' ? tab.type !== 'sql' : tab.type === 'sql' - ) - ) - - const filteredOpenTabs = tabs.openTabs.filter((tabId) => filteredTabsMap[tabId]) - localStorage.setItem( - getTabsStorageKey(ref), - JSON.stringify({ - activeTab: filteredOpenTabs.includes(tabs.activeTab ?? '') - ? tabs.activeTab - : filteredOpenTabs[0] ?? null, - openTabs: filteredOpenTabs, - tabsMap: filteredTabsMap, - previewTabId: tabs.previewTabId, - }) - ) -} From b09b569a59249ec0e9c336c450cac8b283090656 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:23:08 +1100 Subject: [PATCH 2/5] chore(design-system): sandwiched admonition (#43120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? - Design system docs addition ## What is the current behavior? - We used a ’sandwiched’ style Admonition a lot but have no clear docs/examples for it ## What is the new behavior? - An example file and documentation around the sandwiched Admonition - Minor unrelated changes - Copywriting docs expansion on capitalization and declarative writing - `pnpm format` on charts ## Additional context | Preview | | --- | | CleanShot 2026-02-24 at 16 02
21@2x | --- apps/design-system/README.md | 4 +-- .../default/block/chart-bar-interactive.tsx | 14 +++++++-- .../default/block/chart-composed-actions.tsx | 12 ++++---- .../default/block/chart-composed-basic.tsx | 12 ++++---- .../default/block/chart-composed-demo.tsx | 6 ++-- .../default/block/chart-composed-metrics.tsx | 10 +++---- .../default/block/chart-composed-states.tsx | 10 +++---- .../default/block/chart-composed-table.tsx | 16 +++++----- apps/design-system/__registry__/index.tsx | 11 +++++++ .../content/docs/copywriting.mdx | 17 +++++++++-- .../content/docs/fragments/admonition.mdx | 17 +++++++---- .../default/example/admonition-sandwiched.tsx | 29 +++++++++++++++++++ apps/design-system/registry/examples.ts | 6 ++++ 13 files changed, 119 insertions(+), 45 deletions(-) create mode 100644 apps/design-system/registry/default/example/admonition-sandwiched.tsx diff --git a/apps/design-system/README.md b/apps/design-system/README.md index 06296fb3f4b05..4708b9866f5e7 100644 --- a/apps/design-system/README.md +++ b/apps/design-system/README.md @@ -55,7 +55,7 @@ The design system _references_ components rather than housing them. That’s an - [`packages/ui`](https://github.com/supabase/supabase/tree/master/packages/ui): basic UI components - [`packages/ui-patterns`](https://github.com/supabase/supabase/tree/master/packages/ui-patterns): components which are built using NPM libraries or amalgamations of components from `patterns/ui` -With that out of the way, there are several parts of this design system that need to be manually updated after components have been added or removed (from documentation). These include: +There are several parts of this design system that need to be manually updated after components have been added or removed (from documentation). These include: - `config/docs.ts`: list of components in the sidebar - `content/docs`: the actual component documentation @@ -64,7 +64,7 @@ With that out of the way, there are several parts of this design system that nee - `registry/charts.ts`: list of chart components - `registry/default/example/*`: the actual example components -You will probably need to rebuild the design system’s registry after making new additions. You can do that via: +You will need to rebuild the design system’s registry after making new additions: ```bash cd apps/design-system diff --git a/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx b/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx index 34374419f5993..655787cda6470 100644 --- a/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx +++ b/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx @@ -2,9 +2,17 @@ import * as React from 'react' import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui' -import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from 'ui' export const description = 'An interactive bar chart' diff --git a/apps/design-system/__registry__/default/block/chart-composed-actions.tsx b/apps/design-system/__registry__/default/block/chart-composed-actions.tsx index f6eac7312b247..ee4212e416ed3 100644 --- a/apps/design-system/__registry__/default/block/chart-composed-actions.tsx +++ b/apps/design-system/__registry__/default/block/chart-composed-actions.tsx @@ -1,19 +1,19 @@ 'use client' +import { BarChart2, ExternalLink, LineChart } from 'lucide-react' +import { useEffect, useState } from 'react' import { Chart, ChartActions, + ChartBar, ChartCard, ChartContent, - ChartHeader, - ChartTitle, ChartEmptyState, - ChartLoadingState, - ChartBar, + ChartHeader, ChartLine, + ChartLoadingState, + ChartTitle, } from 'ui-patterns/Chart' -import { BarChart2, ExternalLink, LineChart } from 'lucide-react' -import { useState, useEffect } from 'react' export default function ChartComposedActions() { const [isLoading, setIsLoading] = useState(true) diff --git a/apps/design-system/__registry__/default/block/chart-composed-basic.tsx b/apps/design-system/__registry__/default/block/chart-composed-basic.tsx index d9457c3d3082d..607363710eb09 100644 --- a/apps/design-system/__registry__/default/block/chart-composed-basic.tsx +++ b/apps/design-system/__registry__/default/block/chart-composed-basic.tsx @@ -1,19 +1,19 @@ 'use client' +import { BarChart2, ExternalLink } from 'lucide-react' +import { useEffect, useState } from 'react' import { Chart, ChartActions, + ChartBar, ChartCard, ChartContent, - ChartHeader, - ChartTitle, ChartEmptyState, - ChartLoadingState, - ChartBar, + ChartHeader, ChartLine, + ChartLoadingState, + ChartTitle, } from 'ui-patterns/Chart' -import { BarChart2, ExternalLink } from 'lucide-react' -import { useState, useEffect } from 'react' export default function ComposedChartBasic() { const [isLoading, setIsLoading] = useState(true) diff --git a/apps/design-system/__registry__/default/block/chart-composed-demo.tsx b/apps/design-system/__registry__/default/block/chart-composed-demo.tsx index c734900d39567..88b131af9254d 100644 --- a/apps/design-system/__registry__/default/block/chart-composed-demo.tsx +++ b/apps/design-system/__registry__/default/block/chart-composed-demo.tsx @@ -1,17 +1,17 @@ 'use client' +import { BarChart2 } from 'lucide-react' +import { useEffect, useState } from 'react' import { Chart, ChartCard, ChartContent, - ChartHeader, ChartEmptyState, + ChartHeader, ChartLoadingState, ChartMetric, } from 'ui-patterns/Chart' -import { BarChart2 } from 'lucide-react' import { LogsBarChart } from 'ui-patterns/LogsBarChart' -import { useState, useEffect } from 'react' export default function ComposedChartDemo() { const [isLoading, setIsLoading] = useState(true) diff --git a/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx b/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx index 0c876d7e5898d..361c35a9a45c1 100644 --- a/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx +++ b/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx @@ -1,17 +1,17 @@ 'use client' +import { ExternalLink } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Skeleton } from 'ui' import { Chart, - ChartCard, - ChartHeader, ChartActions, + ChartCard, ChartContent, + ChartHeader, ChartMetric, ChartSparkline, } from 'ui-patterns/Chart' -import { ExternalLink } from 'lucide-react' -import { useState, useEffect } from 'react' -import { Skeleton } from 'ui' export default function ChartComposedMetrics() { const [data, setData] = useState>([]) diff --git a/apps/design-system/__registry__/default/block/chart-composed-states.tsx b/apps/design-system/__registry__/default/block/chart-composed-states.tsx index 4e5aa5d8bd5b4..c4e25d545c2fe 100644 --- a/apps/design-system/__registry__/default/block/chart-composed-states.tsx +++ b/apps/design-system/__registry__/default/block/chart-composed-states.tsx @@ -1,18 +1,18 @@ 'use client' import { BarChart2, ExternalLink } from 'lucide-react' +import { Badge } from 'ui' import { Chart, - ChartCard, - ChartHeader, - ChartTitle, ChartActions, + ChartCard, ChartContent, + ChartDisabledState, ChartEmptyState, + ChartHeader, ChartLoadingState, - ChartDisabledState, + ChartTitle, } from 'ui-patterns/Chart' -import { Badge } from 'ui' export default function ChartComposedStates() { const actions = [ diff --git a/apps/design-system/__registry__/default/block/chart-composed-table.tsx b/apps/design-system/__registry__/default/block/chart-composed-table.tsx index 303114447e9e6..a8089ed1f57d0 100644 --- a/apps/design-system/__registry__/default/block/chart-composed-table.tsx +++ b/apps/design-system/__registry__/default/block/chart-composed-table.tsx @@ -1,21 +1,21 @@ 'use client' +import { format } from 'date-fns' +import { BarChart2, ExternalLink } from 'lucide-react' +import { useEffect, useState } from 'react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' import { Chart, ChartActions, + ChartBar, ChartCard, ChartContent, - ChartHeader, - ChartTitle, ChartEmptyState, - ChartLoadingState, - ChartBar, ChartFooter, + ChartHeader, + ChartLoadingState, + ChartTitle, } from 'ui-patterns/Chart' -import { BarChart2, ExternalLink } from 'lucide-react' -import { useState, useEffect } from 'react' -import { Table, TableHead, TableBody, TableRow, TableCell, TableHeader } from 'ui' -import { format } from 'date-fns' export default function ChartComposedTable() { const [isLoading, setIsLoading] = useState(true) diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index f8f80ed90d496..a4e25e570bffc 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -137,6 +137,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "admonition-sandwiched": { + name: "admonition-sandwiched", + type: "components:example", + registryDependencies: ["admonition"], + component: React.lazy(() => import("@/registry/default/example/admonition-sandwiched")), + source: "", + files: ["registry/default/example/admonition-sandwiched.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "alert-demo": { name: "alert-demo", type: "components:example", diff --git a/apps/design-system/content/docs/copywriting.mdx b/apps/design-system/content/docs/copywriting.mdx index 751cf4e04975f..4709faa48feb7 100644 --- a/apps/design-system/content/docs/copywriting.mdx +++ b/apps/design-system/content/docs/copywriting.mdx @@ -125,7 +125,19 @@ Supabase UI copy is: ## Navigation and headings -### Use sentence case +### Use title case for page titles and global navigation + +Use title case for page names in the main nav and document titles (e.g. "Database Settings", "Project Settings"). This distinguishes the page as a destination from section labels and in-page headings, which use sentence case. + +### Use declarative page descriptions + +Use a fragment with no trailing period, and prefer declarative over instructional. Describe what the page covers, not what the user should do. + +| Good | Bad | +| ---------------------------------------------------------- | ---------------------------------------------------------------------- | +| "General configuration, domains, ownership, and lifecycle" | "Configure general options, domains, transfers, and project lifecycle" | + +### Use sentence case for section labels and headings | Bad | Good | | ----------------------- | ----------------------- | @@ -216,7 +228,8 @@ Supabase UI copy is: ## Capitalization -- **Sentence case** for all UI text (buttons, labels, headings) +- **Sentence case** for all UI text (buttons, labels, section headings) +- **Title case** for page names in navigation - **Product names:** Database, Auth, Storage, Edge Functions, Realtime, Vector - **Postgres**, not PostgreSQL - **Supabase** (capitalize except in code) diff --git a/apps/design-system/content/docs/fragments/admonition.mdx b/apps/design-system/content/docs/fragments/admonition.mdx index d186a3b28cbe1..d9c0e89a4a96c 100644 --- a/apps/design-system/content/docs/fragments/admonition.mdx +++ b/apps/design-system/content/docs/fragments/admonition.mdx @@ -42,8 +42,6 @@ Only ever use the `primary` (green) button `type` on a `default` Admonition. Eve ## Examples -### With actions - -Reize your browser to see the button(s) change `layout` based on the Admonition’s width. - When `layout="responsive"`, the Alert root gets `@container` so the Admonition is the container-query context. The Admonition stays `vertical` when it’s narrow and switches to `horizontal` when its own width reaches the `@md` container breakpoint, independent of page width. ### Warning @@ -82,3 +78,14 @@ AlertError for example rolls up consistent error handling and support contact me name="admonition-destructive" className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]" /> + +### Sandwiched + +Some [Card](../components/card) or [Dialog](../components/dialog) instances may need to include callout information in-between core content. Admonition can be used ‘full-bleed’ in these cases by removing borders and radii. + + + +Depending on the context, you may want to reset borders on the Admonition itself rather than the surrounding elements. The above Admonition is `type` `"warning"` with stark yellow borders which we want to emphasize. diff --git a/apps/design-system/registry/default/example/admonition-sandwiched.tsx b/apps/design-system/registry/default/example/admonition-sandwiched.tsx new file mode 100644 index 0000000000000..34dec85eeb877 --- /dev/null +++ b/apps/design-system/registry/default/example/admonition-sandwiched.tsx @@ -0,0 +1,29 @@ +import { Button, Card, CardContent, CardHeader, CardTitle } from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +export default function AdmonitionDemo() { + return ( + + + Card with Admonition + + + +

+ This is the subsequent content of this Card. +

+
+ +

+ It might be disabled due some condition that the Admonition above explains. +

+
+
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index bffcbedee6224..905e07d95cf74 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -37,6 +37,12 @@ export const examples: Registry = [ registryDependencies: ['admonition'], files: ['example/admonition-destructive.tsx'], }, + { + name: 'admonition-sandwiched', + type: 'components:example', + registryDependencies: ['admonition'], + files: ['example/admonition-sandwiched.tsx'], + }, { name: 'alert-demo', type: 'components:example', From 43f7b95002b026e583298d5349b61e7a9f2b9c2c Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:23:23 +1100 Subject: [PATCH 3/5] fix: theme switcher discoverability (#43165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Fix ## What is the current behavior? The theme switcher in `CommandMenu` is not showing up when typing in “dark mode”, “toggle theme” or similar commands. ## What is the new behavior? Added hidden root commands: `Toggle theme`, `Use dark theme`, `Use light theme`, `Use system theme`, plus existing `Switch theme...` with search aliases. Also added `value` aliases to the theme items so searches pop up. --------- Co-authored-by: Joshen Lim --- .../prepackaged/ThemeSwitcher.test.tsx | 250 ++++++++++++++++++ .../CommandMenu/prepackaged/ThemeSwitcher.tsx | 55 +++- 2 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.test.tsx diff --git a/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.test.tsx b/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.test.tsx new file mode 100644 index 0000000000000..75df7aa36cc3c --- /dev/null +++ b/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.test.tsx @@ -0,0 +1,250 @@ +import { + CommandProvider, + PageType, + useCommandMenuOpen, + useCommands, + useCurrentPage, + useSetCommandMenuOpen, +} from '..' +import { act, render, waitFor } from '@testing-library/react' +import { useEffect } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { ICommand, ICommandSection } from '../internal/types' +import { useThemeSwitcherCommands } from './ThemeSwitcher' + +const themeMock = vi.hoisted(() => ({ + setTheme: vi.fn(), + state: { + resolvedTheme: 'light' as string | undefined, + }, +})) + +vi.mock('next-themes', () => ({ + useTheme: () => ({ + resolvedTheme: themeMock.state.resolvedTheme, + setTheme: themeMock.setTheme, + }), +})) + +type CurrentPage = ReturnType + +const captured: { + commandSections: ICommandSection[] + currentPage: CurrentPage + open: boolean + setOpen?: (open: boolean) => void +} = { + commandSections: [], + currentPage: undefined, + open: false, + setOpen: undefined, +} + +const ThemeSwitcherHarness = () => { + useThemeSwitcherCommands() + + const commandSections = useCommands() + const currentPage = useCurrentPage() + const open = useCommandMenuOpen() + const setOpen = useSetCommandMenuOpen() + + useEffect(() => { + captured.commandSections = commandSections as ICommandSection[] + captured.currentPage = currentPage + captured.open = open + captured.setOpen = setOpen + }, [commandSections, currentPage, open, setOpen]) + + return null +} + +const renderHarness = async () => { + const renderResult = render( + + + + ) + + await waitFor(() => { + expect(getThemeSection()).toBeDefined() + }) + + return renderResult +} + +const getThemeSection = () => captured.commandSections.find((section) => section.name === 'Theme') + +const getThemeCommand = (id: string) => { + const command = getThemeSection()?.commands.find((x) => x.id === id) + expect(command).toBeDefined() + return command as ICommand +} + +const runAction = (id: string) => { + const command = getThemeCommand(id) + expect('action' in command).toBe(true) + const actionCommand = command as Extract void }> + + act(() => { + actionCommand.action() + }) +} + +describe('useThemeSwitcherCommands', () => { + beforeEach(() => { + themeMock.setTheme.mockReset() + themeMock.state.resolvedTheme = 'light' + + captured.commandSections = [] + captured.currentPage = undefined + captured.open = false + captured.setOpen = undefined + }) + + it('registers hidden direct theme commands in the root Theme section', async () => { + await renderHarness() + + const themeSection = getThemeSection() + expect(themeSection).toBeDefined() + + const ids = themeSection!.commands.map((command) => command.id) + expect(ids).toEqual([ + 'toggle-theme', + 'set-theme-dark', + 'set-theme-light', + 'set-theme-system', + 'switch-theme', + ]) + + expect(themeSection!.commands.every((command) => command.defaultHidden)).toBe(true) + }) + + it('adds root alias values so theme commands are discoverable by search terms', async () => { + await renderHarness() + + const values = getThemeSection()! + .commands.map((command) => command.value ?? '') + .join(' ') + .toLowerCase() + + expect(values).toContain('dark') + expect(values).toContain('light') + expect(values).toContain('toggle') + expect(values).toContain('theme') + }) + + it('direct theme commands call setTheme and close the command menu', async () => { + await renderHarness() + + act(() => { + captured.setOpen?.(true) + }) + + await waitFor(() => { + expect(captured.open).toBe(true) + }) + + runAction('set-theme-dark') + + await waitFor(() => { + expect(themeMock.setTheme).toHaveBeenCalledWith('dark') + expect(captured.open).toBe(false) + }) + + act(() => { + captured.setOpen?.(true) + }) + await waitFor(() => { + expect(captured.open).toBe(true) + }) + + runAction('set-theme-light') + + await waitFor(() => { + expect(themeMock.setTheme).toHaveBeenCalledWith('light') + expect(captured.open).toBe(false) + }) + + act(() => { + captured.setOpen?.(true) + }) + await waitFor(() => { + expect(captured.open).toBe(true) + }) + + runAction('set-theme-system') + + await waitFor(() => { + expect(themeMock.setTheme).toHaveBeenCalledWith('system') + expect(captured.open).toBe(false) + }) + }) + + it('toggle theme switches dark to light', async () => { + themeMock.state.resolvedTheme = 'dark' + await renderHarness() + + runAction('toggle-theme') + + expect(themeMock.setTheme).toHaveBeenCalledWith('light') + }) + + it('toggle theme switches non-dark modes to dark', async () => { + themeMock.state.resolvedTheme = 'light' + await renderHarness() + + runAction('toggle-theme') + + expect(themeMock.setTheme).toHaveBeenCalledWith('dark') + }) + + it('toggle theme uses the latest resolved theme across rerenders in the same session', async () => { + const renderResult = await renderHarness() + + runAction('toggle-theme') + expect(themeMock.setTheme).toHaveBeenLastCalledWith('dark') + + themeMock.state.resolvedTheme = 'dark' + renderResult.rerender( + + + + ) + + await waitFor(() => { + expect(getThemeSection()).toBeDefined() + }) + + runAction('toggle-theme') + expect(themeMock.setTheme).toHaveBeenLastCalledWith('light') + }) + + it('keeps the Switch theme submenu page with System/Dark/Light commands only', async () => { + await renderHarness() + + runAction('switch-theme') + + await waitFor(() => { + expect(captured.currentPage?.name).toBe('Switch theme') + }) + + expect(captured.currentPage?.type).toBe(PageType.Commands) + const sections = + captured.currentPage && 'sections' in captured.currentPage + ? captured.currentPage.sections + : [] + + expect(sections).toHaveLength(1) + expect(sections[0].name).toBe('Switch theme') + + const pageCommands = sections[0].commands + expect(pageCommands.map((command) => command.name)).toEqual(['System', 'Dark', 'Light']) + expect(pageCommands.map((command) => command.value)).toEqual([ + 'System theme, Follow system appearance', + 'Dark theme, Dark mode', + 'Light theme, Light mode', + ]) + expect(pageCommands.some((command) => command.name === 'Classic dark')).toBe(false) + }) +}) diff --git a/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.tsx b/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.tsx index 0d6746dc155c0..d55cdc33d27d2 100644 --- a/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.tsx +++ b/packages/ui-patterns/src/CommandMenu/prepackaged/ThemeSwitcher.tsx @@ -16,7 +16,12 @@ const useThemeSwitcherCommands = ({ options }: { options?: CommandOptions } = {} const setIsOpen = useSetCommandMenuOpen() const setPage = useSetPage() - const { setTheme } = useTheme() + const { resolvedTheme, setTheme } = useTheme() + + const applyTheme = (theme: string) => { + setTheme(theme) + setIsOpen(false) + } useRegisterPage(THEME_SWITCHER_PAGE_NAME, { type: PageType.Commands, @@ -29,10 +34,13 @@ const useThemeSwitcherCommands = ({ options }: { options?: CommandOptions } = {} .map((theme) => ({ id: `switch-theme-${theme.value}`, name: theme.name, - action: () => { - setTheme(theme.value) - setIsOpen(false) - }, + value: + theme.name === 'System' + ? 'System theme, Follow system appearance' + : theme.name === 'Light' + ? 'Light theme, Light mode' + : 'Dark theme, Dark mode', + action: () => applyTheme(theme.value), icon: () => theme.name === 'System' ? : theme.name === 'Light' ? : , })), @@ -43,15 +51,50 @@ const useThemeSwitcherCommands = ({ options }: { options?: CommandOptions } = {} useRegisterCommands( 'Theme', [ + { + id: 'toggle-theme', + name: 'Toggle theme', + value: + 'Toggle theme, Toggle dark mode, Toggle light mode, Theme toggle, Dark mode, Light mode', + action: () => applyTheme(resolvedTheme === 'dark' ? 'light' : 'dark'), + defaultHidden: true, + icon: () => , + }, + { + id: 'set-theme-dark', + name: 'Use dark theme', + value: 'Dark theme, Dark mode, Switch to dark theme, Set theme dark', + action: () => applyTheme('dark'), + defaultHidden: true, + icon: () => , + }, + { + id: 'set-theme-light', + name: 'Use light theme', + value: 'Light theme, Light mode, Switch to light theme, Set theme light', + action: () => applyTheme('light'), + defaultHidden: true, + icon: () => , + }, + { + id: 'set-theme-system', + name: 'Use system theme', + value: 'System theme, Follow system theme, Auto theme, Match system appearance', + action: () => applyTheme('system'), + defaultHidden: true, + icon: () => , + }, { id: 'switch-theme', name: 'Switch theme...', + value: + 'Theme, Switch theme, Change theme, Appearance, Color mode, Dark mode, Light mode, Toggle theme', action: () => setPage(THEME_SWITCHER_PAGE_NAME), defaultHidden: true, icon: () => , }, ], - options + { ...options, deps: [...(options?.deps ?? []), resolvedTheme] } ) } From ea51d278ab76c99e28c6b924d0717f678a4b2076 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:24:15 +0100 Subject: [PATCH 4/5] feat: add link to log explorer filtered by user id (#43144) CleanShot 2026-02-24 at 16 52 43@2x --------- Co-authored-by: Joshen Lim --- .../interfaces/Auth/Users/UserLogs.tsx | 22 +++++++++++++++++++ .../interfaces/Auth/Users/UserPanel.tsx | 10 ++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx b/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx index 457d8a3f24511..5c236eca90174 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserLogs.tsx @@ -19,6 +19,9 @@ interface UserLogsProps { user: User } +const API_LOGS_QUERY = (userId: string) => + `select\n cast(timestamp as datetime) as timestamp,\n event_message, metadata \nfrom edge_logs \nWHERE (\n metadata[SAFE_OFFSET(0)].request[SAFE_OFFSET(0)].sb[SAFE_OFFSET(0)].auth_user\n = '${userId}'\n)\nlimit 100` + export const UserLogs = ({ user }: UserLogsProps) => { const { ref } = useParams() const { filters, setFilters } = useLogsUrlState() @@ -51,6 +54,25 @@ export const UserLogs = ({ user }: UserLogsProps) => { +
+
+

API logs

+

+ View edge logs for requests made by this user +

+
+ + +
+ + +

Authentication logs

diff --git a/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx b/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx index e85ae115477b6..e618c63953fe2 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserPanel.tsx @@ -1,10 +1,9 @@ -import { X } from 'lucide-react' -import { parseAsString, useQueryState } from 'nuqs' -import { useState } from 'react' - import { useUserQuery } from 'data/auth/user-query' import { User } from 'data/auth/users-infinite-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { X } from 'lucide-react' +import { parseAsString, useQueryState } from 'nuqs' +import { useState } from 'react' import { Button, cn, @@ -18,6 +17,7 @@ import { TabsTrigger_Shadcn_, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' + import { UserLogs } from './UserLogs' import { UserOverview } from './UserOverview' import { PANEL_PADDING } from './Users.constants' @@ -57,7 +57,7 @@ export const UserPanel = () => { return ( <> - +