Skip to content

Commit 3c337f5

Browse files
committed
feat(hub): functions
1 parent b08ab09 commit 3c337f5

File tree

33 files changed

+3400
-161
lines changed

33 files changed

+3400
-161
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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
// remove the * from the end of the path
53+
path: values.path.endsWith("/*")
54+
? values.path.slice(0, -2)
55+
: values.path,
56+
stripPrefix: values.path.endsWith("/*"),
57+
routeSubpaths: [],
58+
target: {
59+
actors: {
60+
selectorTags,
61+
},
62+
},
63+
});
64+
onClose?.();
65+
}}
66+
>
67+
<DialogHeader>
68+
<DialogTitle>Add Route</DialogTitle>
69+
</DialogHeader>
70+
<div className="flex gap-2">
71+
<EditRouteForm.Hostname />
72+
<EditRouteForm.Path />
73+
</div>
74+
75+
<Label>Selectors</Label>
76+
77+
<EditRouteForm.Tags
78+
keys={tagKeys}
79+
values={tagValues}
80+
onCreateKeyOption={(option) =>
81+
setTagKeys((opts) => [
82+
...opts,
83+
{ label: option, value: option },
84+
])
85+
}
86+
onCreateValueOption={(option) =>
87+
setTagValues((opts) => [
88+
...opts,
89+
{ label: option, value: option },
90+
])
91+
}
92+
/>
93+
94+
<DialogFooter>
95+
<EditRouteForm.Submit>Save</EditRouteForm.Submit>
96+
<Button type="button" variant="secondary" onClick={onClose}>
97+
Close
98+
</Button>
99+
</DialogFooter>
100+
</EditRouteForm.Form>
101+
);
102+
}
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)