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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ tf/.terraform.lock.hcl
frontend/.env.*
frontend/dist/
frontend/node_modules/
frontend/.tanstack

# Site
site/.next/
Expand Down
1 change: 1 addition & 0 deletions frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ VITE_APP_SENTRY_PROJECT_ID="4506435887366144"
# This is a public-facing token, safe to commit to repo
VITE_APP_POSTHOG_API_KEY=phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo
VITE_APP_POSTHOG_HOST=https://ph.rivet.gg
VITE_APP_CLOUD_API_URL=https://cloud.rivet.gg/api

# Overridden in CI
SENTRY_AUTH_TOKEN=
Expand Down
10 changes: 9 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
"dev": "pnpm run '/^dev:.*/'",
"dev:inspector": "vite --config vite.inspector.config.ts",
"dev:engine": "vite --config vite.engine.config.ts",
"dev:cloud": "vite --config vite.cloud.config.ts",
"ts-check": "tsc --noEmit",
"build": "echo 'Please use build:engine or build:inspector' && exit 1",
"build:inspector": "vite build --mode=production --config vite.inspector.config.ts",
"build:engine": "vite build --mode=production --config vite.engine.config.ts",
"build:cloud": "vite build --mode=production --config vite.cloud.config.ts",
"preview:inspector": "vite preview --config vite.inspector.config.ts",
"preview:engine": "vite preview --config vite.engine.config.ts"
"preview:engine": "vite preview --config vite.engine.config.ts",
"preview:cloud": "vite preview --config vite.cloud.config.ts"
},
"dependencies": {
"@clerk/clerk-js": "^5.91.2",
"@clerk/clerk-react": "^5.46.1",
"@clerk/themes": "^2.4.17",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
Expand Down Expand Up @@ -49,6 +55,7 @@
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.0.3",
"@rivet-gg/cloud": "file:./vendor/rivet-cloud.tgz",
"@rivet-gg/icons": "file:./vendor/rivet-icons.tgz",
"rivetkit": "*",
"@rivetkit/engine-api-full": "workspace:*",
Expand Down Expand Up @@ -109,6 +116,7 @@
"tailwind-merge": "^2.2.2",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"ts-pattern": "^5.8.0",
"typescript": "^5.5.4",
"usehooks-ts": "^3.1.0",
"vite": "^5.2.0",
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/app/dialogs/create-project-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import * as CreateProjectForm from "@/app/forms/create-project-form";
import { DialogFooter, DialogHeader, DialogTitle, Flex } from "@/components";
import { convertStringToId } from "@/lib/utils";
import {
createProjectMutationOptions,
projectsQueryOptions,
} from "@/queries/manager-cloud";
import {
managerClient,
namespacesQueryOptions,
} from "@/queries/manager-engine";

export default function CreateProjectDialogContent() {
const queryClient = useQueryClient();
const navigate = useNavigate();

const { mutateAsync } = useMutation(
createProjectMutationOptions({
onSuccess: async (values) => {
await queryClient.invalidateQueries({
...projectsQueryOptions(),
});
Comment on lines +22 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The projectsQueryOptions() function requires an orgId parameter but is being called without arguments. This will cause a runtime error since the function signature expects { orgId: string }. Consider passing the organization ID from the route params or context:

await queryClient.invalidateQueries({
  ...projectsQueryOptions({ orgId: values.organizationId }),
});
Suggested change
await queryClient.invalidateQueries({
...projectsQueryOptions(),
});
await queryClient.invalidateQueries({
...projectsQueryOptions({ orgId: values.organizationId }),
});

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

navigate({
to: "/orgs/$organization/projects/$project",
params: {
organization: values.organizationId,
project: values.name,
},
});
},
}),
);

return (
<CreateProjectForm.Form
onSubmit={async (values) => {
await mutateAsync({
displayName: values.name,
nameId: values.slug || convertStringToId(values.name),
});
}}
defaultValues={{ name: "", slug: "" }}
>
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
</DialogHeader>
<Flex gap="4" direction="col">
<CreateProjectForm.Name />
<CreateProjectForm.Slug />
</Flex>
<DialogFooter>
<CreateProjectForm.Submit type="submit">
Create
</CreateProjectForm.Submit>
</DialogFooter>
</CreateProjectForm.Form>
);
}
90 changes: 90 additions & 0 deletions frontend/src/app/forms/create-project-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { type UseFormReturn, useFormContext } from "react-hook-form";
import z from "zod";
import {
createSchemaForm,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@/components";
import { convertStringToId } from "@/lib/utils";

export const formSchema = z.object({
name: z
.string()
.max(25)
.refine((value) => value.trim() !== "" && value.trim() === value, {
message: "Name cannot be empty or contain whitespaces",
}),
slug: z.string().max(25).optional(),
});

export type FormValues = z.infer<typeof formSchema>;
export type SubmitHandler = (
values: FormValues,
form: UseFormReturn<FormValues>,
) => Promise<void>;

const { Form, Submit, SetValue } = createSchemaForm(formSchema);
export { Form, Submit, SetValue };

export const Name = ({ className }: { className?: string }) => {
const { control } = useFormContext<FormValues>();
return (
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem className={className}>
<FormLabel className="col-span-1">Name</FormLabel>
<FormControl className="row-start-2">
<Input
placeholder="Enter a project name..."
maxLength={25}
{...field}
/>
</FormControl>
<FormMessage className="col-span-1" />
</FormItem>
)}
/>
);
};

export const Slug = ({ className }: { className?: string }) => {
const { control, watch } = useFormContext<FormValues>();

const name = watch("name");

return (
<FormField
control={control}
name="slug"
render={({ field }) => (
<FormItem className={className}>
<FormLabel className="col-span-2">Slug</FormLabel>
<FormControl className="row-start-2">
<Input
placeholder={
name
? convertStringToId(name)
: "Enter a slug..."
}
maxLength={25}
{...field}
onChange={(event) => {
const value = convertStringToId(
event.target.value,
);
field.onChange({ target: { value } });
}}
/>
</FormControl>
<FormMessage className="col-span-2" />
</FormItem>
)}
/>
);
};
105 changes: 90 additions & 15 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OrganizationSwitcher, useClerk } from "@clerk/clerk-react";
import {
faArrowUpRight,
faLink,
Expand All @@ -6,7 +7,12 @@ import {
Icon,
} from "@rivet-gg/icons";
import { useQuery } from "@tanstack/react-query";
import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router";
import {
Link,
useMatch,
useMatchRoute,
useNavigate,
} from "@tanstack/react-router";
import {
type ComponentProps,
createContext,
Expand All @@ -20,8 +26,10 @@ import {
useState,
} from "react";
import type { ImperativePanelGroupHandle } from "react-resizable-panels";
import { match } from "ts-pattern";
import {
Button,
type ButtonProps,
cn,
DocsSheet,
type ImperativePanelHandle,
Expand Down Expand Up @@ -146,13 +154,25 @@ const Sidebar = ({
/>
</Link>
<div className="flex flex-1 flex-col gap-4 px-2 min-h-0">
{__APP_TYPE__ === "inspector" ? (
<ConnectionStatus />
) : null}
{__APP_TYPE__ === "engine" ? <Breadcrumbs /> : null}
<ScrollArea>
<Subnav />
</ScrollArea>
{match(__APP_TYPE__)
.with("engine", () => (
<>
<ConnectionStatus />
<ScrollArea>
<Subnav />
</ScrollArea>
</>
))
.with("inspector", () => (
<>
<Breadcrumbs />
<ScrollArea>
<Subnav />
</ScrollArea>
</>
))
.with("cloud", () => <CloudSidebar />)
.exhaustive()}
</div>
<div>
<div className="border-t p-2 flex flex-col gap-[1px] text-sm">
Expand Down Expand Up @@ -241,7 +261,7 @@ const Footer = () => {

export { Root, Main, Header, Footer, VisibleInFull, Sidebar };

const Breadcrumbs = () => {
const Breadcrumbs = (): ReactNode => {
const matchRoute = useMatchRoute();
const nsMatch = matchRoute({
to: "/ns/$namespace",
Expand Down Expand Up @@ -341,26 +361,37 @@ const Subnav = () => {

function HeaderLink({ icon, children, className, ...props }: HeaderLinkProps) {
return (
<Button
<HeaderButton
asChild
variant="ghost"
{...props}
className={cn(
"text-muted-foreground px-2 aria-current-page:text-foreground relative h-auto py-1 justify-start",
className,
)}
startIcon={
icon ? (
<Icon className={cn("size-5 opacity-80")} icon={icon} />
) : undefined
}
>
<Link to={props.to}>{children}</Link>
</HeaderButton>
);
}

function HeaderButton({ children, className, ...props }: ButtonProps) {
return (
<Button
variant="ghost"
{...props}
className={cn(
"text-muted-foreground px-2 aria-current-page:text-foreground relative h-auto py-1 justify-start",
className,
)}
>
{children}
</Button>
);
}

function ConnectionStatus() {
function ConnectionStatus(): ReactNode {
const { endpoint, ...queries } = useManager();
const { setCredentials } = useInspectorCredentials();
const { isLoading, isError, isSuccess } = useQuery(
Expand Down Expand Up @@ -414,4 +445,48 @@ function ConnectionStatus() {
</div>
);
}

return null;
}

function CloudSidebar(): ReactNode {
const match = useMatch({
from: "/_layout/orgs/$organization/",
shouldThrow: false,
});

const clerk = useClerk();
return (
<>
<OrganizationSwitcher />

<ScrollArea>
<div className="flex gap-1.5 flex-col">
<HeaderLink
to="/orgs/$organization"
className="font-normal"
params={match?.params}
>
Projects
</HeaderLink>
<HeaderButton
onClick={() => {
clerk.openUserProfile({
__experimental_startPath: "/billing",
});
}}
>
Billing
</HeaderButton>
<HeaderButton
onClick={() => {
clerk.openUserProfile();
}}
>
Settings
</HeaderButton>
</div>
</ScrollArea>
</>
);
}
3 changes: 3 additions & 0 deletions frontend/src/app/use-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export const useDialog = {
CreateNamespace: createDialogHook(
import("@/app/dialogs/create-namespace-dialog"),
),
CreateProject: createDialogHook(
import("@/app/dialogs/create-project-dialog"),
),
ProvideEngineCredentials: createDialogHook(
import("@/app/dialogs/provide-engine-credentials-dialog"),
),
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/components/header/header-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@ export function HeaderLink({
icon ? <Icon className={cn("size-5")} icon={icon} /> : undefined
}
>
<Link to={props.to}>
{children}
<div className="absolute inset-x-0 -bottom-2 z-[1] h-[2px] rounded-sm bg-primary group-data-active:block hidden" />
</Link>
{children}
<div className="absolute inset-x-0 -bottom-2 z-[1] h-[2px] rounded-sm bg-primary group-data-active:block hidden" />
</Button>
);
}
4 changes: 4 additions & 0 deletions frontend/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Clerk } from "@clerk/clerk-js";
import { cloudEnv } from "./env";

export const clerk = new Clerk(cloudEnv().VITE_CLERK_PUBLISHABLE_KEY);
Loading
Loading