mirror of
https://github.com/fosrl/pangolin.git
synced 2025-09-01 07:20:06 +02:00
Edit client page done
This commit is contained in:
parent
581fdd67b1
commit
dc49027b30
20 changed files with 491 additions and 26 deletions
|
@ -66,6 +66,7 @@ export enum ActionsEnum {
|
||||||
deleteClient = "deleteClient",
|
deleteClient = "deleteClient",
|
||||||
updateClient = "updateClient",
|
updateClient = "updateClient",
|
||||||
listClients = "listClients",
|
listClients = "listClients",
|
||||||
|
getClient = "getClient",
|
||||||
listOrgDomains = "listOrgDomains",
|
listOrgDomains = "listOrgDomains",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
74
server/routers/client/getClient.ts
Normal file
74
server/routers/client/getClient.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { clients } from "@server/db/schema";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import stoi from "@server/lib/stoi";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const getClientSchema = z
|
||||||
|
.object({
|
||||||
|
clientId: z
|
||||||
|
.string()
|
||||||
|
.transform(stoi)
|
||||||
|
.pipe(z.number().int().positive()),
|
||||||
|
orgId: z.string().optional()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
async function query(clientId: number) {
|
||||||
|
const [res] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||||
|
|
||||||
|
export async function getClient(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = getClientSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
logger.error(
|
||||||
|
`Error parsing params: ${fromError(parsedParams.error).toString()}`
|
||||||
|
);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientId } = parsedParams.data;
|
||||||
|
|
||||||
|
const client = await query(clientId);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return next(createHttpError(HttpCode.NOT_FOUND, "Client not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetClientResponse>(res, {
|
||||||
|
data: client,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Client retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,4 +2,5 @@ export * from "./pickClientDefaults";
|
||||||
export * from "./createClient";
|
export * from "./createClient";
|
||||||
export * from "./deleteClient";
|
export * from "./deleteClient";
|
||||||
export * from "./listClients";
|
export * from "./listClients";
|
||||||
export * from "./updateClient";
|
export * from "./updateClient";
|
||||||
|
export * from "./getClient";
|
|
@ -120,6 +120,13 @@ authenticated.get(
|
||||||
client.listClients
|
client.listClients
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/client/:clientId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getClient),
|
||||||
|
client.getClient
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/client",
|
"/org/:orgId/client",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|
|
@ -42,7 +42,8 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
|
||||||
megabytesOut: sites.megabytesOut,
|
megabytesOut: sites.megabytesOut,
|
||||||
orgName: orgs.name,
|
orgName: orgs.name,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online
|
online: sites.online,
|
||||||
|
address: sites.address,
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
|
||||||
|
|
|
@ -30,6 +30,8 @@ import CreateClientFormModal from "./CreateClientsModal";
|
||||||
export type ClientRow = {
|
export type ClientRow = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
subnet: string;
|
||||||
|
// siteIds: string;
|
||||||
mbIn: string;
|
mbIn: string;
|
||||||
mbOut: string;
|
mbOut: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
@ -53,7 +55,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
const deleteSite = (clientId: number) => {
|
const deleteClient = (clientId: number) => {
|
||||||
api.delete(`/client/${clientId}`)
|
api.delete(`/client/${clientId}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("Error deleting client", e);
|
console.error("Error deleting client", e);
|
||||||
|
@ -218,25 +220,41 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "subnet",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Address
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const clientRow = row.original;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Link
|
||||||
|
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||||
|
>
|
||||||
|
<Button variant={"outlinePrimary"} className="ml-2">
|
||||||
|
Edit
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// id: "actions",
|
|
||||||
// cell: ({ row }) => {
|
|
||||||
// const siteRow = row.original;
|
|
||||||
// return (
|
|
||||||
// <div className="flex items-center justify-end">
|
|
||||||
// <Link
|
|
||||||
// href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
|
||||||
// >
|
|
||||||
// <Button variant={"outline"} className="ml-2">
|
|
||||||
// Edit
|
|
||||||
// <ArrowRight className="ml-2 w-4 h-4" />
|
|
||||||
// </Button>
|
|
||||||
// </Link>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -281,7 +299,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText="Confirm Delete Client"
|
buttonText="Confirm Delete Client"
|
||||||
onConfirm={async () => deleteSite(selectedClient!.id)}
|
onConfirm={async () => deleteClient(selectedClient!.id)}
|
||||||
string={selectedClient.name}
|
string={selectedClient.name}
|
||||||
title="Delete Client"
|
title="Delete Client"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -222,6 +222,7 @@ export default function CreateClientForm({
|
||||||
onCreate?.({
|
onCreate?.({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
id: data.clientId,
|
id: data.clientId,
|
||||||
|
subnet: data.subnet,
|
||||||
mbIn: "0 MB",
|
mbIn: "0 MB",
|
||||||
mbOut: "0 MB",
|
mbOut: "0 MB",
|
||||||
orgId: orgId as string,
|
orgId: orgId as string,
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
import { useClientContext } from "@app/hooks/useClientContext";
|
||||||
|
import {
|
||||||
|
InfoSection,
|
||||||
|
InfoSectionContent,
|
||||||
|
InfoSections,
|
||||||
|
InfoSectionTitle
|
||||||
|
} from "@app/components/InfoSection";
|
||||||
|
|
||||||
|
type ClientInfoCardProps = {};
|
||||||
|
|
||||||
|
export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||||
|
const { client, updateClient } = useClientContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
<AlertTitle className="font-semibold">Client Information</AlertTitle>
|
||||||
|
<AlertDescription className="mt-4">
|
||||||
|
<InfoSections cols={2}>
|
||||||
|
<>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>Status</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{client.online ? (
|
||||||
|
<div className="text-green-500 flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span>Online</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-neutral-500 flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||||
|
<span>Offline</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>Address</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{client.subnet.split("/")[0]}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
143
src/app/[orgId]/settings/clients/[clientId]/general/page.tsx
Normal file
143
src/app/[orgId]/settings/clients/[clientId]/general/page.tsx
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useClientContext } from "@app/hooks/useClientContext";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
SettingsContainer,
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionForm,
|
||||||
|
SettingsSectionFooter
|
||||||
|
} from "@app/components/Settings";
|
||||||
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const GeneralFormSchema = z.object({
|
||||||
|
name: z.string().nonempty("Name is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
|
||||||
|
export default function GeneralPage() {
|
||||||
|
const { client, updateClient } = useClientContext();
|
||||||
|
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const form = useForm<GeneralFormValues>({
|
||||||
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: client?.name
|
||||||
|
},
|
||||||
|
mode: "onChange"
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(data: GeneralFormValues) {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
await api
|
||||||
|
.post(`/client/${client?.clientId}`, {
|
||||||
|
name: data.name
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to update client",
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
"An error occurred while updating the client."
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateClient({ name: data.name });
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Client updated",
|
||||||
|
description: "The client has been updated."
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContainer>
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
General Settings
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
Configure the general settings for this client
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="general-settings-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
This is the display name of the
|
||||||
|
client.
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="general-settings-form"
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsContainer>
|
||||||
|
);
|
||||||
|
}
|
77
src/app/[orgId]/settings/clients/[clientId]/layout.tsx
Normal file
77
src/app/[orgId]/settings/clients/[clientId]/layout.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { internal } from "@app/lib/api";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
import { SidebarSettings } from "@app/components/SidebarSettings";
|
||||||
|
import Link from "next/link";
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator
|
||||||
|
} from "@app/components/ui/breadcrumb";
|
||||||
|
import { GetClientResponse } from "@server/routers/client";
|
||||||
|
import ClientInfoCard from "./ClientInfoCard";
|
||||||
|
import ClientProvider from "@app/providers/ClientProvider";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface SettingsLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: Promise<{ clientId: number; orgId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
let client = null;
|
||||||
|
try {
|
||||||
|
const res = await internal.get<AxiosResponse<GetClientResponse>>(
|
||||||
|
`/org/${params.orgId}/client/${params.clientId}`,
|
||||||
|
await authCookieHeader()
|
||||||
|
);
|
||||||
|
client = res.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching client data:", error);
|
||||||
|
redirect(`/${params.orgId}/settings/clients`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarNavItems = [
|
||||||
|
{
|
||||||
|
title: "General",
|
||||||
|
href: "/{orgId}/settings/clients/{clientId}/general"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 flex-row">
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<Link href="../">Clients</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbPage>{client.name}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsSectionTitle
|
||||||
|
title={`${client?.name} Settings`}
|
||||||
|
description="Configure the settings on your site"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ClientProvider client={client}>
|
||||||
|
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||||
|
<ClientInfoCard />
|
||||||
|
{children}
|
||||||
|
</SidebarSettings>
|
||||||
|
</ClientProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
8
src/app/[orgId]/settings/clients/[clientId]/page.tsx
Normal file
8
src/app/[orgId]/settings/clients/[clientId]/page.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function ClientPage(props: {
|
||||||
|
params: Promise<{ orgId: string; clientId: number }>;
|
||||||
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
redirect(`/${params.orgId}/settings/clients/${params.clientId}/general`);
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
||||||
return {
|
return {
|
||||||
name: client.name,
|
name: client.name,
|
||||||
id: client.clientId,
|
id: client.clientId,
|
||||||
|
subnet: client.subnet.split("/")[0],
|
||||||
mbIn: formatSize(client.megabytesIn || 0),
|
mbIn: formatSize(client.megabytesIn || 0),
|
||||||
mbOut: formatSize(client.megabytesOut || 0),
|
mbOut: formatSize(client.megabytesOut || 0),
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
|
|
|
@ -206,7 +206,6 @@ export default function GeneralPage() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* New FormField for subnet input */}
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="subnet"
|
name="subnet"
|
||||||
|
@ -215,8 +214,8 @@ export default function GeneralPage() {
|
||||||
<FormLabel>Subnet</FormLabel>
|
<FormLabel>Subnet</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
placeholder="e.g., 192.168.1.0/24"
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
|
@ -224,6 +224,7 @@ export default function CreateSiteForm({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
id: data.siteId,
|
id: data.siteId,
|
||||||
nice: data.niceId.toString(),
|
nice: data.niceId.toString(),
|
||||||
|
address: data.address?.split("/")[0],
|
||||||
mbIn:
|
mbIn:
|
||||||
data.type == "wireguard" || data.type == "newt"
|
data.type == "wireguard" || data.type == "newt"
|
||||||
? "0 MB"
|
? "0 MB"
|
||||||
|
|
|
@ -37,6 +37,7 @@ export type SiteRow = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
type: "newt" | "wireguard";
|
type: "newt" | "wireguard";
|
||||||
online: boolean;
|
online: boolean;
|
||||||
|
address?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SitesTableProps = {
|
type SitesTableProps = {
|
||||||
|
@ -259,6 +260,22 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "address",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Address
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||||
name: site.name,
|
name: site.name,
|
||||||
id: site.siteId,
|
id: site.siteId,
|
||||||
nice: site.niceId.toString(),
|
nice: site.niceId.toString(),
|
||||||
|
address: site.address?.split("/")[0],
|
||||||
mbIn: formatSize(site.megabytesIn || 0, site.type),
|
mbIn: formatSize(site.megabytesIn || 0, site.type),
|
||||||
mbOut: formatSize(site.megabytesOut || 0, site.type),
|
mbOut: formatSize(site.megabytesOut || 0, site.type),
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
|
|
|
@ -34,6 +34,7 @@ export function SidebarNav({
|
||||||
const niceId = params.niceId as string;
|
const niceId = params.niceId as string;
|
||||||
const resourceId = params.resourceId as string;
|
const resourceId = params.resourceId as string;
|
||||||
const userId = params.userId as string;
|
const userId = params.userId as string;
|
||||||
|
const clientId = params.clientId as string;
|
||||||
|
|
||||||
const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
|
const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
|
||||||
|
|
||||||
|
@ -59,7 +60,8 @@ export function SidebarNav({
|
||||||
.replace("{orgId}", orgId)
|
.replace("{orgId}", orgId)
|
||||||
.replace("{niceId}", niceId)
|
.replace("{niceId}", niceId)
|
||||||
.replace("{resourceId}", resourceId)
|
.replace("{resourceId}", resourceId)
|
||||||
.replace("{userId}", userId);
|
.replace("{userId}", userId)
|
||||||
|
.replace("{clientId}", clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
11
src/contexts/clientContext.ts
Normal file
11
src/contexts/clientContext.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { GetClientResponse } from "@server/routers/client/getClient";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
interface ClientContextType {
|
||||||
|
client: GetClientResponse;
|
||||||
|
updateClient: (updatedClient: Partial<GetClientResponse>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientContext = createContext<ClientContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export default ClientContext;
|
10
src/hooks/useClientContext.ts
Normal file
10
src/hooks/useClientContext.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import ClientContext from "@app/contexts/clientContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export function useClientContext() {
|
||||||
|
const context = useContext(ClientContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useSiteContext must be used within a SiteProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
40
src/providers/ClientProvider.tsx
Normal file
40
src/providers/ClientProvider.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import ClientContext from "@app/contexts/clientContext";
|
||||||
|
import { GetClientResponse } from "@server/routers/client/getClient";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ClientProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
client: GetClientResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClientProvider({
|
||||||
|
children,
|
||||||
|
client: serverClient
|
||||||
|
}: ClientProviderProps) {
|
||||||
|
const [client, setClient] = useState<GetClientResponse>(serverClient);
|
||||||
|
|
||||||
|
const updateClient = (updatedClient: Partial<GetClientResponse>) => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("No client to update");
|
||||||
|
}
|
||||||
|
setClient((prev) => {
|
||||||
|
if (!prev) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
...updatedClient
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider value={{ client, updateClient }}>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClientProvider;
|
Loading…
Add table
Add a link
Reference in a new issue