Skip to content

Commit 8a4930f

Browse files
committed
feat(cloud): runners (#3066)
### TL;DR Added Railway integration with automatic token passing and created a runner configuration editor. ### What changed? - Updated the Railway integration to automatically pass the Rivet token and engine URL to Railway when deploying - Added a new `runnerConfigQueryOptions` function to fetch runner configuration data - Created a new edit runner config dialog and form components to allow editing runner configurations - Fixed the `runnerNamesQueryOptions` function to use the namespace from context instead of requiring it as a parameter - Added a dropdown menu for selecting providers in the connect page ### How to test? 1. Navigate to the namespace connect page 2. Click on the Railway option in the providers dropdown 3. Verify that the Railway deployment link includes the Rivet token and engine URL 4. Test the runner configuration editor by selecting a runner and editing its configuration ### Why make this change? This change improves the Railway integration experience by automatically passing necessary credentials, eliminating manual configuration steps. It also adds the ability to edit runner configurations directly from the UI, giving users more control over their runner settings without having to use the API directly.
1 parent f81012a commit 8a4930f

File tree

5 files changed

+313
-11
lines changed

5 files changed

+313
-11
lines changed

frontend/src/app/data-providers/engine-data-provider.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,14 +404,14 @@ export const createNamespaceContext = ({
404404
},
405405
});
406406
},
407-
runnerNamesQueryOptions(opts: { namespace: string }) {
407+
runnerNamesQueryOptions() {
408408
return infiniteQueryOptions({
409-
queryKey: [opts.namespace, "runner", "names"],
409+
queryKey: [{ namespace }, "runner", "names"],
410410
initialPageParam: undefined as string | undefined,
411411
queryFn: async ({ signal: abortSignal, pageParam }) => {
412412
const data = await client.runners.listNames(
413413
{
414-
namespace: opts.namespace,
414+
namespace,
415415
cursor: pageParam ?? undefined,
416416
limit: RECORDS_PER_PAGE,
417417
},
@@ -552,6 +552,33 @@ export const createNamespaceContext = ({
552552
},
553553
});
554554
},
555+
runnerConfigQueryOptions(runnerName: string) {
556+
return queryOptions({
557+
queryKey: [{ namespace }, "runners", "config", runnerName],
558+
enabled: !!runnerName,
559+
queryFn: async ({ signal: abortSignal }) => {
560+
const response = await client.runnerConfigs.list(
561+
{
562+
namespace,
563+
runnerNames: runnerName,
564+
},
565+
{ abortSignal },
566+
);
567+
568+
const config = response.runnerConfigs[runnerName];
569+
570+
if (!config) {
571+
throw new Error("Runner config not found");
572+
}
573+
574+
return config;
575+
},
576+
retry: shouldRetryAllExpect403,
577+
meta: {
578+
mightRequireAuth,
579+
},
580+
});
581+
},
555582
connectRunnerTokenQueryOptions() {
556583
return queryOptions({
557584
staleTime: 1000,

frontend/src/app/dialogs/connect-railway-frame.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "@/components";
1212
import { useEngineCompatDataProvider } from "@/components/actors";
1313
import { defineStepper } from "@/components/ui/stepper";
14-
import { engineEnv } from "@/lib/env";
14+
import { cloudEnv, engineEnv } from "@/lib/env";
1515

1616
const { Stepper } = defineStepper(
1717
{
@@ -58,6 +58,10 @@ export default function ConnectRailwayFrameContent({
5858
}
5959

6060
function FormStepper({ onClose }: { onClose?: () => void }) {
61+
const dataProvider = useEngineCompatDataProvider();
62+
63+
const { data } = useQuery(dataProvider.connectRunnerTokenQueryOptions());
64+
6165
return (
6266
<Stepper.Provider variant="vertical">
6367
{({ methods }) => (
@@ -86,7 +90,14 @@ function FormStepper({ onClose }: { onClose?: () => void }) {
8690
quickly.
8791
</p>
8892
<a
89-
href="https://railway.com/deploy/rivet?referralCode=RC7bza&utm_medium=integration&utm_source=template&utm_campaign=generic"
93+
href={`https://railway.com/new/template/rivet-cloud-starter?referralCode=RC7bza&utm_medium=integration&utm_source=template&utm_campaign=generic&RIVET_TOKEN=${data}&RIVET_ENGINE=${
94+
__APP_TYPE__ ===
95+
"engine"
96+
? engineEnv()
97+
.VITE_APP_API_URL
98+
: cloudEnv()
99+
.VITE_APP_CLOUD_ENGINE_URL
100+
}`}
90101
target="_blank"
91102
rel="noreferrer"
92103
className="inline-block h-10"
@@ -156,7 +167,6 @@ function EnvVariablesStep() {
156167
const { data, isLoading } = useQuery(
157168
dataProvider.connectRunnerTokenQueryOptions(),
158169
);
159-
160170
return (
161171
<>
162172
<p>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { faQuestionCircle, faRailway, Icon } from "@rivet-gg/icons";
2+
import { useSuspenseQuery } from "@tanstack/react-query";
3+
import * as EditRunnerConfigForm from "@/app/forms/edit-runner-config-form";
4+
import { HelpDropdown } from "@/app/help-dropdown";
5+
import { Button, type DialogContentProps, Frame } from "@/components";
6+
import { useEngineCompatDataProvider } from "@/components/actors";
7+
8+
interface EditRunnerConfigFrameContentProps extends DialogContentProps {
9+
name: string;
10+
}
11+
12+
export default function EditRunnerConfigFrameContent({
13+
name,
14+
onClose,
15+
}: EditRunnerConfigFrameContentProps) {
16+
const { data } = useSuspenseQuery({
17+
...useEngineCompatDataProvider().runnerConfigQueryOptions(name),
18+
refetchInterval: 5000,
19+
});
20+
21+
return (
22+
<EditRunnerConfigForm.Form
23+
onSubmit={async () => {
24+
onClose?.();
25+
}}
26+
defaultValues={{
27+
url: data.serverless.url,
28+
maxRunners: data.serverless.maxRunners,
29+
minRunners: data.serverless.minRunners,
30+
requestLifespan: data.serverless.requestLifespan,
31+
runnersMargin: data.serverless.runnersMargin,
32+
slotsPerRunner: data.serverless.slotsPerRunner,
33+
}}
34+
>
35+
<Frame.Header>
36+
<Frame.Title className="justify-between flex items-center">
37+
<div>
38+
Add <Icon icon={faRailway} className="ml-0.5" /> Railway
39+
</div>
40+
<HelpDropdown>
41+
<Button variant="ghost" size="icon">
42+
<Icon icon={faQuestionCircle} />
43+
</Button>
44+
</HelpDropdown>
45+
</Frame.Title>
46+
</Frame.Header>
47+
<Frame.Content>
48+
<EditRunnerConfigForm.Url />
49+
<div className="grid grid-cols-2">
50+
<EditRunnerConfigForm.MinRunners />
51+
<EditRunnerConfigForm.MaxRunners />
52+
<EditRunnerConfigForm.RunnersMargin />
53+
</div>
54+
<div className="grid grid-cols-2">
55+
<EditRunnerConfigForm.RequestLifespan />
56+
<EditRunnerConfigForm.RunnersMargin />
57+
</div>
58+
<EditRunnerConfigForm.SlotsPerRunner />
59+
<div className="flex justify-end mt-4">
60+
<EditRunnerConfigForm.Submit>
61+
Save
62+
</EditRunnerConfigForm.Submit>
63+
</div>
64+
</Frame.Content>
65+
</EditRunnerConfigForm.Form>
66+
);
67+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { type UseFormReturn, useFormContext } from "react-hook-form";
2+
import z from "zod";
3+
import {
4+
createSchemaForm,
5+
FormControl,
6+
FormField,
7+
FormItem,
8+
FormLabel,
9+
FormMessage,
10+
Input,
11+
} from "@/components";
12+
13+
export const formSchema = z.object({
14+
url: z.string().url(),
15+
maxRunners: z.number().positive(),
16+
minRunners: z.number().positive(),
17+
requestLifespan: z.number().positive(),
18+
runnersMargin: z.number().positive(),
19+
slotsPerRunner: z.number().positive(),
20+
});
21+
22+
export type FormValues = z.infer<typeof formSchema>;
23+
export type SubmitHandler = (
24+
values: FormValues,
25+
form: UseFormReturn<FormValues>,
26+
) => Promise<void>;
27+
28+
const { Form, Submit, SetValue } = createSchemaForm(formSchema);
29+
export { Form, Submit, SetValue };
30+
31+
export const Url = ({ className }: { className?: string }) => {
32+
const { control } = useFormContext<FormValues>();
33+
return (
34+
<FormField
35+
control={control}
36+
name="url"
37+
render={({ field }) => (
38+
<FormItem className={className}>
39+
<FormLabel className="col-span-1">Url</FormLabel>
40+
<FormControl className="row-start-2">
41+
<Input
42+
placeholder="https://your-rivet-runner"
43+
maxLength={25}
44+
{...field}
45+
/>
46+
</FormControl>
47+
<FormMessage className="col-span-1" />
48+
</FormItem>
49+
)}
50+
/>
51+
);
52+
};
53+
54+
export const MinRunners = ({ className }: { className?: string }) => {
55+
const { control } = useFormContext<FormValues>();
56+
return (
57+
<FormField
58+
control={control}
59+
name="url"
60+
render={({ field }) => (
61+
<FormItem className={className}>
62+
<FormLabel className="col-span-1">Min Runners</FormLabel>
63+
<FormControl className="row-start-2">
64+
<Input type="number" {...field} />
65+
</FormControl>
66+
<FormMessage className="col-span-1" />
67+
</FormItem>
68+
)}
69+
/>
70+
);
71+
};
72+
73+
export const MaxRunners = ({ className }: { className?: string }) => {
74+
const { control } = useFormContext<FormValues>();
75+
return (
76+
<FormField
77+
control={control}
78+
name="maxRunners"
79+
render={({ field }) => (
80+
<FormItem className={className}>
81+
<FormLabel className="col-span-1">Max Runners</FormLabel>
82+
<FormControl className="row-start-2">
83+
<Input type="number" {...field} />
84+
</FormControl>
85+
<FormMessage className="col-span-1" />
86+
</FormItem>
87+
)}
88+
/>
89+
);
90+
};
91+
92+
export const RequestLifespan = ({ className }: { className?: string }) => {
93+
const { control } = useFormContext<FormValues>();
94+
return (
95+
<FormField
96+
control={control}
97+
name="requestLifespan"
98+
render={({ field }) => (
99+
<FormItem className={className}>
100+
<FormLabel className="col-span-1">
101+
Request Lifespan
102+
</FormLabel>
103+
<FormControl className="row-start-2">
104+
<Input type="number" {...field} />
105+
</FormControl>
106+
<FormMessage className="col-span-1" />
107+
</FormItem>
108+
)}
109+
/>
110+
);
111+
};
112+
113+
export const RunnersMargin = ({ className }: { className?: string }) => {
114+
const { control } = useFormContext<FormValues>();
115+
return (
116+
<FormField
117+
control={control}
118+
name="runnersMargin"
119+
render={({ field }) => (
120+
<FormItem className={className}>
121+
<FormLabel className="col-span-1">Runners Margin</FormLabel>
122+
<FormControl className="row-start-2">
123+
<Input type="number" {...field} />
124+
</FormControl>
125+
<FormMessage className="col-span-1" />
126+
</FormItem>
127+
)}
128+
/>
129+
);
130+
};
131+
132+
export const SlotsPerRunner = ({ className }: { className?: string }) => {
133+
const { control } = useFormContext<FormValues>();
134+
return (
135+
<FormField
136+
control={control}
137+
name="slotsPerRunner"
138+
render={({ field }) => (
139+
<FormItem className={className}>
140+
<FormLabel className="col-span-1">
141+
Slots Per Runner
142+
</FormLabel>
143+
<FormControl className="row-start-2">
144+
<Input type="number" {...field} />
145+
</FormControl>
146+
<FormMessage className="col-span-1" />
147+
</FormItem>
148+
)}
149+
/>
150+
);
151+
};

0 commit comments

Comments
 (0)