Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/packages/icons/manifest.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/packages/icons/scripts/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ if (process.env.FONTAWESOME_PACKAGE_TOKEN) {
private: true,
sideEffects: false,
dependencies: {
"@awesome.me/kit-63db24046b": "^1.0.18",
"@awesome.me/kit-63db24046b": "1.0.26",
"@fortawesome/pro-regular-svg-icons": "6.6.0",
"@fortawesome/pro-solid-svg-icons": "6.6.0",
},
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/app/data-providers/engine-data-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,15 +388,20 @@ export const createNamespaceContext = ({
},
};
},
runnerHealthCheckQueryOptions(opts: { runnerUrl: string }) {
runnerHealthCheckQueryOptions(opts: {
runnerUrl: string;
headers: Record<string, string>;
}) {
return queryOptions({
queryKey: ["runner", "healthcheck", opts.runnerUrl],
queryKey: ["runner", "healthcheck", opts],
enabled: !!opts.runnerUrl,
queryFn: async ({ signal: abortSignal }) => {
const res =
await client.runnerConfigs.serverlessHealthCheck(
{
url: opts.runnerUrl,
headers: opts.headers,
namespace,
},
{ abortSignal },
);
Expand Down
59 changes: 2 additions & 57 deletions frontend/src/app/dialogs/connect-vercel-frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,59 +19,9 @@ import {
Frame,
} from "@/components";
import { type Region, useEngineCompatDataProvider } from "@/components/actors";
import { defineStepper } from "@/components/ui/stepper";
import { type JoinStepSchemas, StepperForm } from "../forms/stepper-form";

const stepper = defineStepper(
{
id: "step-1",
title: "Configure",
assist: false,
next: "Next",
schema: z.object({
plan: z.string().min(1, "Please select a Vercel plan"),
runnerName: z.string().min(1, "Runner name is required"),
datacenters: z
.record(z.boolean())
.refine(
(data) => Object.values(data).some(Boolean),
"At least one datacenter must be selected",
),
headers: z.array(z.tuple([z.string(), z.string()])).default([]),
slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"),
maxRunners: z.coerce.number().min(1, "Must be at least 1"),
runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"),
}),
},
{
id: "step-2",
title: "Edit vercel.json",
assist: false,
next: "Next",
schema: z.object({}),
},
{
id: "step-3",
title: "Deploy to Vercel",
assist: false,
next: "Next",
schema: z.object({
endpoint: z
.string()
.nonempty("Endpoint is required")
.url("Please enter a valid URL"),
}),
},
{
id: "step-4",
title: "Confirm Connection",
assist: true,
next: "Add",
schema: z.object({
success: z.boolean().refine((val) => val, "Connection failed"),
}),
},
);
const {stepper} = ConnectVercelForm;

type FormValues = z.infer<JoinStepSchemas<typeof stepper.steps>>;

Expand Down Expand Up @@ -141,7 +91,6 @@ function FormStepper({
"step-1": () => <Step1 />,
"step-2": () => <Step2 />,
"step-3": () => <Step3 />,
"step-4": () => <Step4 />,
}}
onSubmit={async ({ values }) => {
const selectedDatacenters = Object.entries(values.datacenters)
Expand Down Expand Up @@ -234,12 +183,8 @@ function Step3() {
</p>
<div className="mt-2">
<ConnectVercelForm.Endpoint />
<ConnectVercelForm.ConnectionCheck />
</div>
</>
);
}

function Step4() {
const endpoint = useWatch<FormValues>({ name: "endpoint" });
return <ConnectVercelForm.ConnectionCheck endpoint={endpoint || ""} />;
}
84 changes: 72 additions & 12 deletions frontend/src/app/forms/connect-vercel-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect } from "react";
import { useController, useFieldArray, useFormContext } from "react-hook-form";
import { useController, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
import { useDebounceValue } from "usehooks-ts";
import z from "zod";
import {
Expand All @@ -34,6 +34,54 @@ import {
} from "@/components";
import { useEngineCompatDataProvider } from "@/components/actors";
import { VisibilitySensor } from "@/components/visibility-sensor";
import { defineStepper } from "@/components/ui/stepper";
import { Rivet } from "@rivetkit/engine-api-full";

const endpointSchema = z
.string()
.nonempty("Endpoint is required")
.url("Please enter a valid URL")
.endsWith("/api/rivet", "Endpoint must end with /api/rivet");

export const stepper = defineStepper(
{
id: "step-1",
title: "Configure",
assist: false,
next: "Next",
schema: z.object({
plan: z.string().min(1, "Please select a Vercel plan"),
runnerName: z.string().min(1, "Runner name is required"),
datacenters: z
.record(z.boolean())
.refine(
(data) => Object.values(data).some(Boolean),
"At least one datacenter must be selected",
),
headers: z.array(z.tuple([z.string(), z.string()])).default([]),
slotsPerRunner: z.coerce.number().min(1, "Must be at least 1"),
maxRunners: z.coerce.number().min(1, "Must be at least 1"),
runnerMargin: z.coerce.number().min(0, "Must be 0 or greater"),
}),
},
{
id: "step-2",
title: "Edit vercel.json",
assist: false,
next: "Next",
schema: z.object({}),
},
{
id: "step-3",
title: "Deploy to Vercel",
assist: false,
next: "Done",
schema: z.object({
success: z.boolean().refine((val) => val, "Connection failed"),
endpoint: endpointSchema,
}),
},
);

export const Plan = ({ className }: { className?: string }) => {
const { control } = useFormContext();
Expand Down Expand Up @@ -346,18 +394,18 @@ export const PLAN_TO_MAX_DURATION: Record<string, number> = {
const code = ({ plan }: { plan: string }) =>
`{
"$schema": "https://openapi.vercel.sh/vercel.json",
"fluid": false, // [!code highlight]
"fluid": false, // [!code highlight]
"functions": {
"**": {
"maxDuration": ${PLAN_TO_MAX_DURATION[plan] || 60}, // [!code highlight]
"app/api/rivet/**": {
"maxDuration": ${PLAN_TO_MAX_DURATION[plan] || 60}, // [!code highlight]
},
},
}
}`;

export const Json = ({ plan }: { plan: string }) => {
return (
<div className="space-y-2 mt-2">
<CodeFrame language="json" title="vercel.json">
<CodeFrame language="json" title="vercel.json" code={() => code({plan}).replaceAll(" // [!code highlight]", "")}>
<CodePreview
className="w-full min-w-0"
language="json"
Expand Down Expand Up @@ -394,7 +442,7 @@ export const Endpoint = ({
<Input
type="url"
placeholder={
placeholder || "https://my-rivet-app.vercel.app"
placeholder || "https://my-rivet-app.vercel.app/api/rivet"
}
{...field}
/>
Expand All @@ -406,17 +454,23 @@ export const Endpoint = ({
);
};

export function ConnectionCheck({ endpoint }: { endpoint: string }) {
const { setValue, trigger } = useFormContext();
export function ConnectionCheck() {
const dataProvider = useEngineCompatDataProvider();
const enabled = !!endpoint && z.string().url().safeParse(endpoint).success;

const endpoint: string = useWatch({ name: "endpoint" });
const headers: [string, string][] = useWatch({ name: "headers" });

const enabled = !!endpoint && endpointSchema.safeParse(endpoint).success;

console.log(enabled);

const [debounced] = useDebounceValue(endpoint, 300);

const { isSuccess, data, isError, isRefetchError, isLoadingError } =
const { isSuccess, data, isError, isRefetchError, isLoadingError, error } =
useQuery({
...dataProvider.runnerHealthCheckQueryOptions({
runnerUrl: debounced,
headers: Object.fromEntries(headers.filter(([k, v]) => k && v).map(([k, v]) => [k, v])),
}),
enabled,
retry: 0,
Expand All @@ -442,7 +496,7 @@ export function ConnectionCheck({ endpoint }: { endpoint: string }) {
isError && "text-destructive-foreground",
)}
initial={{ height: 0, opacity: 0.5 }}
animate={{ height: "6rem", opacity: 1 }}
animate={{ height: "8rem", opacity: 1 }}
>
{isSuccess ? (
<>
Expand All @@ -463,6 +517,7 @@ export function ConnectionCheck({ endpoint }: { endpoint: string }) {
Health check failed, verify the endpoint is
correct.
</p>
{isRivetHealthCheckFailureResponse(error) ? <p className="font-mono-console">{JSON.stringify(error.failure.error)}</p> : null}
<p>
Endpoint{" "}
<a
Expand Down Expand Up @@ -491,3 +546,8 @@ export function ConnectionCheck({ endpoint }: { endpoint: string }) {
</AnimatePresence>
);
}


function isRivetHealthCheckFailureResponse(error: any): error is Rivet.RunnerConfigsServerlessHealthCheckResponseFailure {
return error && 'failure' in error;
}
9 changes: 8 additions & 1 deletion frontend/src/app/forms/stepper-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function Content<const Steps extends Step[]>({
const ref = useRef<z.infer<JoinStepSchemas<Steps>> | null>({});

const handleSubmit = (values: z.infer<JoinStepSchemas<Steps>>) => {
console.log("submitting");
ref.current = { ...ref.current, ...values };
if (stepper.isLast) {
return onSubmit?.({ values: ref.current, form, stepper });
Expand Down Expand Up @@ -122,7 +123,13 @@ function Content<const Steps extends Step[]>({
<Button
type="button"
variant="outline"
onClick={stepper.prev}
onClick={() => {
form.reset(undefined, {
keepErrors: false,
keepValues: true,
});
stepper.prev();
}}
disabled={stepper.isFirst}
>
Previous
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ interface CodeFrameProps {
title?: string;
language: keyof typeof languageNames;
isInGroup?: boolean;
code?: string;
code?: () => string | string;
children?: ReactElement;
}
export const CodeFrame = ({
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
import { Clerk } from "@clerk/clerk-js";
import { redirect } from "@tanstack/react-router";
import { cloudEnv } from "./env";

export const clerk =
__APP_TYPE__ === "cloud"
? new Clerk(cloudEnv().VITE_APP_CLERK_PUBLISHABLE_KEY)
: (null as unknown as Clerk);

export const redirectToOrganization = async (clerk: Clerk) => {
if (clerk.user) {
if (clerk.organization) {
throw redirect({
to: "/orgs/$organization",
params: {
organization: clerk.organization.id,
},
});
}
const { data: orgs } = await clerk.user.getOrganizationMemberships();

if (orgs.length > 0) {
throw redirect({
to: "/orgs/$organization",
params: { organization: orgs[0].organization.id },
});
}
throw redirect({
to: "/onboarding/choose-organization",
});
}

return false;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
faAws,
faGoogleCloud,
faHetzner,
faHetznerH,
faPlus,
faQuestionCircle,
faRailway,
Expand Down Expand Up @@ -146,7 +146,7 @@ export function RouteComponent() {
size="lg"
variant="outline"
className="min-w-48 h-auto min-h-28 text-xl"
startIcon={<Icon icon={faHetzner} />}
startIcon={<Icon icon={faHetznerH} />}
asChild
>
<RouterLink
Expand Down Expand Up @@ -307,7 +307,7 @@ function ProviderDropdown({ children }: { children: React.ReactNode }) {
Google Cloud Run
</DropdownMenuItem>
<DropdownMenuItem
indicator={<Icon icon={faHetzner} />}
indicator={<Icon icon={faHetznerH} />}
onSelect={() => {
navigate({
to: ".",
Expand Down
16 changes: 5 additions & 11 deletions frontend/src/routes/_context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CreateNamespacesFrameContent from "@/app/dialogs/create-namespace-frame";
import { InspectorRoot } from "@/app/inspector-root";
import { Logo } from "@/app/logo";
import { Card } from "@/components";
import { redirectToOrganization } from "@/lib/auth";

export const Route = createFileRoute("/_context/")({
component: () =>
Expand All @@ -13,18 +14,11 @@ export const Route = createFileRoute("/_context/")({
.with("inspector", () => <InspectorRoute />)
.exhaustive(),
beforeLoad: async ({ context, search }) => {
return match(context)
.with({ __type: "cloud" }, () => {
const { organization } = context.clerk ?? {};
if (!organization) {
throw redirect({
to: "/onboarding/choose-organization",
});
return await match(context)
.with({ __type: "cloud" }, async () => {
if (!(await redirectToOrganization(context.clerk))) {
throw redirect({ to: "/login" });
}
throw redirect({
to: "/orgs/$organization",
params: { organization: organization?.id },
});
})
.with({ __type: "engine" }, async (ctx) => {
try {
Expand Down
Loading
Loading