Merge pull request #16605 from prometheus/rules-page-filters
	
		
			
	
		
	
	
		
			
				
	
				buf.build / lint and publish (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Go tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / More Go tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Go tests with previous Go version (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / UI tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Go tests on Windows (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Mixins tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for common architectures (0) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for common architectures (1) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for common architectures (2) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (0) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (1) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (10) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (11) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (2) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (3) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (4) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (5) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (6) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (7) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (8) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (9) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				CI / Check generated parser (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / golangci-lint (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / fuzzing (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / codeql (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Publish main branch artifacts (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				CI / Publish release artefacts (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				CI / Publish UI on npm Registry (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
				
					Details
				
			
		
	
				
					
				
			
				
	
				buf.build / lint and publish (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Go tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / More Go tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Go tests with previous Go version (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / UI tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Go tests on Windows (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Mixins tests (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for common architectures (0) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for common architectures (1) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for common architectures (2) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (0) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (1) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (10) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (11) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (2) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (3) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (4) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (5) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (6) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (7) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (8) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Build Prometheus for all architectures (9) (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Report status of build Prometheus for all architectures (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				CI / Check generated parser (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / golangci-lint (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / fuzzing (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / codeql (push) Waiting to run
				
					Details
				
			
		
			
				
	
				CI / Publish main branch artifacts (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				CI / Publish release artefacts (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				CI / Publish UI on npm Registry (push) Blocked by required conditions
				
					Details
				
			
		
			
				
	
				Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
				
					Details
				
			
		
	Add health & text filtering on the /rules page
This commit is contained in:
		
						commit
						6c930e8506
					
				|  | @ -37,7 +37,7 @@ type RecordingRule = { | |||
| 
 | ||||
| export type Rule = AlertingRule | RecordingRule; | ||||
| 
 | ||||
| interface RuleGroup { | ||||
| export interface RuleGroup { | ||||
|   name: string; | ||||
|   file: string; | ||||
|   interval: string; | ||||
|  |  | |||
|  | @ -217,12 +217,7 @@ export default function AlertsPage() { | |||
|   const renderedPageItems = useMemo( | ||||
|     () => | ||||
|       currentPageGroups.map((g) => ( | ||||
|         <Card | ||||
|           shadow="xs" | ||||
|           withBorder | ||||
|           p="md" | ||||
|           key={`${g.file}-${g.name}`} | ||||
|         > | ||||
|         <Card shadow="xs" withBorder p="md" key={`${g.file}-${g.name}`}> | ||||
|           <Group mb="sm" justify="space-between"> | ||||
|             <Group align="baseline"> | ||||
|               <Text fz="xl" fw={600} c="var(--mantine-primary-color-filled)"> | ||||
|  | @ -460,7 +455,6 @@ export default function AlertsPage() { | |||
|           </Alert> | ||||
|         ) | ||||
|       )} | ||||
|       <Stack> | ||||
|       <Pagination | ||||
|         total={totalPageCount} | ||||
|         value={effectiveActivePage} | ||||
|  | @ -469,6 +463,5 @@ export default function AlertsPage() { | |||
|       /> | ||||
|       {renderedPageItems} | ||||
|     </Stack> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { | ||||
|   Accordion, | ||||
|   Alert, | ||||
|   Anchor, | ||||
|   Badge, | ||||
|   Card, | ||||
|   Group, | ||||
|  | @ -8,9 +9,9 @@ import { | |||
|   rem, | ||||
|   Stack, | ||||
|   Text, | ||||
|   TextInput, | ||||
|   Tooltip, | ||||
| } from "@mantine/core"; | ||||
| // import { useQuery } from "react-query";
 | ||||
| import { | ||||
|   humanizeDurationRelative, | ||||
|   humanizeDuration, | ||||
|  | @ -23,17 +24,55 @@ import { | |||
|   IconInfoCircle, | ||||
|   IconRefresh, | ||||
|   IconRepeat, | ||||
|   IconSearch, | ||||
|   IconTimeline, | ||||
| } from "@tabler/icons-react"; | ||||
| import { useSuspenseAPIQuery } from "../api/api"; | ||||
| import { RulesResult } from "../api/responseTypes/rules"; | ||||
| import { Rule, RuleGroup, RulesResult } from "../api/responseTypes/rules"; | ||||
| import badgeClasses from "../Badge.module.css"; | ||||
| import RuleDefinition from "../components/RuleDefinition"; | ||||
| import { badgeIconStyle } from "../styles"; | ||||
| import { NumberParam, useQueryParam, withDefault } from "use-query-params"; | ||||
| import { badgeIconStyle, inputIconStyle } from "../styles"; | ||||
| import { | ||||
|   ArrayParam, | ||||
|   NumberParam, | ||||
|   StringParam, | ||||
|   useQueryParam, | ||||
|   withDefault, | ||||
| } from "use-query-params"; | ||||
| import { useSettings } from "../state/settingsSlice"; | ||||
| import { useEffect } from "react"; | ||||
| import { useEffect, useMemo } from "react"; | ||||
| import CustomInfiniteScroll from "../components/CustomInfiniteScroll"; | ||||
| import { useDebouncedValue, useLocalStorage } from "@mantine/hooks"; | ||||
| import { KVSearch } from "@nexucis/kvsearch"; | ||||
| import { StateMultiSelect } from "../components/StateMultiSelect"; | ||||
| 
 | ||||
| const kvSearch = new KVSearch<Rule>({ | ||||
|   shouldSort: true, | ||||
|   indexedKeys: ["name", "labels", ["labels", /.*/]], | ||||
| }); | ||||
| 
 | ||||
| type RulesPageData = { | ||||
|   groups: (RuleGroup & { prefilterRulesCount: number })[]; | ||||
| }; | ||||
| 
 | ||||
| const buildRulesPageData = ( | ||||
|   data: RulesResult, | ||||
|   search: string, | ||||
|   healthFilter: (string | null)[] | ||||
| ): RulesPageData => { | ||||
|   const groups = data.groups.map((group) => ({ | ||||
|     ...group, | ||||
|     prefilterRulesCount: group.rules.length, | ||||
|     rules: (search === "" | ||||
|       ? group.rules | ||||
|       : kvSearch.filter(search, group.rules).map((value) => value.original) | ||||
|     ).filter( | ||||
|       (r) => healthFilter.length === 0 || healthFilter.includes(r.health) | ||||
|     ), | ||||
|   })); | ||||
| 
 | ||||
|   return { groups }; | ||||
| }; | ||||
| 
 | ||||
| const healthBadgeClass = (state: string) => { | ||||
|   switch (state) { | ||||
|  | @ -48,18 +87,53 @@ const healthBadgeClass = (state: string) => { | |||
|   } | ||||
| }; | ||||
| 
 | ||||
| // Should be defined as a constant here instead of inline as a value
 | ||||
| // to avoid unnecessary re-renders. Otherwise the empty array has
 | ||||
| // a different reference on each render and causes subsequent memoized
 | ||||
| // computations to re-run as long as no health filter is selected.
 | ||||
| const emptyHealthFilter: string[] = []; | ||||
| 
 | ||||
| export default function RulesPage() { | ||||
|   const { data } = useSuspenseAPIQuery<RulesResult>({ path: `/rules` }); | ||||
|   const { ruleGroupsPerPage } = useSettings(); | ||||
| 
 | ||||
|   // Define URL query params.
 | ||||
|   const [healthFilter, setHealthFilter] = useQueryParam( | ||||
|     "health", | ||||
|     withDefault(ArrayParam, emptyHealthFilter) | ||||
|   ); | ||||
|   const [searchFilter, setSearchFilter] = useQueryParam( | ||||
|     "search", | ||||
|     withDefault(StringParam, "") | ||||
|   ); | ||||
|   const [debouncedSearch] = useDebouncedValue<string>(searchFilter.trim(), 250); | ||||
|   const [showEmptyGroups, setShowEmptyGroups] = useLocalStorage<boolean>({ | ||||
|     key: "alertsPage.showEmptyGroups", | ||||
|     defaultValue: false, | ||||
|   }); | ||||
| 
 | ||||
|   const { ruleGroupsPerPage } = useSettings(); | ||||
|   const [activePage, setActivePage] = useQueryParam( | ||||
|     "page", | ||||
|     withDefault(NumberParam, 1) | ||||
|   ); | ||||
| 
 | ||||
|   // If we were e.g. on page 10 and the number of total pages decreases to 5 (due
 | ||||
|   // changing the max number of items per page), go to the largest possible page.
 | ||||
|   const totalPageCount = Math.ceil(data.data.groups.length / ruleGroupsPerPage); | ||||
|   // Update the page data whenever the fetched data or filters change.
 | ||||
|   const rulesPageData = useMemo( | ||||
|     () => buildRulesPageData(data.data, debouncedSearch, healthFilter), | ||||
|     [data, healthFilter, debouncedSearch] | ||||
|   ); | ||||
| 
 | ||||
|   const shownGroups = useMemo( | ||||
|     () => | ||||
|       showEmptyGroups | ||||
|         ? rulesPageData.groups | ||||
|         : rulesPageData.groups.filter((g) => g.rules.length > 0), | ||||
|     [rulesPageData.groups, showEmptyGroups] | ||||
|   ); | ||||
| 
 | ||||
|   // If we were e.g. on page 10 and the number of total pages decreases to 5 (due to filtering
 | ||||
|   // or changing the max number of items per page), go to the largest possible page.
 | ||||
|   const totalPageCount = Math.ceil(shownGroups.length / ruleGroupsPerPage); | ||||
|   const effectiveActivePage = Math.max(1, Math.min(activePage, totalPageCount)); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|  | @ -68,32 +142,25 @@ export default function RulesPage() { | |||
|     } | ||||
|   }, [effectiveActivePage, activePage, setActivePage]); | ||||
| 
 | ||||
|   return ( | ||||
|     <Stack mt="xs"> | ||||
|       {data.data.groups.length === 0 && ( | ||||
|         <Alert title="No rule groups" icon={<IconInfoCircle />}> | ||||
|           No rule groups configured. | ||||
|         </Alert> | ||||
|       )} | ||||
|       <Pagination | ||||
|         total={totalPageCount} | ||||
|         value={effectiveActivePage} | ||||
|         onChange={setActivePage} | ||||
|         hideWithOnePage | ||||
|       /> | ||||
|       {data.data.groups | ||||
|         .slice( | ||||
|   const currentPageGroups = useMemo( | ||||
|     () => | ||||
|       shownGroups.slice( | ||||
|         (effectiveActivePage - 1) * ruleGroupsPerPage, | ||||
|         effectiveActivePage * ruleGroupsPerPage | ||||
|         ) | ||||
|         .map((g) => ( | ||||
|           <Card | ||||
|             shadow="xs" | ||||
|             withBorder | ||||
|             p="md" | ||||
|             mb="md" | ||||
|             key={`${g.file}-${g.name}`} | ||||
|           > | ||||
|       ), | ||||
|     [shownGroups, effectiveActivePage, ruleGroupsPerPage] | ||||
|   ); | ||||
| 
 | ||||
|   // We memoize the actual rendering of the page items to avoid re-rendering
 | ||||
|   // them on every state change. This is especially important when the user
 | ||||
|   // types into the search box, as the search filter changes on every keystroke,
 | ||||
|   // even before debouncing takes place (extracting the filters and results list
 | ||||
|   // into separate components would be an alternative to this, but it's kinda
 | ||||
|   // convenient to have in the same file IMO).
 | ||||
|   const renderedPageItems = useMemo( | ||||
|     () => | ||||
|       currentPageGroups.map((g) => ( | ||||
|         <Card shadow="xs" withBorder p="md" key={`${g.file}-${g.name}`}> | ||||
|           <Group mb="sm" justify="space-between"> | ||||
|             <Group align="baseline"> | ||||
|               <Text fz="xl" fw={600} c="var(--mantine-primary-color-filled)"> | ||||
|  | @ -136,11 +203,30 @@ export default function RulesPage() { | |||
|               </Tooltip> | ||||
|             </Group> | ||||
|           </Group> | ||||
|             {g.rules.length === 0 && ( | ||||
|           {g.prefilterRulesCount === 0 ? ( | ||||
|             <Alert title="No rules" icon={<IconInfoCircle />}> | ||||
|                 No rules in rule group. | ||||
|               No rules in this group. | ||||
|               <Anchor | ||||
|                 ml="md" | ||||
|                 fz="1em" | ||||
|                 onClick={() => setShowEmptyGroups(false)} | ||||
|               > | ||||
|                 Hide empty groups | ||||
|               </Anchor> | ||||
|             </Alert> | ||||
|             )} | ||||
|           ) : g.rules.length === 0 ? ( | ||||
|             <Alert title="No matching rules" icon={<IconInfoCircle />}> | ||||
|               No rules in this group match your filter criteria (omitted{" "} | ||||
|               {g.prefilterRulesCount} filtered rules). | ||||
|               <Anchor | ||||
|                 ml="md" | ||||
|                 fz="1em" | ||||
|                 onClick={() => setShowEmptyGroups(false)} | ||||
|               > | ||||
|                 Hide empty groups | ||||
|               </Anchor> | ||||
|             </Alert> | ||||
|           ) : ( | ||||
|             <CustomInfiniteScroll | ||||
|               allItems={g.rules} | ||||
|               child={({ items }) => ( | ||||
|  | @ -248,8 +334,64 @@ export default function RulesPage() { | |||
|                 </Accordion> | ||||
|               )} | ||||
|             /> | ||||
|           )} | ||||
|         </Card> | ||||
|         ))} | ||||
|       )), | ||||
|     [currentPageGroups, setShowEmptyGroups] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <Stack mt="xs"> | ||||
|       <Group> | ||||
|         <StateMultiSelect | ||||
|           options={["ok", "unknown", "err"]} | ||||
|           optionClass={(o) => | ||||
|             o === "ok" | ||||
|               ? badgeClasses.healthOk | ||||
|               : o === "unknown" | ||||
|                 ? badgeClasses.healthWarn | ||||
|                 : badgeClasses.healthErr | ||||
|           } | ||||
|           placeholder="Filter by rule health" | ||||
|           values={(healthFilter?.filter((v) => v !== null) as string[]) || []} | ||||
|           onChange={(values) => setHealthFilter(values)} | ||||
|         /> | ||||
|         <TextInput | ||||
|           flex={1} | ||||
|           leftSection={<IconSearch style={inputIconStyle} />} | ||||
|           placeholder="Filter by rule name or labels" | ||||
|           value={searchFilter || ""} | ||||
|           onChange={(event) => | ||||
|             setSearchFilter(event.currentTarget.value || null) | ||||
|           } | ||||
|         ></TextInput> | ||||
|       </Group> | ||||
|       {rulesPageData.groups.length === 0 ? ( | ||||
|         <Alert title="No rules found" icon={<IconInfoCircle />}> | ||||
|           No rules found. | ||||
|         </Alert> | ||||
|       ) : ( | ||||
|         !showEmptyGroups && | ||||
|         rulesPageData.groups.length !== shownGroups.length && ( | ||||
|           <Alert | ||||
|             title="Hiding groups with no matching rules" | ||||
|             icon={<IconInfoCircle />} | ||||
|           > | ||||
|             Hiding {rulesPageData.groups.length - shownGroups.length} empty | ||||
|             groups due to filters or no rules. | ||||
|             <Anchor ml="md" fz="1em" onClick={() => setShowEmptyGroups(true)}> | ||||
|               Show empty groups | ||||
|             </Anchor> | ||||
|           </Alert> | ||||
|         ) | ||||
|       )} | ||||
|       <Pagination | ||||
|         total={totalPageCount} | ||||
|         value={effectiveActivePage} | ||||
|         onChange={setActivePage} | ||||
|         hideWithOnePage | ||||
|       /> | ||||
|       {renderedPageItems} | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue