mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 20:29:50 +00:00
add compact mode for large orgs
This commit is contained in:
@@ -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,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
server/routers/launcher/launcherScale.ts
Normal file
124
server/routers/launcher/launcherScale.ts
Normal 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;
|
||||
}
|
||||
60
server/routers/launcher/listLauncherScale.ts
Normal file
60
server/routers/launcher/listLauncherScale.ts
Normal file
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user