Skip to content

Commit b192b71

Browse files
authored
Feat(webapp): add create custom dashboard button to metrics page (#3095)
Adds a "Create custom dashboard" button to the top right of the metrics dashboard <img width="3546" height="1934" alt="CleanShot 2026-02-19 at 11 25 12@2x" src="https://github.com/user-attachments/assets/0bb46ade-47c9-4396-b62a-f4801d7d90b4" />
1 parent a9163df commit b192b71

File tree

2 files changed

+103
-36
lines changed
  • apps/webapp/app
    • components/navigation
    • routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey

2 files changed

+103
-36
lines changed

apps/webapp/app/components/navigation/DashboardDialogs.tsx

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,14 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../pri
2626
import { v3BillingPath } from "~/utils/pathBuilder";
2727
import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu";
2828

29-
export function CreateDashboardButton({
29+
function useCreateDashboard({
3030
organization,
3131
project,
3232
environment,
33-
isCollapsed,
3433
}: {
35-
organization: MatchedOrganization;
36-
project: SideMenuProject;
37-
environment: SideMenuEnvironment;
38-
isCollapsed: boolean;
34+
organization: { slug: string };
35+
project: { slug: string };
36+
environment: { slug: string };
3937
}) {
4038
const [isOpen, setIsOpen] = useState(false);
4139
const navigation = useNavigation();
@@ -46,20 +44,45 @@ export function CreateDashboardButton({
4644
const planLimits = (plan?.v3Subscription?.plan?.limits as any)?.metricDashboards;
4745
const canExceed = typeof planLimits === "object" && planLimits.canExceed === true;
4846
const canUpgrade = plan?.v3Subscription?.plan && !canExceed;
47+
const isFreePlan = plan?.v3Subscription?.isPaying === false;
4948

5049
const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dashboards/create`;
5150

52-
// Close dialog when form submission starts (redirect is happening)
5351
useEffect(() => {
5452
if (navigation.formAction === formAction && navigation.state === "loading") {
5553
setIsOpen(false);
5654
}
5755
}, [navigation.formAction, navigation.state, formAction]);
5856

57+
return {
58+
isOpen,
59+
setIsOpen,
60+
isAtLimit,
61+
canUpgrade: !!canUpgrade,
62+
isFreePlan,
63+
formAction,
64+
limits,
65+
organization,
66+
};
67+
}
68+
69+
export function CreateDashboardButton({
70+
organization,
71+
project,
72+
environment,
73+
isCollapsed,
74+
}: {
75+
organization: MatchedOrganization;
76+
project: SideMenuProject;
77+
environment: SideMenuEnvironment;
78+
isCollapsed: boolean;
79+
}) {
80+
const dashboard = useCreateDashboard({ organization, project, environment });
81+
5982
if (isCollapsed) return null;
6083

6184
return (
62-
<Dialog open={isOpen} onOpenChange={setIsOpen}>
85+
<Dialog open={dashboard.isOpen} onOpenChange={dashboard.setIsOpen}>
6386
<TooltipProvider disableHoverableContent>
6487
<Tooltip>
6588
<TooltipTrigger asChild>
@@ -77,15 +100,47 @@ export function CreateDashboardButton({
77100
</TooltipContent>
78101
</Tooltip>
79102
</TooltipProvider>
80-
{isAtLimit ? (
103+
{dashboard.isAtLimit ? (
104+
<CreateDashboardUpgradeDialog
105+
limits={dashboard.limits}
106+
canUpgrade={dashboard.canUpgrade}
107+
isFreePlan={dashboard.isFreePlan}
108+
organization={dashboard.organization}
109+
/>
110+
) : (
111+
<CreateDashboardDialog formAction={dashboard.formAction} limits={dashboard.limits} />
112+
)}
113+
</Dialog>
114+
);
115+
}
116+
117+
export function CreateDashboardPageButton({
118+
organization,
119+
project,
120+
environment,
121+
}: {
122+
organization: { slug: string };
123+
project: { slug: string };
124+
environment: { slug: string };
125+
}) {
126+
const dashboard = useCreateDashboard({ organization, project, environment });
127+
128+
return (
129+
<Dialog open={dashboard.isOpen} onOpenChange={dashboard.setIsOpen}>
130+
<DialogTrigger asChild>
131+
<Button variant="primary/small" LeadingIcon={PlusIcon}>
132+
Create custom dashboard
133+
</Button>
134+
</DialogTrigger>
135+
{dashboard.isAtLimit ? (
81136
<CreateDashboardUpgradeDialog
82-
limits={limits}
83-
canUpgrade={!!canUpgrade}
84-
isFreePlan={plan?.v3Subscription?.isPaying === false}
85-
organization={organization}
137+
limits={dashboard.limits}
138+
canUpgrade={dashboard.canUpgrade}
139+
isFreePlan={dashboard.isFreePlan}
140+
organization={dashboard.organization}
86141
/>
87142
) : (
88-
<CreateDashboardDialog formAction={formAction} limits={limits} />
143+
<CreateDashboardDialog formAction={dashboard.formAction} limits={dashboard.limits} />
89144
)}
90145
</Dialog>
91146
);
@@ -105,7 +160,7 @@ function CreateDashboardUpgradeDialog({
105160
limits: { used: number; limit: number };
106161
canUpgrade: boolean;
107162
isFreePlan: boolean;
108-
organization: MatchedOrganization;
163+
organization: { slug: string };
109164
}) {
110165

111166
if (isFreePlan) {

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
1+
import { type LoaderFunctionArgs } from "@remix-run/node";
12
import type { TaskTriggerSource } from "@trigger.dev/database";
3+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4+
import ReactGridLayout from "react-grid-layout";
5+
import { typedjson, useTypedLoaderData } from "remix-typedjson";
6+
import { z } from "zod";
7+
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
8+
import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter";
9+
import { type WidgetData } from "~/components/metrics/QueryWidget";
10+
import { QueuesFilter } from "~/components/metrics/QueuesFilter";
11+
import { ScopeFilter } from "~/components/metrics/ScopeFilter";
12+
import { TitleWidget } from "~/components/metrics/TitleWidget";
13+
import { CreateDashboardPageButton } from "~/components/navigation/DashboardDialogs";
14+
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
15+
import { TimeFilter } from "~/components/runs/v3/SharedFilters";
216
import { $replica } from "~/db.server";
17+
import { useEnvironment } from "~/hooks/useEnvironment";
18+
import { useOrganization } from "~/hooks/useOrganizations";
19+
import { useProject } from "~/hooks/useProject";
20+
import { useSearchParams } from "~/hooks/useSearchParam";
321
import { findProjectBySlug } from "~/models/project.server";
422
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
523
import { getAllTaskIdentifiers } from "~/models/task.server";
6-
import { requireUser } from "~/services/session.server";
7-
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
824
import {
925
type LayoutItem,
1026
type Widget,
1127
MetricDashboardPresenter,
1228
} from "~/presenters/v3/MetricDashboardPresenter.server";
13-
import { type LoaderFunctionArgs } from "@remix-run/node";
14-
import { typedjson, useTypedLoaderData } from "remix-typedjson";
15-
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
16-
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
17-
import { z } from "zod";
18-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
29+
import { requireUser } from "~/services/session.server";
1930
import { cn } from "~/utils/cn";
20-
import ReactGridLayout from "react-grid-layout";
21-
import { MetricWidget } from "../resources.metric";
22-
import { TitleWidget } from "~/components/metrics/TitleWidget";
23-
import { useOrganization } from "~/hooks/useOrganizations";
24-
import { useProject } from "~/hooks/useProject";
25-
import { useEnvironment } from "~/hooks/useEnvironment";
26-
import { TimeFilter } from "~/components/runs/v3/SharedFilters";
27-
import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter";
28-
import { ScopeFilter } from "~/components/metrics/ScopeFilter";
29-
import { QueuesFilter } from "~/components/metrics/QueuesFilter";
30-
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
31-
import { useSearchParams } from "~/hooks/useSearchParam";
32-
import { type WidgetData } from "~/components/metrics/QueryWidget";
31+
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
3332
import { QueryScopeSchema } from "~/v3/querySchemas";
33+
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
34+
import { MetricWidget } from "../resources.metric";
3435

3536
const ParamSchema = EnvironmentParamSchema.extend({
3637
dashboardKey: z.string(),
@@ -82,10 +83,21 @@ export default function Page() {
8283
possibleTasks,
8384
} = useTypedLoaderData<typeof loader>();
8485

86+
const organization = useOrganization();
87+
const project = useProject();
88+
const environment = useEnvironment();
89+
8590
return (
8691
<PageContainer>
8792
<NavBar>
8893
<PageTitle title={title} />
94+
<PageAccessories>
95+
<CreateDashboardPageButton
96+
organization={organization}
97+
project={project}
98+
environment={environment}
99+
/>
100+
</PageAccessories>
89101
</NavBar>
90102
<PageBody scrollable={false}>
91103
<div className="h-full">

0 commit comments

Comments
 (0)