feat(webapp): name of the page in tab titles (#4517)
Adds a shared `pageMeta()` helper and 74 route declarations, so a title reads `run_abc | Runs | Trigger.dev` — the specific thing first, then the page. Org pages also carry the organization: `Team | Acme | Trigger.dev`. Inside a project no scope is added, because the dashboard switches projects in every tab at once. Page names are unchanged; what's new is that a page says which one it is at all. Three wording changes on purpose: the queue page now names the queue, the model page names the model, and entity pages carry their section.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
area: webapp
|
||||
type: improvement
|
||||
---
|
||||
|
||||
Browser tabs now name the page you're on, and the run, task or queue when you're looking at one, instead of all reading "Trigger.dev".
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "~/utils/themePreference";
|
||||
import { cachedFlag } from "~/v3/featureFlags.server";
|
||||
import { getTimezonePreference } from "./services/preferences/uiPreferences.server";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
import { appTitle } from "./utils/pageTitle";
|
||||
|
||||
// Derived here (not inside StaleAssetRecovery) so the shared component takes
|
||||
// the flag as a prop. NODE_ENV is statically replaced in browser bundles, and
|
||||
@@ -51,7 +51,8 @@ export const headers = () => ({
|
||||
export const meta: MetaFunction = ({ data }) => {
|
||||
const typedData = data as UseDataFunctionReturn<typeof loader>;
|
||||
return [
|
||||
{ title: typedData?.appEnv ? `Trigger.dev${appEnvTitleTag(typedData.appEnv)}` : "Trigger.dev" },
|
||||
// Pages declare their own title with `pageMeta`; this is the fallback.
|
||||
{ title: appTitle(typedData?.appEnv) },
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=1024, initial-scale=1",
|
||||
|
||||
@@ -38,6 +38,9 @@ import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashb
|
||||
import { acceptInvitePath, organizationTeamPath, v3BillingPath } from "~/utils/pathBuilder";
|
||||
import { isAtOrBelow } from "~/utils/inviteRoleLadder";
|
||||
import { PurchaseSeatsModal } from "../_app.orgs.$organizationSlug.settings.team/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Invite team members");
|
||||
|
||||
const Params = z.object({
|
||||
organizationSlug: z.string(),
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
import { BookOpenIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { json, type MetaFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import type { TaskRunStatus } from "@trigger.dev/database";
|
||||
@@ -100,10 +100,9 @@ import {
|
||||
v3TasksStreamingPath,
|
||||
v3TestTaskPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: `Tasks | Trigger.dev` }];
|
||||
};
|
||||
export const meta = pageMeta("Tasks");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+5
-5
@@ -1,5 +1,4 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -50,11 +49,12 @@ import {
|
||||
v3PlaygroundAgentPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { parseFiniteInt } from "~/utils/searchParams";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
const slug = (data as { agent?: AgentDetail | null } | undefined)?.agent?.slug;
|
||||
return [{ title: slug ? `${slug} | Agents | Trigger.dev` : "Agent | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.agent?.slug ?? params.agentParam ?? "Agent",
|
||||
"Agents",
|
||||
]);
|
||||
|
||||
const AgentParamSchema = EnvironmentParamSchema.extend({
|
||||
agentParam: z.string(),
|
||||
|
||||
+3
@@ -46,6 +46,9 @@ import {
|
||||
assertSafeWebhookUrl,
|
||||
UnsafeWebhookUrlError,
|
||||
} from "~/v3/services/alerts/safeWebhookUrl.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("New alert");
|
||||
|
||||
const FormSchema = z
|
||||
.object({
|
||||
|
||||
+3
-8
@@ -10,7 +10,7 @@ import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Form, type MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { Form, Outlet, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { SlackIcon } from "@trigger.dev/companyicons";
|
||||
import type { ProjectAlertChannelType, ProjectAlertType } from "@trigger.dev/database";
|
||||
@@ -66,14 +66,9 @@ import {
|
||||
v3ProjectAlertsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { alertsWorker } from "~/v3/alertsWorker.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Alerts | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Alerts");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+2
-8
@@ -1,5 +1,4 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { CopyableText } from "~/components/primitives/CopyableText";
|
||||
@@ -38,14 +37,9 @@ import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
|
||||
import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { docsPath, EnvironmentParamSchema } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `API keys | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("API keys");
|
||||
|
||||
export const loader = dashboardLoader(
|
||||
{
|
||||
|
||||
+3
@@ -24,6 +24,9 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { formatNumber } from "~/utils/numberFormatter";
|
||||
import { EnvironmentParamSchema, v3BatchesPath, v3BatchRunsPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta(({ params }) => [params.batchParam ?? "Batch", "Batches"]);
|
||||
|
||||
const BatchParamSchema = EnvironmentParamSchema.extend({
|
||||
batchParam: z.string(),
|
||||
|
||||
+3
-8
@@ -1,6 +1,6 @@
|
||||
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { type MetaFunction, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react";
|
||||
import { Outlet, useLocation, useNavigation, useParams } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -62,14 +62,9 @@ import {
|
||||
v3BatchRunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { throwNotFound } from "~/utils/httpErrors";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Batches | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Batches");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -79,6 +79,9 @@ import { ArchiveButton } from "../resources.branches.archive";
|
||||
import { NewBranchPanel } from "~/routes/resources.branches.create";
|
||||
import { BranchesOptions } from "~/utils/branches";
|
||||
import { IconArrowBearRight2 } from "@tabler/icons-react";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Preview branches");
|
||||
|
||||
const PurchaseSchema = z.discriminatedUnion("action", [
|
||||
z.object({
|
||||
|
||||
+6
@@ -42,6 +42,12 @@ import {
|
||||
v3RunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { BulkActionService } from "~/v3/services/bulk/BulkActionV2.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.bulkAction?.name || params.bulkActionParam || "Bulk action",
|
||||
"Bulk actions",
|
||||
]);
|
||||
|
||||
const BulkActionParamSchema = EnvironmentParamSchema.extend({
|
||||
bulkActionParam: z.string(),
|
||||
|
||||
+3
-8
@@ -1,6 +1,6 @@
|
||||
import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid";
|
||||
import { NoSymbolIcon } from "@heroicons/react/24/solid";
|
||||
import { Outlet, useParams, type MetaFunction } from "@remix-run/react";
|
||||
import { Outlet, useParams } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -54,14 +54,9 @@ import {
|
||||
v3BulkActionPath,
|
||||
v3CreateBulkActionPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Bulk actions | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Bulk actions");
|
||||
|
||||
const SearchParamsSchema = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
|
||||
+3
-14
@@ -7,13 +7,7 @@ import {
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useNavigation,
|
||||
useSearchParams,
|
||||
type MetaFunction,
|
||||
} from "@remix-run/react";
|
||||
import { Form, useActionData, useNavigation, useSearchParams } from "@remix-run/react";
|
||||
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -74,14 +68,9 @@ import { concurrencyPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/
|
||||
import { AllocateConcurrencyService } from "~/v3/services/allocateConcurrency.server";
|
||||
import { SetConcurrencyAddOnService } from "~/v3/services/setConcurrencyAddOn.server";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Manage concurrency | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Manage concurrency");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+6
@@ -42,6 +42,12 @@ import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server";
|
||||
import { QueryScopeSchema } from "~/v3/querySchemas";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { MetricWidget } from "../resources.metric";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta<typeof loader>(({ data }) => [
|
||||
data?.title ?? "Dashboard",
|
||||
"Dashboards",
|
||||
]);
|
||||
|
||||
const ParamSchema = EnvironmentParamSchema.extend({
|
||||
dashboardKey: z.string(),
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||
import { Link, type MetaFunction } from "@remix-run/react";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -18,10 +18,9 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { EnvironmentParamSchema, v3BuiltInDashboardPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Dashboards | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta("Dashboards");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+6
@@ -58,6 +58,12 @@ import {
|
||||
} from "~/utils/pathBuilder";
|
||||
import { MetricDashboard } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta<typeof loader>(({ data }) => [
|
||||
data?.title ?? "Dashboard",
|
||||
"Dashboards",
|
||||
]);
|
||||
|
||||
const ParamSchema = EnvironmentParamSchema.extend({
|
||||
dashboardId: z.string(),
|
||||
|
||||
+6
@@ -52,6 +52,12 @@ import { v3DeploymentParams, v3DeploymentsPath, v3RunsPath } from "~/utils/pathB
|
||||
import { capitalizeWord } from "~/utils/string";
|
||||
import { UserTag } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route";
|
||||
import { DeploymentEventFromString } from "@trigger.dev/core/v3/schemas";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta(({ params }) => [
|
||||
params.deploymentParam ?? "Deployment",
|
||||
"Deployments",
|
||||
]);
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
-16
@@ -4,15 +4,7 @@ import {
|
||||
BookOpenIcon,
|
||||
NoSymbolIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
Form,
|
||||
type MetaFunction,
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
useParams,
|
||||
} from "@remix-run/react";
|
||||
import { Form, Outlet, useLocation, useNavigate, useNavigation, useParams } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { CogIcon, GitBranchIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
@@ -87,14 +79,9 @@ import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions";
|
||||
import { useAutoRevalidate } from "~/hooks/useAutoRevalidate";
|
||||
import { env } from "~/env.server";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Deployments | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Deployments");
|
||||
|
||||
const SearchParams = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
|
||||
+3
@@ -46,6 +46,9 @@ import { NewBranchPanel } from "~/routes/resources.branches.create";
|
||||
import { BranchesOptions } from "~/utils/branches";
|
||||
import { IconArrowBearRight2 } from "@tabler/icons-react";
|
||||
import { useAutoRevalidate } from "~/hooks/useAutoRevalidate";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Dev branches");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ import {
|
||||
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository";
|
||||
import { findUnauthorizedEnvironmentId } from "~/v3/writableEnvironments";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("New environment variable");
|
||||
|
||||
const Variable = z.object({
|
||||
key: EnvironmentVariableKey,
|
||||
|
||||
+2
-8
@@ -16,7 +16,6 @@ import {
|
||||
useFetcher,
|
||||
useNavigation,
|
||||
useRevalidator,
|
||||
type MetaFunction,
|
||||
} from "@remix-run/react";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
@@ -88,14 +87,9 @@ import {
|
||||
shouldSyncEnvVar,
|
||||
type TriggerEnvironmentType,
|
||||
} from "~/v3/vercel/vercelProjectIntegrationSchema";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Environment variables | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Environment variables");
|
||||
|
||||
type PageVercelIntegration = NonNullable<
|
||||
Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>["vercelIntegration"]
|
||||
|
||||
+6
-8
@@ -1,6 +1,6 @@
|
||||
import { parseWithZod } from "@conform-to/zod";
|
||||
import { BellAlertIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction, useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { IconAlarmSnooze as IconAlarmSnoozeBase, IconCircleDotted } from "@tabler/icons-react";
|
||||
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
|
||||
@@ -83,14 +83,12 @@ import {
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { ErrorGroupActions } from "~/v3/services/errorGroupActions.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
return [
|
||||
{
|
||||
title: `Error Details | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta(({ params }) => [
|
||||
params.fingerprint ? ErrorId.toFriendlyId(params.fingerprint) : "Error",
|
||||
"Errors",
|
||||
]);
|
||||
|
||||
const emptyStringToUndefined = z.preprocess(
|
||||
(v) => (v === "" ? undefined : v),
|
||||
|
||||
+3
-8
@@ -1,6 +1,6 @@
|
||||
import * as Ariakit from "@ariakit/react";
|
||||
import { BellAlertIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useFetcher, useRevalidator, type MetaFunction } from "@remix-run/react";
|
||||
import { Form, useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
|
||||
import { type ErrorGroupStatus } from "@trigger.dev/database";
|
||||
@@ -76,14 +76,9 @@ import { requireUser } from "~/services/session.server";
|
||||
import { formatNumberCompact } from "~/utils/numberFormatter";
|
||||
import { EnvironmentParamSchema, v3ErrorPath } from "~/utils/pathBuilder";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Errors | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Errors");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
+2
-8
@@ -1,5 +1,4 @@
|
||||
import { CheckIcon, BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { IconCardsFilled, IconDiamondFilled, IconTallymark4 } from "@tabler/icons-react";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
@@ -48,14 +47,9 @@ import {
|
||||
EnvironmentParamSchema,
|
||||
organizationBillingPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Limits | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Limits");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
-8
@@ -1,5 +1,5 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { type MetaFunction, useFetcher, useNavigation, useLocation, Form } from "@remix-run/react";
|
||||
import { useFetcher, useNavigation, useLocation, Form } from "@remix-run/react";
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import {
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
// Valid log levels for filtering
|
||||
const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
|
||||
@@ -52,13 +53,7 @@ function parseLevelsFromUrl(url: URL): LogLevel[] | undefined {
|
||||
return levelParams.filter((l): l is LogLevel => validLevels.includes(l as LogLevel));
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Logs | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Logs");
|
||||
|
||||
// TODO: Move this to a more appropriate shared location
|
||||
async function hasLogsPageAccess(
|
||||
|
||||
+5
-4
@@ -1,5 +1,4 @@
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -33,14 +32,16 @@ import {
|
||||
formatTokenCount,
|
||||
} from "~/utils/modelFormatters";
|
||||
import { EnvironmentParamSchema, v3ModelComparePath, v3ModelsPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
const ParamSchema = EnvironmentParamSchema.extend({
|
||||
modelId: z.string(),
|
||||
});
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Model Detail | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.model?.modelName ?? params.modelId ?? "Model",
|
||||
"Models",
|
||||
]);
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
-9
@@ -6,12 +6,7 @@ import {
|
||||
CubeIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
Form,
|
||||
type MetaFunction,
|
||||
type ShouldRevalidateFunctionArgs,
|
||||
useFetcher,
|
||||
} from "@remix-run/react";
|
||||
import { Form, type ShouldRevalidateFunctionArgs, useFetcher } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -110,10 +105,9 @@ import { parseFiniteInt } from "~/utils/searchParams";
|
||||
|
||||
import { IconColumns3 } from "@tabler/icons-react";
|
||||
import { type loader as compareLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Models | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta("Models");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+2
-4
@@ -1,5 +1,4 @@
|
||||
import { ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
@@ -28,10 +27,9 @@ import { useEnvironment } from "~/hooks/useEnvironment";
|
||||
import { EnvironmentParamSchema, v3ModelsPath } from "~/utils/pathBuilder";
|
||||
import { formatModelCost } from "~/utils/modelFormatters";
|
||||
import { formatNumberCompact } from "~/utils/numberFormatter";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Compare Models | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta("Compare Models");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+2
-4
@@ -2,7 +2,6 @@ import type { UIMessage } from "@ai-sdk/react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { BoltIcon, CheckIcon, StopIcon } from "@heroicons/react/20/solid";
|
||||
import { ClipboardIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
import { Link, useFetcher, useNavigate, useRouteLoaderData } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { generateJWT as internal_generateJWT, MachinePresetName } from "@trigger.dev/core/v3";
|
||||
@@ -59,10 +58,9 @@ import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Playground | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta(({ params }) => [params.agentParam ?? "Agent", "Playground"]);
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
import { BookOpenIcon, ChevronUpDownIcon, CpuChipIcon } from "@heroicons/react/20/solid";
|
||||
import { json, type MetaFunction } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Outlet, useLoaderData, useNavigate, useParams } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { CubeSparkleIcon } from "~/assets/icons/CubeSparkleIcon";
|
||||
@@ -24,10 +24,9 @@ import { playgroundPresenter } from "~/presenters/v3/PlaygroundPresenter.server"
|
||||
import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { docsPath, EnvironmentParamSchema, v3PlaygroundAgentPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Playground | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta("Playground");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
+6
-4
@@ -1,7 +1,7 @@
|
||||
import * as Ariakit from "@ariakit/react";
|
||||
import { ArrowPathIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { type MetaFunction, useFetcher } from "@remix-run/react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { json, type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@@ -85,14 +85,16 @@ import { MetricWidget } from "~/routes/resources.metric";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { EnvironmentParamSchema, v3PromptsPath, v3RunSpanPath } from "~/utils/pathBuilder";
|
||||
import { parsePeriodToMs } from "~/utils/periods";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
const ParamSchema = EnvironmentParamSchema.extend({
|
||||
promptSlug: z.string(),
|
||||
});
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
return [{ title: `${(data as any)?.prompt.slug ?? "Prompt"} | Trigger.dev` }];
|
||||
};
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.prompt?.slug ?? params.promptSlug ?? "Prompt",
|
||||
"Prompts",
|
||||
]);
|
||||
|
||||
// ─── Action ──────────────────────────────────────────────
|
||||
|
||||
|
||||
+2
-4
@@ -1,4 +1,3 @@
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import {
|
||||
Bar,
|
||||
@@ -39,10 +38,9 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, EnvironmentParamSchema, v3PromptsPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Prompts | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta("Prompts");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -18,6 +18,9 @@ import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useEnvironment } from "~/hooks/useEnvironment";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Query");
|
||||
|
||||
/** Convert a Date or ISO string to ISO string format */
|
||||
function toISOString(value: Date | string): string {
|
||||
|
||||
+3
-8
@@ -7,7 +7,7 @@ import {
|
||||
RectangleStackIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { Form, useNavigation, type MetaFunction } from "@remix-run/react";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
@@ -112,6 +112,7 @@ import {
|
||||
useRememberQueueMetricsPeriod,
|
||||
} from "~/components/queues/queueMetricsPeriod";
|
||||
import { queueMetricsMaxPeriodDays } from "~/components/queues/queueMetricsPeriod.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
const SearchParamsSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
@@ -134,13 +135,7 @@ const QUEUE_LIVE_BLOCKS_QUERY =
|
||||
// inspector).
|
||||
const LIVE_GAUGE_FRESH_MS = 90_000;
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Queues | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Queues");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+5
-2
@@ -1,4 +1,3 @@
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import type { QueueItem } from "@trigger.dev/core/v3/schemas";
|
||||
@@ -79,8 +78,12 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => [{ title: `Queue metrics | Trigger.dev` }];
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.queue?.name ?? params.queueParam ?? "Queue",
|
||||
"Queues",
|
||||
]);
|
||||
|
||||
const ParamsSchema = EnvironmentParamSchema.extend({ queueParam: z.string() });
|
||||
|
||||
|
||||
+3
@@ -64,6 +64,9 @@ import {
|
||||
v3BillingPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { SetDefaultRegionService } from "~/v3/services/setDefaultRegion.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Regions");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
+3
@@ -121,6 +121,9 @@ import {
|
||||
import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta(({ params }) => [params.runParam ?? "Run", "Runs"]);
|
||||
|
||||
const resizableSettings = {
|
||||
parent: {
|
||||
|
||||
+3
-8
@@ -1,5 +1,5 @@
|
||||
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { type MetaFunction, useLocation, useNavigation, useRevalidator } from "@remix-run/react";
|
||||
import { useLocation, useNavigation, useRevalidator } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Suspense, useState } from "react";
|
||||
import {
|
||||
@@ -76,16 +76,11 @@ import {
|
||||
shouldRevalidateRunsList,
|
||||
} from "./shouldRevalidateRunsList";
|
||||
import { useRunsLiveReload } from "./useRunsLiveReload";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export { shouldRevalidateRunsList as shouldRevalidate };
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Runs metrics | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Runs");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -17,6 +17,9 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { v3EnvironmentPath, v3ScheduleParams, v3SchedulePath } from "~/utils/pathBuilder";
|
||||
import { DeleteTaskScheduleService } from "~/v3/services/deleteTaskSchedule.server";
|
||||
import { SetActiveOnTaskScheduleService } from "~/v3/services/setActiveOnTaskSchedule.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta(({ params }) => [params.scheduleParam ?? "Schedule", "Schedules"]);
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -5,6 +5,9 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { v3EnvironmentPath, v3ScheduleParams } from "~/utils/pathBuilder";
|
||||
import { humanToCronSupported } from "~/v3/humanToCron.server";
|
||||
import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Edit schedule");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -5,6 +5,9 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
|
||||
import { humanToCronSupported } from "~/v3/humanToCron.server";
|
||||
import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("New schedule");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
import { BoltIcon, BoltSlashIcon } from "@heroicons/react/20/solid";
|
||||
import { BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Clipboard, ClipboardCheck } from "lucide-react";
|
||||
@@ -72,14 +71,13 @@ import {
|
||||
v3RunsPath,
|
||||
v3SessionsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
const ParamsSchema = EnvironmentParamSchema.extend({
|
||||
sessionParam: z.string(),
|
||||
});
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: `Session | Trigger.dev` }];
|
||||
};
|
||||
export const meta = pageMeta(({ params }) => [params.sessionParam ?? "Session", "Sessions"]);
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+2
-8
@@ -1,5 +1,4 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { QuestionMarkIcon } from "~/assets/icons/QuestionMarkIcon";
|
||||
@@ -24,14 +23,9 @@ import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstan
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, EnvironmentParamSchema } from "~/utils/pathBuilder";
|
||||
import { throwNotFound } from "~/utils/httpErrors";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Sessions | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Sessions");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
@@ -40,6 +40,9 @@ import {
|
||||
VercelOnboardingModal,
|
||||
VercelSettingsPanel,
|
||||
} from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Integrations");
|
||||
|
||||
export const handle = { pageTitle: "Integrations" };
|
||||
|
||||
|
||||
+3
-8
@@ -1,4 +1,4 @@
|
||||
import { Outlet, useMatches, type MetaFunction } from "@remix-run/react";
|
||||
import { Outlet, useMatches } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
@@ -12,14 +12,9 @@ import {
|
||||
v3ProjectSettingsGeneralPath,
|
||||
v3ProjectSettingsIntegrationsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Project settings | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Project settings");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
await requireUserId(request);
|
||||
|
||||
+2
-4
@@ -1,5 +1,4 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -28,10 +27,9 @@ import {
|
||||
v3EnvironmentPath,
|
||||
v3RunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: "Tasks | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta("Tasks");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+6
-7
@@ -1,5 +1,5 @@
|
||||
import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid";
|
||||
import { useFetcher, useRevalidator, type MetaFunction } from "@remix-run/react";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { TypedAwait, typeddefer, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -105,13 +105,12 @@ import type { loader as scheduleNewLoader } from "../_app.orgs.$organizationSlug
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route";
|
||||
import { NewRunsButton, TaskRunsList } from "~/components/runs/v3/TaskRunsList";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug;
|
||||
return [
|
||||
{ title: slug ? `${slug} | Scheduled tasks | Trigger.dev` : "Scheduled task | Trigger.dev" },
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.task?.slug ?? params.taskParam ?? "Task",
|
||||
"Scheduled tasks",
|
||||
]);
|
||||
|
||||
const ParamsSchema = EnvironmentParamSchema.extend({
|
||||
taskParam: z.string(),
|
||||
|
||||
+5
-5
@@ -1,4 +1,3 @@
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { Suspense, useMemo, useRef, useState } from "react";
|
||||
@@ -65,11 +64,12 @@ import { parseFiniteInt } from "~/utils/searchParams";
|
||||
import { NewRunsButton, TaskRunsList } from "~/components/runs/v3/TaskRunsList";
|
||||
import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server";
|
||||
import { engine } from "~/v3/runEngine.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug;
|
||||
return [{ title: slug ? `${slug} | Tasks | Trigger.dev` : "Task | Trigger.dev" }];
|
||||
};
|
||||
export const meta = pageMeta<typeof loader>(({ data, params }) => [
|
||||
data?.task?.slug ?? params.taskParam ?? "Task",
|
||||
"Tasks",
|
||||
]);
|
||||
|
||||
const ParamsSchema = EnvironmentParamSchema.extend({
|
||||
taskParam: z.string(),
|
||||
|
||||
+3
@@ -80,6 +80,9 @@ import { AIPayloadTabContent } from "./AIPayloadTabContent";
|
||||
import { SchemaTabContent } from "./SchemaTabContent";
|
||||
import { TestSidebarTabs } from "./TestSidebarTabs";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta(({ params }) => [params.taskParam ?? "Task", "Test"]);
|
||||
|
||||
type FormAction = "create-template" | "delete-template" | "run-scheduled" | "run-standard";
|
||||
|
||||
|
||||
+3
-8
@@ -1,5 +1,5 @@
|
||||
import { BookOpenIcon, MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction, Outlet, useParams } from "@remix-run/react";
|
||||
import { Outlet, useParams } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { TestHasNoTasks } from "~/components/BlankStatePanels";
|
||||
@@ -36,14 +36,9 @@ import { type TaskListItem, TestPresenter } from "~/presenters/v3/TestPresenter.
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { docsPath, EnvironmentParamSchema, v3TestTaskPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Test | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Test");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+6
@@ -25,6 +25,12 @@ import { WaitpointDetailTable } from "~/components/runs/v3/WaitpointDetails";
|
||||
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
|
||||
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta(({ params }) => [
|
||||
params.waitpointParam ?? "Waitpoint",
|
||||
"Waitpoint tokens",
|
||||
]);
|
||||
|
||||
const Params = EnvironmentParamSchema.extend({
|
||||
waitpointParam: z.string(),
|
||||
|
||||
+3
-8
@@ -1,5 +1,5 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { Outlet, useParams, type MetaFunction } from "@remix-run/react";
|
||||
import { Outlet, useParams } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
@@ -49,14 +49,9 @@ import {
|
||||
type PrismaClientOrTransaction,
|
||||
} from "~/db.server";
|
||||
import { docsPath, EnvironmentParamSchema, v3WaitpointTokenPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Waitpoint tokens | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Waitpoint tokens");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FolderIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Form, type MetaFunction, useActionData, useNavigation, useSubmit } from "@remix-run/react";
|
||||
import { Form, useActionData, useNavigation, useSubmit } from "@remix-run/react";
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -54,14 +54,9 @@ import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { extractDomain, faviconUrl as buildFaviconUrl } from "~/utils/favicon";
|
||||
import { OrganizationParamsSchema, organizationSettingsPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Organization settings | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Organization settings");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+2
-4
@@ -1,5 +1,4 @@
|
||||
import { parseWithZod } from "@conform-to/zod";
|
||||
import type { MetaFunction } from "@remix-run/react";
|
||||
import { json, redirect } from "@remix-run/server-runtime";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -74,15 +73,14 @@ import {
|
||||
v3BillingLimitsPath,
|
||||
v3BillingPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
const billingLimitsAuthorization = {
|
||||
action: "manage" as const,
|
||||
resource: { type: "billing-limits" as const },
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: `Billing limits | Trigger.dev` }];
|
||||
};
|
||||
export const meta = pageMeta("Billing limits");
|
||||
|
||||
export const loader = dashboardLoader(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CalendarDaysIcon, CreditCardIcon, StarIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { type PlanDefinition } from "@trigger.dev/platform";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
@@ -21,14 +20,9 @@ import {
|
||||
v3StripePortalPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Billing | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Billing");
|
||||
|
||||
export const loader = dashboardLoader(
|
||||
{
|
||||
|
||||
@@ -40,6 +40,9 @@ import { $transaction, prisma } from "~/db.server";
|
||||
import { requireOrganization } from "~/services/org.server";
|
||||
import { OrganizationParamsSchema, organizationSlackIntegrationPath } from "~/utils/pathBuilder";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Slack integration");
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
@@ -35,6 +35,9 @@ import { requireOrganization } from "~/services/org.server";
|
||||
import { rbac } from "~/services/rbac.server";
|
||||
import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { OrganizationParamsSchema, v3ProjectSettingsIntegrationsPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Vercel integration");
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
|
||||
+3
-4
@@ -4,7 +4,7 @@ import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Form, useRevalidator, type MetaFunction } from "@remix-run/react";
|
||||
import { Form, useRevalidator } from "@remix-run/react";
|
||||
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { tryCatch } from "@trigger.dev/core/utils";
|
||||
import type { PrivateLinkConnectionStatus } from "@trigger.dev/platform";
|
||||
@@ -33,10 +33,9 @@ import {
|
||||
} from "~/utils/pathBuilder";
|
||||
import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: `Private Connections | Trigger.dev` }];
|
||||
};
|
||||
export const meta = pageMeta("Private Connections");
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+3
-4
@@ -1,6 +1,6 @@
|
||||
import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react";
|
||||
import { parseWithZod } from "@conform-to/zod";
|
||||
import { Form, useActionData, useParams, type MetaFunction } from "@remix-run/react";
|
||||
import { Form, useActionData, useParams } from "@remix-run/react";
|
||||
import { json, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { tryCatch } from "@trigger.dev/core/utils";
|
||||
import { useState } from "react";
|
||||
@@ -45,10 +45,9 @@ import {
|
||||
SparklesIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: `Add Private Connection | Trigger.dev` }];
|
||||
};
|
||||
export const meta = pageMeta("Add Private Connection");
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -27,14 +26,9 @@ import { useShowSelfServe } from "~/hooks/useShowSelfServe";
|
||||
import { resolveOrgIdFromSlug } from "~/models/organization.server";
|
||||
import { rbac } from "~/services/rbac.server";
|
||||
import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Roles | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Roles");
|
||||
|
||||
const Params = z.object({
|
||||
organizationSlug: z.string(),
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ClockIcon,
|
||||
ExclamationCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
@@ -48,8 +47,9 @@ import { FEATURE_FLAG } from "~/v3/featureFlags";
|
||||
import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { throwPermissionDenied } from "~/utils/permissionDenied";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => [{ title: "SSO & Directory Sync | Trigger.dev" }];
|
||||
export const meta = pageMeta("SSO & Directory Sync");
|
||||
|
||||
const Params = z.object({ organizationSlug: z.string() });
|
||||
|
||||
|
||||
@@ -2,13 +2,7 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react";
|
||||
import { parseWithZod } from "@conform-to/zod";
|
||||
import { EnvelopeIcon, NoSymbolIcon, UserPlusIcon } from "@heroicons/react/20/solid";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import {
|
||||
Form,
|
||||
type MetaFunction,
|
||||
useActionData,
|
||||
useFetcher,
|
||||
useNavigation,
|
||||
} from "@remix-run/react";
|
||||
import { Form, useActionData, useFetcher, useNavigation } from "@remix-run/react";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { tryCatch } from "@trigger.dev/core/utils";
|
||||
import { cloneElement, useEffect, useRef, useState } from "react";
|
||||
@@ -61,14 +55,9 @@ import {
|
||||
} from "~/utils/pathBuilder";
|
||||
import { SetSeatsAddOnService } from "~/v3/services/setSeatsAddOn.server";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Team | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Team");
|
||||
|
||||
const Params = z.object({
|
||||
organizationSlug: z.string(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Await, type MetaFunction } from "@remix-run/react";
|
||||
import { Await } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { Suspense, useMemo } from "react";
|
||||
@@ -36,14 +36,9 @@ import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/nu
|
||||
import { useBillingLimit } from "~/hooks/useOrganizations";
|
||||
import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Usage | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Usage");
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
@@ -36,6 +36,9 @@ import {
|
||||
v3ProjectPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("New project");
|
||||
|
||||
const WORKING_ON_OTHER = "Other/not sure yet";
|
||||
const GOALS_OTHER = "Other/not sure yet";
|
||||
|
||||
@@ -9,6 +9,9 @@ import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
|
||||
import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder";
|
||||
import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Choose a plan");
|
||||
|
||||
export const loader = dashboardLoader(
|
||||
{
|
||||
|
||||
@@ -27,6 +27,9 @@ import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.
|
||||
import { requireUser, requireUserId } from "~/services/session.server";
|
||||
import { extractDomain, faviconUrl } from "~/utils/favicon";
|
||||
import { organizationPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("New organization");
|
||||
|
||||
const schema = z.object({
|
||||
orgName: z.string().min(3).max(50),
|
||||
|
||||
@@ -5,6 +5,9 @@ import { LogoIcon } from "~/components/LogoIcon";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Supported timezones");
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
return typedjson({
|
||||
|
||||
@@ -2,13 +2,7 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { conformZodMessage, parseWithZod } from "@conform-to/zod";
|
||||
import { ComputerDesktopIcon, MoonIcon, SunIcon, SwatchIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
Form,
|
||||
type MetaFunction,
|
||||
useActionData,
|
||||
useFetcher,
|
||||
useLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { Form, useActionData, useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
|
||||
@@ -44,6 +38,7 @@ import { cachedFlag } from "~/v3/featureFlags.server";
|
||||
import { requireUser, requireUserId } from "~/services/session.server";
|
||||
import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation";
|
||||
import { accountPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
const THEME_LABELS: Record<ThemePreference, string> = {
|
||||
classic: "Classic",
|
||||
@@ -75,13 +70,7 @@ function themeIcon(value: ThemePreference) {
|
||||
}
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Your profile | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Your profile");
|
||||
|
||||
function createSchema(
|
||||
constraints: {
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
isAuthorizationCodeMintable,
|
||||
} from "~/services/personalAccessToken.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Authorize login");
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
authorizationCode: z.string(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type MetaFunction } from "@remix-run/react";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import {
|
||||
@@ -16,14 +15,9 @@ import {
|
||||
} from "~/services/sessionDuration.server";
|
||||
import { MfaSetup } from "../resources.account.mfa.setup/route";
|
||||
import { SessionDurationSetting } from "../resources.account.session-duration/SessionDurationSetting";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Security | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Security");
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const user = await requireUser(request);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { parseWithZod } from "@conform-to/zod";
|
||||
import { BookOpenIcon, ShieldCheckIcon, TrashIcon } from "@heroicons/react/20/solid";
|
||||
import { ShieldExclamationIcon } from "@heroicons/react/24/solid";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { Form, type MetaFunction, useActionData, useFetcher } from "@remix-run/react";
|
||||
import { Form, useActionData, useFetcher } from "@remix-run/react";
|
||||
import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -47,14 +47,9 @@ import {
|
||||
} from "~/services/personalAccessToken.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, personalAccessTokensPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Personal Access Tokens | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const meta = pageMeta("Personal Access Tokens");
|
||||
|
||||
// Shared between the create-token panel hint and the listing column
|
||||
// header tooltip so the cap is explained identically in both places.
|
||||
|
||||
@@ -30,6 +30,9 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation";
|
||||
import { rootPath } from "~/utils/pathBuilder";
|
||||
import { getVercelInstallParams } from "~/v3/vercel";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Confirm your details");
|
||||
|
||||
const referralSourceOptions = [
|
||||
"Search engine",
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { invitesPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Your invitations");
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
@@ -11,6 +11,9 @@ import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { rootPath } from "~/utils/pathBuilder";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Unsubscribe");
|
||||
|
||||
export const ParamsSchema = z.object({
|
||||
userId: z.string(),
|
||||
|
||||
@@ -24,6 +24,9 @@ import { ssoRedirectForEmail } from "~/services/ssoAutoDiscovery.server";
|
||||
import { confirmBasicDetailsPath, newProjectPath } from "~/utils/pathBuilder";
|
||||
import { redirectWithErrorMessage } from "~/models/message.server";
|
||||
import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server";
|
||||
import { pageMeta } from "~/utils/pageTitle";
|
||||
|
||||
export const meta = pageMeta("Vercel setup");
|
||||
|
||||
const LoaderParamsSchema = z.object({
|
||||
organizationId: z.string().optional().nullable(),
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { MetaDescriptor, MetaFunction } from "@remix-run/node";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { appTitle, pageMeta } from "./pageTitle";
|
||||
|
||||
const ORGANIZATION_MATCH_ID = "routes/_app.orgs.$organizationSlug";
|
||||
|
||||
type Route = { id: string; data?: unknown; meta?: MetaFunction };
|
||||
|
||||
/**
|
||||
* Mirrors how Remix v2 resolves meta (see @remix-run/react `Meta`): each match is visited from the
|
||||
* root down, a match without a meta export inherits the nearest ancestor's array, and the last
|
||||
* match's array is what renders. It also only passes the matches visited *so far*, which is why
|
||||
* titles are declared per route rather than centrally.
|
||||
*/
|
||||
/** The URL params a page inside a project carries; the org scope is dropped when they are present. */
|
||||
const ENV_PARAMS = { organizationSlug: "acme", projectParam: "my-project", envParam: "prod" };
|
||||
|
||||
function renderTitle(
|
||||
routes: Route[],
|
||||
params: Record<string, string> = ENV_PARAMS
|
||||
): string | undefined {
|
||||
const matches: Array<{ id: string; data: unknown; meta: MetaDescriptor[]; params: any }> = [];
|
||||
let leafMeta: MetaDescriptor[] = [];
|
||||
|
||||
routes.forEach((route, index) => {
|
||||
matches[index] = { id: route.id, data: route.data, meta: [], params };
|
||||
const routeMeta = route.meta
|
||||
? route.meta({ data: route.data, params, matches, location: {} as any } as any)
|
||||
: [...leafMeta];
|
||||
matches[index].meta = routeMeta as MetaDescriptor[];
|
||||
leafMeta = routeMeta as MetaDescriptor[];
|
||||
});
|
||||
|
||||
const rendered = leafMeta.find((descriptor) => "title" in descriptor) as
|
||||
| { title: string }
|
||||
| undefined;
|
||||
return rendered?.title;
|
||||
}
|
||||
|
||||
const rootRoute: Route = {
|
||||
id: "root",
|
||||
data: { appEnv: "production" },
|
||||
meta: () => [{ title: appTitle("production") }, { name: "viewport", content: "width=1024" }],
|
||||
};
|
||||
|
||||
const orgRoute: Route = {
|
||||
id: ORGANIZATION_MATCH_ID,
|
||||
data: {
|
||||
organization: { title: "Acme" },
|
||||
},
|
||||
};
|
||||
|
||||
describe("pageMeta", () => {
|
||||
it("titles a page inside a project without any scope", () => {
|
||||
const title = renderTitle([rootRoute, orgRoute, { id: "routes/runs", meta: pageMeta("Runs") }]);
|
||||
|
||||
expect(title).toBe("Runs | Trigger.dev");
|
||||
});
|
||||
|
||||
it("uses the deepest declared title", () => {
|
||||
const title = renderTitle([
|
||||
rootRoute,
|
||||
orgRoute,
|
||||
{ id: "routes/runs", meta: pageMeta("Runs") },
|
||||
{ id: "routes/runs.$runParam", meta: pageMeta(["run_abc", "Runs"]) },
|
||||
]);
|
||||
|
||||
expect(title).toBe("run_abc | Runs | Trigger.dev");
|
||||
});
|
||||
|
||||
it("falls back to the nearest declared title, then the app title", () => {
|
||||
expect(
|
||||
renderTitle([
|
||||
rootRoute,
|
||||
orgRoute,
|
||||
{ id: "routes/runs", meta: pageMeta("Runs") },
|
||||
{ id: "routes/runs._index" },
|
||||
])
|
||||
).toBe("Runs | Trigger.dev");
|
||||
|
||||
expect(renderTitle([rootRoute, orgRoute, { id: "routes/runs._index" }])).toBe("Trigger.dev");
|
||||
});
|
||||
|
||||
it("renders an entity title from loader data, with a fallback when it is missing", () => {
|
||||
const meta = pageMeta<() => { task?: { slug: string } }>(({ data, params }) => [
|
||||
data?.task?.slug ?? params.taskParam ?? "Task",
|
||||
"Tasks",
|
||||
]);
|
||||
|
||||
expect(
|
||||
renderTitle([
|
||||
rootRoute,
|
||||
orgRoute,
|
||||
{ id: "routes/task", data: { task: { slug: "my-task" } }, meta },
|
||||
])
|
||||
).toBe("my-task | Tasks | Trigger.dev");
|
||||
|
||||
expect(
|
||||
renderTitle([rootRoute, orgRoute, { id: "routes/task", meta }], {
|
||||
...ENV_PARAMS,
|
||||
taskParam: "other",
|
||||
})
|
||||
).toBe("other | Tasks | Trigger.dev");
|
||||
});
|
||||
|
||||
it("names the organization on its own pages, with the app env tag", () => {
|
||||
const stagingRoot: Route = {
|
||||
id: "root",
|
||||
data: { appEnv: "staging" },
|
||||
meta: () => [{ title: appTitle("staging") }],
|
||||
};
|
||||
|
||||
const title = renderTitle(
|
||||
[
|
||||
stagingRoot,
|
||||
{ id: ORGANIZATION_MATCH_ID, data: { organization: { title: "Acme" } } },
|
||||
{ id: "routes/team", meta: pageMeta("Team") },
|
||||
],
|
||||
{ organizationSlug: "acme" }
|
||||
);
|
||||
|
||||
expect(title).toBe("Team | Acme | Trigger.dev (staging)");
|
||||
});
|
||||
|
||||
it("carries the root's non-title tags through", () => {
|
||||
const routes = [rootRoute, orgRoute, { id: "routes/runs", meta: pageMeta("Runs") }];
|
||||
const descriptors = pageMeta("Runs")({
|
||||
data: undefined,
|
||||
params: {},
|
||||
matches: [
|
||||
{ id: "root", data: rootRoute.data, meta: [{ name: "viewport", content: "width=1024" }] },
|
||||
{ id: "routes/runs", data: undefined, meta: [] },
|
||||
],
|
||||
location: {} as any,
|
||||
} as any) as MetaDescriptor[];
|
||||
|
||||
expect(descriptors).toContainEqual({ name: "viewport", content: "width=1024" });
|
||||
expect(renderTitle(routes)).toBe("Runs | Trigger.dev");
|
||||
});
|
||||
|
||||
it("does not name the organization inside a project", () => {
|
||||
// The dashboard switches projects in every tab at once, so the scope adds nothing there.
|
||||
expect(renderTitle([rootRoute, orgRoute, { id: "routes/runs", meta: pageMeta("Runs") }])).toBe(
|
||||
"Runs | Trigger.dev"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { MetaDescriptor, MetaFunction } from "@remix-run/node";
|
||||
import type { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import { appEnvTitleTag } from "~/utils";
|
||||
|
||||
/**
|
||||
* Tab titles. Every page exports `export const meta = pageMeta(...)` and only says what the page
|
||||
* is; the app title is added here.
|
||||
*
|
||||
* Shape (leading words carry the information, so a narrow tab still reads):
|
||||
* env page: `Runs | Trigger.dev`
|
||||
* env entity: `run_abc | Runs | Trigger.dev`
|
||||
* org page: `Team | Acme | Trigger.dev`
|
||||
* else: `Login to Trigger.dev` etc.
|
||||
*
|
||||
* Remix v2 picks the meta of the deepest route that exports one; a route without a meta export
|
||||
* inherits its nearest ancestor's. So a layout's `pageMeta` is the fallback for children that
|
||||
* don't declare their own, and non-title tags from the root (viewport, robots) are carried over
|
||||
* here rather than re-declared per route.
|
||||
*/
|
||||
|
||||
const APP_NAME = "Trigger.dev";
|
||||
|
||||
/** The org route carries the organization the URL resolved to. */
|
||||
const ORGANIZATION_MATCH_ID = "routes/_app.orgs.$organizationSlug";
|
||||
|
||||
/** One or more title segments, most specific first: `["run_abc", "Runs"]`. */
|
||||
export type TitleSegments = string | string[];
|
||||
|
||||
type MetaArgs = Parameters<MetaFunction>[0];
|
||||
type Matches = MetaArgs["matches"];
|
||||
|
||||
type PageInput<TLoader> =
|
||||
| TitleSegments
|
||||
| ((args: {
|
||||
data: UseDataFunctionReturn<TLoader> | undefined;
|
||||
params: Record<string, string | undefined>;
|
||||
}) => TitleSegments | undefined);
|
||||
|
||||
/** The title used when no page declares one. */
|
||||
export function appTitle(appEnv?: string): string {
|
||||
return `${APP_NAME}${appEnvTitleTag(appEnv)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares this page's tab title. Pass a string (or segments), or a function of the route's
|
||||
* loader data and params for entity pages.
|
||||
*/
|
||||
export function pageMeta<TLoader = unknown>(page: PageInput<TLoader>): MetaFunction {
|
||||
return ({ data, params, matches }) => {
|
||||
const segments = resolveSegments(
|
||||
page,
|
||||
data as UseDataFunctionReturn<TLoader> | undefined,
|
||||
params
|
||||
);
|
||||
return [...inheritedMeta(matches), { title: composePageTitle(segments, matches) }];
|
||||
};
|
||||
}
|
||||
|
||||
/** Builds the full title from the page segments, the org scope and the app title. */
|
||||
export function composePageTitle(segments: string[], matches: Matches): string {
|
||||
return [...segments, scopeFromMatches(matches), appTitle(appEnvFromMatches(matches))]
|
||||
.filter((segment): segment is string => Boolean(segment))
|
||||
.join(" | ");
|
||||
}
|
||||
|
||||
/**
|
||||
* The organization, and only on its own pages: inside a project the tab is already about one
|
||||
* project, and the dashboard switches projects in every tab at once, so naming it adds nothing.
|
||||
*/
|
||||
export function scopeFromMatches(matches: Matches): string | undefined {
|
||||
const match = matches.find((m) => m.id === ORGANIZATION_MATCH_ID);
|
||||
if (!match || match.params?.projectParam) return undefined;
|
||||
const data = match.data as { organization?: { title?: string | null } } | undefined;
|
||||
return data?.organization?.title ?? undefined;
|
||||
}
|
||||
|
||||
function appEnvFromMatches(matches: Matches): string | undefined {
|
||||
const rootData = matches[0]?.data as { appEnv?: string } | undefined;
|
||||
return rootData?.appEnv;
|
||||
}
|
||||
|
||||
/** Non-title tags (viewport, robots) from the nearest ancestor, which Remix would otherwise drop. */
|
||||
function inheritedMeta(matches: Matches): MetaDescriptor[] {
|
||||
const parent = matches[matches.length - 2];
|
||||
return (parent?.meta ?? []).filter((descriptor) => !("title" in descriptor));
|
||||
}
|
||||
|
||||
function resolveSegments<TLoader>(
|
||||
page: PageInput<TLoader>,
|
||||
data: UseDataFunctionReturn<TLoader> | undefined,
|
||||
params: Record<string, string | undefined>
|
||||
): string[] {
|
||||
const resolved = typeof page === "function" ? safeResolve(() => page({ data, params })) : page;
|
||||
if (!resolved) return [];
|
||||
return (Array.isArray(resolved) ? resolved : [resolved]).filter(Boolean);
|
||||
}
|
||||
|
||||
// Loader data can be missing (error boundaries, redirects), so a bad lookup must not break the page.
|
||||
function safeResolve(resolve: () => TitleSegments | undefined): TitleSegments | undefined {
|
||||
try {
|
||||
return resolve();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user