Compare commits

..

11 Commits

Author SHA1 Message Date
Fred KISSIE 92775a51ca 🐛 fix invalid semver version causing crashes in frontend 2026-07-08 01:44:28 +02:00
Fred KISSIE ea60aa701e 🐛 fix sitetable page crashing because of invalid version 2026-07-08 01:39:52 +02:00
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
24 changed files with 367 additions and 180 deletions
+11 -1
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",
+1 -1
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);
-6
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,
@@ -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(
@@ -161,7 +161,8 @@ const createSiteResourceSchema = z
return true;
}
return (
data.destination !== undefined && data.destination.trim() !== ""
data.destination !== undefined &&
data.destination?.trim() !== ""
);
},
{
@@ -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)
);
}
+3 -1
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;
+2 -2
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;
+104
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`);
}
@@ -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");
@@ -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"
>
@@ -195,9 +195,9 @@ export default function GeneralPage() {
if (agent in latestPlatformVersions) {
const agentVersion = latestPlatformVersions[agent];
updateAvailable = semver.lt(
client.olmVersion,
agentVersion.latestVersion
updateAvailable = Boolean(
semver.valid(client.olmVersion) &&
semver.lt(client.olmVersion, agentVersion.latestVersion)
);
}
}
+1 -5
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")}
+1 -6
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")}
+6 -3
View File
@@ -404,9 +404,12 @@ export default function MachineClientsTable({
if (agent in latestPlatformVersions) {
const agentVersion = latestPlatformVersions[agent];
updateAvailable = semver.lt(
originalRow.olmVersion,
agentVersion.latestVersion
updateAvailable = Boolean(
semver.valid(originalRow.olmVersion) &&
semver.lt(
originalRow.olmVersion,
agentVersion.latestVersion
)
);
}
}
+72 -26
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>
);
}}
/>
);
}
+14
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",
+21 -21
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}
/>
)
}
];
+18 -18
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} />
)
}
];
+21 -19
View File
@@ -359,10 +359,12 @@ export default function SitesTable({
cell: ({ row }) => {
const originalRow = row.original;
let updateAvailable =
let updateAvailable = Boolean(
latestNewtVersion &&
originalRow.newtVersion &&
semver.lt(originalRow.newtVersion, latestNewtVersion);
semver.valid(originalRow.newtVersion) &&
semver.lt(originalRow.newtVersion, latestNewtVersion)
);
if (originalRow.type === "newt") {
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",
enableHiding: false,
@@ -599,23 +618,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} />
)
}
];
+6 -3
View File
@@ -588,9 +588,12 @@ export default function UserDevicesTable({
if (agent in latestPlatformVersions) {
const agentVersion = latestPlatformVersions[agent];
updateAvailable = semver.lt(
originalRow.olmVersion,
agentVersion.latestVersion
updateAvailable = Boolean(
semver.valid(originalRow.olmVersion) &&
semver.lt(
originalRow.olmVersion,
agentVersion.latestVersion
)
);
}
}
@@ -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>
@@ -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}
+3
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}
/>