mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 22:55:08 +02:00
show sites and labels for non admins
This commit is contained in:
@@ -468,6 +468,18 @@ authenticated.get(
|
|||||||
launcher.listLauncherResources
|
launcher.listLauncherResources
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/launcher/sites",
|
||||||
|
verifyOrgAccess,
|
||||||
|
launcher.listLauncherSites
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/launcher/labels",
|
||||||
|
verifyOrgAccess,
|
||||||
|
launcher.listLauncherLabels
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/launcher/views",
|
"/org/:orgId/launcher/views",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export * from "./types";
|
export * from "./types";
|
||||||
export { listLauncherGroups } from "./listLauncherGroups";
|
export { listLauncherGroups } from "./listLauncherGroups";
|
||||||
export { listLauncherResources } from "./listLauncherResources";
|
export { listLauncherResources } from "./listLauncherResources";
|
||||||
|
export { listLauncherSites } from "./listLauncherSites";
|
||||||
|
export { listLauncherLabels } from "./listLauncherLabels";
|
||||||
export { listLauncherViews } from "./listLauncherViews";
|
export { listLauncherViews } from "./listLauncherViews";
|
||||||
export { createLauncherView } from "./createLauncherView";
|
export { createLauncherView } from "./createLauncherView";
|
||||||
export { updateLauncherView } from "./updateLauncherView";
|
export { updateLauncherView } from "./updateLauncherView";
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
LAUNCHER_NO_SITE_GROUP_KEY,
|
LAUNCHER_NO_SITE_GROUP_KEY,
|
||||||
LAUNCHER_UNLABELED_GROUP_KEY,
|
LAUNCHER_UNLABELED_GROUP_KEY,
|
||||||
|
type LauncherFilterListQuery,
|
||||||
type LauncherGroup,
|
type LauncherGroup,
|
||||||
type LauncherLabel,
|
type LauncherLabel,
|
||||||
type LauncherListQuery,
|
type LauncherListQuery,
|
||||||
type LauncherResource,
|
type LauncherResource,
|
||||||
|
type LauncherSiteInfo,
|
||||||
parseIdListParam
|
parseIdListParam
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -1138,3 +1140,261 @@ export async function listLauncherResourcesForUser(
|
|||||||
total
|
total
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSiteNameSearchCondition(query: string) {
|
||||||
|
if (!query.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const pattern = searchPattern(query.toLowerCase());
|
||||||
|
return or(
|
||||||
|
like(sql`LOWER(${sites.name})`, pattern),
|
||||||
|
like(sql`LOWER(${sites.niceId})`, pattern)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLabelNameSearchCondition(query: string) {
|
||||||
|
if (!query.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const pattern = searchPattern(query.toLowerCase());
|
||||||
|
return like(sql`LOWER(${labels.name})`, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectAccessibleSites(
|
||||||
|
orgId: string,
|
||||||
|
accessible: AccessibleIds,
|
||||||
|
siteNameSearch?: ReturnType<typeof buildSiteNameSearchCondition>
|
||||||
|
): Promise<Map<number, SiteGroupRow>> {
|
||||||
|
const siteCountMap = new Map<number, SiteGroupRow>();
|
||||||
|
|
||||||
|
if (accessible.resourceIds.length > 0) {
|
||||||
|
const publicConditions = [
|
||||||
|
inArray(resources.resourceId, accessible.resourceIds),
|
||||||
|
eq(resources.orgId, orgId),
|
||||||
|
eq(resources.enabled, true)
|
||||||
|
];
|
||||||
|
if (siteNameSearch) {
|
||||||
|
publicConditions.push(siteNameSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicRows = await db
|
||||||
|
.select({
|
||||||
|
siteId: sites.siteId,
|
||||||
|
name: sites.name,
|
||||||
|
type: sites.type,
|
||||||
|
online: sites.online,
|
||||||
|
itemCount: countDistinct(resources.resourceId)
|
||||||
|
})
|
||||||
|
.from(targets)
|
||||||
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
|
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
|
.where(and(...publicConditions))
|
||||||
|
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
||||||
|
|
||||||
|
for (const row of publicRows) {
|
||||||
|
const existing = siteCountMap.get(row.siteId);
|
||||||
|
if (existing) {
|
||||||
|
existing.itemCount += Number(row.itemCount);
|
||||||
|
} else {
|
||||||
|
siteCountMap.set(row.siteId, {
|
||||||
|
siteId: row.siteId,
|
||||||
|
name: row.name,
|
||||||
|
type: row.type,
|
||||||
|
online: row.online,
|
||||||
|
itemCount: Number(row.itemCount)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessible.siteResourceIds.length > 0) {
|
||||||
|
const siteConditions = [
|
||||||
|
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||||
|
eq(siteResources.orgId, orgId),
|
||||||
|
eq(siteResources.enabled, true)
|
||||||
|
];
|
||||||
|
if (siteNameSearch) {
|
||||||
|
siteConditions.push(siteNameSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteRows = await db
|
||||||
|
.select({
|
||||||
|
siteId: sites.siteId,
|
||||||
|
name: sites.name,
|
||||||
|
type: sites.type,
|
||||||
|
online: sites.online,
|
||||||
|
itemCount: countDistinct(siteResources.siteResourceId)
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.innerJoin(
|
||||||
|
siteNetworks,
|
||||||
|
eq(siteResources.networkId, siteNetworks.networkId)
|
||||||
|
)
|
||||||
|
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||||
|
.where(and(...siteConditions))
|
||||||
|
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
||||||
|
|
||||||
|
for (const row of siteRows) {
|
||||||
|
const existing = siteCountMap.get(row.siteId);
|
||||||
|
if (existing) {
|
||||||
|
existing.itemCount += Number(row.itemCount);
|
||||||
|
} else {
|
||||||
|
siteCountMap.set(row.siteId, {
|
||||||
|
siteId: row.siteId,
|
||||||
|
name: row.name,
|
||||||
|
type: row.type,
|
||||||
|
online: row.online,
|
||||||
|
itemCount: Number(row.itemCount)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return siteCountMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectAccessibleLabels(
|
||||||
|
orgId: string,
|
||||||
|
accessible: AccessibleIds,
|
||||||
|
labelNameSearch?: ReturnType<typeof buildLabelNameSearchCondition>
|
||||||
|
): Promise<Map<number, LauncherLabel>> {
|
||||||
|
const labelMap = new Map<number, LauncherLabel>();
|
||||||
|
|
||||||
|
if (!(await labelsEnabled(orgId))) {
|
||||||
|
return labelMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessible.resourceIds.length > 0) {
|
||||||
|
const publicConditions = [
|
||||||
|
inArray(resources.resourceId, accessible.resourceIds),
|
||||||
|
eq(resources.orgId, orgId),
|
||||||
|
eq(resources.enabled, true),
|
||||||
|
eq(labels.orgId, orgId)
|
||||||
|
];
|
||||||
|
if (labelNameSearch) {
|
||||||
|
publicConditions.push(labelNameSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labeledPublic = await db
|
||||||
|
.select({
|
||||||
|
labelId: labels.labelId,
|
||||||
|
name: labels.name,
|
||||||
|
color: labels.color
|
||||||
|
})
|
||||||
|
.from(resourceLabels)
|
||||||
|
.innerJoin(labels, eq(resourceLabels.labelId, labels.labelId))
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(resourceLabels.resourceId, resources.resourceId)
|
||||||
|
)
|
||||||
|
.where(and(...publicConditions))
|
||||||
|
.groupBy(labels.labelId, labels.name, labels.color);
|
||||||
|
|
||||||
|
for (const row of labeledPublic) {
|
||||||
|
labelMap.set(row.labelId, {
|
||||||
|
labelId: row.labelId,
|
||||||
|
name: row.name,
|
||||||
|
color: row.color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessible.siteResourceIds.length > 0) {
|
||||||
|
const siteConditions = [
|
||||||
|
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
|
||||||
|
eq(siteResources.orgId, orgId),
|
||||||
|
eq(siteResources.enabled, true),
|
||||||
|
eq(labels.orgId, orgId)
|
||||||
|
];
|
||||||
|
if (labelNameSearch) {
|
||||||
|
siteConditions.push(labelNameSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labeledSite = await db
|
||||||
|
.select({
|
||||||
|
labelId: labels.labelId,
|
||||||
|
name: labels.name,
|
||||||
|
color: labels.color
|
||||||
|
})
|
||||||
|
.from(siteResourceLabels)
|
||||||
|
.innerJoin(labels, eq(siteResourceLabels.labelId, labels.labelId))
|
||||||
|
.innerJoin(
|
||||||
|
siteResources,
|
||||||
|
eq(
|
||||||
|
siteResourceLabels.siteResourceId,
|
||||||
|
siteResources.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(and(...siteConditions))
|
||||||
|
.groupBy(labels.labelId, labels.name, labels.color);
|
||||||
|
|
||||||
|
for (const row of labeledSite) {
|
||||||
|
labelMap.set(row.labelId, {
|
||||||
|
labelId: row.labelId,
|
||||||
|
name: row.name,
|
||||||
|
color: row.color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAccessibleLauncherSitesForUser(
|
||||||
|
orgId: string,
|
||||||
|
userId: string,
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherFilterListQuery
|
||||||
|
): Promise<{ sites: LauncherSiteInfo[]; total: number }> {
|
||||||
|
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
||||||
|
const siteNameSearch = buildSiteNameSearchCondition(query.query);
|
||||||
|
const siteCountMap = await collectAccessibleSites(
|
||||||
|
orgId,
|
||||||
|
accessible,
|
||||||
|
siteNameSearch
|
||||||
|
);
|
||||||
|
|
||||||
|
const sites: LauncherSiteInfo[] = Array.from(siteCountMap.values())
|
||||||
|
.map((row) => ({
|
||||||
|
siteId: row.siteId,
|
||||||
|
name: row.name,
|
||||||
|
type: row.type,
|
||||||
|
online: row.online
|
||||||
|
}))
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = sites.length;
|
||||||
|
const offset = (query.page - 1) * query.pageSize;
|
||||||
|
return {
|
||||||
|
sites: sites.slice(offset, offset + query.pageSize),
|
||||||
|
total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAccessibleLauncherLabelsForUser(
|
||||||
|
orgId: string,
|
||||||
|
userId: string,
|
||||||
|
userRoleIds: number[],
|
||||||
|
query: LauncherFilterListQuery
|
||||||
|
): Promise<{ labels: LauncherLabel[]; total: number }> {
|
||||||
|
const accessible = await resolveAccessibleIds(orgId, userId, userRoleIds);
|
||||||
|
const labelNameSearch = buildLabelNameSearchCondition(query.query);
|
||||||
|
const labelMap = await collectAccessibleLabels(
|
||||||
|
orgId,
|
||||||
|
accessible,
|
||||||
|
labelNameSearch
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelsList = Array.from(labelMap.values()).sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = labelsList.length;
|
||||||
|
const offset = (query.page - 1) * query.pageSize;
|
||||||
|
return {
|
||||||
|
labels: labelsList.slice(offset, offset + query.pageSize),
|
||||||
|
total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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 { listAccessibleLauncherLabelsForUser } from "./launcherResourceAccess";
|
||||||
|
import { launcherFilterListQuerySchema } from "./types";
|
||||||
|
|
||||||
|
export async function listLauncherLabels(
|
||||||
|
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 = launcherFilterListQuerySchema.safeParse(req.query);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromZodError(parsed.error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { labels, total } = await listAccessibleLauncherLabelsForUser(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
req.userOrgRoleIds ?? [],
|
||||||
|
parsed.data
|
||||||
|
);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
page: parsed.data.page,
|
||||||
|
pageSize: parsed.data.pageSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Launcher labels retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (createHttpError.isHttpError(error)) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
console.error("Error listing launcher labels:", error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal server error"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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 { listAccessibleLauncherSitesForUser } from "./launcherResourceAccess";
|
||||||
|
import { launcherFilterListQuerySchema } from "./types";
|
||||||
|
|
||||||
|
export async function listLauncherSites(
|
||||||
|
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 = launcherFilterListQuerySchema.safeParse(req.query);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromZodError(parsed.error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sites, total } = await listAccessibleLauncherSitesForUser(
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
req.userOrgRoleIds ?? [],
|
||||||
|
parsed.data
|
||||||
|
);
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
sites,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
page: parsed.data.page,
|
||||||
|
pageSize: parsed.data.pageSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Launcher sites retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (createHttpError.isHttpError(error)) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
console.error("Error listing launcher sites:", error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Internal server error"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,40 @@ export type ListLauncherViewsResponse = {
|
|||||||
views: LauncherViewRecord[];
|
views: LauncherViewRecord[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const launcherFilterListQuerySchema = z.strictObject({
|
||||||
|
pageSize: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.catch(500)
|
||||||
|
.default(500),
|
||||||
|
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
|
||||||
|
query: z.string().optional().default("")
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LauncherFilterListQuery = z.infer<
|
||||||
|
typeof launcherFilterListQuerySchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ListLauncherSitesResponse = {
|
||||||
|
sites: LauncherSiteInfo[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListLauncherLabelsResponse = {
|
||||||
|
labels: LauncherLabel[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const launcherListQuerySchema = z.strictObject({
|
export const launcherListQuerySchema = z.strictObject({
|
||||||
pageSize: z.coerce
|
pageSize: z.coerce
|
||||||
.number()
|
.number()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from "@app/components/ui/command";
|
} from "@app/components/ui/command";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { launcherQueries, orgQueries } from "@app/lib/queries";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -27,6 +27,7 @@ type LabelsFilterSelectorProps = {
|
|||||||
onToggle: (label: LabelFilterOption) => void;
|
onToggle: (label: LabelFilterOption) => void;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
showClear?: boolean;
|
showClear?: boolean;
|
||||||
|
scope?: "org" | "launcher";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LabelsFilterSelector({
|
export function LabelsFilterSelector({
|
||||||
@@ -34,19 +35,33 @@ export function LabelsFilterSelector({
|
|||||||
isSelected,
|
isSelected,
|
||||||
onToggle,
|
onToggle,
|
||||||
onClear,
|
onClear,
|
||||||
showClear = false
|
showClear = false,
|
||||||
|
scope = "org"
|
||||||
}: LabelsFilterSelectorProps) {
|
}: LabelsFilterSelectorProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [labelSearchQuery, setlabelsSearchQuery] = useState("");
|
const [labelSearchQuery, setlabelsSearchQuery] = useState("");
|
||||||
const [debouncedQuery] = useDebounce(labelSearchQuery, 150);
|
const [debouncedQuery] = useDebounce(labelSearchQuery, 150);
|
||||||
|
|
||||||
const { data: labels = [] } = useQuery(
|
const orgLabelsQuery = useQuery({
|
||||||
orgQueries.labels({
|
...orgQueries.labels({
|
||||||
orgId,
|
orgId,
|
||||||
query: debouncedQuery,
|
query: debouncedQuery,
|
||||||
perPage: 500
|
perPage: 500
|
||||||
})
|
}),
|
||||||
);
|
enabled: scope === "org"
|
||||||
|
});
|
||||||
|
const launcherLabelsQuery = useQuery({
|
||||||
|
...launcherQueries.labels({
|
||||||
|
orgId,
|
||||||
|
query: debouncedQuery,
|
||||||
|
perPage: 500
|
||||||
|
}),
|
||||||
|
enabled: scope === "launcher"
|
||||||
|
});
|
||||||
|
const labels =
|
||||||
|
scope === "launcher"
|
||||||
|
? (launcherLabelsQuery.data ?? [])
|
||||||
|
: (orgLabelsQuery.data ?? []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { orgQueries } from "@app/lib/queries";
|
import { launcherQueries, orgQueries } from "@app/lib/queries";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +19,7 @@ export type MultiSitesSelectorProps = {
|
|||||||
selectedSites: Selectedsite[];
|
selectedSites: Selectedsite[];
|
||||||
onSelectionChange: (sites: Selectedsite[]) => void;
|
onSelectionChange: (sites: Selectedsite[]) => void;
|
||||||
filterTypes?: string[];
|
filterTypes?: string[];
|
||||||
|
scope?: "org" | "launcher";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatMultiSitesSelectorLabel(
|
export function formatMultiSitesSelectorLabel(
|
||||||
@@ -40,19 +41,33 @@ export function MultiSitesSelector({
|
|||||||
orgId,
|
orgId,
|
||||||
selectedSites,
|
selectedSites,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
filterTypes
|
filterTypes,
|
||||||
|
scope = "org"
|
||||||
}: MultiSitesSelectorProps) {
|
}: MultiSitesSelectorProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [siteSearchQuery, setSiteSearchQuery] = useState("");
|
const [siteSearchQuery, setSiteSearchQuery] = useState("");
|
||||||
const [debouncedQuery] = useDebounce(siteSearchQuery, 150);
|
const [debouncedQuery] = useDebounce(siteSearchQuery, 150);
|
||||||
|
|
||||||
const { data: sites = [] } = useQuery(
|
const orgSitesQuery = useQuery({
|
||||||
orgQueries.sites({
|
...orgQueries.sites({
|
||||||
orgId,
|
orgId,
|
||||||
query: debouncedQuery,
|
query: debouncedQuery,
|
||||||
perPage: 10
|
perPage: 10
|
||||||
})
|
}),
|
||||||
);
|
enabled: scope === "org"
|
||||||
|
});
|
||||||
|
const launcherSitesQuery = useQuery({
|
||||||
|
...launcherQueries.sites({
|
||||||
|
orgId,
|
||||||
|
query: debouncedQuery,
|
||||||
|
perPage: 500
|
||||||
|
}),
|
||||||
|
enabled: scope === "launcher"
|
||||||
|
});
|
||||||
|
const sites =
|
||||||
|
scope === "launcher"
|
||||||
|
? (launcherSitesQuery.data ?? [])
|
||||||
|
: (orgSitesQuery.data ?? []);
|
||||||
|
|
||||||
const sitesShown = useMemo(() => {
|
const sitesShown = useMemo(() => {
|
||||||
const base = filterTypes
|
const base = filterTypes
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { launcherQueries } from "@app/lib/queries";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ChevronsUpDown, Funnel } from "lucide-react";
|
import { ChevronsUpDown, Funnel } from "lucide-react";
|
||||||
@@ -44,12 +44,37 @@ export function LauncherFilterPopover({
|
|||||||
const [labelsOpen, setLabelsOpen] = useState(false);
|
const [labelsOpen, setLabelsOpen] = useState(false);
|
||||||
|
|
||||||
const { data: labels = [] } = useQuery(
|
const { data: labels = [] } = useQuery(
|
||||||
orgQueries.labels({
|
launcherQueries.labels({
|
||||||
orgId,
|
orgId,
|
||||||
perPage: 500
|
perPage: 500
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: sites = [] } = useQuery(
|
||||||
|
launcherQueries.sites({
|
||||||
|
orgId,
|
||||||
|
perPage: 500
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedSelectedSites: Selectedsite[] = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedSites.map((selected) => {
|
||||||
|
const found = sites.find(
|
||||||
|
(site) => site.siteId === selected.siteId
|
||||||
|
);
|
||||||
|
return found
|
||||||
|
? {
|
||||||
|
siteId: found.siteId,
|
||||||
|
name: found.name,
|
||||||
|
type: found.type,
|
||||||
|
online: found.online
|
||||||
|
}
|
||||||
|
: selected;
|
||||||
|
}),
|
||||||
|
[sites, selectedSites]
|
||||||
|
);
|
||||||
|
|
||||||
const selectedLabelIds = useMemo(
|
const selectedLabelIds = useMemo(
|
||||||
() => new Set(selectedLabels.map((label) => label.labelId)),
|
() => new Set(selectedLabels.map((label) => label.labelId)),
|
||||||
[selectedLabels]
|
[selectedLabels]
|
||||||
@@ -98,7 +123,7 @@ export function LauncherFilterPopover({
|
|||||||
>
|
>
|
||||||
<span className="truncate text-left">
|
<span className="truncate text-left">
|
||||||
{formatMultiSitesSelectorLabel(
|
{formatMultiSitesSelectorLabel(
|
||||||
selectedSites,
|
resolvedSelectedSites,
|
||||||
t
|
t
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -111,8 +136,9 @@ export function LauncherFilterPopover({
|
|||||||
>
|
>
|
||||||
<MultiSitesSelector
|
<MultiSitesSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
selectedSites={selectedSites}
|
selectedSites={resolvedSelectedSites}
|
||||||
onSelectionChange={onSitesChange}
|
onSelectionChange={onSitesChange}
|
||||||
|
scope="launcher"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -145,6 +171,7 @@ export function LauncherFilterPopover({
|
|||||||
>
|
>
|
||||||
<LabelsFilterSelector
|
<LabelsFilterSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
|
scope="launcher"
|
||||||
isSelected={(label) =>
|
isSelected={(label) =>
|
||||||
selectedLabelIds.has(label.labelId)
|
selectedLabelIds.has(label.labelId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ import type { ListResourcePoliciesResponse } from "@server/routers/resource/type
|
|||||||
import type { GetResourcePolicyResponse } from "@server/routers/policy";
|
import type { GetResourcePolicyResponse } from "@server/routers/policy";
|
||||||
import type {
|
import type {
|
||||||
ListLauncherGroupsResponse,
|
ListLauncherGroupsResponse,
|
||||||
|
ListLauncherLabelsResponse,
|
||||||
ListLauncherResourcesResponse,
|
ListLauncherResourcesResponse,
|
||||||
|
ListLauncherSitesResponse,
|
||||||
ListLauncherViewsResponse,
|
ListLauncherViewsResponse,
|
||||||
LauncherListQuery,
|
LauncherListQuery,
|
||||||
LauncherViewConfig
|
LauncherViewConfig
|
||||||
@@ -1190,6 +1192,72 @@ export const launcherQueries = {
|
|||||||
return res.data.data.views;
|
return res.data.data.views;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
sites: ({
|
||||||
|
orgId,
|
||||||
|
query,
|
||||||
|
perPage = 500
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
query?: string;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"ORG",
|
||||||
|
orgId,
|
||||||
|
"LAUNCHER",
|
||||||
|
"SITES",
|
||||||
|
{ query, perPage }
|
||||||
|
] as const,
|
||||||
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const sp = new URLSearchParams({
|
||||||
|
pageSize: perPage.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query?.trim()) {
|
||||||
|
sp.set("query", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await meta!.api.get<
|
||||||
|
AxiosResponse<ListLauncherSitesResponse>
|
||||||
|
>(`/org/${orgId}/launcher/sites?${sp.toString()}`, { signal });
|
||||||
|
return res.data.data.sites;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
labels: ({
|
||||||
|
orgId,
|
||||||
|
query,
|
||||||
|
perPage = 500
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
query?: string;
|
||||||
|
perPage?: number;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [
|
||||||
|
"ORG",
|
||||||
|
orgId,
|
||||||
|
"LAUNCHER",
|
||||||
|
"LABELS",
|
||||||
|
{ query, perPage }
|
||||||
|
] as const,
|
||||||
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const sp = new URLSearchParams({
|
||||||
|
pageSize: perPage.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query?.trim()) {
|
||||||
|
sp.set("query", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await meta!.api.get<
|
||||||
|
AxiosResponse<ListLauncherLabelsResponse>
|
||||||
|
>(`/org/${orgId}/launcher/labels?${sp.toString()}`, {
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
return res.data.data.labels;
|
||||||
|
}
|
||||||
|
}),
|
||||||
groups: (orgId: string, filters: LauncherQueryFilters) =>
|
groups: (orgId: string, filters: LauncherQueryFilters) =>
|
||||||
infiniteQueryOptions({
|
infiniteQueryOptions({
|
||||||
queryKey: ["ORG", orgId, "LAUNCHER", "GROUPS", filters] as const,
|
queryKey: ["ORG", orgId, "LAUNCHER", "GROUPS", filters] as const,
|
||||||
|
|||||||
Reference in New Issue
Block a user