Compare commits

..

9 Commits

Author SHA1 Message Date
miloschwartz
dbb1e7b274 dont show non sso resoruces 2026-07-07 18:10:12 -04:00
miloschwartz
b2d1a66279 bump version 2026-07-07 17:08:49 -04:00
miloschwartz
202466792c fix search icon alignment 2026-07-07 16:34:37 -04:00
miloschwartz
4dcf684868 add 1.20.0 migration 2026-07-07 16:04:41 -04:00
miloschwartz
2b8f50af6f add missing api key perms to permissions selector 2026-07-07 15:23:41 -04:00
Owen
a8c255ef71 Fix type issue 2026-07-07 15:21:23 -04:00
miloschwartz
d9608bd408 improve role required feedback on user 2026-07-07 15:17:29 -04:00
Owen
be193731c1 Reorder labels columns 2026-07-07 15:15:00 -04:00
Owen
0c3edc7560 Further remove labels 2026-07-07 15:12:14 -04:00
21 changed files with 348 additions and 169 deletions

View File

@@ -615,7 +615,8 @@
"idpNameInternal": "Internal",
"emailInvalid": "Invalid email address",
"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?",
"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",
@@ -1433,6 +1434,15 @@
"actionSetResourcePincode": "Set Resource Pincode",
"actionSetResourceEmailWhitelist": "Set 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",
"actionDeleteTarget": "Delete Target",
"actionGetTarget": "Get Target",

View File

@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// 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 __DIRNAME = path.dirname(__FILENAME);

View File

@@ -895,12 +895,6 @@ authenticated.delete(
user.removeUserOrg
);
// authenticated.put(
// "/newt",
// verifyApiKeyHasAction(ActionsEnum.createNewt),
// newt.createNewt
// );
authenticated.get(
`/org/:orgId/api-keys`,
verifyApiKeyIsRoot,

View File

@@ -1,11 +1,10 @@
import { createHash } from "node:crypto";
import { alias, db } from "@server/db";
import { db } from "@server/db";
import {
exitNodes,
labels,
launcherViews,
resourceLabels,
resourcePolicies,
resources,
rolePolicies,
roleResources,
@@ -32,7 +31,6 @@ import {
inArray,
isNull,
like,
not,
or,
sql,
type SQL
@@ -136,48 +134,6 @@ function launcherAccessibleIdsCacheKey(
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(
orgId: string,
userId: string,
@@ -189,8 +145,7 @@ async function resolveAccessibleIdsUncached(
directPolicyResourceResults,
rolePolicyResourceResults,
directSiteResourceResults,
roleSiteResourceResults,
orgNonSsoPublicResources
roleSiteResourceResults
] = await Promise.all([
db
.select({ resourceId: userResources.resourceId })
@@ -259,8 +214,7 @@ async function resolveAccessibleIdsUncached(
})
.from(roleSiteResources)
.where(inArray(roleSiteResources.roleId, userRoleIds))
: Promise.resolve([]),
fetchOrgNonSsoPublicResourceIds(orgId)
: Promise.resolve([])
]);
return {
@@ -269,8 +223,7 @@ async function resolveAccessibleIdsUncached(
...directResources.map((r) => r.resourceId),
...roleResourceResults.map((r) => r.resourceId),
...directPolicyResourceResults.map((r) => r.resourceId),
...rolePolicyResourceResults.map((r) => r.resourceId),
...orgNonSsoPublicResources
...rolePolicyResourceResults.map((r) => r.resourceId)
])
),
siteResourceIds: Array.from(

View File

@@ -161,7 +161,8 @@ const createSiteResourceSchema = z
return true;
}
return (
data.destination !== undefined && data.destination.trim() !== ""
data.destination !== undefined &&
data.destination?.trim() !== ""
);
},
{

View File

@@ -157,7 +157,8 @@ const updateSiteResourceSchema = z
return true;
}
return (
data.destination !== undefined && data.destination.trim() !== ""
data.destination !== undefined &&
data.destination?.trim() !== ""
);
},
{
@@ -616,10 +617,7 @@ export async function updateSiteResource(
await trx
.delete(roleSiteResources)
.where(
eq(
roleSiteResources.siteResourceId,
siteResourceId
)
eq(roleSiteResources.siteResourceId, siteResourceId)
);
}

View File

@@ -26,6 +26,7 @@ import m17 from "./scriptsPg/1.18.0";
import m18 from "./scriptsPg/1.18.3";
import m19 from "./scriptsPg/1.18.4";
import m20 from "./scriptsPg/1.19.0";
import m21 from "./scriptsPg/1.20.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -51,7 +52,8 @@ const migrations = [
{ version: "1.18.0", run: m17 },
{ version: "1.18.3", run: m18 },
{ 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
] as {
version: string;

View File

@@ -45,7 +45,7 @@ import m39 from "./scriptsSqlite/1.18.3";
import m40 from "./scriptsSqlite/1.18.4";
import m41 from "./scriptsSqlite/1.19.0";
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
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -89,7 +89,7 @@ const migrations = [
{ version: "1.18.4", run: m40 },
{ version: "1.19.0", run: m41 },
{ 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
] as const;

View File

@@ -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`);
}

View File

@@ -2,7 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import path from "path";
const version = "1.19.5";
const version = "1.20.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
@@ -92,6 +92,64 @@ export default async function migration() {
db.prepare(
`ALTER TABLE 'resourceSessions_new' RENAME TO 'resourceSessions';`
).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");

View File

@@ -111,7 +111,7 @@ export default function AccessControlsPage() {
if (values.roles.length === 0) {
toast({
variant: "destructive",
title: t("accessRoleErrorAdd"),
title: t("accessRoleRequired"),
description: t("accessRoleSelectPlease")
});
return;
@@ -173,15 +173,13 @@ export default function AccessControlsPage() {
if (values.roles.length === 0) {
toast({
variant: "destructive",
title: t("accessRoleErrorAdd"),
title: t("accessRoleRequired"),
description: t("accessRoleSelectPlease")
});
return;
}
const willHaveAdminRole = values.roles.some(
(r) => r.isAdmin === true
);
const willHaveAdminRole = values.roles.some((r) => r.isAdmin === true);
const isRemovingOwnAdmin =
sessionUser.userId === user.userId &&
@@ -226,7 +224,9 @@ export default function AccessControlsPage() {
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={(e) => void handleAccessControlsSubmit(e)}
onSubmit={(e) =>
void handleAccessControlsSubmit(e)
}
className="space-y-4"
id="access-controls-form"
>

View File

@@ -39,7 +39,6 @@ export function CreateOrgLabelDialog({
const t = useTranslations();
const api = createApiClient(useEnvContext());
const { isPaidUser } = usePaidStatus();
const canManageLabels = isPaidUser(tierMatrix.labels);
const [isSubmitting, startTransition] = useTransition();
async function createOrgLabel(data: { name: string; color: string }) {
@@ -84,11 +83,8 @@ export function CreateOrgLabelDialog({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<PaidFeaturesAlert tiers={tierMatrix.labels} />
<OrgLabelForm
disabled={!canManageLabels}
onSubmit={(data) => {
if (!canManageLabels) return;
startTransition(async () => createOrgLabel(data));
}}
/>
@@ -106,7 +102,7 @@ export function CreateOrgLabelDialog({
<Button
type="submit"
form="org-label-form"
disabled={isSubmitting || !canManageLabels}
disabled={isSubmitting}
loading={isSubmitting}
>
{t("labelCreate")}

View File

@@ -44,8 +44,6 @@ export function EditOrgLabelDialog({
}: EditOrgLabelDialogProps) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const { isPaidUser } = usePaidStatus();
const canManageLabels = isPaidUser(tierMatrix.labels);
const [isSubmitting, startTransition] = useTransition();
async function editOrgLabel(data: { name: string; color: string }) {
@@ -90,12 +88,9 @@ export function EditOrgLabelDialog({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<PaidFeaturesAlert tiers={tierMatrix.labels} />
<OrgLabelForm
disabled={!canManageLabels}
defaultValue={label}
onSubmit={(data) => {
if (!canManageLabels) return;
startTransition(async () => editOrgLabel(data));
}}
/>
@@ -113,7 +108,7 @@ export function EditOrgLabelDialog({
<Button
type="submit"
form="org-label-form"
disabled={isSubmitting || !canManageLabels}
disabled={isSubmitting}
loading={isSubmitting}
>
{t("labelEdit")}

View File

@@ -12,6 +12,7 @@ import {
import { toast } from "@app/hooks/useToast";
import { useTranslations } from "next-intl";
import { useRef } from "react";
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
import { RolesSelector, type SelectedRole } from "./roles-selector";
@@ -41,6 +42,46 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
disabled
}: OrgRolesTagFieldProps<TFieldValues>) {
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[]) {
const prev = form.getValues(name) as SelectedRole[];
@@ -61,39 +102,44 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
return;
}
if (next.length === 0) {
toast({
variant: "destructive",
title: t("accessRoleErrorAdd"),
description: t("accessRoleSelectPlease")
});
return;
}
form.setValue(name, next as never, { shouldDirty: true });
if (next.length > 0 && !isPopoverOpenRef.current) {
lastValidRolesRef.current = next;
} else if (!isPopoverOpenRef.current) {
validateRolesSelection();
}
}
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>{label ?? t("roles")}</FormLabel>
<FormControl>
<RolesSelector
orgId={orgId}
selectedRoles={field.value ?? []}
onSelectRoles={setRoleTags}
disabled={disabled}
/>
</FormControl>
{showMultiRolePaywallMessage && (
<FormDescription>{paywallMessage}</FormDescription>
)}
<FormMessage />
</FormItem>
)}
render={({ field }) => {
const selectedRoles = (field.value ?? []) as SelectedRole[];
if (!isPopoverOpenRef.current && selectedRoles.length > 0) {
lastValidRolesRef.current = selectedRoles;
}
return (
<FormItem className="flex flex-col items-start">
<FormLabel>{label ?? t("roles")}</FormLabel>
<FormControl>
<RolesSelector
orgId={orgId}
selectedRoles={selectedRoles}
onSelectRoles={setRoleTags}
onPopoverOpenChange={handlePopoverOpenChange}
disabled={disabled}
/>
</FormControl>
{showMultiRolePaywallMessage && (
<FormDescription>{paywallMessage}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}}
/>
);
}

View File

@@ -111,6 +111,20 @@ function getActionsCategories(root: boolean) {
[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: {
[t("actionCreateClient")]: "createClient",
[t("actionDeleteClient")]: "deleteClient",

View File

@@ -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",
enableHiding: false,
@@ -487,27 +508,6 @@ export default function PrivateResourcesTable({
</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}
/>
)
}
];

View File

@@ -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",
enableHiding: false,
@@ -603,24 +621,6 @@ export default function PublicResourcesTable({
</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} />
)
}
];

View File

@@ -498,6 +498,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",
enableHiding: false,
@@ -599,23 +616,6 @@ export default function SitesTable({
</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} />
)
}
];

View File

@@ -17,11 +17,13 @@ export interface MultiSelectInputProps<
> extends MultiSelectTagsProps<T> {
buttonText?: string;
lockedIds?: Set<string>;
onPopoverOpenChange?: (open: boolean) => void;
}
export function MultiSelectTagInput<T extends TagValue>({
buttonText,
lockedIds,
onPopoverOpenChange,
...props
}: MultiSelectInputProps<T>) {
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
props.onSearch("");
}
onPopoverOpenChange?.(open);
}}
>
<PopoverTrigger asChild>

View File

@@ -587,7 +587,9 @@ export default function ResourceLauncher({
const renderToolbarSearch = (searchClassName: string) => (
<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
key={`${activeViewId}-${searchInputResetKey}`}
defaultValue={config.query}

View File

@@ -12,6 +12,7 @@ export type RolesSelectorProps = {
orgId: string;
selectedRoles?: SelectedRole[];
onSelectRoles: (roles: SelectedRole[]) => void;
onPopoverOpenChange?: (open: boolean) => void;
disabled?: boolean;
restrictAdminRole?: boolean;
mapRolesByName?: boolean;
@@ -23,6 +24,7 @@ export function RolesSelector({
orgId,
selectedRoles = [],
onSelectRoles,
onPopoverOpenChange,
disabled,
restrictAdminRole,
mapRolesByName,
@@ -77,6 +79,7 @@ export function RolesSelector({
options={rolesShown}
value={selectedRoles}
onChange={onSelectRoles}
onPopoverOpenChange={onPopoverOpenChange}
disabled={disabled}
lockedIds={lockedIds}
/>