Able to update sites from the frontend

This commit is contained in:
Owen 2025-05-11 11:11:27 -04:00
parent 160a7ff3db
commit 8ad8f98f48
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
4 changed files with 2135 additions and 522 deletions

2530
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients } from "@server/db/schemas";
import { clients, clientSites } from "@server/db/schemas";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@ -19,12 +19,28 @@ const getClientSchema = z
.strict();
async function query(clientId: number) {
const [res] = await db
// Get the client
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
return res;
if (!client) {
return null;
}
// Get the siteIds associated with this client
const sites = await db
.select({ siteId: clientSites.siteId })
.from(clientSites)
.where(eq(clientSites.clientId, clientId));
// Add the siteIds to the client object
return {
...client,
siteIds: sites.map(site => site.siteId)
};
}
export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
@ -82,4 +98,4 @@ export async function getClient(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}

View file

@ -30,10 +30,19 @@ import {
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Tag, TagInput } from "@app/components/tags/tag-input";
import { AxiosResponse } from "axios";
import { ListSitesResponse } from "@server/routers/site";
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required")
name: z.string().nonempty("Name is required"),
siteIds: z.array(
z.object({
id: z.string(),
text: z.string()
})
)
});
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
@ -43,21 +52,68 @@ export default function GeneralPage() {
const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false);
const router = useRouter();
const [sites, setSites] = useState<Tag[]>([]);
const [clientSites, setClientSites] = useState<Tag[]>([]);
const [activeSitesTagIndex, setActiveSitesTagIndex] = useState<number | null>(null);
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: client?.name
name: client?.name,
siteIds: []
},
mode: "onChange"
});
// Fetch available sites and client's assigned sites
useEffect(() => {
const fetchSites = async () => {
try {
// Fetch all available sites
const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${client?.orgId}/sites/`
);
const availableSites = res.data.data.sites
.filter((s) => s.type === "newt" && s.subnet)
.map((site) => ({
id: site.siteId.toString(),
text: site.name
}));
setSites(availableSites);
// Filter sites to only include those assigned to the client
const assignedSites = availableSites.filter((site) =>
client?.siteIds?.includes(parseInt(site.id))
);
setClientSites(assignedSites);
// Set the default values for the form
form.setValue("siteIds", assignedSites);
} catch (e) {
toast({
variant: "destructive",
title: "Failed to fetch sites",
description: formatAxiosError(
e,
"An error occurred while fetching sites."
)
});
}
};
if (client?.clientId) {
fetchSites();
}
}, [client?.clientId, client?.orgId, api, form]);
async function onSubmit(data: GeneralFormValues) {
setLoading(true);
try {
await api.post(`/client/${client?.clientId}`, {
name: data.name
name: data.name,
siteIds: data.siteIds.map(site => site.id)
});
updateClient({ name: data.name });
@ -119,6 +175,41 @@ export default function GeneralPage() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="siteIds"
render={(field) => (
<FormItem className="flex flex-col">
<FormLabel>Sites</FormLabel>
<TagInput
{...field}
activeTagIndex={activeSitesTagIndex}
setActiveTagIndex={setActiveSitesTagIndex}
placeholder="Select sites"
size="sm"
tags={form.getValues().siteIds}
setTags={(newTags) => {
form.setValue(
"siteIds",
newTags as [Tag, ...Tag[]]
);
}}
enableAutocomplete={true}
autocompleteOptions={sites}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={true}
sortTags={true}
/>
<FormDescription>
The client will have connectivity to the
selected sites. The sites must be configured
to accept client connections.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
@ -137,4 +228,4 @@ export default function GeneralPage() {
</SettingsSection>
</SettingsContainer>
);
}
}

View file

@ -586,7 +586,7 @@ WantedBy=default.target`
render={({ field }) => (
<FormItem>
<FormLabel>
Client Address
Site Address
</FormLabel>
<FormControl>
<Input