Skip to content

Commit

Permalink
[UI v2] Adds Create or edit dialog for concurrency limit
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa committed Dec 6, 2024
1 parent 09412d1 commit e9d76c9
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 15 deletions.
30 changes: 30 additions & 0 deletions ui-v2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";

import { type GlobalConcurrencyLimit } from "@/hooks/global-concurrency-limits";
import { useCreateOrEditLimitDialog } from "./use-create-or-edit-limit-dialog";

type Props = {
limitToUpdate: undefined | GlobalConcurrencyLimit;
onOpenChange: (open: boolean) => void;
onSubmit: () => void;
open: boolean;
};

export const CreateOrEditLimitDialog = ({
limitToUpdate,
onOpenChange,
onSubmit,
open,
}: Props) => {
const { dialogTitle, form, isLoading, saveOrUpdate } =
useCreateOrEditLimitDialog({
limitToUpdate,
onSubmit,
});

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{dialogTitle}</DialogTitle>
</DialogHeader>

<Form {...form}>
<form
onSubmit={(e) => void form.handleSubmit(saveOrUpdate)(e)}
className="space-y-4"
>
<FormMessage>{form.formState.errors.root?.message}</FormMessage>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" autoComplete="off" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="limit"
render={({ field }) => (
<FormItem>
<FormLabel>Concurrency Limit</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slot_decay_per_second"
render={({ field }) => (
<FormItem>
<FormLabel>Slot Decay Per Second</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="active"
render={({ field }) => (
<FormItem>
<FormLabel>Active</FormLabel>
<FormControl>
<Switch
className="block"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<DialogTrigger asChild>
<Button variant="outline">Close</Button>
</DialogTrigger>
<Button type="submit" loading={isLoading}>
{limitToUpdate ? "Update" : "Save"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
GlobalConcurrencyLimit,
useCreateGlobalConcurrencyLimit,
useUpdateGlobalConcurrencyLimit,
} from "@/hooks/global-concurrency-limits";
import { useToast } from "@/hooks/use-toast";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

const formSchema = z.object({
active: z.boolean().default(true),
/** Coerce to solve common issue of transforming a string number to a number type */
denied_slots: z.number().default(0).or(z.string()).pipe(z.coerce.number()),
/** Coerce to solve common issue of transforming a string number to a number type */
limit: z.number().default(0).or(z.string()).pipe(z.coerce.number()),
name: z
.string()
.min(2, { message: "Name must be at least 2 characters" })
.default(""),
/** Coerce to solve common issue of transforming a string number to a number type */
slot_decay_per_second: z
.number()
.default(0)
.or(z.string())
.pipe(z.coerce.number()),
/** Additional fields post creation. Coerce to solve common issue of transforming a string number to a number type */
active_slots: z.number().default(0).or(z.string()).pipe(z.coerce.number()),
});

const DEFAULT_VALUES = {
active: true,
name: "",
limit: 0,
slot_decay_per_second: 0,
denied_slots: 0,
active_slots: 0,
} as const;

type UseCreateOrEditLimitDialog = {
/** Limit to edit. Pass undefined if creating a new limit */
limitToUpdate: GlobalConcurrencyLimit | undefined;
/** Callback after hitting Save or Update */
onSubmit: () => void;
};

export const useCreateOrEditLimitDialog = ({
limitToUpdate,
onSubmit,
}: UseCreateOrEditLimitDialog) => {
const { toast } = useToast();

const { createGlobalConcurrencyLimit, status: createStatus } =
useCreateGlobalConcurrencyLimit();
const { updateGlobalConcurrencyLimit, status: updateStatus } =
useUpdateGlobalConcurrencyLimit();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: DEFAULT_VALUES,
});

// Sync form data with limit-to-edit data
useEffect(() => {
if (limitToUpdate) {
const { active, name, limit, slot_decay_per_second, active_slots } =
limitToUpdate;
form.reset({ active, name, limit, slot_decay_per_second, active_slots });
} else {
form.reset(DEFAULT_VALUES);
}
}, [form, limitToUpdate]);

const saveOrUpdate = (values: z.infer<typeof formSchema>) => {
const onSettled = () => {
form.reset(DEFAULT_VALUES);
onSubmit();
};

if (limitToUpdate?.id) {
updateGlobalConcurrencyLimit(
{
id_or_name: limitToUpdate.id,
...values,
},
{
onSuccess: () => {
toast({ title: "Limit updated" });
},
onError: (error) => {
const message =
error.message || "Unknown error while updating limit.";
form.setError("root", { message });
},
onSettled,
},
);
} else {
createGlobalConcurrencyLimit(values, {
onSuccess: () => {
toast({ title: "Limit created" });
},
onError: (error) => {
const message =
error.message || "Unknown error while creating variable.";
form.setError("root", {
message,
});
},
onSettled,
});
}
};

return {
form,
saveOrUpdate,
dialogTitle: limitToUpdate
? `Update ${limitToUpdate.name}`
: "Add Concurrency Limit",
isLoading: createStatus === "pending" || updateStatus === "pending",
};
};
60 changes: 45 additions & 15 deletions ui-v2/src/components/concurrency/global-concurrency-view/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,58 @@
import { useListGlobalConcurrencyLimits } from "@/hooks/global-concurrency-limits";
import { useState } from "react";

import { useMemo, useState } from "react";
import { CreateOrEditLimitDialog } from "./create-or-edit-limit-dialog";
import { GlobalConcurrencyLimitEmptyState } from "./global-concurrency-limit-empty-state";
import { GlobalConcurrencyLimitsHeader } from "./global-concurrency-limits-header";

type AddOrEditDialogState = {
open: boolean;
limitIdToEdit?: string;
};

export const GlobalConcurrencyView = () => {
const [showAddDialog, setShowAddDialog] = useState(false);
const [openAddOrEditDialog, setOpenAddOrEditDialog] =
useState<AddOrEditDialogState>({
open: false,
});

const { data } = useListGlobalConcurrencyLimits();

const openAddDialog = () => setShowAddDialog(true);
const closeAddDialog = () => setShowAddDialog(false);
const selectedlimitToUpdate = useMemo(() => {
if (!openAddOrEditDialog.limitIdToEdit) {
return undefined;
}
return data.find((limit) => limit.id === openAddOrEditDialog.limitIdToEdit);
}, [data, openAddOrEditDialog.limitIdToEdit]);

const openAddDialog = () =>
setOpenAddOrEditDialog((curr) => ({ ...curr, open: true }));

// close and deselect any selected limits
const closeAddDialog = () => setOpenAddOrEditDialog({ open: false });

return (
<>
<div className="flex flex-col gap-2">
<GlobalConcurrencyLimitsHeader onAdd={openAddDialog} />
</div>
<div>TODO</div>
<ul>
{data.map((limit) => (
<li key={limit.id}>{JSON.stringify(limit)}</li>
))}
</ul>
{showAddDialog && <div onClick={closeAddDialog}>TODO: DIALOG</div>}
{data.length === 0 ? (
<GlobalConcurrencyLimitEmptyState onAdd={openAddDialog} />
) : (
<div className="flex flex-col gap-2">
<GlobalConcurrencyLimitsHeader onAdd={openAddDialog} />
<div>TODO</div>
<ul>
{data.map((limit) => (
<li key={limit.id}>{JSON.stringify(limit)}</li>
))}
</ul>
</div>
)}
<CreateOrEditLimitDialog
open={openAddOrEditDialog.open}
onOpenChange={(open) =>
setOpenAddOrEditDialog((curr) => ({ ...curr, open }))
}
limitToUpdate={selectedlimitToUpdate}
onSubmit={closeAddDialog}
/>
</>
);
};
Loading

0 comments on commit e9d76c9

Please sign in to comment.