Skip to content

Commit c142858

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: add ability to configure which sidebar tabs are visible in embedded dashboard (#42412)
Allows the embedded dashboard to specify which tabs should be visible. If no tabs are visible, the sidebar is hidden completely GitOrigin-RevId: 9e3657a35942696f023dc6ec3441a9adf92dfe60
1 parent e3820fa commit c142858

File tree

4 files changed

+121
-41
lines changed

4 files changed

+121
-41
lines changed

npm-packages/dashboard-common/src/features/data/components/DataContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ export function DataContent({
287287

288288
<LoadingTransition
289289
loadingState={
290-
<div className="flex h-full flex-col items-center justify-center gap-8 rounded-lg border bg-background-secondary">
290+
<div className="flex h-full flex-col items-center justify-center gap-8 rounded-b-lg border bg-background-secondary">
291291
<LoadingLogo />
292292
</div>
293293
}

npm-packages/dashboard-common/src/layouts/DeploymentDashboardLayout.tsx

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ import Image from "next/image";
3131
type LayoutProps = {
3232
children: JSX.Element;
3333
auditLogsEnabled?: boolean;
34+
visiblePages?: string[];
3435
};
3536

3637
export function DeploymentDashboardLayout({
3738
children,
3839
auditLogsEnabled = true,
40+
visiblePages,
3941
}: LayoutProps) {
4042
const [collapsed, setCollapsed] = useCollapseSidebarState();
4143
const [isGlobalRunnerVertical, setIsGlobalRunnerVertical] =
@@ -46,9 +48,9 @@ export function DeploymentDashboardLayout({
4648
const { isCloudDeploymentInSelfHostedDashboard, deploymentName } =
4749
useIsCloudDeploymentInSelfHostedDashboard();
4850

49-
const exploreDeploymentPages = [
51+
const allExploreDeploymentPages = [
5052
{
51-
key: null,
53+
key: "health",
5254
label: "Health",
5355
Icon: PulseIcon,
5456
href: `${uriPrefix}/`,
@@ -90,53 +92,69 @@ export function DeploymentDashboardLayout({
9092
},
9193
];
9294

95+
// Filter tabs based on visiblePages if provided
96+
const exploreDeploymentPages = visiblePages
97+
? allExploreDeploymentPages.filter((page) =>
98+
visiblePages.includes(page.key),
99+
)
100+
: allExploreDeploymentPages;
101+
102+
const allConfigureItems = [
103+
{
104+
key: "history",
105+
label: "History",
106+
Icon: CounterClockwiseClockIcon,
107+
href: isCloudDeploymentInSelfHostedDashboard
108+
? `https://dashboard.convex.dev/d/${deploymentName}/history`
109+
: `${uriPrefix}/history`,
110+
target: isCloudDeploymentInSelfHostedDashboard ? "_blank" : undefined,
111+
muted: !auditLogsEnabled,
112+
tooltip: auditLogsEnabled
113+
? undefined
114+
: "Deployment history is only available on the Pro plan.",
115+
},
116+
{
117+
key: "settings",
118+
label: "Settings",
119+
Icon: GearIcon,
120+
href: `${uriPrefix}/settings`,
121+
},
122+
];
123+
124+
// Filter configure items based on visiblePages if provided
125+
const configureItems = visiblePages
126+
? allConfigureItems.filter((item) => visiblePages.includes(item.key))
127+
: allConfigureItems;
128+
93129
const sidebarItems: SidebarGroup[] = [
94130
{
95131
key: "explore",
96132
items: exploreDeploymentPages,
97133
},
98134
{
99135
key: "configure",
100-
items: [
101-
{
102-
key: "history",
103-
label: "History",
104-
Icon: CounterClockwiseClockIcon,
105-
href: isCloudDeploymentInSelfHostedDashboard
106-
? `https://dashboard.convex.dev/d/${deploymentName}/history`
107-
: `${uriPrefix}/history`,
108-
target: isCloudDeploymentInSelfHostedDashboard ? "_blank" : undefined,
109-
muted: !auditLogsEnabled,
110-
tooltip: auditLogsEnabled
111-
? undefined
112-
: "Deployment history is only available on the Pro plan.",
113-
},
114-
{
115-
key: "settings",
116-
label: "Settings",
117-
Icon: GearIcon,
118-
href: `${uriPrefix}/settings`,
119-
},
120-
],
136+
items: configureItems,
121137
},
122-
];
138+
].filter((group) => group.items.length > 0);
123139

124140
return (
125141
<FunctionsProvider>
126142
<div className="flex h-full grow flex-col overflow-y-hidden">
127143
<PauseBanner />
128144
<NodeVersionBanner />
129145
<div className="flex h-full flex-col overflow-y-auto sm:flex-row">
130-
<Sidebar
131-
collapsed={!!collapsed}
132-
setCollapsed={setCollapsed}
133-
items={sidebarItems}
134-
header={
135-
process.env.NEXT_PUBLIC_HIDE_HEADER ? (
136-
<EmbeddedConvexLogo collapsed={!!collapsed} />
137-
) : undefined
138-
}
139-
/>
146+
{sidebarItems.length > 0 && (
147+
<Sidebar
148+
collapsed={!!collapsed}
149+
setCollapsed={setCollapsed}
150+
items={sidebarItems}
151+
header={
152+
process.env.NEXT_PUBLIC_HIDE_HEADER ? (
153+
<EmbeddedConvexLogo collapsed={!!collapsed} />
154+
) : undefined
155+
}
156+
/>
157+
)}
140158
<div
141159
className={classNames(
142160
"flex w-full grow overflow-x-hidden",

npm-packages/dashboard-self-hosted/src/pages/_app.tsx

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import { Favicon } from "@common/elements/Favicon";
1515
import { ToggleTheme } from "@common/elements/ToggleTheme";
1616
import { Menu, MenuItem } from "@ui/Menu";
1717
import { ThemeProvider } from "next-themes";
18-
import React, { useCallback, useEffect, useMemo, useState } from "react";
18+
import React, {
19+
useCallback,
20+
useContext,
21+
useEffect,
22+
useMemo,
23+
useState,
24+
createContext,
25+
} from "react";
1926
import { ErrorBoundary } from "components/ErrorBoundary";
2027
import { DeploymentDashboardLayout } from "@common/layouts/DeploymentDashboardLayout";
2128
import {
@@ -33,6 +40,31 @@ import { z } from "zod";
3340
import { UIProvider } from "@ui/UIContext";
3441
import Link from "next/link";
3542

43+
// Context for self-hosted dashboard sidebar settings
44+
const SelfHostedSettingsContext = createContext<{
45+
visiblePages?: string[];
46+
}>({
47+
visiblePages: undefined,
48+
});
49+
50+
/**
51+
* Wrapper component that consumes SelfHostedSettingsContext and passes
52+
* the settings to DeploymentDashboardLayout
53+
*/
54+
function DeploymentDashboardLayoutWrapper({
55+
children,
56+
}: {
57+
children: JSX.Element;
58+
}) {
59+
const { visiblePages } = useContext(SelfHostedSettingsContext);
60+
61+
return (
62+
<DeploymentDashboardLayout visiblePages={visiblePages}>
63+
{children}
64+
</DeploymentDashboardLayout>
65+
);
66+
}
67+
3668
function App({
3769
Component,
3870
pageProps: {
@@ -67,12 +99,12 @@ function App({
6799
>
68100
<DeploymentApiProvider deploymentOverride="local">
69101
<WaitForDeploymentApi>
70-
<DeploymentDashboardLayout>
102+
<DeploymentDashboardLayoutWrapper>
71103
<>
72104
<Component {...pageProps} />
73105
<ConvexCloudReminderToast />
74106
</>
75-
</DeploymentDashboardLayout>
107+
</DeploymentDashboardLayoutWrapper>
76108
</WaitForDeploymentApi>
77109
</DeploymentApiProvider>
78110
</DeploymentInfoProvider>
@@ -257,15 +289,27 @@ function DeploymentInfoProvider({
257289
SESSION_STORAGE_DEPLOYMENT_NAME_KEY,
258290
"",
259291
);
292+
const [visiblePages, setVisiblePages] = useState<string[] | undefined>(
293+
undefined,
294+
);
295+
296+
// Memoize this so it can safely be passed into the context
297+
const settingsContextValue = useMemo(
298+
() => ({ visiblePages }),
299+
[visiblePages],
300+
);
301+
260302
const onSubmit = useCallback(
261303
async ({
262304
submittedAdminKey,
263305
submittedDeploymentUrl,
264306
submittedDeploymentName,
307+
submittedVisiblePages,
265308
}: {
266309
submittedAdminKey: string;
267310
submittedDeploymentUrl: string;
268311
submittedDeploymentName: string;
312+
submittedVisiblePages?: string[];
269313
}) => {
270314
const isValid = await checkDeploymentInfo(
271315
submittedAdminKey,
@@ -282,6 +326,7 @@ function DeploymentInfoProvider({
282326
setStoredAdminKey(submittedAdminKey);
283327
setStoredDeploymentUrl(submittedDeploymentUrl);
284328
setStoredDeploymentName(submittedDeploymentName);
329+
setVisiblePages(submittedVisiblePages);
285330
},
286331
[setStoredAdminKey, setStoredDeploymentUrl, setStoredDeploymentName],
287332
);
@@ -367,7 +412,9 @@ function DeploymentInfoProvider({
367412
}}
368413
/>
369414
<DeploymentInfoContext.Provider value={finalValue}>
370-
<ErrorBoundary>{children}</ErrorBoundary>
415+
<SelfHostedSettingsContext.Provider value={settingsContextValue}>
416+
<ErrorBoundary>{children}</ErrorBoundary>
417+
</SelfHostedSettingsContext.Provider>
371418
</DeploymentInfoContext.Provider>
372419
</>
373420
);
@@ -412,10 +459,12 @@ function useEmbeddedDashboardCredentials(
412459
submittedAdminKey,
413460
submittedDeploymentUrl,
414461
submittedDeploymentName,
462+
submittedVisiblePages,
415463
}: {
416464
submittedAdminKey: string;
417465
submittedDeploymentUrl: string;
418466
submittedDeploymentName: string;
467+
submittedVisiblePages?: string[];
419468
}) => void,
420469
) {
421470
// Send a message to the parent iframe to request the credentials.
@@ -437,6 +486,7 @@ function useEmbeddedDashboardCredentials(
437486
adminKey: z.string(),
438487
deploymentUrl: z.string().url(),
439488
deploymentName: z.string(),
489+
visiblePages: z.array(z.string()).optional(),
440490
});
441491

442492
try {
@@ -450,6 +500,7 @@ function useEmbeddedDashboardCredentials(
450500
submittedAdminKey: event.data.adminKey,
451501
submittedDeploymentUrl: event.data.deploymentUrl,
452502
submittedDeploymentName: event.data.deploymentName,
503+
submittedVisiblePages: event.data.visiblePages,
453504
});
454505
}
455506
};

npm-packages/docs/docs/platform-apis/embedded-dashboard.mdx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ Required information for `postMessage`:
3636
- `adminKey`: A deploy key scoped to the specified `deploymentName`. Can be
3737
retrieved with the [Create deploy key API](/management-api/create-deploy-key).
3838

39+
Optional configuration:
40+
41+
- `visiblePages`: An array of page keys to show in the sidebar. If not provided,
42+
all pages are shown. If an empty array is provided, the sidebar will be
43+
hidden. Available page keys: `"health"`, `"data"`, `"functions"`, `"files"`,
44+
`"schedules"`, `"logs"`, `"history"`, `"settings"`.
45+
3946
Here's an example of the Convex dashboard embedded in a React application:
4047

4148
```tsx
@@ -45,17 +52,19 @@ export function Dashboard({
4552
deploymentUrl,
4653
deploymentName,
4754
deployKey,
55+
visiblePages,
4856
}: {
4957
deploymentUrl: string;
5058
deploymentName: string;
5159
deployKey: string;
60+
visiblePages?: string[];
5261
}) {
5362
const iframeRef = useRef<HTMLIFrameElement>(null);
5463

5564
useEffect(() => {
5665
const handleMessage = (event: MessageEvent) => {
5766
// We first wait for the iframe to send a dashboard-credentials-request message.
58-
// This makes sure that we dont send the credentials until the iframe is ready.
67+
// This makes sure that we don't send the credentials until the iframe is ready.
5968
if (event.data?.type !== "dashboard-credentials-request") {
6069
return;
6170
}
@@ -65,14 +74,16 @@ export function Dashboard({
6574
adminKey: deployKey,
6675
deploymentUrl,
6776
deploymentName,
77+
// Optional: specify which pages to show
78+
visiblePages,
6879
},
6980
"*",
7081
);
7182
};
7283

7384
window.addEventListener("message", handleMessage);
7485
return () => window.removeEventListener("message", handleMessage);
75-
}, [deploymentUrl, adminKey, deploymentName]);
86+
}, [deploymentUrl, adminKey, deploymentName, visiblePages]);
7687

7788
return (
7889
<iframe

0 commit comments

Comments
 (0)