mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-31 08:04:54 +02:00
Able to update sites from the frontend
This commit is contained in:
parent
160a7ff3db
commit
8ad8f98f48
4 changed files with 2135 additions and 522 deletions
2530
package-lock.json
generated
2530
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
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 { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
@ -19,12 +19,28 @@ const getClientSchema = z
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
async function query(clientId: number) {
|
async function query(clientId: number) {
|
||||||
const [res] = await db
|
// Get the client
|
||||||
|
const [client] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.clientId, clientId))
|
.where(eq(clients.clientId, clientId))
|
||||||
.limit(1);
|
.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>>>;
|
export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||||
|
@ -82,4 +98,4 @@ export async function getClient(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -30,10 +30,19 @@ import {
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
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({
|
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>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
@ -43,21 +52,68 @@ export default function GeneralPage() {
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [sites, setSites] = useState<Tag[]>([]);
|
||||||
|
const [clientSites, setClientSites] = useState<Tag[]>([]);
|
||||||
|
const [activeSitesTagIndex, setActiveSitesTagIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const form = useForm<GeneralFormValues>({
|
const form = useForm<GeneralFormValues>({
|
||||||
resolver: zodResolver(GeneralFormSchema),
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: client?.name
|
name: client?.name,
|
||||||
|
siteIds: []
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
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) {
|
async function onSubmit(data: GeneralFormValues) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post(`/client/${client?.clientId}`, {
|
await api.post(`/client/${client?.clientId}`, {
|
||||||
name: data.name
|
name: data.name,
|
||||||
|
siteIds: data.siteIds.map(site => site.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
updateClient({ name: data.name });
|
updateClient({ name: data.name });
|
||||||
|
@ -119,6 +175,41 @@ export default function GeneralPage() {
|
||||||
</FormItem>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
@ -137,4 +228,4 @@ export default function GeneralPage() {
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -586,7 +586,7 @@ WantedBy=default.target`
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Client Address
|
Site Address
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue