Hooks can use passed in matches. Breadcrumb updated to be in the route
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
+9
-4
@@ -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() {
|
||||
|
||||
+4
-3
@@ -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() {
|
||||
|
||||
+4
-3
@@ -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() {
|
||||
|
||||
+6
-3
@@ -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() {
|
||||
|
||||
+11
-4
@@ -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() {
|
||||
|
||||
+8
-4
@@ -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() {
|
||||
|
||||
+10
-2
@@ -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"}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+5
-3
@@ -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() {
|
||||
|
||||
+27
-5
@@ -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
-4
@@ -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}";
|
||||
|
||||
+18
-3
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+8
-16
@@ -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() {
|
||||
|
||||
+5
-4
@@ -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() {
|
||||
|
||||
+42
-7
@@ -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"
|
||||
|
||||
+66
-7
@@ -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() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Breadcrumb } from "~/components/navigation/Breadcrumb";
|
||||
import { BreadcrumbItem } from "~/components/navigation/Breadcrumb";
|
||||
|
||||
export type Handle = {
|
||||
breadcrumb?: Breadcrumb;
|
||||
breadcrumb?: BreadcrumbItem;
|
||||
};
|
||||
|
||||
@@ -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 `/`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user