Hooks can use passed in matches. Breadcrumb updated to be in the route

This commit is contained in:
Matt Aitken
2023-07-11 12:46:39 +01:00
parent 686cf9dc7e
commit 396663d65c
30 changed files with 406 additions and 412 deletions
@@ -1,240 +1,29 @@
import { useMatches } from "@remix-run/react";
import { Fragment } from "react";
import {
useIntegrationClient,
useOptionalIntegrationClient,
} from "~/hooks/useIntegrationClient";
import { useJob, useOptionalJob } from "~/hooks/useJob";
import {
useOptionalOrganization,
useOrganization,
} from "~/hooks/useOrganizations";
import { useOptionalProject, useProject } from "~/hooks/useProject";
import { useOptionalRun, useRun } from "~/hooks/useRun";
import {
integrationClientConnectionsPath,
integrationClientPath,
integrationClientScopesPath,
jobPath,
jobTestPath,
projectEnvironmentsPath,
projectIntegrationsPath,
projectPath,
projectTriggersPath,
jobRunDashboardPath,
} from "~/utils/pathBuilder";
import { RouteMatch, useMatches } from "@remix-run/react";
import { Fragment, ReactNode } from "react";
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
import { JobsMenu } from "./JobsMenu";
import { BreadcrumbLink } from "./NavBar";
import { ProjectsMenu } from "./ProjectsMenu";
export type Breadcrumb = {
slug?:
| "projects"
| "jobs"
| "integrations"
| "integration"
| "integration-job"
| "integration-connections"
| "integration-scopes"
| "triggers"
| "trigger"
| "environments"
| "job"
| "test"
| "runs"
| "run";
link?: {
title: string;
};
};
export type BreadcrumbItem = (
match: RouteMatch,
allMatches: RouteMatch[]
) => ReactNode;
export function Breadcrumb() {
const matches = useMatches();
const organization = useOptionalOrganization();
const project = useOptionalProject();
const job = useOptionalJob();
const run = useOptionalRun();
const client = useOptionalIntegrationClient();
return (
<div className="hidden items-center md:flex">
{matches.map((match) => {
if (!match.handle || !match.handle.breadcrumb) return null;
const breadcrumb = match.handle.breadcrumb as Breadcrumb;
const breadcrumb = match.handle.breadcrumb as BreadcrumbItem;
return (
<Fragment key={match.id}>
<BreadcrumbIcon />
{breadcrumb.link ? (
<BreadcrumbLink
to={match.pathname}
title={breadcrumb.link.title}
/>
) : (
<BreadcrumbItem
breadcrumb={breadcrumb}
organization={organization}
project={project}
job={job}
run={run}
client={client}
/>
)}
{breadcrumb(match, matches)}
</Fragment>
);
})}
</div>
);
}
function BreadcrumbItem({
breadcrumb,
organization,
project,
job,
run,
client,
}: {
breadcrumb: Breadcrumb;
organization?: ReturnType<typeof useOrganization>;
project?: ReturnType<typeof useProject>;
job?: ReturnType<typeof useJob>;
run?: ReturnType<typeof useRun>;
client?: ReturnType<typeof useIntegrationClient>;
}) {
switch (breadcrumb.slug) {
case "projects":
return <ProjectsMenu key={breadcrumb.slug} />;
case "jobs":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectPath(organization, project)}
title="Jobs"
/>
);
case "environments":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectEnvironmentsPath(organization, project)}
title="Environments"
/>
);
case "integrations":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectIntegrationsPath(organization, project)}
title="Integrations"
/>
);
case "integration":
if (!organization || !project || !client) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectIntegrationsPath(organization, project)}
title="Integrations"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={integrationClientPath(organization, project, client)}
title={client.title}
/>
</Fragment>
);
case "integration-job":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientPath(organization, project, client)}
title={"Jobs"}
/>
);
case "integration-connections":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientConnectionsPath(organization, project, client)}
title={"Connections"}
/>
);
case "integration-scopes":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientScopesPath(organization, project, client)}
title={"Scopes"}
/>
);
case "triggers":
if (!organization || !project) return null;
return (
<BreadcrumbLink
to={projectTriggersPath(organization, project)}
title={"Triggers"}
/>
);
case "trigger":
if (!organization || !project) return null;
return (
<BreadcrumbLink
to={projectTriggersPath(organization, project)}
title={"Triggers"}
/>
);
case "job":
if (!organization || !project || !job) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectPath(organization, project)}
title="Jobs"
/>
<BreadcrumbIcon />
<JobsMenu key={breadcrumb.slug} />
</Fragment>
);
case "test":
if (!organization || !project || !job) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobTestPath(organization, project, job)}
title="Test"
/>
);
case "runs":
if (!organization || !project || !job) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobPath(organization, project, job)}
title="Runs"
/>
);
case "run":
if (!organization || !project || !job || !run) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={jobPath(organization, project, job)}
title="Runs"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={jobRunDashboardPath(organization, project, job, run)}
title={`Run #${run.number}`}
/>
</Fragment>
);
}
return <span className="text-red-500">{breadcrumb.slug}</span>;
}
@@ -1,4 +1,4 @@
import { Link } from "@remix-run/react";
import { Link, RouteMatch } from "@remix-run/react";
import { useState } from "react";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
@@ -14,11 +14,11 @@ import {
PopoverSectionHeader,
} from "../primitives/Popover";
export function JobsMenu() {
export function JobsMenu({ matches }: { matches: RouteMatch[] }) {
const [isOpen, setIsOpen] = useState(false);
const organization = useOrganization();
const project = useProject();
const currentJob = useJob();
const organization = useOrganization(matches);
const project = useProject(matches);
const currentJob = useJob(matches);
return (
<>
@@ -1,3 +1,4 @@
import { RouteMatch } from "@remix-run/react";
import { Fragment, useState } from "react";
import {
useIsNewOrganizationPage,
@@ -17,11 +18,11 @@ import {
PopoverSectionHeader,
} from "../primitives/Popover";
export function ProjectsMenu() {
export function ProjectsMenu({ matches }: { matches: RouteMatch[] }) {
const [isOpen, setIsOpen] = useState(false);
const organizations = useOrganizations();
const isNewOrgPage = useIsNewOrganizationPage();
const currentProject = useOptionalProject();
const organizations = useOrganizations(matches);
const isNewOrgPage = useIsNewOrganizationPage(matches);
const currentProject = useOptionalProject(matches);
if (isNewOrgPage) {
return null;
+8 -13
View File
@@ -1,29 +1,24 @@
import { DEV_ENVIRONMENT, LIVE_ENVIRONMENT } from "~/consts";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { useMatchesData } from "~/utils";
import { RouteMatch } from "@remix-run/react";
import { MatchedProject, useOptionalProject } from "./useProject";
export type ProjectJobEnvironment = MatchedProject["environments"][number];
export function useEnvironments() {
const project = useOptionalProject();
if (!project) {
return undefined;
}
export function useEnvironments(matches?: RouteMatch[]) {
const project = useOptionalProject(matches);
if (!project) return;
return project.environments;
}
export function useDevEnvironment() {
const environments = useEnvironments();
export function useDevEnvironment(matches?: RouteMatch[]) {
const environments = useEnvironments(matches);
if (!environments) return;
return environments.find((environment) => environment.type === "DEVELOPMENT");
}
export function useProdEnvironment() {
const environments = useEnvironments();
export function useProdEnvironment(matches?: RouteMatch[]) {
const environments = useEnvironments(matches);
if (!environments) return;
return environments.find((environment) => environment.type === "PRODUCTION");
+10 -10
View File
@@ -1,22 +1,22 @@
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import { RouteMatch } from "@remix-run/react";
import { UseDataFunctionReturn } from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route";
import { useTypedMatchesData } from "./useTypedMatchData";
export type MatchedClient = UseDataFunctionReturn<typeof loader>["client"];
export function useOptionalIntegrationClient() {
const routeMatch = useTypedRouteLoaderData<typeof loader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam"
);
export function useOptionalIntegrationClient(matches?: RouteMatch[]) {
const routeMatch = useTypedMatchesData<typeof loader>({
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam",
matches,
});
return routeMatch?.client;
}
export function useIntegrationClient() {
const integration = useOptionalIntegrationClient();
export function useIntegrationClient(matches?: RouteMatch[]) {
const integration = useOptionalIntegrationClient(matches);
invariant(integration, "Integration must be defined");
return integration;
}
@@ -1,9 +1,11 @@
import { useMatches } from "@remix-run/react";
import { RouteMatch, useMatches } from "@remix-run/react";
export function useIsProjectChildPage() {
const matchesData = useMatches();
export function useIsProjectChildPage(matches?: RouteMatch[]) {
if (!matches) {
matches = useMatches();
}
return matchesData.some((matchData) => {
return matches.some((matchData) => {
return matchData.id.startsWith(
"routes/_app.orgs.$organizationSlug.projects.$projectParam"
);
+12 -11
View File
@@ -6,29 +6,30 @@ import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
import { useOptionalProject } from "./useProject";
import { useChanged } from "./useChanged";
import { RouteMatch } from "@remix-run/react";
import { useTypedMatchesData } from "./useTypedMatchData";
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
export function useOptionalJob() {
const project = useOptionalProject();
const routeMatch = useTypedRouteLoaderData<typeof loader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam"
);
export const jobMatchId =
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
export function useOptionalJob(matches?: RouteMatch[]) {
const project = useOptionalProject(matches);
const routeMatch = useTypedMatchesData<typeof loader>({
id: jobMatchId,
matches,
});
if (!project || !routeMatch || !routeMatch.job) {
return undefined;
}
if (routeMatch.job == null) {
return undefined;
}
//get the job from the list on the project
return project.jobs.find((j) => j.id === routeMatch.job.id);
}
export function useJob() {
const job = useOptionalJob();
export function useJob(matches?: RouteMatch[]) {
const job = useOptionalJob(matches);
invariant(job, "Job must be defined");
return job;
}
+26 -31
View File
@@ -7,58 +7,53 @@ import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/r
import type { loader as appLoader } from "~/routes/_app/route";
import { hydrateObject, useMatchesData } from "~/utils";
import { useChanged } from "./useChanged";
import { RouteMatch } from "@remix-run/react";
import { useTypedMatchesData } from "./useTypedMatchData";
export type MatchedOrganization = UseDataFunctionReturn<
typeof appLoader
>["organizations"][number];
export function useOptionalOrganizations() {
return useOrganizationsFromMatchesData(["routes/_app"]);
export function useOptionalOrganizations(matches?: RouteMatch[]) {
const data = useTypedMatchesData<typeof appLoader>({
id: "routes/_app",
matches,
});
return data?.organizations;
}
export function useOrganizations() {
const orgs = useOptionalOrganizations();
export function useOrganizations(matches?: RouteMatch[]) {
const orgs = useOptionalOrganizations(matches);
invariant(orgs, "No organizations found in loader.");
return orgs;
}
export function useOptionalOrganization() {
const orgs = useOptionalOrganizations();
const routeMatch = useTypedRouteLoaderData<typeof orgLoader>(
"routes/_app.orgs.$organizationSlug"
);
export function useOptionalOrganization(matches?: RouteMatch[]) {
const orgs = useOptionalOrganizations(matches);
const org = useTypedMatchesData<typeof orgLoader>({
id: "routes/_app.orgs.$organizationSlug",
matches,
});
if (!orgs || !routeMatch || !routeMatch.organization) {
if (!orgs || !org || !org.organization) {
return undefined;
}
if (routeMatch.organization === null) {
return undefined;
}
return orgs.find((o) => o.id === routeMatch.organization.id);
return orgs.find((o) => o.id === org.organization.id);
}
export function useOrganization() {
const org = useOptionalOrganization();
export function useOrganization(matches?: RouteMatch[]) {
const org = useOptionalOrganization(matches);
invariant(org, "No organization found in loader.");
return org;
}
export function useIsNewOrganizationPage(): boolean {
const routeMatch = useMatchesData("routes/_app.orgs.new");
return !!routeMatch;
}
function useOrganizationsFromMatchesData(paths: string[]) {
const routeMatch = useMatchesData(paths);
if (!routeMatch || !routeMatch.data.organizations) {
return undefined;
}
return hydrateObject<
UseDataFunctionReturn<typeof appLoader>["organizations"]
>(routeMatch.data.organizations);
export function useIsNewOrganizationPage(matches?: RouteMatch[]): boolean {
const data = useTypedMatchesData<any>({
id: "routes/_app.orgs.new",
matches,
});
return !!data;
}
export const useOrganizationChanged = (
+13 -11
View File
@@ -1,25 +1,27 @@
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import { RouteMatch } from "@remix-run/react";
import { UseDataFunctionReturn } from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
import { useChanged } from "./useChanged";
import { useTypedMatchesData } from "./useTypedMatchData";
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
export type ProjectJob = MatchedProject["jobs"][number];
export function useOptionalProject() {
const routeMatch = useTypedRouteLoaderData<typeof loader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam"
);
export const projectMatchId =
"routes/_app.orgs.$organizationSlug.projects.$projectParam";
export function useOptionalProject(matches?: RouteMatch[]) {
const routeMatch = useTypedMatchesData<typeof loader>({
id: projectMatchId,
matches,
});
return routeMatch?.project;
}
export function useProject() {
const project = useOptionalProject();
export function useProject(matches?: RouteMatch[]) {
const project = useOptionalProject(matches);
invariant(project, "Project must be defined");
return project;
}
+11 -15
View File
@@ -1,32 +1,28 @@
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import { RouteMatch } from "@remix-run/react";
import { UseDataFunctionReturn } from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route";
import { useOptionalProject } from "./useProject";
import { useTypedMatchesData } from "./useTypedMatchData";
export type MatchedRun = UseDataFunctionReturn<typeof runLoader>["run"];
export function useOptionalRun() {
const project = useOptionalProject();
const routeMatch = useTypedRouteLoaderData<typeof runLoader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam"
);
export function useOptionalRun(matches?: RouteMatch[]) {
const project = useOptionalProject(matches);
const routeMatch = useTypedMatchesData<typeof runLoader>({
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam",
matches,
});
if (!project || !routeMatch || !routeMatch.run) {
return undefined;
}
if (routeMatch.run == null) {
return undefined;
}
return routeMatch.run;
}
export function useRun() {
const run = useOptionalRun();
export function useRun(matches?: RouteMatch[]) {
const run = useOptionalRun(matches);
invariant(run, "Run must be present");
return run;
}
@@ -0,0 +1,44 @@
import { RouteMatch, useMatches } from "@remix-run/react";
import {
RemixSerializedType,
UseDataFunctionReturn,
deserializeRemix,
} from "remix-typedjson";
type AppData = any;
function useTypedDataFromMatches<T = AppData>({
id,
matches,
}: {
id: string;
matches: RouteMatch[];
}): UseDataFunctionReturn<T> | undefined {
const match = matches.find((m) => m.id === id);
return useTypedMatchData<T>(match);
}
export function useTypedMatchesData<T = AppData>({
id,
matches,
}: {
id: string;
matches?: RouteMatch[];
}): UseDataFunctionReturn<T> | undefined {
if (!matches) {
matches = useMatches();
}
return useTypedDataFromMatches<T>({ id, matches });
}
export function useTypedMatchData<T = AppData>(
match: RouteMatch | undefined
): UseDataFunctionReturn<T> | undefined {
if (!match) {
return undefined;
}
return deserializeRemix<T>(match.data as RemixSerializedType<T>) as
| UseDataFunctionReturn<T>
| undefined;
}
+12 -12
View File
@@ -1,21 +1,21 @@
import type { User } from "~/models/user.server";
import { useMatchesData } from "~/utils";
import { useChanged } from "./useChanged";
import { RouteMatch } from "@remix-run/react";
import { useTypedMatchesData } from "./useTypedMatchData";
import { loader } from "~/root";
function isUser(user: any): user is User {
return user && typeof user === "object" && typeof user.email === "string";
export function useOptionalUser(matches?: RouteMatch[]): User | undefined {
const routeMatch = useTypedMatchesData<typeof loader>({
id: "root",
matches,
});
return routeMatch?.user ?? undefined;
}
export function useOptionalUser(): User | undefined {
const routeMatch = useMatchesData("root");
if (!routeMatch || !isUser(routeMatch.data.user)) {
return undefined;
}
return routeMatch.data.user;
}
export function useUser(): User {
const maybeUser = useOptionalUser();
export function useUser(matches?: RouteMatch[]): User {
const maybeUser = useOptionalUser(matches);
if (!maybeUser) {
throw new Error(
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead."
@@ -21,12 +21,17 @@ import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import useWindowSize from "react-use/lib/useWindowSize";
import { docsPath, projectIntegrationsPath } from "~/utils/pathBuilder";
import {
docsPath,
projectIntegrationsPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
export const handle: Handle = {
breadcrumb: {
slug: "jobs",
},
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Jobs" />
),
};
export default function Page() {
@@ -42,6 +42,7 @@ import {
HowToSetupYourProject,
HowToUseApiKeysAndEndpoints,
} from "~/components/helpContent/HelpContentText";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -72,9 +73,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "environments",
},
breadcrumb: (match) => (
<BreadcrumbLink to={match.pathname} title="Environments" />
),
};
export default function Page() {
@@ -8,6 +8,7 @@ import { ConnectToIntegrationSheet } from "~/components/integrations/ConnectToIn
import { IntegrationWithMissingFieldSheet } from "~/components/integrations/IntegrationWithMissingFieldSheet";
import { NoIntegrationSheet } from "~/components/integrations/NoIntegrationSheet";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
@@ -68,9 +69,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "integrations",
},
breadcrumb: (match) => (
<BreadcrumbLink to={match.pathname} title="Integrations" />
),
};
export default function Integrations() {
@@ -4,6 +4,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { HowToUseThisIntegration } from "~/components/helpContent/HelpContentText";
import { JobSkeleton } from "~/components/jobs/JobSkeleton";
import { JobsTable } from "~/components/jobs/JobsTable";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
@@ -11,6 +12,7 @@ import { Input } from "~/components/primitives/Input";
import { useFilterJobs } from "~/hooks/useFilterJobs";
import { useIntegrationClient } from "~/hooks/useIntegrationClient";
import { useProject } from "~/hooks/useProject";
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
import { IntegrationClientJobsPresenter } from "~/presenters/IntegrationClientJobsPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
@@ -19,6 +21,7 @@ import {
IntegrationClientParamSchema,
docsIntegrationPath,
docsRoot,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -38,9 +41,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "integration-job",
},
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Jobs" />
),
};
export default function Page() {
@@ -1,6 +1,7 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { connectionType } from "~/components/integrations/connectionType";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
@@ -15,7 +16,10 @@ import {
import { IntegrationClientConnectionsPresenter } from "~/presenters/IntegrationClientConnectionsPresenter.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { IntegrationClientParamSchema } from "~/utils/pathBuilder";
import {
IntegrationClientParamSchema,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -34,9 +38,12 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "integration-connections",
},
breadcrumb: (match) => (
<BreadcrumbLink
to={trimTrailingSlash(match.pathname)}
title="Connections"
/>
),
};
export default function Page() {
@@ -1,10 +1,14 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Paragraph } from "~/components/primitives/Paragraph";
import { IntegrationClientScopesPresenter } from "~/presenters/IntegrationClientScopesPresenter.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { IntegrationClientParamSchema } from "~/utils/pathBuilder";
import {
IntegrationClientParamSchema,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -23,9 +27,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "integration-scopes",
},
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Scopes" />
),
};
export default function Page() {
@@ -3,6 +3,7 @@ import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { connectionType } from "~/components/integrations/connectionType";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import {
@@ -16,6 +17,7 @@ import {
} from "~/components/primitives/PageHeader";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
import { IntegrationClientPresenter } from "~/presenters/IntegrationClientPresenter.server";
import { requireUser } from "~/services/session.server";
import { Handle } from "~/utils/handle";
@@ -48,8 +50,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "integration",
breadcrumb: (match) => {
const data = useTypedMatchData<typeof loader>(match);
return (
<BreadcrumbLink
to={match.pathname}
title={data?.client.title ?? "Integration"}
/>
);
},
};
@@ -13,6 +13,7 @@ import {
JobParamsSchema,
projectIntegrationsPath,
jobRunsParentPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
import { ListPagination } from "./ListPagination";
import { Callout } from "~/components/primitives/Callout";
@@ -20,6 +21,7 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useJob } from "~/hooks/useJob";
import simplur from "simplur";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
export const DirectionSchema = z.union([
z.literal("forward"),
@@ -56,9 +58,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "runs",
},
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Runs" />
),
};
export default function Page() {
@@ -1,15 +1,20 @@
import { parse } from "@conform-to/zod";
import { useRevalidator } from "@remix-run/react";
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
import { useEffect } from "react";
import { match } from "assert";
import { Fragment, useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils";
import { z } from "zod";
import { JobsMenu } from "~/components/navigation/JobsMenu";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { RunOverview } from "~/components/run/RunOverview";
import { runBasicStatus } from "~/components/runs/RunStatuses";
import { useJob } from "~/hooks/useJob";
import { jobMatchId, useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { projectMatchId, useProject } from "~/hooks/useProject";
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
import {
redirectBackWithErrorMessage,
redirectWithSuccessMessage,
@@ -25,6 +30,7 @@ import {
jobRunDashboardPath,
runPath,
runStreamingPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -101,8 +107,24 @@ export const action: ActionFunction = async ({ request, params }) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "run",
breadcrumb: (match, matches) => {
const jobMatch = matches.find((m) => m.id === jobMatchId);
const runData = useTypedMatchData<typeof loader>(match);
return (
<Fragment>
<BreadcrumbLink
to={trimTrailingSlash(jobMatch?.pathname ?? "")}
title="Runs"
/>
<BreadcrumbIcon />
{runData && (
<BreadcrumbLink
to={match.pathname}
title={`Run #${runData.run.number}`}
/>
)}
</Fragment>
);
},
};
@@ -9,6 +9,7 @@ import { z } from "zod";
import { JSONEditor } from "~/components/code/JSONEditor";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { HowToRunATest } from "~/components/helpContent/HelpContentText";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { FormError } from "~/components/primitives/FormError";
@@ -31,7 +32,11 @@ import { TestJobService } from "~/services/jobs/testJob.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { JobParamsSchema, jobRunDashboardPath } from "~/utils/pathBuilder";
import {
JobParamsSchema,
jobRunDashboardPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -114,9 +119,9 @@ export const action: ActionFunction = async ({ request, params }) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "test",
},
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Test" />
),
};
const startingJson = "{\n\n}";
@@ -1,7 +1,11 @@
import { Outlet, useLocation } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { Fragment } from "react";
import { typedjson } from "remix-typedjson";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { JobsMenu } from "~/components/navigation/JobsMenu";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { LinkButton } from "~/components/primitives/Buttons";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import {
@@ -17,7 +21,7 @@ import {
import { Paragraph } from "~/components/primitives/Paragraph";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { projectMatchId, useProject } from "~/hooks/useProject";
import { useOptionalRun } from "~/hooks/useRun";
import { findJobByParams } from "~/models/job.server";
import { requireUserId } from "~/services/session.server";
@@ -28,6 +32,7 @@ import {
jobSettingsPath,
jobTestPath,
jobTriggerPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -58,8 +63,18 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "job",
breadcrumb: (_match, matches) => {
const projectMatch = matches.find((m) => m.id === projectMatchId);
return (
<Fragment>
<BreadcrumbLink
to={trimTrailingSlash(projectMatch?.pathname ?? "")}
title="Jobs"
/>
<BreadcrumbIcon />
<JobsMenu matches={matches} />
</Fragment>
);
},
};
@@ -1,19 +1,10 @@
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Header2 } from "~/components/primitives/Headers";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { LabelValueStack } from "~/components/primitives/LabelValueStack";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import {
PageDescription,
PageHeader,
PageTabs,
PageTitle,
PageTitleRow,
} from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
@@ -35,7 +26,7 @@ import { Handle } from "~/utils/handle";
import {
ProjectParamSchema,
triggerSourcePath,
projectTriggersPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -53,11 +44,12 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
link: {
title: "External",
},
},
breadcrumb: (match) => (
<BreadcrumbLink
to={trimTrailingSlash(match.pathname)}
title="External Triggers"
/>
),
};
export default function Integrations() {
@@ -1,5 +1,6 @@
import { Outlet } from "@remix-run/react";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import {
PageDescription,
PageHeader,
@@ -10,12 +11,12 @@ import {
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import { projectTriggersPath } from "~/utils/pathBuilder";
import { projectTriggersPath, trimTrailingSlash } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: {
slug: "triggers",
},
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Triggers" />
),
};
export default function Integrations() {
@@ -1,10 +1,15 @@
import { Response } from "@remix-run/node";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { Fragment } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { JobsMenu } from "~/components/navigation/JobsMenu";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import {
PageHeader,
PageInfoGroup,
@@ -16,14 +21,17 @@ import {
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunsTable } from "~/components/runs/RunsTable";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { projectMatchId, useProject } from "~/hooks/useProject";
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
import { TriggerSourcePresenter } from "~/presenters/TriggerSourcePresenter.server";
import { requireUser } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import {
TriggerSourceParamSchema,
parentPath,
projectTriggersPath,
triggerSourceRunsPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -50,8 +58,31 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "trigger",
breadcrumb: (match, matches) => {
const data = useTypedMatchData<typeof loader>(match);
if (!data) return null;
const org = useOrganization(matches);
const project = useProject(matches);
return (
<Fragment>
<BreadcrumbLink
to={projectTriggersPath(org, project)}
title="Triggers"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={projectTriggersPath(org, project)}
title="External Triggers"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={trimTrailingSlash(match.pathname)}
title={`${data.trigger.integration.title}: ${data.trigger.integration.slug}`}
/>
</Fragment>
);
},
};
@@ -68,7 +99,7 @@ export default function Integrations() {
title={`${trigger.integration.title}: ${trigger.integration.slug}`}
backButton={{
to: projectTriggersPath(organization, project),
text: "Triggers",
text: "External Triggers",
}}
/>
</PageTitleRow>
@@ -80,9 +111,13 @@ export default function Integrations() {
value={trigger.integration.slug}
/>
<PageInfoProperty
icon={trigger.active ? "active" : "inactive"}
label="Active"
value={trigger.active ? "Yes" : "No"}
label={trigger.active ? "Active" : "Inactive"}
value={
<NamedIcon
name={trigger.active ? "active" : "inactive"}
className="h-4 w-4"
/>
}
/>
<PageInfoProperty
label="Environment"
@@ -1,18 +1,25 @@
import { useRevalidator } from "@remix-run/react";
import { LoaderArgs } from "@remix-run/server-runtime";
import { useEffect } from "react";
import { Fragment, useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { RunOverview } from "~/components/run/RunOverview";
import { prisma } from "~/db.server";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useTypedMatchData } from "~/hooks/useTypedMatchData";
import { RunPresenter } from "~/presenters/RunPresenter.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import {
TriggerSourceRunParamsSchema,
projectTriggersPath,
triggerSourcePath,
triggerSourceRunPath,
triggerSourceRunStreamingPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -25,7 +32,25 @@ export const loader = async ({ request, params }: LoaderArgs) => {
id: runParam,
});
if (!run) {
const trigger = await prisma.triggerSource.findUnique({
select: {
id: true,
integration: {
select: {
id: true,
title: true,
slug: true,
definitionId: true,
setupStatus: true,
},
},
},
where: {
id: triggerParam,
},
});
if (!run || !trigger) {
throw new Response(null, {
status: 404,
});
@@ -33,12 +58,46 @@ export const loader = async ({ request, params }: LoaderArgs) => {
return typedjson({
run,
triggerParam,
trigger,
});
};
export const handle: Handle = {
breadcrumb: (match, matches) => {
const data = useTypedMatchData<typeof loader>(match);
if (!data) return null;
const org = useOrganization(matches);
const project = useProject(matches);
return (
<Fragment>
<BreadcrumbLink
to={projectTriggersPath(org, project)}
title="Triggers"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={projectTriggersPath(org, project)}
title="External Triggers"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={triggerSourcePath(org, project, { id: data.trigger.id })}
title={`${data.trigger.integration.title}: ${data.trigger.integration.slug}`}
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={trimTrailingSlash(match.pathname)}
title={`Run #${data.run.number}`}
/>
</Fragment>
);
},
};
export default function Page() {
const { run, triggerParam } = useTypedLoaderData<typeof loader>();
const { run, trigger } = useTypedLoaderData<typeof loader>();
const organization = useOrganization();
const project = useProject();
@@ -47,7 +106,7 @@ export default function Page() {
triggerSourceRunStreamingPath(
organization,
project,
{ id: triggerParam },
{ id: trigger.id },
run
),
{
@@ -67,11 +126,11 @@ export default function Page() {
trigger={{ icon: "register-source", title: "Register external source" }}
showRerun={false}
paths={{
back: triggerSourcePath(organization, project, { id: triggerParam }),
back: triggerSourcePath(organization, project, { id: trigger.id }),
run: triggerSourceRunPath(
organization,
project,
{ id: triggerParam },
{ id: trigger.id },
run
),
}}
@@ -7,6 +7,7 @@ import {
ProjectSideMenu,
SideMenuContainer,
} from "~/components/navigation/ProjectSideMenu";
import { ProjectsMenu } from "~/components/navigation/ProjectsMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
@@ -55,9 +56,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
export const handle: Handle = {
breadcrumb: {
slug: "projects",
},
breadcrumb: (_match, matches) => <ProjectsMenu matches={matches} />,
};
export default function Project() {
+2 -2
View File
@@ -1,5 +1,5 @@
import { Breadcrumb } from "~/components/navigation/Breadcrumb";
import { BreadcrumbItem } from "~/components/navigation/Breadcrumb";
export type Handle = {
breadcrumb?: Breadcrumb;
breadcrumb?: BreadcrumbItem;
};
+10
View File
@@ -48,6 +48,16 @@ export const TriggerSourceRunTaskParamsSchema =
taskParam: z.string(),
});
export function trimTrailingSlash(path: string) {
return path.replace(/\/$/, "");
}
export function parentPath(path: string) {
const trimmedTrailingSlash = trimTrailingSlash(path);
const lastSlashIndex = trimmedTrailingSlash.lastIndexOf("/");
return trimmedTrailingSlash.substring(0, lastSlashIndex);
}
export function organizationsPath() {
return `/`;
}