add compact mode for large orgs

This commit is contained in:
miloschwartz
2026-07-02 21:53:52 -04:00
parent 3699f8f9cb
commit 1717a99cee
20 changed files with 757 additions and 43 deletions
+6
View File
@@ -470,6 +470,12 @@ authenticated.get(
launcher.listLauncherGroups
);
authenticated.get(
"/org/:orgId/launcher/scale",
verifyOrgAccess,
launcher.listLauncherScale
);
authenticated.get(
"/org/:orgId/launcher/resources",
verifyOrgAccess,
+1
View File
@@ -1,5 +1,6 @@
export * from "./types";
export { listLauncherGroups } from "./listLauncherGroups";
export { listLauncherScale } from "./listLauncherScale";
export { listLauncherResources } from "./listLauncherResources";
export { listLauncherSites } from "./listLauncherSites";
export { listLauncherLabels } from "./listLauncherLabels";
@@ -38,6 +38,7 @@ import {
formatSiteResourceAccess
} from "./formatLauncherAccess";
import {
LAUNCHER_FLAT_GROUP_KEY,
LAUNCHER_NO_SITE_GROUP_KEY,
LAUNCHER_UNLABELED_GROUP_KEY,
type LauncherFilterListQuery,
@@ -1161,10 +1162,12 @@ export async function listLauncherResourcesForUser(
let items = [...publicItems, ...siteItems];
items = filterResourcesBySearch(items, query.query);
if (query.groupBy === "label") {
items = filterResourcesByLabel(items, query.groupKey);
} else if (query.groupBy === "site") {
items = filterResourcesBySite(items, query.groupKey);
if (query.groupKey !== LAUNCHER_FLAT_GROUP_KEY) {
if (query.groupBy === "label") {
items = filterResourcesByLabel(items, query.groupKey);
} else if (query.groupBy === "site") {
items = filterResourcesBySite(items, query.groupKey);
}
}
items = sortLauncherResources(items, query.order);
+124
View File
@@ -0,0 +1,124 @@
import { regionalCache as cache } from "#dynamic/lib/cache";
import {
listLauncherGroupsForUser,
resolveAccessibleIds
} from "./launcherResourceAccess";
import {
parseIdListParam,
type LauncherScaleInfo,
type LauncherScaleQuery
} from "./types";
export const LAUNCHER_FULL_MAX_RESOURCES = 2000;
export const LAUNCHER_FULL_MAX_SITE_GROUPS = 200;
export const LAUNCHER_FULL_MAX_LABEL_GROUPS = 100;
export const LAUNCHER_FILTERED_SITE_GROUPING_MAX = 20;
const LAUNCHER_SCALE_TTL_SEC = 60;
function launcherScaleCacheKey(
orgId: string,
userId: string,
roleIds: number[],
query: LauncherScaleQuery
) {
const rolesKey = [...roleIds].sort((a, b) => a - b).join(",");
const filterKey = [
query.query,
query.groupBy,
query.siteIds ?? "",
query.labelIds ?? "",
query.sort_by,
query.order
].join("|");
return `launcherScale:${orgId}:${userId}:${rolesKey}:${filterKey}`;
}
async function getLauncherScaleForUserUncached(
orgId: string,
userId: string,
userRoleIds: number[],
query: LauncherScaleQuery
): Promise<LauncherScaleInfo> {
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
const resourceCount =
accessible.resourceIds.length + accessible.siteResourceIds.length;
const baselineQuery = {
query: "",
groupBy: "site" as const,
siteIds: undefined,
labelIds: undefined,
sort_by: "name" as const,
order: "asc" as const,
page: 1,
pageSize: 1
};
const [{ total: siteGroupCount }, { total: labelGroupCount }] =
await Promise.all([
listLauncherGroupsForUser(
orgId,
userId,
userRoleIds,
baselineQuery
),
listLauncherGroupsForUser(orgId, userId, userRoleIds, {
...baselineQuery,
groupBy: "label"
})
]);
const mode =
resourceCount <= LAUNCHER_FULL_MAX_RESOURCES &&
siteGroupCount <= LAUNCHER_FULL_MAX_SITE_GROUPS
? "full"
: "compact";
const siteFilterIds = parseIdListParam(query.siteIds);
const allowSiteGrouping =
siteGroupCount <= LAUNCHER_FULL_MAX_SITE_GROUPS ||
(siteFilterIds.length > 0 &&
siteFilterIds.length <= LAUNCHER_FILTERED_SITE_GROUPING_MAX);
const allowLabelGrouping =
labelGroupCount <= LAUNCHER_FULL_MAX_LABEL_GROUPS;
const requireSearchOrFilter =
mode === "compact" && resourceCount > LAUNCHER_FULL_MAX_RESOURCES;
return {
mode,
resourceCount,
siteGroupCount,
labelGroupCount,
capabilities: {
allowSiteGrouping,
allowLabelGrouping,
requireSearchOrFilter
}
};
}
export async function getLauncherScaleForUser(
orgId: string,
userId: string,
userRoleIds: number[],
query: LauncherScaleQuery
): Promise<LauncherScaleInfo> {
const cacheKey = launcherScaleCacheKey(orgId, userId, userRoleIds, query);
const cached = await cache.get<LauncherScaleInfo>(cacheKey);
if (cached) {
return cached;
}
const result = await getLauncherScaleForUserUncached(
orgId,
userId,
userRoleIds,
query
);
await cache.set(cacheKey, result, LAUNCHER_SCALE_TTL_SEC);
return result;
}
@@ -0,0 +1,60 @@
import { response } from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { fromZodError } from "zod-validation-error";
import { getLauncherScaleForUser } from "./launcherScale";
import { launcherScaleQuerySchema } from "./types";
export async function listLauncherScale(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const orgId = req.userOrgId;
const userId = req.user!.userId;
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
const parsed = launcherScaleQuerySchema.safeParse(req.query);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromZodError(parsed.error)
)
);
}
const scale = await getLauncherScaleForUser(
orgId,
userId,
req.userOrgRoleIds ?? [],
parsed.data
);
return response(res, {
data: { scale },
success: true,
error: false,
message: "Launcher scale retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
console.error("Error listing launcher scale:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Internal server error"
)
);
}
}
+32 -2
View File
@@ -2,9 +2,10 @@ import { z } from "zod";
export const LAUNCHER_UNLABELED_GROUP_KEY = "unlabeled";
export const LAUNCHER_NO_SITE_GROUP_KEY = "no-site";
export const LAUNCHER_FLAT_GROUP_KEY = "__all__";
export const launcherViewConfigSchema = z.object({
groupBy: z.enum(["site", "label"]).default("site"),
groupBy: z.enum(["site", "label", "none"]).default("site"),
layout: z.enum(["grid", "list"]).default("grid"),
sortBy: z.literal("name").default("name"),
order: z.enum(["asc", "desc"]).default("asc"),
@@ -138,7 +139,7 @@ export const launcherListQuerySchema = z.strictObject({
.default(20),
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
query: z.string().optional().default(""),
groupBy: z.enum(["site", "label"]).optional().default("site"),
groupBy: z.enum(["site", "label", "none"]).optional().default("site"),
groupKey: z.string().optional(),
siteIds: z.string().optional(),
labelIds: z.string().optional(),
@@ -163,3 +164,32 @@ export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
export type LauncherViewSelection =
| { type: "default" }
| { type: "saved"; viewId: number };
export type LauncherScaleCapabilities = {
allowSiteGrouping: boolean;
allowLabelGrouping: boolean;
requireSearchOrFilter: boolean;
};
export type LauncherScaleInfo = {
mode: "full" | "compact";
resourceCount: number;
siteGroupCount: number;
labelGroupCount: number;
capabilities: LauncherScaleCapabilities;
};
export type ListLauncherScaleResponse = {
scale: LauncherScaleInfo;
};
export const launcherScaleQuerySchema = z.strictObject({
query: z.string().optional().default(""),
groupBy: z.enum(["site", "label", "none"]).optional().default("site"),
siteIds: z.string().optional(),
labelIds: z.string().optional(),
sort_by: z.literal("name").optional().default("name"),
order: z.enum(["asc", "desc"]).optional().default("asc")
});
export type LauncherScaleQuery = z.infer<typeof launcherScaleQuerySchema>;