Skip to content

Commit 7d42682

Browse files
committed
feat(hub): functions
1 parent 7e9c119 commit 7d42682

File tree

30 files changed

+2448
-126
lines changed

30 files changed

+2448
-126
lines changed

frontend/apps/hub/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@tanstack/react-query-devtools": "^5.58.0",
2929
"@tanstack/react-query-persist-client": "^5.56.2",
3030
"@tanstack/react-router": "^1.114.25",
31-
"@tanstack/react-table": "^8.20.6",
31+
"@tanstack/react-table": "^8.21.2",
3232
"@tanstack/router-devtools": "^1.114.25",
3333
"@tanstack/router-plugin": "^1.114.25",
3434
"@tanstack/zod-adapter": "^1.114.25",

frontend/apps/hub/src/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ declare module "@tanstack/react-router" {
2727
router: typeof router;
2828
}
2929
interface StaticDataRouteOption {
30-
layout?: "full" | "compact" | "onboarding" | "actors";
30+
layout?: "full" | "compact" | "onboarding" | "actors" | "v2";
3131
}
3232
}
3333

frontend/apps/hub/src/components/header/header-sub-nav.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import { useAuth } from "@/domains/auth/contexts/auth";
22
import { Skeleton, cn } from "@rivet-gg/components";
33
import { ErrorBoundary } from "@sentry/react";
4-
import { useMatchRoute } from "@tanstack/react-router";
4+
import {
5+
useMatches,
6+
useMatchRoute,
7+
useRouterState,
8+
} from "@tanstack/react-router";
59
import { Suspense, useContext } from "react";
610
import { MobileBreadcrumbsContext } from "../breadcrumbs/mobile-breadcrumbs";
711
import { HeaderEnvironmentLinks } from "./links/header-environment-links";
812
import { HeaderGroupLinks } from "./links/header-group-links";
913
import { HeaderProjectLinks } from "./links/header-project-links";
1014

1115
function Content() {
16+
const allMatches = useMatches();
17+
18+
const v2Envs = allMatches.find((match) => match.id.includes("/_v2/"));
19+
1220
const matchRoute = useMatchRoute();
1321

1422
const { profile } = useAuth();
1523

16-
if (!profile?.identity.isRegistered) {
24+
if (!profile?.identity.isRegistered || v2Envs) {
1725
return null;
1826
}
1927

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as EditRouteForm from "@/domains/project/forms/route-edit-form";
2+
import type { DialogContentProps } from "@/hooks/use-dialog";
3+
import {
4+
Button,
5+
DialogFooter,
6+
DialogHeader,
7+
DialogTitle,
8+
Label,
9+
} from "@rivet-gg/components";
10+
import { useState } from "react";
11+
import { useCreateRouteMutation } from "../../queries";
12+
import { convertStringToId } from "@/lib/utils";
13+
14+
interface ContentProps extends DialogContentProps {
15+
projectNameId: string;
16+
environmentNameId: string;
17+
}
18+
19+
export default function EditRouteDialogContent(props: ContentProps) {
20+
return <Content {...props} />;
21+
}
22+
23+
function Content({ projectNameId, environmentNameId, onClose }: ContentProps) {
24+
const { mutateAsync } = useCreateRouteMutation();
25+
26+
const [tagKeys, setTagKeys] = useState<{ label: string; value: string }[]>(
27+
() => [],
28+
);
29+
30+
const [tagValues, setTagValues] = useState<
31+
{ label: string; value: string }[]
32+
>(() => []);
33+
34+
return (
35+
<EditRouteForm.Form
36+
defaultValues={{
37+
routeName: "",
38+
slug: "",
39+
tags: [],
40+
hostname: "",
41+
path: "",
42+
}}
43+
onSubmit={async (values) => {
44+
const selectorTags = Object.fromEntries(
45+
values.tags.map(({ key, value }) => [key, value]),
46+
);
47+
48+
await mutateAsync({
49+
projectNameId,
50+
environmentNameId,
51+
hostname: values.hostname,
52+
nameId: values.slug || convertStringToId(values.routeName),
53+
// remove the * from the end of the path
54+
path: values.path.endsWith("/*")
55+
? values.path.slice(0, -2)
56+
: values.path,
57+
selectorTags,
58+
routeSubpaths: values.path.endsWith("/*"),
59+
});
60+
onClose?.();
61+
}}
62+
>
63+
<DialogHeader>
64+
<DialogTitle>Add Route</DialogTitle>
65+
</DialogHeader>
66+
67+
<div className="flex gap-2">
68+
<EditRouteForm.Name className="flex-1" />
69+
<EditRouteForm.Slug className="flex-1" />
70+
</div>
71+
72+
<div className="flex gap-2">
73+
<EditRouteForm.Hostname />
74+
<EditRouteForm.Path />
75+
</div>
76+
77+
<Label>Selectors</Label>
78+
79+
<EditRouteForm.Tags
80+
keys={tagKeys}
81+
values={tagValues}
82+
onCreateKeyOption={(option) =>
83+
setTagKeys((opts) => [
84+
...opts,
85+
{ label: option, value: option },
86+
])
87+
}
88+
onCreateValueOption={(option) =>
89+
setTagValues((opts) => [
90+
...opts,
91+
{ label: option, value: option },
92+
])
93+
}
94+
/>
95+
96+
<DialogFooter>
97+
<EditRouteForm.Submit>Save</EditRouteForm.Submit>
98+
<Button type="button" variant="secondary" onClick={onClose}>
99+
Close
100+
</Button>
101+
</DialogFooter>
102+
</EditRouteForm.Form>
103+
);
104+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as EditRouteForm from "@/domains/project/forms/route-edit-form";
2+
import type { DialogContentProps } from "@/hooks/use-dialog";
3+
import {
4+
Button,
5+
DialogFooter,
6+
DialogHeader,
7+
DialogTitle,
8+
Label,
9+
} from "@rivet-gg/components";
10+
import { useSuspenseQuery } from "@tanstack/react-query";
11+
import { useState } from "react";
12+
import { routeQueryOptions, usePatchRouteMutation } from "../../queries";
13+
import type { Rivet } from "@rivet-gg/api";
14+
15+
interface OptionalContentProps extends DialogContentProps {
16+
projectNameId: string;
17+
environmentNameId: string;
18+
routeNameId?: string;
19+
}
20+
21+
export default function EditRouteDialogContent(props: OptionalContentProps) {
22+
if (!props.routeNameId) {
23+
return null;
24+
}
25+
26+
return <GuardedContent {...props} routeNameId={props.routeNameId} />;
27+
}
28+
29+
function GuardedContent({
30+
routeNameId,
31+
projectNameId,
32+
environmentNameId,
33+
onClose,
34+
}: Omit<OptionalContentProps, "routeNameId"> & { routeNameId: string }) {
35+
const { data } = useSuspenseQuery(
36+
routeQueryOptions({
37+
routeNameId,
38+
projectNameId,
39+
environmentNameId,
40+
}),
41+
);
42+
43+
if (!data) {
44+
return null;
45+
}
46+
47+
return (
48+
<Content
49+
{...data}
50+
projectNameId={projectNameId}
51+
environmentNameId={environmentNameId}
52+
onClose={onClose}
53+
/>
54+
);
55+
}
56+
57+
interface ContentProps extends DialogContentProps, Rivet.routes.Route {
58+
projectNameId: string;
59+
environmentNameId: string;
60+
}
61+
62+
function Content({
63+
projectNameId,
64+
environmentNameId,
65+
nameId,
66+
hostname,
67+
routeSubpaths,
68+
path,
69+
selectorTags,
70+
onClose,
71+
}: ContentProps) {
72+
const { mutateAsync } = usePatchRouteMutation();
73+
74+
const tags = Object.entries(selectorTags || {}).map(([key, value]) => ({
75+
key,
76+
value,
77+
}));
78+
79+
const [tagKeys, setTagKeys] = useState<{ label: string; value: string }[]>(
80+
() => tags.map(({ key }) => ({ label: key, value: key })),
81+
);
82+
83+
const [tagValues, setTagValues] = useState<
84+
{ label: string; value: string }[]
85+
>(() => tags.map(({ value }) => ({ label: value, value })));
86+
87+
return (
88+
<EditRouteForm.Form
89+
defaultValues={{
90+
routeName: nameId,
91+
slug: nameId,
92+
tags: Object.entries(selectorTags || {}).map(
93+
([key, value]) => ({
94+
key,
95+
value,
96+
}),
97+
),
98+
hostname,
99+
path: routeSubpaths ? `${path}/*` : path,
100+
}}
101+
onSubmit={async (values) => {
102+
const selectorTags = Object.fromEntries(
103+
values.tags.map(({ key, value }) => [key, value]),
104+
);
105+
106+
await mutateAsync({
107+
projectNameId,
108+
environmentNameId,
109+
routeNameId: nameId,
110+
hostname: values.hostname,
111+
path: values.path.endsWith("/*")
112+
? values.path.slice(0, -2)
113+
: values.path,
114+
selectorTags,
115+
routeSubpaths: values.path.endsWith("/*"),
116+
});
117+
onClose?.();
118+
}}
119+
>
120+
<DialogHeader>
121+
<DialogTitle>Edit Route</DialogTitle>
122+
</DialogHeader>
123+
124+
<div className="flex gap-2">
125+
<EditRouteForm.Hostname />
126+
<EditRouteForm.Path />
127+
</div>
128+
129+
<Label>Selectors</Label>
130+
<EditRouteForm.Tags
131+
keys={tagKeys}
132+
values={tagValues}
133+
onCreateKeyOption={(option) =>
134+
setTagKeys((opts) => [
135+
...opts,
136+
{ label: option, value: option },
137+
])
138+
}
139+
onCreateValueOption={(option) =>
140+
setTagValues((opts) => [
141+
...opts,
142+
{ label: option, value: option },
143+
])
144+
}
145+
/>
146+
147+
<DialogFooter>
148+
<EditRouteForm.Submit>Save</EditRouteForm.Submit>
149+
<Button type="button" variant="secondary" onClick={onClose}>
150+
Close
151+
</Button>
152+
</DialogFooter>
153+
</EditRouteForm.Form>
154+
);
155+
}

0 commit comments

Comments
 (0)