diff --git a/src/api/query-hooks/index.ts b/src/api/query-hooks/index.ts index d52b90403..260c41812 100644 --- a/src/api/query-hooks/index.ts +++ b/src/api/query-hooks/index.ts @@ -19,7 +19,7 @@ import { import { getHypothesisResponse } from "../services/hypothesis"; import { getIncident } from "../services/incident"; import { LogsResponse, SearchLogsPayload, searchLogs } from "../services/logs"; -import { getPagingParams } from "../services/notifications"; +import { appendPagingParamsToSearchParams } from "../services/notifications"; import { getComponentTeams, getHealthCheckSpecByID, @@ -179,24 +179,34 @@ export function prepareConfigListQuery({ pageIndex, pageSize }: ConfigListFilterQueryOptions) { - let query = - "select=id,type,config_class,status,health,labels,name,tags,created_at,updated_at,deleted_at,cost_per_minute,cost_total_1d,cost_total_7d,cost_total_30d,changes,analysis"; + const query = new URLSearchParams({ + select: + "id,type,config_class,status,health,labels,name,tags,created_at,updated_at,deleted_at,cost_per_minute,cost_total_1d,cost_total_7d,cost_total_30d,changes,analysis" + }); + if (includeAgents) { - query = `${query},agent:agents(id,name)`; + query.append("select", "agent:agents(id,name)"); } + if (configType && configType !== "All") { - query = `${query}&type=eq.${configType}`; + query.append("type", `eq.${configType}`); } + if (status && status !== "All") { const statusParam = tristateOutputToQueryFilterParam(status, "status"); - query = `${query}${statusParam}`; + query.append("status", statusParam); } + if (health) { const healthParam = tristateOutputToQueryFilterParam(health, "health"); - query = `${query}${healthParam}`; + query.append("health", healthParam); } + if (search) { - query = `${query}&or=(name.ilike.*${search}*,type.ilike.*${search}*,description.ilike.*${search}*,namespace.ilike.*${search}*)`; + query.append( + "or", + `(name.ilike.*${search}*,type.ilike.*${search}*,description.ilike.*${search}*,namespace.ilike.*${search}*)` + ); } else { const filterQueries: string[] = []; if (label && label !== "All") { @@ -205,32 +215,35 @@ export function prepareConfigListQuery({ } if (labels) { labels.split(",").forEach((label) => { - const [k, v] = label.split("__:__"); + const [k, v] = label.split("____"); + const [realValue, operand] = v.split(":"); + const operator = parseInt(operand) === -1 ? "neq" : "eq"; if (!isNull(v)) { - filterQueries.push(`labels->>${k}=eq.${encodeURIComponent(v)}`); + filterQueries.push( + `labels->>${k}.${operator}.${encodeURIComponent(realValue)}` + ); } else { filterQueries.push(`labels->>${k}=is.null`); } }); } if (filterQueries.length) { - query = `${query}&${filterQueries.join("&")}`; + query.append("or", `(and(${filterQueries.join(",")}))`); } } + if (sortBy && sortOrder) { const sortField = sortBy === "type" ? `${sortBy},name` : sortBy; - query = `${query}&order=${sortField}.${sortOrder}`; + query.append("order", `${sortField}.${sortOrder}`); } + if (hideDeletedConfigs) { - query = `${query}&deleted_at=is.null`; + query.append("deleted_at", "is.null"); } - const pagingParams = getPagingParams({ pageIndex, pageSize }); - if (pagingParams) { - query = `${query}${pagingParams}`; - } + appendPagingParamsToSearchParams(query, { pageIndex, pageSize }); - return query; + return query.toString(); } export const useConfigNameQuery = ( diff --git a/src/api/query-hooks/useConfigSummaryQuery.ts b/src/api/query-hooks/useConfigSummaryQuery.ts index 90afbefd7..bdc9f1434 100644 --- a/src/api/query-hooks/useConfigSummaryQuery.ts +++ b/src/api/query-hooks/useConfigSummaryQuery.ts @@ -9,6 +9,23 @@ import { useSearchParams } from "react-router-dom"; import { ConfigSummaryRequest, getConfigsSummary } from "../services/configs"; import { ConfigSummary } from "../types/configs"; +export function useLabelFiltersFromParams() { + const [searchParams] = useSearchParams(); + const labels = searchParams.get("labels") ?? undefined; + + return useMemo(() => { + if (labels) { + return labels.split(",").reduce((acc, label) => { + const [filterValue, operand] = label.split(":"); + const [key, value] = filterValue.split("____"); + const symbol = parseInt(operand) === -1 ? "!" : ""; + return { ...acc, [key]: `${symbol}${value}` }; + }, {}); + } + return undefined; + }, [labels]); +} + export function useConfigSummaryQuery({ enabled = true }: UseQueryOptions = {}) { @@ -18,7 +35,6 @@ export function useConfigSummaryQuery({ groupBy: "config_class,type" }); const hideDeletedConfigs = useHideDeletedConfigs(); - const labels = searchParams.get("labels") ?? undefined; const status = searchParams.get("status") ?? undefined; const health = searchParams.get("health") ?? undefined; @@ -26,15 +42,7 @@ export function useConfigSummaryQuery({ const groupBy = useGroupBySearchParam(); - const filterSummaryByLabel = useMemo(() => { - if (labels) { - return labels.split(",").reduce((acc, label) => { - const [key, value] = label.split("__:__"); - return { ...acc, [key]: value }; - }, {}); - } - return undefined; - }, [labels]); + const filterSummaryByLabel = useLabelFiltersFromParams(); const req: ConfigSummaryRequest = { // group by config_class is always done on the frontend diff --git a/src/api/services/notifications.ts b/src/api/services/notifications.ts index e16e73f50..b3b959580 100644 --- a/src/api/services/notifications.ts +++ b/src/api/services/notifications.ts @@ -24,6 +24,22 @@ export function getPagingParams({ return pagingParams; } +export function appendPagingParamsToSearchParams( + params: URLSearchParams, + { + pageIndex, + pageSize + }: { + pageIndex?: number; + pageSize?: number; + } +) { + if (pageIndex || pageSize) { + params.append("limit", pageSize!.toString()); + params.append("offset", (pageIndex! * pageSize!).toString()); + } +} + export const getNotificationsSummary = async ({ pageIndex, pageSize, diff --git a/src/components/Configs/ConfigList/Cells/ConfigListTagsCell.tsx b/src/components/Configs/ConfigList/Cells/ConfigListTagsCell.tsx index 77e5a7b40..f38ae35f4 100644 --- a/src/components/Configs/ConfigList/Cells/ConfigListTagsCell.tsx +++ b/src/components/Configs/ConfigList/Cells/ConfigListTagsCell.tsx @@ -11,6 +11,7 @@ type ConfigListTagsCellProps> = Pick< hideGroupByView?: boolean; label?: string; enableFilterByTag?: boolean; + filterByTagParamKey?: string; }; export default function ConfigListTagsCell< @@ -19,7 +20,8 @@ export default function ConfigListTagsCell< row, getValue, hideGroupByView = false, - enableFilterByTag = false + enableFilterByTag = false, + filterByTagParamKey = "tags" }: ConfigListTagsCellProps): JSX.Element | null { const [params, setParams] = useSearchParams(); @@ -65,10 +67,10 @@ export default function ConfigListTagsCell< .join(","); // Update the URL - params.set("tags", updatedValue); + params.set(filterByTagParamKey, updatedValue); setParams(params); }, - [enableFilterByTag, params, setParams] + [enableFilterByTag, filterByTagParamKey, params, setParams] ); const groupByProp = decodeURIComponent(params.get("groupByProp") ?? ""); diff --git a/src/components/Configs/ConfigList/Cells/MRTConfigListTagsCell.tsx b/src/components/Configs/ConfigList/Cells/MRTConfigListTagsCell.tsx index ecde2a5a0..e6b6ed5f2 100644 --- a/src/components/Configs/ConfigList/Cells/MRTConfigListTagsCell.tsx +++ b/src/components/Configs/ConfigList/Cells/MRTConfigListTagsCell.tsx @@ -12,6 +12,7 @@ type MRTConfigListTagsCellProps< > = MRTCellProps & { hideGroupByView?: boolean; enableFilterByTag?: boolean; + filterByTagParamKey?: string; }; export default function MRTConfigListTagsCell< @@ -20,7 +21,8 @@ export default function MRTConfigListTagsCell< row, cell, hideGroupByView = false, - enableFilterByTag = false + enableFilterByTag = false, + filterByTagParamKey = "tags" }: MRTConfigListTagsCellProps): JSX.Element | null { const [params, setParams] = useSearchParams(); @@ -46,7 +48,7 @@ export default function MRTConfigListTagsCell< e.stopPropagation(); // Get the current tags from the URL - const currentTags = params.get("tags"); + const currentTags = params.get(filterByTagParamKey); const currentTagsArray = ( currentTags ? currentTags.split(",") : [] ).filter((value) => { @@ -66,10 +68,10 @@ export default function MRTConfigListTagsCell< .join(","); // Update the URL - params.set("tags", updatedValue); + params.set(filterByTagParamKey, updatedValue); setParams(params); }, - [enableFilterByTag, params, setParams] + [enableFilterByTag, filterByTagParamKey, params, setParams] ); const groupByProp = decodeURIComponent(params.get("groupByProp") ?? ""); diff --git a/src/components/Configs/ConfigList/MRTConfigListColumn.tsx b/src/components/Configs/ConfigList/MRTConfigListColumn.tsx index 38ef569a8..757634991 100644 --- a/src/components/Configs/ConfigList/MRTConfigListColumn.tsx +++ b/src/components/Configs/ConfigList/MRTConfigListColumn.tsx @@ -143,7 +143,13 @@ export const mrtConfigListColumns: MRT_ColumnDef[] = [ header: "Tags", accessorKey: "tags", enableColumnActions: false, - Cell: (props) => , + Cell: (props) => ( + + ), maxSize: 300, minSize: 100 }, diff --git a/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx b/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx index ea4fd8850..f1b46d62f 100644 --- a/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx +++ b/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx @@ -234,6 +234,7 @@ export default function ConfigSummaryList({ const handleRowClick = useCallback( (row: Row) => { + params.delete("labels"); if (groupBy.includes("type")) { const { type } = row.original; params.set("configType", type); diff --git a/src/components/Configs/ConfigsListFilters/ConfigLabelsDropdown.tsx b/src/components/Configs/ConfigsListFilters/ConfigLabelsDropdown.tsx index 5ad4f55d6..12bf69541 100644 --- a/src/components/Configs/ConfigsListFilters/ConfigLabelsDropdown.tsx +++ b/src/components/Configs/ConfigsListFilters/ConfigLabelsDropdown.tsx @@ -1,7 +1,9 @@ import { useGetConfigLabelsListQuery } from "@flanksource-ui/api/query-hooks"; +import TristateReactSelect, { + TriStateOptions +} from "@flanksource-ui/ui/Dropdowns/TristateReactSelect"; import { useField } from "formik"; import { useMemo } from "react"; -import { ReactSelectDropdown } from "../../ReactSelectDropdown"; type Props = { searchParamKey?: string; @@ -16,30 +18,33 @@ export function ConfigLabelsDropdown({ searchParamKey = "labels" }: Props) { const labelItems = useMemo(() => { if (data && Array.isArray(data)) { - const options = data.map((tag) => ({ - label: ( -
- {tag.key}: - {tag.value} -
- ), - value: `${tag.key}__:__${tag.value}` - })); - return [{ label: "All", value: "All" }, ...options]; + return data.map( + (tag) => + ({ + label: ( + + {tag.key}: + {tag.value} + + ), + value: `${tag.key}____${tag.value}`, + id: `${tag.key}____${tag.value}` + }) satisfies TriStateOptions + ); } else { // Adding this console.error to help debug the issue I noticed happening // inside the Saas, that's leading to the catalog page crashing console.error("Invalid data for ConfigLabelsDropdown", data); - return [{ label: "All", value: "All" }]; + return []; } }, [data]); return ( - { - if (value && value !== "All") { + if (value && value !== "all") { field.onChange({ target: { name: searchParamKey, value: value } }); @@ -49,17 +54,10 @@ export function ConfigLabelsDropdown({ searchParamKey = "labels" }: Props) { }); } }} - value={field.value ?? "All"} - className="w-auto max-w-[38rem]" - dropDownClassNames="w-auto max-w-[38rem] left-0" - hideControlBorder - isMulti - prefix={ -
- Labels: -
- } - isLoading={isLoading} + minMenuWidth="400px" + value={field.value} + className="w-auto max-w-[400px]" + label={"Labels"} /> ); }