mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-20 04:26:28 +02:00
add more caching
This commit is contained in:
@@ -3587,6 +3587,7 @@
|
|||||||
"resourceLauncherSaveForEveryoneDescription": "Share this view with all organization members. When unchecked, the view is only visible to you.",
|
"resourceLauncherSaveForEveryoneDescription": "Share this view with all organization members. When unchecked, the view is only visible to you.",
|
||||||
"resourceLauncherMakePersonal": "Make Personal",
|
"resourceLauncherMakePersonal": "Make Personal",
|
||||||
"resourceLauncherFilter": "Filter",
|
"resourceLauncherFilter": "Filter",
|
||||||
|
"resourceLauncherFilterWithCount": "Filter, {count} applied",
|
||||||
"resourceLauncherSort": "Sort",
|
"resourceLauncherSort": "Sort",
|
||||||
"resourceLauncherSortAscending": "Sort ascending",
|
"resourceLauncherSortAscending": "Sort ascending",
|
||||||
"resourceLauncherSortDescending": "Sort descending",
|
"resourceLauncherSortDescending": "Sort descending",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import {
|
import {
|
||||||
exitNodes,
|
exitNodes,
|
||||||
@@ -31,7 +32,8 @@ import {
|
|||||||
isNull,
|
isNull,
|
||||||
like,
|
like,
|
||||||
or,
|
or,
|
||||||
sql
|
sql,
|
||||||
|
type SQL
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
formatPublicResourceAccess,
|
formatPublicResourceAccess,
|
||||||
@@ -60,6 +62,68 @@ export type AccessibleIds = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LAUNCHER_ACCESSIBLE_IDS_TTL_SEC = 60;
|
const LAUNCHER_ACCESSIBLE_IDS_TTL_SEC = 60;
|
||||||
|
const LAUNCHER_RESOURCES_RESULT_TTL_SEC = 60;
|
||||||
|
const LAUNCHER_GROUPS_RESULT_TTL_SEC = 60;
|
||||||
|
|
||||||
|
type LauncherResourcesCacheEntry = {
|
||||||
|
items: LauncherResource[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LauncherGroupsCacheEntry = {
|
||||||
|
groups: LauncherGroup[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function launcherListQueryHash(
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherListQuery,
|
||||||
|
extra?: Record<string, string>
|
||||||
|
) {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
roles: [...userRoleIds].sort((a, b) => a - b),
|
||||||
|
query: query.query,
|
||||||
|
groupBy: query.groupBy,
|
||||||
|
siteIds: query.siteIds ?? "",
|
||||||
|
labelIds: query.labelIds ?? "",
|
||||||
|
sort_by: query.sort_by,
|
||||||
|
order: query.order,
|
||||||
|
...extra
|
||||||
|
});
|
||||||
|
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function launcherResourcesQueryHash(
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherListQuery & { groupKey: string }
|
||||||
|
) {
|
||||||
|
return launcherListQueryHash(userRoleIds, query, {
|
||||||
|
groupKey: query.groupKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function launcherGroupsQueryHash(
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherListQuery
|
||||||
|
) {
|
||||||
|
return launcherListQueryHash(userRoleIds, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function launcherResourcesCacheKey(
|
||||||
|
orgId: string,
|
||||||
|
userId: string,
|
||||||
|
queryHash: string
|
||||||
|
) {
|
||||||
|
return `launcher:results:${orgId}:${userId}:${queryHash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function launcherGroupsCacheKey(
|
||||||
|
orgId: string,
|
||||||
|
userId: string,
|
||||||
|
queryHash: string
|
||||||
|
) {
|
||||||
|
return `launcher:groups:${orgId}:${userId}:${queryHash}`;
|
||||||
|
}
|
||||||
|
|
||||||
function launcherAccessibleIdsCacheKey(
|
function launcherAccessibleIdsCacheKey(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
@@ -195,6 +259,21 @@ function searchPattern(query: string) {
|
|||||||
return `%${query.trim()}%`;
|
return `%${query.trim()}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function combineOrConditions(
|
||||||
|
...conditions: (SQL | undefined)[]
|
||||||
|
): SQL | undefined {
|
||||||
|
const parts = conditions.filter(
|
||||||
|
(condition): condition is SQL => !!condition
|
||||||
|
);
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (parts.length === 1) {
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
return or(...parts);
|
||||||
|
}
|
||||||
|
|
||||||
function buildSearchConditionForPublic(
|
function buildSearchConditionForPublic(
|
||||||
query: string,
|
query: string,
|
||||||
labelsFeatureEnabled: boolean
|
labelsFeatureEnabled: boolean
|
||||||
@@ -206,7 +285,17 @@ function buildSearchConditionForPublic(
|
|||||||
const queryList = [
|
const queryList = [
|
||||||
like(sql`LOWER(${resources.name})`, pattern),
|
like(sql`LOWER(${resources.name})`, pattern),
|
||||||
like(sql`LOWER(${resources.fullDomain})`, pattern),
|
like(sql`LOWER(${resources.fullDomain})`, pattern),
|
||||||
like(sql`LOWER(cast(${resources.proxyPort} as text))`, pattern)
|
like(sql`LOWER(cast(${resources.proxyPort} as text))`, pattern),
|
||||||
|
inArray(
|
||||||
|
resources.resourceId,
|
||||||
|
db
|
||||||
|
.select({ id: resources.resourceId })
|
||||||
|
.from(resources)
|
||||||
|
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
||||||
|
.leftJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
|
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
|
||||||
|
.where(like(sql`LOWER(${exitNodes.endpoint})`, pattern))
|
||||||
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (labelsFeatureEnabled) {
|
if (labelsFeatureEnabled) {
|
||||||
@@ -239,6 +328,11 @@ function buildSearchConditionForSiteResource(
|
|||||||
const queryList = [
|
const queryList = [
|
||||||
like(sql`LOWER(${siteResources.name})`, pattern),
|
like(sql`LOWER(${siteResources.name})`, pattern),
|
||||||
like(sql`LOWER(${siteResources.destination})`, pattern),
|
like(sql`LOWER(${siteResources.destination})`, pattern),
|
||||||
|
like(
|
||||||
|
sql`LOWER(cast(${siteResources.destinationPort} as text))`,
|
||||||
|
pattern
|
||||||
|
),
|
||||||
|
like(sql`LOWER(${siteResources.scheme})`, pattern),
|
||||||
like(sql`LOWER(${siteResources.alias})`, pattern),
|
like(sql`LOWER(${siteResources.alias})`, pattern),
|
||||||
like(sql`LOWER(${siteResources.fullDomain})`, pattern),
|
like(sql`LOWER(${siteResources.fullDomain})`, pattern),
|
||||||
like(sql`LOWER(${siteResources.aliasAddress})`, pattern)
|
like(sql`LOWER(${siteResources.aliasAddress})`, pattern)
|
||||||
@@ -263,6 +357,79 @@ function buildSearchConditionForSiteResource(
|
|||||||
return or(...queryList);
|
return or(...queryList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function filterPublicResourceIdsByTextSearch(
|
||||||
|
orgId: string,
|
||||||
|
resourceIds: number[],
|
||||||
|
query: string,
|
||||||
|
labelsFeatureEnabled: boolean
|
||||||
|
): Promise<number[]> {
|
||||||
|
if (!query.trim() || resourceIds.length === 0) {
|
||||||
|
return resourceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textMatch = combineOrConditions(
|
||||||
|
buildSearchConditionForPublic(query, labelsFeatureEnabled),
|
||||||
|
buildSiteNameSearchCondition(query)
|
||||||
|
);
|
||||||
|
if (!textMatch) {
|
||||||
|
return resourceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.selectDistinct({ resourceId: resources.resourceId })
|
||||||
|
.from(resources)
|
||||||
|
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
||||||
|
.leftJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(resources.resourceId, resourceIds),
|
||||||
|
eq(resources.orgId, orgId),
|
||||||
|
eq(resources.enabled, true),
|
||||||
|
textMatch
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.map((row) => row.resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filterSiteResourceIdsByTextSearch(
|
||||||
|
orgId: string,
|
||||||
|
siteResourceIds: number[],
|
||||||
|
query: string,
|
||||||
|
labelsFeatureEnabled: boolean
|
||||||
|
): Promise<number[]> {
|
||||||
|
if (!query.trim() || siteResourceIds.length === 0) {
|
||||||
|
return siteResourceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textMatch = combineOrConditions(
|
||||||
|
buildSearchConditionForSiteResource(query, labelsFeatureEnabled),
|
||||||
|
buildSiteNameSearchCondition(query)
|
||||||
|
);
|
||||||
|
if (!textMatch) {
|
||||||
|
return siteResourceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.selectDistinct({ siteResourceId: siteResources.siteResourceId })
|
||||||
|
.from(siteResources)
|
||||||
|
.leftJoin(
|
||||||
|
siteNetworks,
|
||||||
|
eq(siteResources.networkId, siteNetworks.networkId)
|
||||||
|
)
|
||||||
|
.leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(siteResources.siteResourceId, siteResourceIds),
|
||||||
|
eq(siteResources.orgId, orgId),
|
||||||
|
eq(siteResources.enabled, true),
|
||||||
|
textMatch
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.map((row) => row.siteResourceId);
|
||||||
|
}
|
||||||
|
|
||||||
async function labelsEnabled(orgId: string): Promise<boolean> {
|
async function labelsEnabled(orgId: string): Promise<boolean> {
|
||||||
return isLicensedOrSubscribed(orgId, tierMatrix.labels);
|
return isLicensedOrSubscribed(orgId, tierMatrix.labels);
|
||||||
}
|
}
|
||||||
@@ -589,9 +756,8 @@ async function listSiteGroups(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const total = groups.length;
|
const total = groups.length;
|
||||||
const offset = (query.page - 1) * query.pageSize;
|
|
||||||
return {
|
return {
|
||||||
groups: groups.slice(offset, offset + query.pageSize),
|
groups,
|
||||||
total
|
total
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -789,26 +955,53 @@ async function listLabelGroups(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const total = groups.length;
|
const total = groups.length;
|
||||||
const offset = (query.page - 1) * query.pageSize;
|
|
||||||
return {
|
return {
|
||||||
groups: groups.slice(offset, offset + query.pageSize),
|
groups,
|
||||||
total
|
total
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listLauncherGroupsForUserUncached(
|
||||||
|
orgId: string,
|
||||||
|
userId: string,
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherListQuery
|
||||||
|
): Promise<LauncherGroupsCacheEntry> {
|
||||||
|
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
||||||
|
|
||||||
|
if (query.groupBy === "label") {
|
||||||
|
return listLabelGroups(orgId, accessible, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return listSiteGroups(orgId, accessible, query);
|
||||||
|
}
|
||||||
|
|
||||||
export async function listLauncherGroupsForUser(
|
export async function listLauncherGroupsForUser(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
userRoleIds: number[],
|
userRoleIds: number[],
|
||||||
query: LauncherListQuery
|
query: LauncherListQuery
|
||||||
): Promise<{ groups: LauncherGroup[]; total: number }> {
|
): Promise<{ groups: LauncherGroup[]; total: number }> {
|
||||||
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
const queryHash = launcherGroupsQueryHash(userRoleIds, query);
|
||||||
|
const cacheKey = launcherGroupsCacheKey(orgId, userId, queryHash);
|
||||||
|
const cached = await cache.get<LauncherGroupsCacheEntry>(cacheKey);
|
||||||
|
|
||||||
if (query.groupBy === "label") {
|
let result = cached;
|
||||||
return listLabelGroups(orgId, accessible, query);
|
if (!result) {
|
||||||
|
result = await listLauncherGroupsForUserUncached(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
userRoleIds,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
await cache.set(cacheKey, result, LAUNCHER_GROUPS_RESULT_TTL_SEC);
|
||||||
}
|
}
|
||||||
|
|
||||||
return listSiteGroups(orgId, accessible, query);
|
const offset = (query.page - 1) * query.pageSize;
|
||||||
|
return {
|
||||||
|
groups: result.groups.slice(offset, offset + query.pageSize),
|
||||||
|
total: result.total
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mapPublicResources(
|
async function mapPublicResources(
|
||||||
@@ -1019,26 +1212,6 @@ function filterResourcesByLabel(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterResourcesBySearch(
|
|
||||||
items: LauncherResource[],
|
|
||||||
query: string
|
|
||||||
): LauncherResource[] {
|
|
||||||
if (!query.trim()) {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
const pattern = query.trim().toLowerCase();
|
|
||||||
return items.filter(
|
|
||||||
(item) =>
|
|
||||||
item.name.toLowerCase().includes(pattern) ||
|
|
||||||
item.accessDisplay.toLowerCase().includes(pattern) ||
|
|
||||||
item.accessCopyValue.toLowerCase().includes(pattern) ||
|
|
||||||
item.labels.some((label) =>
|
|
||||||
label.name.toLowerCase().includes(pattern)
|
|
||||||
) ||
|
|
||||||
item.site?.name.toLowerCase().includes(pattern)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortLauncherResources(
|
function sortLauncherResources(
|
||||||
items: LauncherResource[],
|
items: LauncherResource[],
|
||||||
order: "asc" | "desc"
|
order: "asc" | "desc"
|
||||||
@@ -1051,12 +1224,12 @@ function sortLauncherResources(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listLauncherResourcesForUser(
|
async function listLauncherResourcesForUserUncached(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
userRoleIds: number[],
|
userRoleIds: number[],
|
||||||
query: LauncherListQuery & { groupKey: string }
|
query: LauncherListQuery & { groupKey: string }
|
||||||
): Promise<{ resources: LauncherResource[]; total: number }> {
|
): Promise<LauncherResourcesCacheEntry> {
|
||||||
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
||||||
|
|
||||||
const siteFilterIds = parseIdListParam(query.siteIds);
|
const siteFilterIds = parseIdListParam(query.siteIds);
|
||||||
@@ -1129,6 +1302,27 @@ export async function listLauncherResourcesForUser(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const labelsFeatureEnabled = await labelsEnabled(orgId);
|
||||||
|
|
||||||
|
if (query.query.trim()) {
|
||||||
|
if (filteredResourceIds.length > 0) {
|
||||||
|
filteredResourceIds = await filterPublicResourceIdsByTextSearch(
|
||||||
|
orgId,
|
||||||
|
filteredResourceIds,
|
||||||
|
query.query,
|
||||||
|
labelsFeatureEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filteredSiteResourceIds.length > 0) {
|
||||||
|
filteredSiteResourceIds = await filterSiteResourceIdsByTextSearch(
|
||||||
|
orgId,
|
||||||
|
filteredSiteResourceIds,
|
||||||
|
query.query,
|
||||||
|
labelsFeatureEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const labelMaps = await fetchLabelsForResources(
|
const labelMaps = await fetchLabelsForResources(
|
||||||
orgId,
|
orgId,
|
||||||
filteredResourceIds,
|
filteredResourceIds,
|
||||||
@@ -1160,7 +1354,6 @@ export async function listLauncherResourcesForUser(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let items = [...publicItems, ...siteItems];
|
let items = [...publicItems, ...siteItems];
|
||||||
items = filterResourcesBySearch(items, query.query);
|
|
||||||
|
|
||||||
if (query.groupKey !== LAUNCHER_FLAT_GROUP_KEY) {
|
if (query.groupKey !== LAUNCHER_FLAT_GROUP_KEY) {
|
||||||
if (query.groupBy === "label") {
|
if (query.groupBy === "label") {
|
||||||
@@ -1172,11 +1365,37 @@ export async function listLauncherResourcesForUser(
|
|||||||
|
|
||||||
items = sortLauncherResources(items, query.order);
|
items = sortLauncherResources(items, query.order);
|
||||||
|
|
||||||
const total = items.length;
|
return {
|
||||||
|
items,
|
||||||
|
total: items.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listLauncherResourcesForUser(
|
||||||
|
orgId: string,
|
||||||
|
userId: string,
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherListQuery & { groupKey: string }
|
||||||
|
): Promise<{ resources: LauncherResource[]; total: number }> {
|
||||||
|
const queryHash = launcherResourcesQueryHash(userRoleIds, query);
|
||||||
|
const cacheKey = launcherResourcesCacheKey(orgId, userId, queryHash);
|
||||||
|
const cached = await cache.get<LauncherResourcesCacheEntry>(cacheKey);
|
||||||
|
|
||||||
|
let result = cached;
|
||||||
|
if (!result) {
|
||||||
|
result = await listLauncherResourcesForUserUncached(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
userRoleIds,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
await cache.set(cacheKey, result, LAUNCHER_RESOURCES_RESULT_TTL_SEC);
|
||||||
|
}
|
||||||
|
|
||||||
const offset = (query.page - 1) * query.pageSize;
|
const offset = (query.page - 1) * query.pageSize;
|
||||||
return {
|
return {
|
||||||
resources: items.slice(offset, offset + query.pageSize),
|
resources: result.items.slice(offset, offset + query.pageSize),
|
||||||
total
|
total: result.total
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,32 +14,57 @@ export const LAUNCHER_FULL_MAX_SITE_GROUPS = 200;
|
|||||||
export const LAUNCHER_FULL_MAX_LABEL_GROUPS = 100;
|
export const LAUNCHER_FULL_MAX_LABEL_GROUPS = 100;
|
||||||
export const LAUNCHER_FILTERED_SITE_GROUPING_MAX = 20;
|
export const LAUNCHER_FILTERED_SITE_GROUPING_MAX = 20;
|
||||||
|
|
||||||
const LAUNCHER_SCALE_TTL_SEC = 60;
|
const LAUNCHER_SCALE_COUNTS_TTL_SEC = 60;
|
||||||
|
|
||||||
function launcherScaleCacheKey(
|
type LauncherScaleCountsCacheEntry = {
|
||||||
|
resourceCount: number;
|
||||||
|
siteGroupCount: number;
|
||||||
|
labelGroupCount: number;
|
||||||
|
mode: LauncherScaleInfo["mode"];
|
||||||
|
};
|
||||||
|
|
||||||
|
function launcherScaleCountsCacheKey(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
roleIds: number[],
|
roleIds: number[]
|
||||||
query: LauncherScaleQuery
|
|
||||||
) {
|
) {
|
||||||
const rolesKey = [...roleIds].sort((a, b) => a - b).join(",");
|
const rolesKey = [...roleIds].sort((a, b) => a - b).join(",");
|
||||||
const filterKey = [
|
return `launcher:scale:counts:${orgId}:${userId}:${rolesKey}`;
|
||||||
query.query,
|
|
||||||
query.groupBy,
|
|
||||||
query.siteIds ?? "",
|
|
||||||
query.labelIds ?? "",
|
|
||||||
query.sort_by,
|
|
||||||
query.order
|
|
||||||
].join("|");
|
|
||||||
return `launcherScale:${orgId}:${userId}:${rolesKey}:${filterKey}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLauncherScaleForUserUncached(
|
function buildScaleCapabilities(
|
||||||
|
counts: LauncherScaleCountsCacheEntry,
|
||||||
|
query: LauncherScaleQuery
|
||||||
|
): LauncherScaleInfo["capabilities"] {
|
||||||
|
const siteFilterIds = parseIdListParam(query.siteIds);
|
||||||
|
const labelFilterIds = parseIdListParam(query.labelIds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowSiteGrouping:
|
||||||
|
counts.siteGroupCount <= LAUNCHER_FULL_MAX_SITE_GROUPS ||
|
||||||
|
(siteFilterIds.length > 0 &&
|
||||||
|
siteFilterIds.length <= LAUNCHER_FILTERED_SITE_GROUPING_MAX),
|
||||||
|
allowLabelGrouping:
|
||||||
|
counts.labelGroupCount <= LAUNCHER_FULL_MAX_LABEL_GROUPS ||
|
||||||
|
(labelFilterIds.length > 0 &&
|
||||||
|
labelFilterIds.length <= LAUNCHER_FILTERED_SITE_GROUPING_MAX),
|
||||||
|
requireSearchOrFilter:
|
||||||
|
counts.mode === "compact" &&
|
||||||
|
counts.resourceCount > LAUNCHER_FULL_MAX_RESOURCES
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLauncherScaleCountsForUser(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
userRoleIds: number[],
|
userRoleIds: number[]
|
||||||
query: LauncherScaleQuery
|
): Promise<LauncherScaleCountsCacheEntry> {
|
||||||
): Promise<LauncherScaleInfo> {
|
const cacheKey = launcherScaleCountsCacheKey(orgId, userId, userRoleIds);
|
||||||
|
const cached = await cache.get<LauncherScaleCountsCacheEntry>(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
||||||
const resourceCount =
|
const resourceCount =
|
||||||
accessible.resourceIds.length + accessible.siteResourceIds.length;
|
accessible.resourceIds.length + accessible.siteResourceIds.length;
|
||||||
@@ -75,33 +100,15 @@ async function getLauncherScaleForUserUncached(
|
|||||||
? "full"
|
? "full"
|
||||||
: "compact";
|
: "compact";
|
||||||
|
|
||||||
const siteFilterIds = parseIdListParam(query.siteIds);
|
const result: LauncherScaleCountsCacheEntry = {
|
||||||
const labelFilterIds = parseIdListParam(query.labelIds);
|
|
||||||
|
|
||||||
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 ||
|
|
||||||
(labelFilterIds.length > 0 &&
|
|
||||||
labelFilterIds.length <= LAUNCHER_FILTERED_SITE_GROUPING_MAX);
|
|
||||||
|
|
||||||
const requireSearchOrFilter =
|
|
||||||
mode === "compact" && resourceCount > LAUNCHER_FULL_MAX_RESOURCES;
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
resourceCount,
|
resourceCount,
|
||||||
siteGroupCount,
|
siteGroupCount,
|
||||||
labelGroupCount,
|
labelGroupCount,
|
||||||
capabilities: {
|
mode
|
||||||
allowSiteGrouping,
|
|
||||||
allowLabelGrouping,
|
|
||||||
requireSearchOrFilter
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await cache.set(cacheKey, result, LAUNCHER_SCALE_COUNTS_TTL_SEC);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLauncherScaleForUser(
|
export async function getLauncherScaleForUser(
|
||||||
@@ -110,18 +117,17 @@ export async function getLauncherScaleForUser(
|
|||||||
userRoleIds: number[],
|
userRoleIds: number[],
|
||||||
query: LauncherScaleQuery
|
query: LauncherScaleQuery
|
||||||
): Promise<LauncherScaleInfo> {
|
): Promise<LauncherScaleInfo> {
|
||||||
const cacheKey = launcherScaleCacheKey(orgId, userId, userRoleIds, query);
|
const counts = await getLauncherScaleCountsForUser(
|
||||||
const cached = await cache.get<LauncherScaleInfo>(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await getLauncherScaleForUserUncached(
|
|
||||||
orgId,
|
orgId,
|
||||||
userId,
|
userId,
|
||||||
userRoleIds,
|
userRoleIds
|
||||||
query
|
|
||||||
);
|
);
|
||||||
await cache.set(cacheKey, result, LAUNCHER_SCALE_TTL_SEC);
|
|
||||||
return result;
|
return {
|
||||||
|
mode: counts.mode,
|
||||||
|
resourceCount: counts.resourceCount,
|
||||||
|
siteGroupCount: counts.siteGroupCount,
|
||||||
|
labelGroupCount: counts.labelGroupCount,
|
||||||
|
capabilities: buildScaleCapabilities(counts, query)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type SelectedLabel
|
type SelectedLabel
|
||||||
} from "@app/components/labels-selector";
|
} from "@app/components/labels-selector";
|
||||||
import { LabelsFilterSelector } from "@app/components/LabelsFilterSelector";
|
import { LabelsFilterSelector } from "@app/components/LabelsFilterSelector";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -96,14 +97,32 @@ export function LauncherFilterPopover({
|
|||||||
[labels, selectedLabels]
|
[labels, selectedLabels]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeFilterCount = selectedSites.length + selectedLabels.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover modal={false}>
|
<Popover modal={false}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" size="icon" className="shrink-0">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="relative shrink-0"
|
||||||
|
>
|
||||||
<Funnel className="size-4" />
|
<Funnel className="size-4" />
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{t("resourceLauncherFilter")}
|
{activeFilterCount > 0
|
||||||
|
? t("resourceLauncherFilterWithCount", {
|
||||||
|
count: activeFilterCount
|
||||||
|
})
|
||||||
|
: t("resourceLauncherFilter")}
|
||||||
</span>
|
</span>
|
||||||
|
{activeFilterCount > 0 && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1.5 text-xs"
|
||||||
|
>
|
||||||
|
{activeFilterCount > 99 ? "99+" : activeFilterCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" className="w-72">
|
<PopoverContent align="end" className="w-72">
|
||||||
|
|||||||
Reference in New Issue
Block a user