mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-30 07:35:15 +02:00
Create clients working again
This commit is contained in:
parent
875fa215c5
commit
96d6ad8142
6 changed files with 101 additions and 86 deletions
|
@ -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"])
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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={() => {
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue