Create clients working again

This commit is contained in:
Owen 2025-04-01 10:13:20 -04:00
parent 875fa215c5
commit 96d6ad8142
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
6 changed files with 101 additions and 86 deletions

View file

@ -30,7 +30,7 @@ const createClientParamsSchema = z
const createClientSchema = z const createClientSchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
siteIds: z.array(z.string().transform(Number).pipe(z.number())), siteIds: z.array(z.number().int().positive()),
olmId: z.string(), olmId: z.string(),
secret: z.string(), secret: z.string(),
type: z.enum(["olm"]) type: z.enum(["olm"])

View file

@ -102,8 +102,8 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
"/pick-client-defaults", "/org/:orgId/pick-client-defaults",
verifySiteAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createClient), verifyUserHasAction(ActionsEnum.createClient),
client.pickClientDefaults client.pickClientDefaults
); );

View file

@ -29,8 +29,6 @@ import CreateClientFormModal from "./CreateClientsModal";
export type ClientRow = { export type ClientRow = {
id: number; id: number;
siteId: string;
siteName: string;
name: string; name: string;
mbIn: string; mbIn: string;
mbOut: string; mbOut: string;
@ -128,33 +126,33 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
); );
} }
}, },
{ // {
accessorKey: "siteName", // accessorKey: "siteName",
header: ({ column }) => { // header: ({ column }) => {
return ( // return (
<Button // <Button
variant="ghost" // variant="ghost"
onClick={() => // onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc") // column.toggleSorting(column.getIsSorted() === "asc")
} // }
> // >
Site // Site
<ArrowUpDown className="ml-2 h-4 w-4" /> // <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> // </Button>
); // );
}, // },
cell: ({ row }) => { // cell: ({ row }) => {
const r = row.original; // const r = row.original;
return ( // return (
<Link href={`/${r.orgId}/settings/sites/${r.siteId}`}> // <Link href={`/${r.orgId}/settings/sites/${r.siteId}`}>
<Button variant="outline"> // <Button variant="outline">
{r.siteName} // {r.siteName}
<ArrowUpRight className="ml-2 h-4 w-4" /> // <ArrowUpRight className="ml-2 h-4 w-4" />
</Button> // </Button>
</Link> // </Link>
); // );
} // }
}, // },
{ {
accessorKey: "online", accessorKey: "online",
header: ({ column }) => { header: ({ column }) => {

View file

@ -57,8 +57,8 @@ const createClientFormSchema = z.object({
.max(30, { .max(30, {
message: "Name must not be longer than 30 characters." message: "Name must not be longer than 30 characters."
}), }),
siteIds: z.array(z.number()).min(1, { siteIds: z.array(z.number()).min(1, {
message: "Select at least one site." message: "Select at least one site."
}) })
}); });
@ -88,11 +88,12 @@ export default function CreateClientForm({
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]); const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false); const [isChecked, setIsChecked] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [clientDefaults, setClientDefaults] = const [clientDefaults, setClientDefaults] =
useState<PickClientDefaultsResponse | null>(null); useState<PickClientDefaultsResponse | null>(null);
const [olmCommand, setOlmCommand] = useState<string | null>(null); const [olmCommand, setOlmCommand] = useState<string | null>(null);
const [selectedSites, setSelectedSites] = useState<Array<{id: number, name: string}>>([]); const [selectedSites, setSelectedSites] = useState<
Array<{ id: number; name: string }>
>([]);
const handleCheckboxChange = (checked: boolean) => { const handleCheckboxChange = (checked: boolean) => {
setIsChecked(checked); setIsChecked(checked);
@ -108,7 +109,10 @@ export default function CreateClientForm({
useEffect(() => { useEffect(() => {
// Update form value when selectedSites changes // Update form value when selectedSites changes
form.setValue('siteIds', selectedSites.map(site => site.id)); form.setValue(
"siteIds",
selectedSites.map((site) => site.id)
);
}, [selectedSites, form]); }, [selectedSites, form]);
useEffect(() => { useEffect(() => {
@ -132,38 +136,39 @@ export default function CreateClientForm({
setSites(sites); setSites(sites);
}; };
const fetchDefaults = async () => {
api.get(`/org/${orgId}/pick-client-defaults`)
.catch((e) => {
toast({
variant: "destructive",
title: `Error fetching client defaults`,
description: formatAxiosError(e)
});
})
.then((res) => {
if (res && res.status === 200) {
const data = res.data.data;
setClientDefaults(data);
const olmConfig = `olm --id ${data?.olmId} --secret ${data?.olmSecret} --endpoint ${env.app.dashboardUrl}`;
setOlmCommand(olmConfig);
}
});
};
fetchSites(); fetchSites();
fetchDefaults();
}, [open]); }, [open]);
useEffect(() => {
if (selectedSites.length === 0) return;
api.get(`/pick-client-defaults`)
.catch((e) => {
toast({
variant: "destructive",
title: `Error fetching client defaults`,
description: formatAxiosError(e)
});
})
.then((res) => {
if (res && res.status === 200) {
const data = res.data.data;
setClientDefaults(data);
const olmConfig = `olm --id ${data?.olmId} --secret ${data?.olmSecret} --endpoint ${env.app.dashboardUrl}`;
setOlmCommand(olmConfig);
}
});
}, [selectedSites]);
const addSite = (siteId: number, siteName: string) => { const addSite = (siteId: number, siteName: string) => {
if (!selectedSites.some(site => site.id === siteId)) { if (!selectedSites.some((site) => site.id === siteId)) {
setSelectedSites([...selectedSites, { id: siteId, name: siteName }]); setSelectedSites([
...selectedSites,
{ id: siteId, name: siteName }
]);
} }
}; };
const removeSite = (siteId: number) => { const removeSite = (siteId: number) => {
setSelectedSites(selectedSites.filter(site => site.id !== siteId)); setSelectedSites(selectedSites.filter((site) => site.id !== siteId));
}; };
async function onSubmit(data: CreateClientFormValues) { async function onSubmit(data: CreateClientFormValues) {
@ -190,10 +195,9 @@ export default function CreateClientForm({
} as CreateClientBody; } as CreateClientBody;
const res = await api const res = await api
.put<AxiosResponse<CreateClientResponse>>( .put<
`/org/${orgId}/client`, AxiosResponse<CreateClientResponse>
payload >(`/org/${orgId}/client`, payload)
)
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
@ -205,14 +209,8 @@ export default function CreateClientForm({
if (res && res.status === 201) { if (res && res.status === 201) {
const data = res.data.data; const data = res.data.data;
// For now we'll just use the first site for display purposes
// The actual client will be associated with all selected sites
const firstSite = sites.find((site) => site.siteId === selectedSites[0]?.id);
onCreate?.({ onCreate?.({
name: data.name, name: data.name,
siteId: firstSite?.niceId || "",
siteName: firstSite?.name || "",
id: data.clientId, id: data.clientId,
mbIn: "0 MB", mbIn: "0 MB",
mbOut: "0 MB", mbOut: "0 MB",
@ -265,12 +263,13 @@ export default function CreateClientForm({
role="combobox" role="combobox"
className={cn( className={cn(
"justify-between", "justify-between",
selectedSites.length === 0 && selectedSites.length ===
0 &&
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
{selectedSites.length > 0 {selectedSites.length > 0
? `${selectedSites.length} site${selectedSites.length !== 1 ? 's' : ''} selected` ? `${selectedSites.length} site${selectedSites.length !== 1 ? "s" : ""} selected`
: "Select sites"} : "Select sites"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@ -288,15 +287,26 @@ export default function CreateClientForm({
{sites.map((site) => ( {sites.map((site) => (
<CommandItem <CommandItem
value={`${site.siteId}:${site.name}:${site.niceId}`} value={`${site.siteId}:${site.name}:${site.niceId}`}
key={site.siteId} key={
site.siteId
}
onSelect={() => { onSelect={() => {
addSite(site.siteId, site.name); addSite(
site.siteId,
site.name
);
}} }}
> >
<CheckIcon <CheckIcon
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
selectedSites.some(s => s.id === site.siteId) selectedSites.some(
(
s
) =>
s.id ===
site.siteId
)
? "opacity-100" ? "opacity-100"
: "opacity-0" : "opacity-0"
)} )}
@ -310,15 +320,20 @@ export default function CreateClientForm({
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{selectedSites.length > 0 && ( {selectedSites.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{selectedSites.map(site => ( {selectedSites.map((site) => (
<Badge key={site.id} variant="secondary"> <Badge
key={site.id}
variant="secondary"
>
{site.name} {site.name}
<button <button
type="button" type="button"
onClick={() => removeSite(site.id)} onClick={() =>
removeSite(site.id)
}
className="ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2" className="ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2"
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
@ -327,9 +342,11 @@ export default function CreateClientForm({
))} ))}
</div> </div>
)} )}
<FormDescription> <FormDescription>
The client will have connectivity to the selected sites. The sites must be configured to accept client connections. The client will have connectivity to the
selected sites. The sites must be configured
to accept client connections.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -370,4 +387,4 @@ export default function CreateClientForm({
</Form> </Form>
</div> </div>
); );
} }

View file

@ -60,7 +60,7 @@ export default function CreateClientFormModal({
<CredenzaFooter> <CredenzaFooter>
<Button <Button
type="submit" type="submit"
form="create-site-form" form="create-client-form"
loading={loading} loading={loading}
disabled={loading || !isChecked} disabled={loading || !isChecked}
onClick={() => { onClick={() => {

View file

@ -3,7 +3,7 @@
import * as React from "react" import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils" import { cn } from "@app/lib/cn"
const ScrollArea = React.forwardRef< const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, React.ElementRef<typeof ScrollAreaPrimitive.Root>,