mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Merge branch 'dev' into refactor/save-button-positions
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"confirmPassword": "Confirm Password",
|
"confirmPassword": "Confirm Password",
|
||||||
"createAccount": "Create Account",
|
"createAccount": "Create Account",
|
||||||
"viewSettings": "View settings",
|
"viewSettings": "View Settings",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
@@ -1621,9 +1621,8 @@
|
|||||||
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
"createInternalResourceDialogName": "Name",
|
"createInternalResourceDialogName": "Name",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
"createInternalResourceDialogSelectSite": "Select site...",
|
"selectSite": "Select site...",
|
||||||
"createInternalResourceDialogSearchSites": "Search sites...",
|
"noSitesFound": "No sites found.",
|
||||||
"createInternalResourceDialogNoSitesFound": "No sites found.",
|
|
||||||
"createInternalResourceDialogProtocol": "Protocol",
|
"createInternalResourceDialogProtocol": "Protocol",
|
||||||
"createInternalResourceDialogTcp": "TCP",
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
"createInternalResourceDialogUdp": "UDP",
|
"createInternalResourceDialogUdp": "UDP",
|
||||||
@@ -2317,5 +2316,6 @@
|
|||||||
"organizationLoginPageDescription": "Customize the login page for this organization",
|
"organizationLoginPageDescription": "Customize the login page for this organization",
|
||||||
"resourceLoginPageTitle": "Resource Login Page",
|
"resourceLoginPageTitle": "Resource Login Page",
|
||||||
"resourceLoginPageDescription": "Customize the login page for individual resources",
|
"resourceLoginPageDescription": "Customize the login page for individual resources",
|
||||||
"enterConfirmation": "Enter confirmation"
|
"enterConfirmation": "Enter confirmation",
|
||||||
|
"blueprintViewDetails": "Details"
|
||||||
}
|
}
|
||||||
|
|||||||
2272
package-lock.json
generated
2272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { db, newts, blueprints, Blueprint } from "@server/db";
|
import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db";
|
||||||
import { Config, ConfigSchema } from "./types";
|
import { Config, ConfigSchema } from "./types";
|
||||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
@@ -15,6 +15,7 @@ import { BlueprintSource } from "@server/routers/blueprints/types";
|
|||||||
import { stringify as stringifyYaml } from "yaml";
|
import { stringify as stringifyYaml } from "yaml";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
|
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
|
||||||
|
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
|
||||||
|
|
||||||
type ApplyBlueprintArgs = {
|
type ApplyBlueprintArgs = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -108,38 +109,136 @@ export async function applyBlueprint({
|
|||||||
|
|
||||||
// We need to update the targets on the newts from the successfully updated information
|
// We need to update the targets on the newts from the successfully updated information
|
||||||
for (const result of clientResourcesResults) {
|
for (const result of clientResourcesResults) {
|
||||||
const [site] = await trx
|
if (
|
||||||
.select()
|
result.oldSiteResource &&
|
||||||
.from(sites)
|
result.oldSiteResource.siteId !=
|
||||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
result.newSiteResource.siteId
|
||||||
.where(
|
) {
|
||||||
and(
|
// query existing associations
|
||||||
eq(sites.siteId, result.newSiteResource.siteId),
|
const existingRoleIds = await trx
|
||||||
eq(sites.orgId, orgId),
|
.select()
|
||||||
eq(sites.type, "newt"),
|
.from(roleSiteResources)
|
||||||
isNotNull(sites.pubKey)
|
.where(
|
||||||
|
eq(
|
||||||
|
roleSiteResources.siteResourceId,
|
||||||
|
result.oldSiteResource.siteResourceId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
.then((rows) => rows.map((row) => row.roleId));
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!site) {
|
const existingUserIds= await trx
|
||||||
logger.debug(
|
.select()
|
||||||
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
.from(userSiteResources)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
userSiteResources.siteResourceId,
|
||||||
|
result.oldSiteResource.siteResourceId
|
||||||
|
)
|
||||||
|
).then((rows) => rows.map((row) => row.userId));
|
||||||
|
|
||||||
|
const existingClientIds = await trx
|
||||||
|
.select()
|
||||||
|
.from(clientSiteResources)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
clientSiteResources.siteResourceId,
|
||||||
|
result.oldSiteResource.siteResourceId
|
||||||
|
)
|
||||||
|
).then((rows) => rows.map((row) => row.clientId));
|
||||||
|
|
||||||
|
// delete the existing site resource
|
||||||
|
await trx
|
||||||
|
.delete(siteResources)
|
||||||
|
.where(
|
||||||
|
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId))
|
||||||
|
);
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
result.oldSiteResource,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
const [insertedSiteResource] = await trx
|
||||||
|
.insert(siteResources)
|
||||||
|
.values({
|
||||||
|
...result.newSiteResource,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// wait some time to allow for messages to be handled
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||||
|
|
||||||
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
|
if (existingRoleIds.length > 0) {
|
||||||
|
await trx.insert(roleSiteResources).values(
|
||||||
|
existingRoleIds.map((roleId) => ({
|
||||||
|
roleId,
|
||||||
|
siteResourceId: insertedSiteResource!.siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUserIds.length > 0) {
|
||||||
|
await trx.insert(userSiteResources).values(
|
||||||
|
existingUserIds.map((userId) => ({
|
||||||
|
userId,
|
||||||
|
siteResourceId: insertedSiteResource!.siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingClientIds.length > 0) {
|
||||||
|
await trx.insert(clientSiteResources).values(
|
||||||
|
existingClientIds.map((clientId) => ({
|
||||||
|
clientId,
|
||||||
|
siteResourceId: insertedSiteResource!.siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
insertedSiteResource,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const [newSite] = await trx
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.siteId, result.newSiteResource.siteId),
|
||||||
|
eq(sites.orgId, orgId),
|
||||||
|
eq(sites.type, "newt"),
|
||||||
|
isNotNull(sites.pubKey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!newSite) {
|
||||||
|
logger.debug(
|
||||||
|
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.sites.siteId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await handleMessagingForUpdatedSiteResource(
|
||||||
|
result.oldSiteResource,
|
||||||
|
result.newSiteResource,
|
||||||
|
{
|
||||||
|
siteId: newSite.sites.siteId,
|
||||||
|
orgId: newSite.sites.orgId
|
||||||
|
},
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await handleMessagingForUpdatedSiteResource(
|
|
||||||
result.oldSiteResource,
|
|
||||||
result.newSiteResource,
|
|
||||||
{ siteId: site.sites.siteId, orgId: site.sites.orgId },
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
|
|
||||||
// await addClientTargets(
|
// await addClientTargets(
|
||||||
// site.newt.newtId,
|
// site.newt.newtId,
|
||||||
// result.resource.destination,
|
// result.resource.destination,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { sites } from "@server/db";
|
|||||||
import { eq, and, ne, inArray } from "drizzle-orm";
|
import { eq, and, ne, inArray } from "drizzle-orm";
|
||||||
import { Config } from "./types";
|
import { Config } from "./types";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { getNextAvailableAliasAddress } from "../ip";
|
||||||
|
|
||||||
export type ClientResourcesResults = {
|
export type ClientResourcesResults = {
|
||||||
newSiteResource: SiteResource;
|
newSiteResource: SiteResource;
|
||||||
@@ -75,17 +76,12 @@ export async function updateClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existingResource) {
|
if (existingResource) {
|
||||||
if (existingResource.siteId !== site.siteId) {
|
|
||||||
throw new Error(
|
|
||||||
`You can not change the site of an existing client resource (${resourceNiceId}). Please delete and recreate it instead.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
const [updatedResource] = await trx
|
const [updatedResource] = await trx
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
.set({
|
.set({
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
|
siteId: site.siteId,
|
||||||
mode: resourceData.mode,
|
mode: resourceData.mode,
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
@@ -208,6 +204,12 @@ export async function updateClientResources(
|
|||||||
oldSiteResource: existingResource
|
oldSiteResource: existingResource
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
let aliasAddress: string | null = null;
|
||||||
|
if (resourceData.mode == "host") {
|
||||||
|
// we can only have an alias on a host
|
||||||
|
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||||
|
}
|
||||||
|
|
||||||
// Create new resource
|
// Create new resource
|
||||||
const [newResource] = await trx
|
const [newResource] = await trx
|
||||||
.insert(siteResources)
|
.insert(siteResources)
|
||||||
@@ -221,6 +223,7 @@ export async function updateClientResources(
|
|||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
// enabled: resourceData.enabled ?? true,
|
// enabled: resourceData.enabled ?? true,
|
||||||
alias: resourceData.alias || null,
|
alias: resourceData.alias || null,
|
||||||
|
aliasAddress: aliasAddress,
|
||||||
disableIcmp: resourceData["disable-icmp"],
|
disableIcmp: resourceData["disable-icmp"],
|
||||||
tcpPortRangeString: resourceData["tcp-ports"],
|
tcpPortRangeString: resourceData["tcp-ports"],
|
||||||
udpPortRangeString: resourceData["udp-ports"]
|
udpPortRangeString: resourceData["udp-ports"]
|
||||||
|
|||||||
@@ -239,9 +239,8 @@ authenticated.get(
|
|||||||
|
|
||||||
// Site Resource endpoints
|
// Site Resource endpoints
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/site/:siteId/resource",
|
"/org/:orgId/site-resource",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifySiteAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.createSiteResource),
|
verifyUserHasAction(ActionsEnum.createSiteResource),
|
||||||
logActionAudit(ActionsEnum.createSiteResource),
|
logActionAudit(ActionsEnum.createSiteResource),
|
||||||
siteResource.createSiteResource
|
siteResource.createSiteResource
|
||||||
@@ -263,18 +262,14 @@ authenticated.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
"/site-resource/:siteResourceId",
|
||||||
verifyOrgAccess,
|
|
||||||
verifySiteAccess,
|
|
||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyUserHasAction(ActionsEnum.getSiteResource),
|
verifyUserHasAction(ActionsEnum.getSiteResource),
|
||||||
siteResource.getSiteResource
|
siteResource.getSiteResource
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
"/site-resource/:siteResourceId",
|
||||||
verifyOrgAccess,
|
|
||||||
verifySiteAccess,
|
|
||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyUserHasAction(ActionsEnum.updateSiteResource),
|
verifyUserHasAction(ActionsEnum.updateSiteResource),
|
||||||
logActionAudit(ActionsEnum.updateSiteResource),
|
logActionAudit(ActionsEnum.updateSiteResource),
|
||||||
@@ -282,9 +277,7 @@ authenticated.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
"/site-resource/:siteResourceId",
|
||||||
verifyOrgAccess,
|
|
||||||
verifySiteAccess,
|
|
||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyUserHasAction(ActionsEnum.deleteSiteResource),
|
verifyUserHasAction(ActionsEnum.deleteSiteResource),
|
||||||
logActionAudit(ActionsEnum.deleteSiteResource),
|
logActionAudit(ActionsEnum.deleteSiteResource),
|
||||||
|
|||||||
@@ -146,9 +146,8 @@ authenticated.get(
|
|||||||
);
|
);
|
||||||
// Site Resource endpoints
|
// Site Resource endpoints
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/site/:siteId/resource",
|
"/org/:orgId/private-resource",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
||||||
logActionAudit(ActionsEnum.createSiteResource),
|
logActionAudit(ActionsEnum.createSiteResource),
|
||||||
siteResource.createSiteResource
|
siteResource.createSiteResource
|
||||||
@@ -170,18 +169,14 @@ authenticated.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
"/site-resource/:siteResourceId",
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeyHasAction(ActionsEnum.getSiteResource),
|
verifyApiKeyHasAction(ActionsEnum.getSiteResource),
|
||||||
siteResource.getSiteResource
|
siteResource.getSiteResource
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
"/site-resource/:siteResourceId",
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
||||||
logActionAudit(ActionsEnum.updateSiteResource),
|
logActionAudit(ActionsEnum.updateSiteResource),
|
||||||
@@ -189,9 +184,7 @@ authenticated.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
"/site-resource/:siteResourceId",
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
|
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
|
||||||
logActionAudit(ActionsEnum.deleteSiteResource),
|
logActionAudit(ActionsEnum.deleteSiteResource),
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export async function deleteSite(
|
|||||||
// Send termination message outside of transaction to prevent blocking
|
// Send termination message outside of transaction to prevent blocking
|
||||||
if (deletedNewtId) {
|
if (deletedNewtId) {
|
||||||
const payload = {
|
const payload = {
|
||||||
type: `newt/terminate`,
|
type: `newt/wg/terminate`,
|
||||||
data: {}
|
data: {}
|
||||||
};
|
};
|
||||||
// Don't await this to prevent blocking the response
|
// Don't await this to prevent blocking the response
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { z } from "zod";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
const createSiteResourceParamsSchema = z.strictObject({
|
const createSiteResourceParamsSchema = z.strictObject({
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive()),
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,6 +30,7 @@ const createSiteResourceSchema = z
|
|||||||
.strictObject({
|
.strictObject({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "port"]),
|
mode: z.enum(["host", "cidr", "port"]),
|
||||||
|
siteId: z.int(),
|
||||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
// proxyPort: z.int().positive().optional(),
|
// proxyPort: z.int().positive().optional(),
|
||||||
// destinationPort: z.int().positive().optional(),
|
// destinationPort: z.int().positive().optional(),
|
||||||
@@ -101,7 +101,7 @@ export type CreateSiteResourceResponse = SiteResource;
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "put",
|
method: "put",
|
||||||
path: "/org/{orgId}/site/{siteId}/resource",
|
path: "/org/{orgId}/site-resource",
|
||||||
description: "Create a new site resource.",
|
description: "Create a new site resource.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
@@ -145,9 +145,10 @@ export async function createSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteId, orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
siteId,
|
||||||
mode,
|
mode,
|
||||||
// protocol,
|
// protocol,
|
||||||
// proxyPort,
|
// proxyPort,
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const deleteSiteResourceParamsSchema = z.strictObject({
|
const deleteSiteResourceParamsSchema = z.strictObject({
|
||||||
siteResourceId: z.string().transform(Number).pipe(z.int().positive()),
|
siteResourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive()),
|
|
||||||
orgId: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DeleteSiteResourceResponse = {
|
export type DeleteSiteResourceResponse = {
|
||||||
@@ -23,7 +21,7 @@ export type DeleteSiteResourceResponse = {
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}",
|
path: "/site-resource/{siteResourceId}",
|
||||||
description: "Delete a site resource.",
|
description: "Delete a site resource.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
@@ -50,29 +48,13 @@ export async function deleteSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteResourceId, siteId, orgId } = parsedParams.data;
|
const { siteResourceId } = parsedParams.data;
|
||||||
|
|
||||||
const [site] = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if site resource exists
|
// Check if site resource exists
|
||||||
const [existingSiteResource] = await db
|
const [existingSiteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
||||||
and(
|
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!existingSiteResource) {
|
if (!existingSiteResource) {
|
||||||
@@ -85,19 +67,13 @@ export async function deleteSiteResource(
|
|||||||
// Delete the site resource
|
// Delete the site resource
|
||||||
const [removedSiteResource] = await trx
|
const [removedSiteResource] = await trx
|
||||||
.delete(siteResources)
|
.delete(siteResources)
|
||||||
.where(
|
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
||||||
and(
|
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const [newt] = await trx
|
const [newt] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(newts)
|
.from(newts)
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.where(eq(newts.siteId, removedSiteResource.siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
@@ -113,7 +89,7 @@ export async function deleteSiteResource(
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Deleted site resource ${siteResourceId} for site ${siteId}`
|
`Deleted site resource ${siteResourceId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export type GetSiteResourceResponse = NonNullable<
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}",
|
path: "/site-resource/{siteResourceId}",
|
||||||
description: "Get a specific site resource by siteResourceId.",
|
description: "Get a specific site resource by siteResourceId.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
|
|||||||
@@ -32,14 +32,13 @@ import {
|
|||||||
} from "@server/lib/rebuildClientAssociations";
|
} from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const updateSiteResourceParamsSchema = z.strictObject({
|
const updateSiteResourceParamsSchema = z.strictObject({
|
||||||
siteResourceId: z.string().transform(Number).pipe(z.int().positive()),
|
siteResourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive()),
|
|
||||||
orgId: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSiteResourceSchema = z
|
const updateSiteResourceSchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
|
siteId: z.int(),
|
||||||
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
||||||
mode: z.enum(["host", "cidr"]).optional(),
|
mode: z.enum(["host", "cidr"]).optional(),
|
||||||
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||||
@@ -78,7 +77,10 @@ const updateSiteResourceSchema = z
|
|||||||
const domainRegex =
|
const domainRegex =
|
||||||
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
||||||
const isValidDomain = domainRegex.test(data.destination);
|
const isValidDomain = domainRegex.test(data.destination);
|
||||||
const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== "";
|
const isValidAlias =
|
||||||
|
data.alias !== undefined &&
|
||||||
|
data.alias !== null &&
|
||||||
|
data.alias.trim() !== "";
|
||||||
|
|
||||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||||
}
|
}
|
||||||
@@ -111,7 +113,7 @@ export type UpdateSiteResourceResponse = SiteResource;
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}",
|
path: "/site-resource/{siteResourceId}",
|
||||||
description: "Update a site resource.",
|
description: "Update a site resource.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
tags: [OpenAPITags.Client, OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
@@ -155,9 +157,10 @@ export async function updateSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteResourceId, siteId, orgId } = parsedParams.data;
|
const { siteResourceId } = parsedParams.data;
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
siteId, // because it can change
|
||||||
mode,
|
mode,
|
||||||
destination,
|
destination,
|
||||||
alias,
|
alias,
|
||||||
@@ -173,7 +176,7 @@ export async function updateSiteResource(
|
|||||||
const [site] = await db
|
const [site] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
|
.where(eq(sites.siteId, siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!site) {
|
if (!site) {
|
||||||
@@ -184,13 +187,7 @@ export async function updateSiteResource(
|
|||||||
const [existingSiteResource] = await db
|
const [existingSiteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
||||||
and(
|
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!existingSiteResource) {
|
if (!existingSiteResource) {
|
||||||
@@ -199,6 +196,27 @@ export async function updateSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let existingSite = site;
|
||||||
|
let siteChanged = false;
|
||||||
|
if (existingSiteResource.siteId !== siteId) {
|
||||||
|
siteChanged = true;
|
||||||
|
// get the existing site
|
||||||
|
[existingSite] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, existingSiteResource.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingSite) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Existing site not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// make sure the alias is unique within the org if provided
|
// make sure the alias is unique within the org if provided
|
||||||
if (alias) {
|
if (alias) {
|
||||||
const [conflict] = await db
|
const [conflict] = await db
|
||||||
@@ -206,7 +224,7 @@ export async function updateSiteResource(
|
|||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(siteResources.orgId, orgId),
|
eq(siteResources.orgId, existingSiteResource.orgId),
|
||||||
eq(siteResources.alias, alias.trim()),
|
eq(siteResources.alias, alias.trim()),
|
||||||
ne(siteResources.siteResourceId, siteResourceId) // exclude self
|
ne(siteResources.siteResourceId, siteResourceId) // exclude self
|
||||||
)
|
)
|
||||||
@@ -225,100 +243,220 @@ export async function updateSiteResource(
|
|||||||
|
|
||||||
let updatedSiteResource: SiteResource | undefined;
|
let updatedSiteResource: SiteResource | undefined;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Update the site resource
|
// if the site is changed we need to delete and recreate the resource to avoid complications with the rebuild function otherwise we can just update in place
|
||||||
[updatedSiteResource] = await trx
|
if (siteChanged) {
|
||||||
.update(siteResources)
|
// delete the existing site resource
|
||||||
.set({
|
|
||||||
name: name,
|
|
||||||
mode: mode,
|
|
||||||
destination: destination,
|
|
||||||
enabled: enabled,
|
|
||||||
alias: alias && alias.trim() ? alias : null,
|
|
||||||
tcpPortRangeString: tcpPortRangeString,
|
|
||||||
udpPortRangeString: udpPortRangeString,
|
|
||||||
disableIcmp: disableIcmp
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
//////////////////// update the associations ////////////////////
|
|
||||||
|
|
||||||
await trx
|
|
||||||
.delete(clientSiteResources)
|
|
||||||
.where(eq(clientSiteResources.siteResourceId, siteResourceId));
|
|
||||||
|
|
||||||
if (clientIds.length > 0) {
|
|
||||||
await trx.insert(clientSiteResources).values(
|
|
||||||
clientIds.map((clientId) => ({
|
|
||||||
clientId,
|
|
||||||
siteResourceId
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await trx
|
|
||||||
.delete(userSiteResources)
|
|
||||||
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
|
||||||
|
|
||||||
if (userIds.length > 0) {
|
|
||||||
await trx
|
await trx
|
||||||
.insert(userSiteResources)
|
.delete(siteResources)
|
||||||
.values(
|
.where(
|
||||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
and(eq(siteResources.siteResourceId, siteResourceId))
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Get all admin role IDs for this org to exclude from deletion
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
const adminRoles = await trx
|
existingSiteResource,
|
||||||
.select()
|
trx
|
||||||
.from(roles)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(roles.isAdmin, true),
|
|
||||||
eq(roles.orgId, updatedSiteResource.orgId)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const adminRoleIds = adminRoles.map((role) => role.roleId);
|
|
||||||
|
|
||||||
if (adminRoleIds.length > 0) {
|
// create the new site resource from the removed one - the ID should stay the same
|
||||||
await trx.delete(roleSiteResources).where(
|
const [insertedSiteResource] = await trx
|
||||||
and(
|
.insert(siteResources)
|
||||||
eq(roleSiteResources.siteResourceId, siteResourceId),
|
.values({
|
||||||
ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role
|
...existingSiteResource,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// wait some time to allow for messages to be handled
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||||
|
|
||||||
|
[updatedSiteResource] = await trx
|
||||||
|
.update(siteResources)
|
||||||
|
.set({
|
||||||
|
name: name,
|
||||||
|
siteId: siteId,
|
||||||
|
mode: mode,
|
||||||
|
destination: destination,
|
||||||
|
enabled: enabled,
|
||||||
|
alias: alias && alias.trim() ? alias : null,
|
||||||
|
tcpPortRangeString: tcpPortRangeString,
|
||||||
|
udpPortRangeString: udpPortRangeString,
|
||||||
|
disableIcmp: disableIcmp
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
insertedSiteResource.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!updatedSiteResource) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to create updated site resource after site change"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
|
const [adminRole] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roles.isAdmin, true),
|
||||||
|
eq(roles.orgId, updatedSiteResource.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Admin role not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(roleSiteResources).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
siteResourceId: updatedSiteResource.siteResourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (roleIds.length > 0) {
|
||||||
|
await trx.insert(roleSiteResources).values(
|
||||||
|
roleIds.map((roleId) => ({
|
||||||
|
roleId,
|
||||||
|
siteResourceId: updatedSiteResource!.siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
await trx.insert(userSiteResources).values(
|
||||||
|
userIds.map((userId) => ({
|
||||||
|
userId,
|
||||||
|
siteResourceId: updatedSiteResource!.siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientIds.length > 0) {
|
||||||
|
await trx.insert(clientSiteResources).values(
|
||||||
|
clientIds.map((clientId) => ({
|
||||||
|
clientId,
|
||||||
|
siteResourceId: updatedSiteResource!.siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
updatedSiteResource,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await trx
|
// Update the site resource
|
||||||
.delete(roleSiteResources)
|
[updatedSiteResource] = await trx
|
||||||
|
.update(siteResources)
|
||||||
|
.set({
|
||||||
|
name: name,
|
||||||
|
siteId: siteId,
|
||||||
|
mode: mode,
|
||||||
|
destination: destination,
|
||||||
|
enabled: enabled,
|
||||||
|
alias: alias && alias.trim() ? alias : null,
|
||||||
|
tcpPortRangeString: tcpPortRangeString,
|
||||||
|
udpPortRangeString: udpPortRangeString,
|
||||||
|
disableIcmp: disableIcmp
|
||||||
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(roleSiteResources.siteResourceId, siteResourceId)
|
and(eq(siteResources.siteResourceId, siteResourceId))
|
||||||
);
|
)
|
||||||
}
|
.returning();
|
||||||
|
|
||||||
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
if (roleIds.length > 0) {
|
|
||||||
await trx
|
await trx
|
||||||
.insert(roleSiteResources)
|
.delete(clientSiteResources)
|
||||||
.values(
|
.where(
|
||||||
roleIds.map((roleId) => ({ roleId, siteResourceId }))
|
eq(clientSiteResources.siteResourceId, siteResourceId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (clientIds.length > 0) {
|
||||||
|
await trx.insert(clientSiteResources).values(
|
||||||
|
clientIds.map((clientId) => ({
|
||||||
|
clientId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(userSiteResources)
|
||||||
|
.where(
|
||||||
|
eq(userSiteResources.siteResourceId, siteResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
await trx.insert(userSiteResources).values(
|
||||||
|
userIds.map((userId) => ({
|
||||||
|
userId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all admin role IDs for this org to exclude from deletion
|
||||||
|
const adminRoles = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roles.isAdmin, true),
|
||||||
|
eq(roles.orgId, updatedSiteResource.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const adminRoleIds = adminRoles.map((role) => role.roleId);
|
||||||
|
|
||||||
|
if (adminRoleIds.length > 0) {
|
||||||
|
await trx.delete(roleSiteResources).where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
roleSiteResources.siteResourceId,
|
||||||
|
siteResourceId
|
||||||
|
),
|
||||||
|
ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await trx
|
||||||
|
.delete(roleSiteResources)
|
||||||
|
.where(
|
||||||
|
eq(roleSiteResources.siteResourceId, siteResourceId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleIds.length > 0) {
|
||||||
|
await trx.insert(roleSiteResources).values(
|
||||||
|
roleIds.map((roleId) => ({
|
||||||
|
roleId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Updated site resource ${siteResourceId} for site ${siteId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await handleMessagingForUpdatedSiteResource(
|
||||||
|
existingSiteResource,
|
||||||
|
updatedSiteResource,
|
||||||
|
{ siteId: site.siteId, orgId: site.orgId },
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Updated site resource ${siteResourceId} for site ${siteId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await handleMessagingForUpdatedSiteResource(
|
|
||||||
existingSiteResource,
|
|
||||||
updatedSiteResource!,
|
|
||||||
{ siteId: site.siteId, orgId: site.orgId },
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
@@ -345,6 +483,10 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
site: { siteId: number; orgId: string },
|
site: { siteId: number; orgId: string },
|
||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
logger.debug("handleMessagingForUpdatedSiteResource: existingSiteResource is: ", existingSiteResource);
|
||||||
|
logger.debug("handleMessagingForUpdatedSiteResource: updatedSiteResource is: ", updatedSiteResource);
|
||||||
|
|
||||||
const { mergedAllClients } =
|
const { mergedAllClients } =
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
existingSiteResource || updatedSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below
|
existingSiteResource || updatedSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below
|
||||||
|
|||||||
@@ -4,124 +4,161 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--font-sans: var(--font-sans);
|
|
||||||
--font-mono: var(--font-geist-mono);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--radius-2xl: calc(var(--radius) + 8px);
|
|
||||||
--radius-3xl: calc(var(--radius) + 12px);
|
|
||||||
--radius-4xl: calc(var(--radius) + 16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.99 0 0);
|
--radius: 0.75rem;
|
||||||
--foreground: oklch(0.145 0 0);
|
--background: oklch(0.985 0 0);
|
||||||
--card: oklch(1 0 0);
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card: oklch(1 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--primary: oklch(0.6734 0.195 41.36);
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
--primary: oklch(0.6717 0.1946 41.93);
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
--muted: oklch(0.97 0 0);
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
--accent: oklch(0.97 0 0);
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
--destructive: oklch(0.58 0.22 27);
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--border: oklch(0.922 0 0);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--input: oklch(0.922 0 0);
|
--border: oklch(0.91 0.004 286.32);
|
||||||
--ring: oklch(0.6734 0.195 41.36);
|
--input: oklch(0.92 0.004 286.32);
|
||||||
--chart-1: oklch(0.837 0.128 66.29);
|
--ring: oklch(0.705 0.213 47.604);
|
||||||
--chart-2: oklch(0.705 0.213 47.604);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-3: oklch(0.646 0.222 41.116);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-4: oklch(0.553 0.195 38.402);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
--chart-5: oklch(0.47 0.157 37.304);
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--radius: 0.75rem;
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
--sidebar-primary: oklch(0.705 0.213 47.604);
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.705 0.213 47.604);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.160 0 0);
|
--background: oklch(0.17 0.006 285.885);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.21 0.006 285.885);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.6734 0.195 41.36);
|
--primary: oklch(0.6717 0.1946 41.93);
|
||||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
--accent: oklch(0.371 0 0);
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.5382 0.1949 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 13%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.6734 0.195 41.36);
|
--ring: oklch(0.646 0.222 41.116);
|
||||||
--chart-1: oklch(0.837 0.128 66.29);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.705 0.213 47.604);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.646 0.222 41.116);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--chart-4: oklch(0.553 0.195 38.402);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-5: oklch(0.47 0.157 37.304);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
--sidebar-primary: oklch(0.646 0.222 41.116);
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.646 0.222 41.116);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: 0 1px 1px rgba(0, 0, 0, 0.03);
|
||||||
|
--inset-shadow-2xs: inset 0 1px 1px rgba(0, 0, 1, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
*,
|
||||||
@apply border-border outline-ring/50;
|
::after,
|
||||||
}
|
::before,
|
||||||
body {
|
::backdrop,
|
||||||
@apply bg-background text-foreground;
|
::file-selector-button {
|
||||||
}
|
border-color: var(--color-gray-200, currentcolor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
word-break: keep-all;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
background: var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,11 +96,6 @@ export const orgNavSections = (): SidebarNavSection[] => [
|
|||||||
title: "sidebarDomains",
|
title: "sidebarDomains",
|
||||||
href: "/{orgId}/settings/domains",
|
href: "/{orgId}/settings/domains",
|
||||||
icon: <Globe className="size-4 flex-none" />
|
icon: <Globe className="size-4 flex-none" />
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "sidebarBluePrints",
|
|
||||||
href: "/{orgId}/settings/blueprints",
|
|
||||||
icon: <ReceiptText className="size-4 flex-none" />
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -200,6 +195,17 @@ export const orgNavSections = (): SidebarNavSection[] => [
|
|||||||
href: "/{orgId}/settings/api-keys",
|
href: "/{orgId}/settings/api-keys",
|
||||||
icon: <KeyRound className="size-4 flex-none" />
|
icon: <KeyRound className="size-4 flex-none" />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "sidebarBluePrints",
|
||||||
|
href: "/{orgId}/settings/blueprints",
|
||||||
|
icon: <ReceiptText className="size-4 flex-none" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "sidebarSettings",
|
||||||
|
href: "/{orgId}/settings/general",
|
||||||
|
icon: <Settings className="size-4 flex-none" />
|
||||||
|
},
|
||||||
|
|
||||||
...(build == "saas"
|
...(build == "saas"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -217,12 +223,7 @@ export const orgNavSections = (): SidebarNavSection[] => [
|
|||||||
icon: <TicketCheck className="size-4 flex-none" />
|
icon: <TicketCheck className="size-4 flex-none" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: [])
|
||||||
{
|
|
||||||
title: "sidebarSettings",
|
|
||||||
href: "/{orgId}/settings/general",
|
|
||||||
icon: <Settings className="size-4 flex-none" />
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { UsersDataTable } from "@app/components/AdminUsersDataTable";
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
@@ -72,6 +72,11 @@ export default function UsersTable({ users }: Props) {
|
|||||||
useState<AdminGeneratePasswordResetCodeResponse | null>(null);
|
useState<AdminGeneratePasswordResetCodeResponse | null>(null);
|
||||||
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
||||||
|
|
||||||
|
// Update local state when props change (e.g., after refresh)
|
||||||
|
useEffect(() => {
|
||||||
|
setRows(users);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
console.log("Data refreshed");
|
console.log("Data refreshed");
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|||||||
@@ -52,8 +52,14 @@ export default function BlueprintDetailsForm({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription className="space-y-2">
|
||||||
<InfoSections cols={3}>
|
<InfoSections cols={4}>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>{t("name")}</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{blueprint.name}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("status")}
|
{t("status")}
|
||||||
@@ -121,6 +127,8 @@ export default function BlueprintDetailsForm({
|
|||||||
</time>
|
</time>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
<InfoSections cols={1}>
|
||||||
{blueprint.message && (
|
{blueprint.message && (
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
@@ -138,60 +146,39 @@ export default function BlueprintDetailsForm({
|
|||||||
</Alert>
|
</Alert>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
{t("blueprintInfo")}
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm className="max-w-2xl">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="contents"
|
||||||
name="name"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>
|
||||||
<FormLabel>{t("name")}</FormLabel>
|
{t("parsedContents")}
|
||||||
<FormControl>
|
</FormLabel>
|
||||||
<Input {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<div
|
||||||
<FormMessage />
|
className={cn(
|
||||||
</FormItem>
|
"resize-y h-64 min-h-128 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
|
<Editor
|
||||||
<FormField
|
className="w-full h-full max-w-full"
|
||||||
control={form.control}
|
language="yaml"
|
||||||
name="contents"
|
theme="vs-dark"
|
||||||
render={({ field }) => (
|
options={{
|
||||||
<FormItem>
|
minimap: {
|
||||||
<FormLabel>
|
enabled: false
|
||||||
{t("parsedContents")}
|
},
|
||||||
</FormLabel>
|
readOnly: true
|
||||||
<FormControl>
|
}}
|
||||||
<div
|
{...field}
|
||||||
className={cn(
|
/>
|
||||||
"resize-y h-64 min-h-64 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
</div>
|
||||||
)}
|
</FormControl>
|
||||||
>
|
<FormMessage />
|
||||||
<Editor
|
</FormItem>
|
||||||
className="w-full h-full max-w-full"
|
)}
|
||||||
language="yaml"
|
/>
|
||||||
theme="vs-dark"
|
|
||||||
options={{
|
|
||||||
minimap: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
readOnly: true
|
|
||||||
}}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|||||||
@@ -32,35 +32,6 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const columns: ExtendedColumnDef<BlueprintRow>[] = [
|
const columns: ExtendedColumnDef<BlueprintRow>[] = [
|
||||||
{
|
|
||||||
accessorKey: "createdAt",
|
|
||||||
friendlyName: t("appliedAt"),
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("appliedAt")}
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<time
|
|
||||||
className="text-muted-foreground"
|
|
||||||
dateTime={row.original.createdAt.toString()}
|
|
||||||
>
|
|
||||||
{new Date(
|
|
||||||
row.original.createdAt * 1000
|
|
||||||
).toLocaleString()}
|
|
||||||
</time>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@@ -79,7 +50,32 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
friendlyName: t("appliedAt"),
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("appliedAt")}
|
||||||
|
<ArrowUpDown className="ml-2 size-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<time dateTime={row.original.createdAt.toString()}>
|
||||||
|
{new Date(
|
||||||
|
row.original.createdAt * 1000
|
||||||
|
).toLocaleString()}
|
||||||
|
</time>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "source",
|
accessorKey: "source",
|
||||||
friendlyName: t("source"),
|
friendlyName: t("source"),
|
||||||
@@ -104,7 +100,7 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
<span className="inline-flex items-center gap-1 ">
|
<span className="inline-flex items-center gap-1 ">
|
||||||
API
|
API
|
||||||
<Webhook className="size-4 flex-none" />
|
<Webhook className="w-3 h-3 flex-none" />
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -114,7 +110,7 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
<span className="inline-flex items-center gap-1 ">
|
<span className="inline-flex items-center gap-1 ">
|
||||||
Newt CLI
|
Newt CLI
|
||||||
<Terminal className="size-4 flex-none" />
|
<Terminal className="w-3 h-3 flex-none" />
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -174,7 +170,7 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
|||||||
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`}
|
||||||
>
|
>
|
||||||
<Button variant="outline" className="items-center">
|
<Button variant="outline" className="items-center">
|
||||||
View Details
|
{t("blueprintViewDetails")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function ClientResourcesTable({
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await api
|
await api
|
||||||
.delete(`/org/${orgId}/site/${siteId}/resource/${resourceId}`)
|
.delete(`/site-resource/${resourceId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -327,6 +327,7 @@ export default function ClientResourcesTable({
|
|||||||
setOpen={setIsEditDialogOpen}
|
setOpen={setIsEditDialogOpen}
|
||||||
resource={editingResource}
|
resource={editingResource}
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
|
sites={sites}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
setEditingResource(null);
|
setEditingResource(null);
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default function CreateBlueprintForm({
|
|||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm className="max-w-2xl">
|
<SettingsSectionForm>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -141,44 +141,40 @@ export default function CreateBlueprintForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="contents"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("contents")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"blueprintContentsDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"resize-y h-64 min-h-64 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
className="w-full h-full max-w-full"
|
|
||||||
language="yaml"
|
|
||||||
theme="vs-dark"
|
|
||||||
options={{
|
|
||||||
minimap: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="contents"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("contents")}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("blueprintContentsDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"resize-y h-64 min-h-128 overflow-y-auto overflow-x-clip max-w-full rounded-md"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
className="w-full h-full max-w-full"
|
||||||
|
language="yaml"
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
|||||||
@@ -395,9 +395,10 @@ export default function CreateInternalResourceDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.put<AxiosResponse<any>>(
|
const response = await api.put<AxiosResponse<any>>(
|
||||||
`/org/${orgId}/site/${data.siteId}/resource`,
|
`/org/${orgId}/site-resource`,
|
||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
siteId: data.siteId,
|
||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
// protocol: data.protocol,
|
// protocol: data.protocol,
|
||||||
// proxyPort: data.mode === "port" ? data.proxyPort : undefined,
|
// proxyPort: data.mode === "port" ? data.proxyPort : undefined,
|
||||||
@@ -548,7 +549,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"createInternalResourceDialogSite"
|
"site"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover>
|
<Popover>
|
||||||
@@ -572,7 +573,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
field.value
|
field.value
|
||||||
)?.name
|
)?.name
|
||||||
: t(
|
: t(
|
||||||
"createInternalResourceDialogSelectSite"
|
"selectSite"
|
||||||
)}
|
)}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -582,13 +583,13 @@ export default function CreateInternalResourceDialog({
|
|||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"createInternalResourceDialogSearchSites"
|
"searchSites"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{t(
|
{t(
|
||||||
"createInternalResourceDialogNoSitesFound"
|
"noSitesFound"
|
||||||
)}
|
)}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
|
|||||||
@@ -41,6 +41,22 @@ import { Tag, TagInput } from "@app/components/tags/tag-input";
|
|||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from "@app/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { ListSitesResponse } from "@server/routers/site";
|
||||||
|
import { Check, ChevronsUpDown } from "lucide-react";
|
||||||
// import { InfoPopup } from "@app/components/ui/info-popup";
|
// import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
|
||||||
// Helper to validate port range string format
|
// Helper to validate port range string format
|
||||||
@@ -118,6 +134,8 @@ const getPortStringFromMode = (mode: PortMode, customValue: string): string | un
|
|||||||
return customValue;
|
return customValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Site = ListSitesResponse["sites"][0];
|
||||||
|
|
||||||
type InternalResourceData = {
|
type InternalResourceData = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -141,6 +159,7 @@ type EditInternalResourceDialogProps = {
|
|||||||
setOpen: (val: boolean) => void;
|
setOpen: (val: boolean) => void;
|
||||||
resource: InternalResourceData;
|
resource: InternalResourceData;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
sites: Site[];
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,6 +168,7 @@ export default function EditInternalResourceDialog({
|
|||||||
setOpen,
|
setOpen,
|
||||||
resource,
|
resource,
|
||||||
orgId,
|
orgId,
|
||||||
|
sites,
|
||||||
onSuccess
|
onSuccess
|
||||||
}: EditInternalResourceDialogProps) {
|
}: EditInternalResourceDialogProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -161,6 +181,7 @@ export default function EditInternalResourceDialog({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, t("editInternalResourceDialogNameRequired"))
|
.min(1, t("editInternalResourceDialogNameRequired"))
|
||||||
.max(255, t("editInternalResourceDialogNameMaxLength")),
|
.max(255, t("editInternalResourceDialogNameMaxLength")),
|
||||||
|
siteId: z.number().int().positive(),
|
||||||
mode: z.enum(["host", "cidr", "port"]),
|
mode: z.enum(["host", "cidr", "port"]),
|
||||||
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||||
// proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
|
// proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
|
||||||
@@ -349,10 +370,15 @@ export default function EditInternalResourceDialog({
|
|||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const availableSites = sites.filter(
|
||||||
|
(site) => site.type === "newt" && site.subnet
|
||||||
|
);
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
|
siteId: resource.siteId,
|
||||||
mode: resource.mode || "host",
|
mode: resource.mode || "host",
|
||||||
// protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
|
// protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
|
||||||
// proxyPort: resource.proxyPort ?? undefined,
|
// proxyPort: resource.proxyPort ?? undefined,
|
||||||
@@ -421,9 +447,10 @@ export default function EditInternalResourceDialog({
|
|||||||
|
|
||||||
// Update the site resource
|
// Update the site resource
|
||||||
await api.post(
|
await api.post(
|
||||||
`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`,
|
`/site-resource/${resource.id}`,
|
||||||
{
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
siteId: data.siteId,
|
||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
// protocol: data.mode === "port" ? data.protocol : null,
|
// protocol: data.mode === "port" ? data.protocol : null,
|
||||||
// proxyPort: data.mode === "port" ? data.proxyPort : null,
|
// proxyPort: data.mode === "port" ? data.proxyPort : null,
|
||||||
@@ -504,6 +531,7 @@ export default function EditInternalResourceDialog({
|
|||||||
if (resourceChanged) {
|
if (resourceChanged) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
|
siteId: resource.siteId,
|
||||||
mode: resource.mode || "host",
|
mode: resource.mode || "host",
|
||||||
destination: resource.destination || "",
|
destination: resource.destination || "",
|
||||||
alias: resource.alias ?? null,
|
alias: resource.alias ?? null,
|
||||||
@@ -559,6 +587,7 @@ export default function EditInternalResourceDialog({
|
|||||||
// reset only on close
|
// reset only on close
|
||||||
form.reset({
|
form.reset({
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
|
siteId: resource.siteId,
|
||||||
mode: resource.mode || "host",
|
mode: resource.mode || "host",
|
||||||
// protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
|
// protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
|
||||||
// proxyPort: resource.proxyPort ?? undefined,
|
// proxyPort: resource.proxyPort ?? undefined,
|
||||||
@@ -636,6 +665,99 @@ export default function EditInternalResourceDialog({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="siteId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"site"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between",
|
||||||
|
!field.value &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? availableSites.find(
|
||||||
|
(
|
||||||
|
site
|
||||||
|
) =>
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
)?.name
|
||||||
|
: t(
|
||||||
|
"selectSite"
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={t(
|
||||||
|
"searchSites"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
{t(
|
||||||
|
"noSitesFound"
|
||||||
|
)}
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{availableSites.map(
|
||||||
|
(
|
||||||
|
site
|
||||||
|
) => (
|
||||||
|
<CommandItem
|
||||||
|
key={
|
||||||
|
site.siteId
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
site.name
|
||||||
|
}
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(
|
||||||
|
site.siteId
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
field.value ===
|
||||||
|
site.siteId
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
site.name
|
||||||
|
}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="mode"
|
name="mode"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function LayoutSidebar({
|
|||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
|
"flex items-center transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md",
|
||||||
isSidebarCollapsed
|
isSidebarCollapsed
|
||||||
? "px-2 py-2 justify-center"
|
? "px-2 py-2 justify-center"
|
||||||
: "px-3 py-1.5"
|
: "px-3 py-1.5"
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ function ProductUpdatesListPopup({
|
|||||||
{t("productUpdateWhatsNew")}
|
{t("productUpdateWhatsNew")}
|
||||||
</p>
|
</p>
|
||||||
<div className="p-1 cursor-pointer">
|
<div className="p-1 cursor-pointer">
|
||||||
<ChevronRightIcon className="size-4 flex-none" />
|
<ChevronRightIcon className="size-4 flex-none opacity-50" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ function CollapsibleNavItem({
|
|||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center w-full rounded transition-colors hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
|
"flex items-center w-full rounded transition-colors hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md",
|
||||||
level === 0 ? "p-3 py-1.5" : "py-1.5",
|
level === 0 ? "p-3 py-1.5" : "py-1.5",
|
||||||
isChildActive
|
isChildActive
|
||||||
? "text-primary font-medium"
|
? "text-primary font-medium"
|
||||||
@@ -252,7 +252,7 @@ export function SidebarNav({
|
|||||||
<Link
|
<Link
|
||||||
href={isDisabled ? "#" : hydratedHref}
|
href={isDisabled ? "#" : hydratedHref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center rounded transition-colors hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md",
|
"flex items-center rounded transition-colors hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md",
|
||||||
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
|
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
|
||||||
isActive
|
isActive
|
||||||
? "text-primary font-medium"
|
? "text-primary font-medium"
|
||||||
@@ -338,7 +338,7 @@ export function SidebarNav({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center rounded transition-colors hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md px-2 py-2 justify-center w-full",
|
"flex items-center rounded transition-colors hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md px-2 py-2 justify-center w-full",
|
||||||
isChildActive
|
isChildActive
|
||||||
? "text-primary font-medium"
|
? "text-primary font-medium"
|
||||||
: "text-muted-foreground hover:text-foreground",
|
: "text-muted-foreground hover:text-foreground",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
|
import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
|
||||||
import { UsersDataTable } from "@app/components/UsersDataTable";
|
import { UsersDataTable } from "@app/components/UsersDataTable";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
@@ -54,6 +54,11 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
// Update local state when props change (e.g., after refresh)
|
||||||
|
useEffect(() => {
|
||||||
|
setUsers(u);
|
||||||
|
}, [u]);
|
||||||
|
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
console.log("Data refreshed");
|
console.log("Data refreshed");
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user