mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-14 22:06:36 +00:00
Fixing up the crud for multiple sites
This commit is contained in:
@@ -222,8 +222,7 @@ export async function createSiteResource(
|
|||||||
const sitesToAssign = await db
|
const sitesToAssign = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(and(inArray(sites.siteId, siteIds), eq(sites.orgId, orgId)))
|
.where(and(inArray(sites.siteId, siteIds), eq(sites.orgId, orgId)));
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (sitesToAssign.length !== siteIds.length) {
|
if (sitesToAssign.length !== siteIds.length) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, SiteResource, siteNetworks, siteResources, sites } from "@server/db";
|
import { db, DB_TYPE, SiteResource, siteNetworks, siteResources, sites } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -81,6 +81,40 @@ export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
|
|||||||
})[];
|
})[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an aggregation expression compatible with both SQLite and PostgreSQL.
|
||||||
|
* - SQLite: json_group_array(col) → returns a JSON array string, parsed after fetch
|
||||||
|
* - PostgreSQL: array_agg(col) → returns a native array
|
||||||
|
*/
|
||||||
|
function aggCol<T>(column: any) {
|
||||||
|
if (DB_TYPE === "sqlite") {
|
||||||
|
return sql<T>`json_group_array(${column})`;
|
||||||
|
}
|
||||||
|
return sql<T>`array_agg(${column})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For SQLite the aggregated columns come back as JSON strings; parse them into
|
||||||
|
* proper arrays. For PostgreSQL the driver already returns native arrays, so
|
||||||
|
* the row is returned unchanged.
|
||||||
|
*/
|
||||||
|
function transformSiteResourceRow(row: any) {
|
||||||
|
if (DB_TYPE !== "sqlite") {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
siteNames: JSON.parse(row.siteNames) as string[],
|
||||||
|
siteNiceIds: JSON.parse(row.siteNiceIds) as string[],
|
||||||
|
siteIds: JSON.parse(row.siteIds) as number[],
|
||||||
|
siteAddresses: JSON.parse(row.siteAddresses) as (string | null)[],
|
||||||
|
// SQLite stores booleans as 0/1 integers
|
||||||
|
siteOnlines: (JSON.parse(row.siteOnlines) as (0 | 1)[]).map(
|
||||||
|
(v) => v === 1
|
||||||
|
) as boolean[]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function querySiteResourcesBase() {
|
function querySiteResourcesBase() {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
@@ -107,19 +141,21 @@ function querySiteResourcesBase() {
|
|||||||
fullDomain: siteResources.fullDomain,
|
fullDomain: siteResources.fullDomain,
|
||||||
networkId: siteResources.networkId,
|
networkId: siteResources.networkId,
|
||||||
defaultNetworkId: siteResources.defaultNetworkId,
|
defaultNetworkId: siteResources.defaultNetworkId,
|
||||||
siteNames: sql<string[]>`array_agg(${sites.name})`,
|
siteNames: aggCol<string[]>(sites.name),
|
||||||
siteNiceIds: sql<string[]>`array_agg(${sites.niceId})`,
|
siteNiceIds: aggCol<string[]>(sites.niceId),
|
||||||
siteIds: sql<number[]>`array_agg(${sites.siteId})`,
|
siteIds: aggCol<number[]>(sites.siteId),
|
||||||
siteAddresses: sql<(string | null)[]>`array_agg(${sites.address})`,
|
siteAddresses: aggCol<(string | null)[]>(sites.address),
|
||||||
siteOnlines: sql<boolean[]>`array_agg(${sites.online})`
|
siteOnlines: aggCol<boolean[]>(sites.online)
|
||||||
})
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.innerJoin(siteNetworks, eq(siteResources.networkId, siteNetworks.networkId))
|
.innerJoin(
|
||||||
|
siteNetworks,
|
||||||
|
eq(siteResources.networkId, siteNetworks.networkId)
|
||||||
|
)
|
||||||
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||||
.groupBy(siteResources.siteResourceId);
|
.groupBy(siteResources.siteResourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/site-resources",
|
path: "/org/{orgId}/site-resources",
|
||||||
@@ -210,7 +246,7 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
.as("filtered_site_resources")
|
.as("filtered_site_resources")
|
||||||
);
|
);
|
||||||
|
|
||||||
const [siteResourcesList, totalCount] = await Promise.all([
|
const [siteResourcesRaw, totalCount] = await Promise.all([
|
||||||
baseQuery
|
baseQuery
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
@@ -224,6 +260,8 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
countQuery
|
countQuery
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const siteResourcesList = siteResourcesRaw.map(transformSiteResourceRow);
|
||||||
|
|
||||||
return response<ListAllSiteResourcesByOrgResponse>(res, {
|
return response<ListAllSiteResourcesByOrgResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
siteResources: siteResourcesList,
|
siteResources: siteResourcesList,
|
||||||
|
|||||||
@@ -280,8 +280,7 @@ export async function updateSiteResource(
|
|||||||
inArray(sites.siteId, siteIds),
|
inArray(sites.siteId, siteIds),
|
||||||
eq(sites.orgId, existingSiteResource.orgId)
|
eq(sites.orgId, existingSiteResource.orgId)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (sitesToAssign.length !== siteIds.length) {
|
if (sitesToAssign.length !== siteIds.length) {
|
||||||
return next(
|
return next(
|
||||||
@@ -727,7 +726,12 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
|
|
||||||
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
|
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
|
||||||
|
|
||||||
if (destinationChanged || aliasChanged || portRangesChanged || destinationPortChanged) {
|
if (
|
||||||
|
destinationChanged ||
|
||||||
|
aliasChanged ||
|
||||||
|
portRangesChanged ||
|
||||||
|
destinationPortChanged
|
||||||
|
) {
|
||||||
for (const site of sites) {
|
for (const site of sites) {
|
||||||
const [newt] = await trx
|
const [newt] = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -742,7 +746,11 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only update targets on newt if destination changed
|
// Only update targets on newt if destination changed
|
||||||
if (destinationChanged || portRangesChanged || destinationPortChanged) {
|
if (
|
||||||
|
destinationChanged ||
|
||||||
|
portRangesChanged ||
|
||||||
|
destinationPortChanged
|
||||||
|
) {
|
||||||
const oldTarget = await generateSubnetProxyTargetV2(
|
const oldTarget = await generateSubnetProxyTargetV2(
|
||||||
existingSiteResource,
|
existingSiteResource,
|
||||||
mergedAllClients
|
mergedAllClients
|
||||||
|
|||||||
@@ -653,11 +653,7 @@ export default function ClientResourcesTable({
|
|||||||
<EditInternalResourceDialog
|
<EditInternalResourceDialog
|
||||||
open={isEditDialogOpen}
|
open={isEditDialogOpen}
|
||||||
setOpen={setIsEditDialogOpen}
|
setOpen={setIsEditDialogOpen}
|
||||||
resource={{
|
resource={editingResource}
|
||||||
...editingResource,
|
|
||||||
siteName: editingResource.siteNames[0] ?? "",
|
|
||||||
siteId: editingResource.siteIds[0]
|
|
||||||
}}
|
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
// Delay refresh to allow modal to close smoothly
|
// Delay refresh to allow modal to close smoothly
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
`/org/${orgId}/site-resource`,
|
`/org/${orgId}/site-resource`,
|
||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
siteId: data.siteIds[0],
|
siteIds: data.siteIds,
|
||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
destination: data.destination,
|
destination: data.destination,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { resourceQueries } from "@app/lib/queries";
|
import { resourceQueries } from "@app/lib/queries";
|
||||||
import { ListSitesResponse } from "@server/routers/site";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
@@ -27,8 +26,6 @@ import {
|
|||||||
isHostname
|
isHostname
|
||||||
} from "./InternalResourceForm";
|
} from "./InternalResourceForm";
|
||||||
|
|
||||||
type Site = ListSitesResponse["sites"][0];
|
|
||||||
|
|
||||||
type EditInternalResourceDialogProps = {
|
type EditInternalResourceDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (val: boolean) => void;
|
setOpen: (val: boolean) => void;
|
||||||
@@ -69,7 +66,7 @@ export default function EditInternalResourceDialog({
|
|||||||
|
|
||||||
await api.post(`/site-resource/${resource.id}`, {
|
await api.post(`/site-resource/${resource.id}`, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
siteId: data.siteIds[0],
|
siteIds: data.siteIds,
|
||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
destination: data.destination,
|
destination: data.destination,
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ export type InternalResourceData = {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
siteName: string;
|
siteNames: string[];
|
||||||
mode: InternalResourceMode;
|
mode: InternalResourceMode;
|
||||||
siteId: number;
|
siteIds: number[];
|
||||||
niceId: string;
|
niceId: string;
|
||||||
destination: string;
|
destination: string;
|
||||||
alias?: string | null;
|
alias?: string | null;
|
||||||
@@ -160,13 +160,11 @@ const tagSchema = z.object({ id: z.string(), text: z.string() });
|
|||||||
function buildSelectedSitesForResource(
|
function buildSelectedSitesForResource(
|
||||||
resource: InternalResourceData,
|
resource: InternalResourceData,
|
||||||
): Selectedsite[] {
|
): Selectedsite[] {
|
||||||
return [
|
return resource.siteIds.map((siteId, idx) => ({
|
||||||
{
|
name: resource.siteNames[idx] ?? "",
|
||||||
name: resource.siteName,
|
siteId,
|
||||||
siteId: resource.siteId,
|
type: "newt" as const
|
||||||
type: "newt"
|
}));
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InternalResourceFormValues = {
|
export type InternalResourceFormValues = {
|
||||||
@@ -483,7 +481,7 @@ export function InternalResourceForm({
|
|||||||
variant === "edit" && resource
|
variant === "edit" && resource
|
||||||
? {
|
? {
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
siteIds: [resource.siteId],
|
siteIds: resource.siteIds,
|
||||||
mode: resource.mode ?? "host",
|
mode: resource.mode ?? "host",
|
||||||
destination: resource.destination ?? "",
|
destination: resource.destination ?? "",
|
||||||
alias: resource.alias ?? null,
|
alias: resource.alias ?? null,
|
||||||
@@ -594,7 +592,7 @@ export function InternalResourceForm({
|
|||||||
if (resourceChanged) {
|
if (resourceChanged) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
siteIds: [resource.siteId],
|
siteIds: resource.siteIds,
|
||||||
mode: resource.mode ?? "host",
|
mode: resource.mode ?? "host",
|
||||||
destination: resource.destination ?? "",
|
destination: resource.destination ?? "",
|
||||||
alias: resource.alias ?? null,
|
alias: resource.alias ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user