mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-09 23:49:47 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92775a51ca | |||
| ea60aa701e | |||
| dbb1e7b274 | |||
| b2d1a66279 | |||
| 202466792c | |||
| 4dcf684868 | |||
| 2b8f50af6f | |||
| a8c255ef71 | |||
| d9608bd408 | |||
| be193731c1 | |||
| 0c3edc7560 |
+11
-1
@@ -615,7 +615,8 @@
|
|||||||
"idpNameInternal": "Internal",
|
"idpNameInternal": "Internal",
|
||||||
"emailInvalid": "Invalid email address",
|
"emailInvalid": "Invalid email address",
|
||||||
"inviteValidityDuration": "Please select a duration",
|
"inviteValidityDuration": "Please select a duration",
|
||||||
"accessRoleSelectPlease": "Please select a role",
|
"accessRoleSelectPlease": "A user must belong to at least one role.",
|
||||||
|
"accessRoleRequired": "Role required",
|
||||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||||
@@ -1433,6 +1434,15 @@
|
|||||||
"actionSetResourcePincode": "Set Resource Pincode",
|
"actionSetResourcePincode": "Set Resource Pincode",
|
||||||
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
||||||
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
||||||
|
"actionGetResourcePolicy": "Get Resource Policy",
|
||||||
|
"actionUpdateResourcePolicy": "Update Resource Policy",
|
||||||
|
"actionSetResourcePolicyUsers": "Set Resource Policy Users",
|
||||||
|
"actionSetResourcePolicyRoles": "Set Resource Policy Roles",
|
||||||
|
"actionSetResourcePolicyPassword": "Set Resource Policy Password",
|
||||||
|
"actionSetResourcePolicyPincode": "Set Resource Policy Pincode",
|
||||||
|
"actionSetResourcePolicyHeaderAuth": "Set Resource Policy Header Authentication",
|
||||||
|
"actionSetResourcePolicyWhitelist": "Set Resource Policy Email Whitelist",
|
||||||
|
"actionSetResourcePolicyRules": "Set Resource Policy Rules",
|
||||||
"actionCreateTarget": "Create Target",
|
"actionCreateTarget": "Create Target",
|
||||||
"actionDeleteTarget": "Delete Target",
|
"actionDeleteTarget": "Delete Target",
|
||||||
"actionGetTarget": "Get Target",
|
"actionGetTarget": "Get Target",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.19.0";
|
export const APP_VERSION = "1.20.0";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -895,12 +895,6 @@ authenticated.delete(
|
|||||||
user.removeUserOrg
|
user.removeUserOrg
|
||||||
);
|
);
|
||||||
|
|
||||||
// authenticated.put(
|
|
||||||
// "/newt",
|
|
||||||
// verifyApiKeyHasAction(ActionsEnum.createNewt),
|
|
||||||
// newt.createNewt
|
|
||||||
// );
|
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
`/org/:orgId/api-keys`,
|
`/org/:orgId/api-keys`,
|
||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { alias, db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import {
|
import {
|
||||||
exitNodes,
|
exitNodes,
|
||||||
labels,
|
labels,
|
||||||
launcherViews,
|
launcherViews,
|
||||||
resourceLabels,
|
resourceLabels,
|
||||||
resourcePolicies,
|
|
||||||
resources,
|
resources,
|
||||||
rolePolicies,
|
rolePolicies,
|
||||||
roleResources,
|
roleResources,
|
||||||
@@ -32,7 +31,6 @@ import {
|
|||||||
inArray,
|
inArray,
|
||||||
isNull,
|
isNull,
|
||||||
like,
|
like,
|
||||||
not,
|
|
||||||
or,
|
or,
|
||||||
sql,
|
sql,
|
||||||
type SQL
|
type SQL
|
||||||
@@ -136,48 +134,6 @@ function launcherAccessibleIdsCacheKey(
|
|||||||
return `launcherAccessibleIds:${orgId}:${userId}:${rolesKey}`;
|
return `launcherAccessibleIds:${orgId}:${userId}:${rolesKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOrgNonSsoPublicResourceIds(
|
|
||||||
orgId: string
|
|
||||||
): Promise<number[]> {
|
|
||||||
const sharedPolicy = alias(resourcePolicies, "sharedPolicy");
|
|
||||||
const defaultPolicy = alias(resourcePolicies, "defaultPolicy");
|
|
||||||
|
|
||||||
const effectiveSso = sql<boolean>`
|
|
||||||
COALESCE(
|
|
||||||
CASE
|
|
||||||
WHEN ${sharedPolicy.resourcePolicyId} IS NOT NULL THEN ${sharedPolicy.sso}
|
|
||||||
ELSE ${defaultPolicy.sso}
|
|
||||||
END,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const rows = await db
|
|
||||||
.select({ resourceId: resources.resourceId })
|
|
||||||
.from(resources)
|
|
||||||
.leftJoin(
|
|
||||||
sharedPolicy,
|
|
||||||
eq(sharedPolicy.resourcePolicyId, resources.resourcePolicyId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
defaultPolicy,
|
|
||||||
eq(
|
|
||||||
defaultPolicy.resourcePolicyId,
|
|
||||||
resources.defaultResourcePolicyId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(resources.orgId, orgId),
|
|
||||||
eq(resources.enabled, true),
|
|
||||||
eq(resources.blockAccess, false),
|
|
||||||
not(effectiveSso)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return rows.map((row) => row.resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveAccessibleIdsUncached(
|
async function resolveAccessibleIdsUncached(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -189,8 +145,7 @@ async function resolveAccessibleIdsUncached(
|
|||||||
directPolicyResourceResults,
|
directPolicyResourceResults,
|
||||||
rolePolicyResourceResults,
|
rolePolicyResourceResults,
|
||||||
directSiteResourceResults,
|
directSiteResourceResults,
|
||||||
roleSiteResourceResults,
|
roleSiteResourceResults
|
||||||
orgNonSsoPublicResources
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({ resourceId: userResources.resourceId })
|
.select({ resourceId: userResources.resourceId })
|
||||||
@@ -259,8 +214,7 @@ async function resolveAccessibleIdsUncached(
|
|||||||
})
|
})
|
||||||
.from(roleSiteResources)
|
.from(roleSiteResources)
|
||||||
.where(inArray(roleSiteResources.roleId, userRoleIds))
|
.where(inArray(roleSiteResources.roleId, userRoleIds))
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([])
|
||||||
fetchOrgNonSsoPublicResourceIds(orgId)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -269,8 +223,7 @@ async function resolveAccessibleIdsUncached(
|
|||||||
...directResources.map((r) => r.resourceId),
|
...directResources.map((r) => r.resourceId),
|
||||||
...roleResourceResults.map((r) => r.resourceId),
|
...roleResourceResults.map((r) => r.resourceId),
|
||||||
...directPolicyResourceResults.map((r) => r.resourceId),
|
...directPolicyResourceResults.map((r) => r.resourceId),
|
||||||
...rolePolicyResourceResults.map((r) => r.resourceId),
|
...rolePolicyResourceResults.map((r) => r.resourceId)
|
||||||
...orgNonSsoPublicResources
|
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
siteResourceIds: Array.from(
|
siteResourceIds: Array.from(
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ const createSiteResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
data.destination !== undefined && data.destination.trim() !== ""
|
data.destination !== undefined &&
|
||||||
|
data.destination?.trim() !== ""
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ const updateSiteResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
data.destination !== undefined && data.destination.trim() !== ""
|
data.destination !== undefined &&
|
||||||
|
data.destination?.trim() !== ""
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -616,10 +617,7 @@ export async function updateSiteResource(
|
|||||||
await trx
|
await trx
|
||||||
.delete(roleSiteResources)
|
.delete(roleSiteResources)
|
||||||
.where(
|
.where(
|
||||||
eq(
|
eq(roleSiteResources.siteResourceId, siteResourceId)
|
||||||
roleSiteResources.siteResourceId,
|
|
||||||
siteResourceId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import m17 from "./scriptsPg/1.18.0";
|
|||||||
import m18 from "./scriptsPg/1.18.3";
|
import m18 from "./scriptsPg/1.18.3";
|
||||||
import m19 from "./scriptsPg/1.18.4";
|
import m19 from "./scriptsPg/1.18.4";
|
||||||
import m20 from "./scriptsPg/1.19.0";
|
import m20 from "./scriptsPg/1.19.0";
|
||||||
|
import m21 from "./scriptsPg/1.20.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -51,7 +52,8 @@ const migrations = [
|
|||||||
{ version: "1.18.0", run: m17 },
|
{ version: "1.18.0", run: m17 },
|
||||||
{ version: "1.18.3", run: m18 },
|
{ version: "1.18.3", run: m18 },
|
||||||
{ version: "1.18.4", run: m19 },
|
{ version: "1.18.4", run: m19 },
|
||||||
{ version: "1.19.0", run: m20 }
|
{ version: "1.19.0", run: m20 },
|
||||||
|
{ version: "1.20.0", run: m21 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import m39 from "./scriptsSqlite/1.18.3";
|
|||||||
import m40 from "./scriptsSqlite/1.18.4";
|
import m40 from "./scriptsSqlite/1.18.4";
|
||||||
import m41 from "./scriptsSqlite/1.19.0";
|
import m41 from "./scriptsSqlite/1.19.0";
|
||||||
import m42 from "./scriptsSqlite/1.19.1";
|
import m42 from "./scriptsSqlite/1.19.1";
|
||||||
import m43 from "./scriptsSqlite/1.19.5";
|
import m43 from "./scriptsSqlite/1.20.0";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -89,7 +89,7 @@ const migrations = [
|
|||||||
{ version: "1.18.4", run: m40 },
|
{ version: "1.18.4", run: m40 },
|
||||||
{ version: "1.19.0", run: m41 },
|
{ version: "1.19.0", run: m41 },
|
||||||
{ version: "1.19.1", run: m42 },
|
{ version: "1.19.1", run: m42 },
|
||||||
{ version: "1.19.5", run: m43 }
|
{ version: "1.20.0", run: m43 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
const version = "1.20.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE "remoteExitNodePreferenceLabels" (
|
||||||
|
"remoteExitNodePreferenceLabelId" serial PRIMARY KEY NOT NULL,
|
||||||
|
"remoteExitNodeId" varchar NOT NULL,
|
||||||
|
"labelId" integer NOT NULL,
|
||||||
|
CONSTRAINT "remote_exit_node_preference_label_uniq" UNIQUE("remoteExitNodeId","labelId")
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE "remoteExitNodeResources" (
|
||||||
|
"remoteExitNodeResourceId" serial PRIMARY KEY NOT NULL,
|
||||||
|
"remoteExitNodeId" varchar NOT NULL,
|
||||||
|
"destination" varchar NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE "launcherViews" (
|
||||||
|
"viewId" serial PRIMARY KEY NOT NULL,
|
||||||
|
"orgId" varchar NOT NULL,
|
||||||
|
"userId" varchar,
|
||||||
|
"name" varchar NOT NULL,
|
||||||
|
"config" text NOT NULL,
|
||||||
|
"isDefault" boolean DEFAULT false NOT NULL,
|
||||||
|
"createdAt" varchar NOT NULL,
|
||||||
|
"updatedAt" varchar NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "domains" ADD COLUMN "lastCheckedAt" integer;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "remoteExitNodePreferenceLabels" ADD CONSTRAINT "remoteExitNodePreferenceLabels_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "remoteExitNodePreferenceLabels" ADD CONSTRAINT "remoteExitNodePreferenceLabels_labelId_labels_labelId_fk" FOREIGN KEY ("labelId") REFERENCES "public"."labels"("labelId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "remoteExitNodeResources" ADD CONSTRAINT "remoteExitNodeResources_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "launcherViews" ADD CONSTRAINT "launcherViews_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "launcherViews" ADD CONSTRAINT "launcherViews_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_clients_orgid_niceid" ON "clients" USING btree ("orgId","niceId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_networks_orgid" ON "networks" USING btree ("orgId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_resourcepolicies_orgid_niceid" ON "resourcePolicies" USING btree ("orgId","niceId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_resources_niceid" ON "resources" USING btree ("niceId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_resources_orgid_niceid" ON "resources" USING btree ("orgId","niceId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_siteresources_orgid_niceid" ON "siteResources" USING btree ("orgId","niceId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX "idx_sites_orgid_niceid" ON "sites" USING btree ("orgId","niceId");
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
|
|||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const version = "1.19.5";
|
const version = "1.20.0";
|
||||||
|
|
||||||
export default async function migration() {
|
export default async function migration() {
|
||||||
console.log(`Running setup script ${version}...`);
|
console.log(`Running setup script ${version}...`);
|
||||||
@@ -92,6 +92,64 @@ export default async function migration() {
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
`ALTER TABLE 'resourceSessions_new' RENAME TO 'resourceSessions';`
|
`ALTER TABLE 'resourceSessions_new' RENAME TO 'resourceSessions';`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE 'remoteExitNodePreferenceLabels' (
|
||||||
|
'remoteExitNodePreferenceLabelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
'remoteExitNodeId' text NOT NULL,
|
||||||
|
'labelId' integer NOT NULL,
|
||||||
|
FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY ('labelId') REFERENCES 'labels'('labelId') ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX 'remote_exit_node_preference_label_uniq' ON 'remoteExitNodePreferenceLabels' ('remoteExitNodeId','labelId');
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE 'remoteExitNodeResources' (
|
||||||
|
'remoteExitNodeResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
'remoteExitNodeId' text NOT NULL,
|
||||||
|
'destination' text NOT NULL,
|
||||||
|
FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE 'launcherViews' (
|
||||||
|
'viewId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
'orgId' text NOT NULL,
|
||||||
|
'userId' text,
|
||||||
|
'name' text NOT NULL,
|
||||||
|
'config' text NOT NULL,
|
||||||
|
'isDefault' integer DEFAULT false NOT NULL,
|
||||||
|
'createdAt' text NOT NULL,
|
||||||
|
'updatedAt' text NOT NULL,
|
||||||
|
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'domains' ADD 'customCertResolver' text;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'domains' ADD 'lastCheckedAt' integer;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
db.pragma("foreign_keys = ON");
|
db.pragma("foreign_keys = ON");
|
||||||
@@ -111,7 +111,7 @@ export default function AccessControlsPage() {
|
|||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("accessRoleErrorAdd"),
|
title: t("accessRoleRequired"),
|
||||||
description: t("accessRoleSelectPlease")
|
description: t("accessRoleSelectPlease")
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -173,15 +173,13 @@ export default function AccessControlsPage() {
|
|||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("accessRoleErrorAdd"),
|
title: t("accessRoleRequired"),
|
||||||
description: t("accessRoleSelectPlease")
|
description: t("accessRoleSelectPlease")
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const willHaveAdminRole = values.roles.some(
|
const willHaveAdminRole = values.roles.some((r) => r.isAdmin === true);
|
||||||
(r) => r.isAdmin === true
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRemovingOwnAdmin =
|
const isRemovingOwnAdmin =
|
||||||
sessionUser.userId === user.userId &&
|
sessionUser.userId === user.userId &&
|
||||||
@@ -226,7 +224,9 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => void handleAccessControlsSubmit(e)}
|
onSubmit={(e) =>
|
||||||
|
void handleAccessControlsSubmit(e)
|
||||||
|
}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -195,9 +195,9 @@ export default function GeneralPage() {
|
|||||||
if (agent in latestPlatformVersions) {
|
if (agent in latestPlatformVersions) {
|
||||||
const agentVersion = latestPlatformVersions[agent];
|
const agentVersion = latestPlatformVersions[agent];
|
||||||
|
|
||||||
updateAvailable = semver.lt(
|
updateAvailable = Boolean(
|
||||||
client.olmVersion,
|
semver.valid(client.olmVersion) &&
|
||||||
agentVersion.latestVersion
|
semver.lt(client.olmVersion, agentVersion.latestVersion)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export function CreateOrgLabelDialog({
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const canManageLabels = isPaidUser(tierMatrix.labels);
|
|
||||||
const [isSubmitting, startTransition] = useTransition();
|
const [isSubmitting, startTransition] = useTransition();
|
||||||
|
|
||||||
async function createOrgLabel(data: { name: string; color: string }) {
|
async function createOrgLabel(data: { name: string; color: string }) {
|
||||||
@@ -84,11 +83,8 @@ export function CreateOrgLabelDialog({
|
|||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
|
||||||
<OrgLabelForm
|
<OrgLabelForm
|
||||||
disabled={!canManageLabels}
|
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
if (!canManageLabels) return;
|
|
||||||
startTransition(async () => createOrgLabel(data));
|
startTransition(async () => createOrgLabel(data));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -106,7 +102,7 @@ export function CreateOrgLabelDialog({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="org-label-form"
|
form="org-label-form"
|
||||||
disabled={isSubmitting || !canManageLabels}
|
disabled={isSubmitting}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{t("labelCreate")}
|
{t("labelCreate")}
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export function EditOrgLabelDialog({
|
|||||||
}: EditOrgLabelDialogProps) {
|
}: EditOrgLabelDialogProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const { isPaidUser } = usePaidStatus();
|
|
||||||
const canManageLabels = isPaidUser(tierMatrix.labels);
|
|
||||||
const [isSubmitting, startTransition] = useTransition();
|
const [isSubmitting, startTransition] = useTransition();
|
||||||
|
|
||||||
async function editOrgLabel(data: { name: string; color: string }) {
|
async function editOrgLabel(data: { name: string; color: string }) {
|
||||||
@@ -90,12 +88,9 @@ export function EditOrgLabelDialog({
|
|||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
|
||||||
<OrgLabelForm
|
<OrgLabelForm
|
||||||
disabled={!canManageLabels}
|
|
||||||
defaultValue={label}
|
defaultValue={label}
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
if (!canManageLabels) return;
|
|
||||||
startTransition(async () => editOrgLabel(data));
|
startTransition(async () => editOrgLabel(data));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +108,7 @@ export function EditOrgLabelDialog({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="org-label-form"
|
form="org-label-form"
|
||||||
disabled={isSubmitting || !canManageLabels}
|
disabled={isSubmitting}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{t("labelEdit")}
|
{t("labelEdit")}
|
||||||
|
|||||||
@@ -404,9 +404,12 @@ export default function MachineClientsTable({
|
|||||||
if (agent in latestPlatformVersions) {
|
if (agent in latestPlatformVersions) {
|
||||||
const agentVersion = latestPlatformVersions[agent];
|
const agentVersion = latestPlatformVersions[agent];
|
||||||
|
|
||||||
updateAvailable = semver.lt(
|
updateAvailable = Boolean(
|
||||||
originalRow.olmVersion,
|
semver.valid(originalRow.olmVersion) &&
|
||||||
agentVersion.latestVersion
|
semver.lt(
|
||||||
|
originalRow.olmVersion,
|
||||||
|
agentVersion.latestVersion
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||||
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
||||||
|
|
||||||
@@ -41,6 +42,46 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
|||||||
disabled
|
disabled
|
||||||
}: OrgRolesTagFieldProps<TFieldValues>) {
|
}: OrgRolesTagFieldProps<TFieldValues>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const isPopoverOpenRef = useRef(false);
|
||||||
|
const lastValidRolesRef = useRef<SelectedRole[]>(
|
||||||
|
(form.getValues(name) as SelectedRole[]) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
function validateRolesSelection() {
|
||||||
|
const current = form.getValues(name) as SelectedRole[];
|
||||||
|
|
||||||
|
if (current.length === 0 && lastValidRolesRef.current.length > 0) {
|
||||||
|
form.setValue(name, lastValidRolesRef.current as never, {
|
||||||
|
shouldDirty: true
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleRequired"),
|
||||||
|
description: t("accessRoleSelectPlease")
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.length > 0) {
|
||||||
|
lastValidRolesRef.current = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopoverOpenChange(open: boolean) {
|
||||||
|
isPopoverOpenRef.current = open;
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
const current = form.getValues(name) as SelectedRole[];
|
||||||
|
if (current.length > 0) {
|
||||||
|
lastValidRolesRef.current = current;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRolesSelection();
|
||||||
|
}
|
||||||
|
|
||||||
function setRoleTags(nextValue: SelectedRole[]) {
|
function setRoleTags(nextValue: SelectedRole[]) {
|
||||||
const prev = form.getValues(name) as SelectedRole[];
|
const prev = form.getValues(name) as SelectedRole[];
|
||||||
@@ -61,39 +102,44 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.length === 0) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("accessRoleErrorAdd"),
|
|
||||||
description: t("accessRoleSelectPlease")
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
form.setValue(name, next as never, { shouldDirty: true });
|
form.setValue(name, next as never, { shouldDirty: true });
|
||||||
|
|
||||||
|
if (next.length > 0 && !isPopoverOpenRef.current) {
|
||||||
|
lastValidRolesRef.current = next;
|
||||||
|
} else if (!isPopoverOpenRef.current) {
|
||||||
|
validateRolesSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem className="flex flex-col items-start">
|
const selectedRoles = (field.value ?? []) as SelectedRole[];
|
||||||
<FormLabel>{label ?? t("roles")}</FormLabel>
|
if (!isPopoverOpenRef.current && selectedRoles.length > 0) {
|
||||||
<FormControl>
|
lastValidRolesRef.current = selectedRoles;
|
||||||
<RolesSelector
|
}
|
||||||
orgId={orgId}
|
|
||||||
selectedRoles={field.value ?? []}
|
return (
|
||||||
onSelectRoles={setRoleTags}
|
<FormItem className="flex flex-col items-start">
|
||||||
disabled={disabled}
|
<FormLabel>{label ?? t("roles")}</FormLabel>
|
||||||
/>
|
<FormControl>
|
||||||
</FormControl>
|
<RolesSelector
|
||||||
{showMultiRolePaywallMessage && (
|
orgId={orgId}
|
||||||
<FormDescription>{paywallMessage}</FormDescription>
|
selectedRoles={selectedRoles}
|
||||||
)}
|
onSelectRoles={setRoleTags}
|
||||||
<FormMessage />
|
onPopoverOpenChange={handlePopoverOpenChange}
|
||||||
</FormItem>
|
disabled={disabled}
|
||||||
)}
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{showMultiRolePaywallMessage && (
|
||||||
|
<FormDescription>{paywallMessage}</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,20 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionUpdateResourceRule")]: "updateResourceRule"
|
[t("actionUpdateResourceRule")]: "updateResourceRule"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"Resource Policy": {
|
||||||
|
[t("actionGetResourcePolicy")]: "getResourcePolicy",
|
||||||
|
[t("actionUpdateResourcePolicy")]: "updateResourcePolicy",
|
||||||
|
[t("actionSetResourcePolicyUsers")]: "setResourcePolicyUsers",
|
||||||
|
[t("actionSetResourcePolicyRoles")]: "setResourcePolicyRoles",
|
||||||
|
[t("actionSetResourcePolicyPassword")]: "setResourcePolicyPassword",
|
||||||
|
[t("actionSetResourcePolicyPincode")]: "setResourcePolicyPincode",
|
||||||
|
[t("actionSetResourcePolicyHeaderAuth")]:
|
||||||
|
"setResourcePolicyHeaderAuth",
|
||||||
|
[t("actionSetResourcePolicyWhitelist")]:
|
||||||
|
"setResourcePolicyWhitelist",
|
||||||
|
[t("actionSetResourcePolicyRules")]: "setResourcePolicyRules"
|
||||||
|
},
|
||||||
|
|
||||||
Client: {
|
Client: {
|
||||||
[t("actionCreateClient")]: "createClient",
|
[t("actionCreateClient")]: "createClient",
|
||||||
[t("actionDeleteClient")]: "deleteClient",
|
[t("actionDeleteClient")]: "deleteClient",
|
||||||
|
|||||||
@@ -427,6 +427,27 @@ export default function PrivateResourcesTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "labels",
|
||||||
|
accessorKey: "labels",
|
||||||
|
header: () => (
|
||||||
|
<LabelColumnFilterButton
|
||||||
|
orgId={orgId}
|
||||||
|
selectedValues={searchParams.getAll("labels")}
|
||||||
|
onSelectedValuesChange={(value) =>
|
||||||
|
handleFilterChange("labels", value)
|
||||||
|
}
|
||||||
|
label={t("labels")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }: { row: { original: InternalResourceRow } }) => (
|
||||||
|
<ClientResourceLabelCell
|
||||||
|
resource={row.original}
|
||||||
|
orgId={orgId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@@ -487,27 +508,6 @@ export default function PrivateResourcesTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "labels",
|
|
||||||
accessorKey: "labels",
|
|
||||||
header: () => (
|
|
||||||
<LabelColumnFilterButton
|
|
||||||
orgId={orgId}
|
|
||||||
selectedValues={searchParams.getAll("labels")}
|
|
||||||
onSelectedValuesChange={(value) =>
|
|
||||||
handleFilterChange("labels", value)
|
|
||||||
}
|
|
||||||
label={t("labels")}
|
|
||||||
className="p-3"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: InternalResourceRow } }) => (
|
|
||||||
<ClientResourceLabelCell
|
|
||||||
resource={row.original}
|
|
||||||
orgId={orgId}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -551,6 +551,24 @@ export default function PublicResourcesTable({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "labels",
|
||||||
|
accessorKey: "labels",
|
||||||
|
header: () => (
|
||||||
|
<LabelColumnFilterButton
|
||||||
|
orgId={orgId}
|
||||||
|
selectedValues={searchParams.getAll("labels")}
|
||||||
|
onSelectedValuesChange={(value) =>
|
||||||
|
handleFilterChange("labels", value)
|
||||||
|
}
|
||||||
|
label={t("labels")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }: { row: { original: ResourceRow } }) => (
|
||||||
|
<ResourceLabelCell resource={row.original} orgId={orgId} />
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@@ -603,24 +621,6 @@ export default function PublicResourcesTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "labels",
|
|
||||||
accessorKey: "labels",
|
|
||||||
header: () => (
|
|
||||||
<LabelColumnFilterButton
|
|
||||||
orgId={orgId}
|
|
||||||
selectedValues={searchParams.getAll("labels")}
|
|
||||||
onSelectedValuesChange={(value) =>
|
|
||||||
handleFilterChange("labels", value)
|
|
||||||
}
|
|
||||||
label={t("labels")}
|
|
||||||
className="p-3"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: ResourceRow } }) => (
|
|
||||||
<ResourceLabelCell resource={row.original} orgId={orgId} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -359,10 +359,12 @@ export default function SitesTable({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
|
|
||||||
let updateAvailable =
|
let updateAvailable = Boolean(
|
||||||
latestNewtVersion &&
|
latestNewtVersion &&
|
||||||
originalRow.newtVersion &&
|
originalRow.newtVersion &&
|
||||||
semver.lt(originalRow.newtVersion, latestNewtVersion);
|
semver.valid(originalRow.newtVersion) &&
|
||||||
|
semver.lt(originalRow.newtVersion, latestNewtVersion)
|
||||||
|
);
|
||||||
|
|
||||||
if (originalRow.type === "newt") {
|
if (originalRow.type === "newt") {
|
||||||
return (
|
return (
|
||||||
@@ -498,6 +500,23 @@ export default function SitesTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "labels",
|
||||||
|
header: () => (
|
||||||
|
<LabelColumnFilterButton
|
||||||
|
orgId={orgId}
|
||||||
|
selectedValues={searchParams.getAll("labels")}
|
||||||
|
onSelectedValuesChange={(value) =>
|
||||||
|
handleFilterChange("labels", value)
|
||||||
|
}
|
||||||
|
label={t("labels")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }: { row: { original: SiteRow } }) => (
|
||||||
|
<SiteLabelCell site={row.original} orgId={orgId} />
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
@@ -599,23 +618,6 @@ export default function SitesTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "labels",
|
|
||||||
header: () => (
|
|
||||||
<LabelColumnFilterButton
|
|
||||||
orgId={orgId}
|
|
||||||
selectedValues={searchParams.getAll("labels")}
|
|
||||||
onSelectedValuesChange={(value) =>
|
|
||||||
handleFilterChange("labels", value)
|
|
||||||
}
|
|
||||||
label={t("labels")}
|
|
||||||
className="p-3"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: SiteRow } }) => (
|
|
||||||
<SiteLabelCell site={row.original} orgId={orgId} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -588,9 +588,12 @@ export default function UserDevicesTable({
|
|||||||
if (agent in latestPlatformVersions) {
|
if (agent in latestPlatformVersions) {
|
||||||
const agentVersion = latestPlatformVersions[agent];
|
const agentVersion = latestPlatformVersions[agent];
|
||||||
|
|
||||||
updateAvailable = semver.lt(
|
updateAvailable = Boolean(
|
||||||
originalRow.olmVersion,
|
semver.valid(originalRow.olmVersion) &&
|
||||||
agentVersion.latestVersion
|
semver.lt(
|
||||||
|
originalRow.olmVersion,
|
||||||
|
agentVersion.latestVersion
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ export interface MultiSelectInputProps<
|
|||||||
> extends MultiSelectTagsProps<T> {
|
> extends MultiSelectTagsProps<T> {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
lockedIds?: Set<string>;
|
lockedIds?: Set<string>;
|
||||||
|
onPopoverOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectTagInput<T extends TagValue>({
|
export function MultiSelectTagInput<T extends TagValue>({
|
||||||
buttonText,
|
buttonText,
|
||||||
lockedIds,
|
lockedIds,
|
||||||
|
onPopoverOpenChange,
|
||||||
...props
|
...props
|
||||||
}: MultiSelectInputProps<T>) {
|
}: MultiSelectInputProps<T>) {
|
||||||
const selectedValues = new Set(props.value.map((v) => v.id));
|
const selectedValues = new Set(props.value.map((v) => v.id));
|
||||||
@@ -33,6 +35,7 @@ export function MultiSelectTagInput<T extends TagValue>({
|
|||||||
// clear input when popover is closed
|
// clear input when popover is closed
|
||||||
props.onSearch("");
|
props.onSearch("");
|
||||||
}
|
}
|
||||||
|
onPopoverOpenChange?.(open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|||||||
@@ -587,7 +587,9 @@ export default function ResourceLauncher({
|
|||||||
|
|
||||||
const renderToolbarSearch = (searchClassName: string) => (
|
const renderToolbarSearch = (searchClassName: string) => (
|
||||||
<div className={cn("relative shrink-0", searchClassName)}>
|
<div className={cn("relative shrink-0", searchClassName)}>
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
<span className="pointer-events-none absolute inset-y-0 left-2 flex items-center">
|
||||||
|
<Search className="size-4 text-muted-foreground" />
|
||||||
|
</span>
|
||||||
<Input
|
<Input
|
||||||
key={`${activeViewId}-${searchInputResetKey}`}
|
key={`${activeViewId}-${searchInputResetKey}`}
|
||||||
defaultValue={config.query}
|
defaultValue={config.query}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type RolesSelectorProps = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
selectedRoles?: SelectedRole[];
|
selectedRoles?: SelectedRole[];
|
||||||
onSelectRoles: (roles: SelectedRole[]) => void;
|
onSelectRoles: (roles: SelectedRole[]) => void;
|
||||||
|
onPopoverOpenChange?: (open: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
restrictAdminRole?: boolean;
|
restrictAdminRole?: boolean;
|
||||||
mapRolesByName?: boolean;
|
mapRolesByName?: boolean;
|
||||||
@@ -23,6 +24,7 @@ export function RolesSelector({
|
|||||||
orgId,
|
orgId,
|
||||||
selectedRoles = [],
|
selectedRoles = [],
|
||||||
onSelectRoles,
|
onSelectRoles,
|
||||||
|
onPopoverOpenChange,
|
||||||
disabled,
|
disabled,
|
||||||
restrictAdminRole,
|
restrictAdminRole,
|
||||||
mapRolesByName,
|
mapRolesByName,
|
||||||
@@ -77,6 +79,7 @@ export function RolesSelector({
|
|||||||
options={rolesShown}
|
options={rolesShown}
|
||||||
value={selectedRoles}
|
value={selectedRoles}
|
||||||
onChange={onSelectRoles}
|
onChange={onSelectRoles}
|
||||||
|
onPopoverOpenChange={onPopoverOpenChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
lockedIds={lockedIds}
|
lockedIds={lockedIds}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user