From 528b3f3c906dd31bfdc2869f7d9ffca1c6d9a5b7 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 19 Feb 2026 17:46:20 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Adds=20a=20=E2=80=9CCreate=20custom=20dashb?= =?UTF-8?q?oard=E2=80=9D=20button=20on=20metrics=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/DashboardDialogs.tsx | 85 +++++++++++++++---- .../route.tsx | 54 +++++++----- 2 files changed, 103 insertions(+), 36 deletions(-) diff --git a/apps/webapp/app/components/navigation/DashboardDialogs.tsx b/apps/webapp/app/components/navigation/DashboardDialogs.tsx index a14681361b1..ed2ebbe4951 100644 --- a/apps/webapp/app/components/navigation/DashboardDialogs.tsx +++ b/apps/webapp/app/components/navigation/DashboardDialogs.tsx @@ -26,16 +26,14 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../pri import { v3BillingPath } from "~/utils/pathBuilder"; import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu"; -export function CreateDashboardButton({ +function useCreateDashboard({ organization, project, environment, - isCollapsed, }: { - organization: MatchedOrganization; - project: SideMenuProject; - environment: SideMenuEnvironment; - isCollapsed: boolean; + organization: T; + project: { slug: string }; + environment: { slug: string }; }) { const [isOpen, setIsOpen] = useState(false); const navigation = useNavigation(); @@ -46,20 +44,45 @@ export function CreateDashboardButton({ const planLimits = (plan?.v3Subscription?.plan?.limits as any)?.metricDashboards; const canExceed = typeof planLimits === "object" && planLimits.canExceed === true; const canUpgrade = plan?.v3Subscription?.plan && !canExceed; + const isFreePlan = plan?.v3Subscription?.isPaying === false; const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dashboards/create`; - // Close dialog when form submission starts (redirect is happening) useEffect(() => { if (navigation.formAction === formAction && navigation.state === "loading") { setIsOpen(false); } }, [navigation.formAction, navigation.state, formAction]); + return { + isOpen, + setIsOpen, + isAtLimit, + canUpgrade: !!canUpgrade, + isFreePlan, + formAction, + limits, + organization, + }; +} + +export function CreateDashboardButton({ + organization, + project, + environment, + isCollapsed, +}: { + organization: MatchedOrganization; + project: SideMenuProject; + environment: SideMenuEnvironment; + isCollapsed: boolean; +}) { + const dashboard = useCreateDashboard({ organization, project, environment }); + if (isCollapsed) return null; return ( - + @@ -77,15 +100,47 @@ export function CreateDashboardButton({ - {isAtLimit ? ( + {dashboard.isAtLimit ? ( + + ) : ( + + )} + + ); +} + +export function CreateDashboardPageButton({ + organization, + project, + environment, +}: { + organization: { slug: string }; + project: { slug: string }; + environment: { slug: string }; +}) { + const dashboard = useCreateDashboard({ organization, project, environment }); + + return ( + + + + + {dashboard.isAtLimit ? ( ) : ( - + )} ); @@ -105,7 +160,7 @@ function CreateDashboardUpgradeDialog({ limits: { used: number; limit: number }; canUpgrade: boolean; isFreePlan: boolean; - organization: MatchedOrganization; + organization: { slug: string }; }) { if (isFreePlan) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.metrics.$dashboardKey/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.metrics.$dashboardKey/route.tsx index 4429e0c06e6..1606158598a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.metrics.$dashboardKey/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.metrics.$dashboardKey/route.tsx @@ -1,36 +1,37 @@ +import { type LoaderFunctionArgs } from "@remix-run/node"; import type { TaskTriggerSource } from "@trigger.dev/database"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import ReactGridLayout from "react-grid-layout"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { z } from "zod"; +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter"; +import { type WidgetData } from "~/components/metrics/QueryWidget"; +import { QueuesFilter } from "~/components/metrics/QueuesFilter"; +import { ScopeFilter } from "~/components/metrics/ScopeFilter"; +import { TitleWidget } from "~/components/metrics/TitleWidget"; +import { CreateDashboardPageButton } from "~/components/navigation/DashboardDialogs"; +import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; +import { TimeFilter } from "~/components/runs/v3/SharedFilters"; import { $replica } from "~/db.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { useSearchParams } from "~/hooks/useSearchParam"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { getAllTaskIdentifiers } from "~/models/task.server"; -import { requireUser } from "~/services/session.server"; -import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { type LayoutItem, type Widget, MetricDashboardPresenter, } from "~/presenters/v3/MetricDashboardPresenter.server"; -import { type LoaderFunctionArgs } from "@remix-run/node"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; -import { z } from "zod"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { requireUser } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import ReactGridLayout from "react-grid-layout"; -import { MetricWidget } from "../resources.metric"; -import { TitleWidget } from "~/components/metrics/TitleWidget"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { useEnvironment } from "~/hooks/useEnvironment"; -import { TimeFilter } from "~/components/runs/v3/SharedFilters"; -import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter"; -import { ScopeFilter } from "~/components/metrics/ScopeFilter"; -import { QueuesFilter } from "~/components/metrics/QueuesFilter"; -import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; -import { useSearchParams } from "~/hooks/useSearchParam"; -import { type WidgetData } from "~/components/metrics/QueryWidget"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { QueryScopeSchema } from "~/v3/querySchemas"; +import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { MetricWidget } from "../resources.metric"; const ParamSchema = EnvironmentParamSchema.extend({ dashboardKey: z.string(), @@ -82,10 +83,21 @@ export default function Page() { possibleTasks, } = useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + return ( + + +
From bd298e499443c18934363b432caf11ceadd92be4 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 19 Feb 2026 18:06:14 +0000 Subject: [PATCH 2/2] Coderabbit nitpick fix --- apps/webapp/app/components/navigation/DashboardDialogs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/navigation/DashboardDialogs.tsx b/apps/webapp/app/components/navigation/DashboardDialogs.tsx index ed2ebbe4951..f0cdd0406e0 100644 --- a/apps/webapp/app/components/navigation/DashboardDialogs.tsx +++ b/apps/webapp/app/components/navigation/DashboardDialogs.tsx @@ -26,12 +26,12 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../pri import { v3BillingPath } from "~/utils/pathBuilder"; import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu"; -function useCreateDashboard({ +function useCreateDashboard({ organization, project, environment, }: { - organization: T; + organization: { slug: string }; project: { slug: string }; environment: { slug: string }; }) {