Allow setting the search term on alerts, targets, and discovery pages via the URL (#10613)

* changes in files of the expr in alerts, service and targets, Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* update prettier corrections

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* update prettier corrections

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* correction suggested fixes PR

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* Update Snap AlertContents.test.tsx and update recommendations in the code

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* Update web/ui/react-app/src/components/SearchBar.tsx

exactly, thanks :)

Co-authored-by: Julius Volz <julius.volz@gmail.com>
Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* Revert "Update web/ui/react-app/src/components/SearchBar.tsx
"

This reverts commit 679c763a02f65297e3f761db372a0928550f288c.

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* update SearchBar component

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* eslint-disable update SearchBar testing failed

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* correction part in eslint-disable

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* fully corrected suggestion with search expression in url

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

* implementation in handleSearchChange with useCallBack

Signed-off-by: AndreSPy1 <elsenorcito@gmail.com>

Co-authored-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
AndreSPy1 2022-04-26 14:20:48 -05:00 committed by GitHub
parent 685493187e
commit d8ca9aa67b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 88 additions and 48 deletions

View file

@ -1,29 +1,34 @@
import React, { ChangeEvent, FC } from 'react'; import React, { ChangeEvent, FC, useEffect } from 'react';
import { Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap'; import { Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons'; import { faSearch } from '@fortawesome/free-solid-svg-icons';
export interface SearchBarProps { export interface SearchBarProps {
handleChange: (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void; handleChange: (e: string) => void;
placeholder: string; placeholder: string;
defaultValue: string;
} }
const SearchBar: FC<SearchBarProps> = ({ handleChange, placeholder }) => { const SearchBar: FC<SearchBarProps> = ({ handleChange, placeholder, defaultValue }) => {
let filterTimeout: NodeJS.Timeout; let filterTimeout: NodeJS.Timeout;
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
clearTimeout(filterTimeout); clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => { filterTimeout = setTimeout(() => {
handleChange(e); handleChange(e.target.value);
}, 300); }, 300);
}; };
useEffect(() => {
handleChange(defaultValue);
}, [defaultValue, handleChange]);
return ( return (
<InputGroup> <InputGroup>
<InputGroupAddon addonType="prepend"> <InputGroupAddon addonType="prepend">
<InputGroupText>{<FontAwesomeIcon icon={faSearch} />}</InputGroupText> <InputGroupText>{<FontAwesomeIcon icon={faSearch} />}</InputGroupText>
</InputGroupAddon> </InputGroupAddon>
<Input autoFocus onChange={handleSearchChange} placeholder={placeholder} /> <Input autoFocus onChange={handleSearchChange} placeholder={placeholder} defaultValue={defaultValue} />
</InputGroup> </InputGroup>
); );
}; };

View file

@ -1,8 +1,8 @@
import React, { ChangeEvent, FC, Fragment, useEffect, useState } from 'react'; import React, { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { Badge, Col, Row } from 'reactstrap'; import { Badge, Col, Row } from 'reactstrap';
import CollapsibleAlertPanel from './CollapsibleAlertPanel'; import CollapsibleAlertPanel from './CollapsibleAlertPanel';
import Checkbox from '../../components/Checkbox'; import Checkbox from '../../components/Checkbox';
import { isPresent } from '../../utils'; import { getQuerySearchFilter, isPresent, setQuerySearchFilter } from '../../utils';
import { Rule } from '../../types/types'; import { Rule } from '../../types/types';
import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useLocalStorage } from '../../hooks/useLocalStorage';
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll'; import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
@ -71,7 +71,6 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
inactive: true, inactive: true,
}); });
const [showAnnotations, setShowAnnotations] = useLocalStorage('alerts-annotations-status', { checked: false }); const [showAnnotations, setShowAnnotations] = useLocalStorage('alerts-annotations-status', { checked: false });
const toggleFilter = (ruleState: RuleState) => () => { const toggleFilter = (ruleState: RuleState) => () => {
setFilter({ setFilter({
...filter, ...filter,
@ -79,26 +78,32 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
}); });
}; };
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { const handleSearchChange = useCallback(
if (e.target.value !== '') { (value: string) => {
const pattern = e.target.value.trim(); setQuerySearchFilter(value);
const result: RuleGroup[] = []; if (value !== '') {
for (const group of groups) { const pattern = value.trim();
const ruleFilterList = kvSearchRule.filter(pattern, group.rules); const result: RuleGroup[] = [];
if (ruleFilterList.length > 0) { for (const group of groups) {
result.push({ const ruleFilterList = kvSearchRule.filter(pattern, group.rules);
file: group.file, if (ruleFilterList.length > 0) {
name: group.name, result.push({
interval: group.interval, file: group.file,
rules: ruleFilterList.map((value) => value.original), name: group.name,
}); interval: group.interval,
rules: ruleFilterList.map((value) => value.original),
});
}
} }
setGroupList(result);
} else {
setGroupList(groups);
} }
setGroupList(result); },
} else { [groups]
setGroupList(groups); );
}
}; const defaultValue = useMemo(getQuerySearchFilter, []);
useEffect(() => { useEffect(() => {
const result: RuleGroup[] = []; const result: RuleGroup[] = [];
@ -131,7 +136,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
})} })}
</Col> </Col>
<Col lg="5" md="4"> <Col lg="5" md="4">
<SearchBar handleChange={handleSearchChange} placeholder="Filter by name or labels" /> <SearchBar defaultValue={defaultValue} handleChange={handleSearchChange} placeholder="Filter by name or labels" />
</Col> </Col>
<Col className="d-flex flex-row-reverse" md="3"> <Col className="d-flex flex-row-reverse" md="3">
<Checkbox <Checkbox

View file

@ -100,6 +100,7 @@ exports[`AlertsContent matches a snapshot 1`] = `
} }
> >
<SearchBar <SearchBar
defaultValue=""
handleChange={[Function]} handleChange={[Function]}
placeholder="Filter by name or labels" placeholder="Filter by name or labels"
/> />

View file

@ -1,10 +1,10 @@
import React, { ChangeEvent, FC, useEffect, useState } from 'react'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { LabelsTable } from './LabelsTable'; import { LabelsTable } from './LabelsTable';
import { DroppedTarget, Labels, Target } from '../targets/target'; import { DroppedTarget, Labels, Target } from '../targets/target';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { mapObjEntries } from '../../utils'; import { setQuerySearchFilter, mapObjEntries, getQuerySearchFilter } from '../../utils';
import { usePathPrefix } from '../../contexts/PathPrefixContext'; import { usePathPrefix } from '../../contexts/PathPrefixContext';
import { API_PATH } from '../../constants/constants'; import { API_PATH } from '../../constants/constants';
import { KVSearch } from '@nexucis/kvsearch'; import { KVSearch } from '@nexucis/kvsearch';
@ -94,14 +94,20 @@ export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, dropped
const [targetList, setTargetList] = useState(processSummary(activeTargets, droppedTargets)); const [targetList, setTargetList] = useState(processSummary(activeTargets, droppedTargets));
const [labelList, setLabelList] = useState(processTargets(activeTargets, droppedTargets)); const [labelList, setLabelList] = useState(processTargets(activeTargets, droppedTargets));
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { const handleSearchChange = useCallback(
if (e.target.value !== '') { (value: string) => {
const result = kvSearch.filter(e.target.value.trim(), activeTargets); setQuerySearchFilter(value);
setActiveTargetList(result.map((value) => value.original)); if (value !== '') {
} else { const result = kvSearch.filter(value.trim(), activeTargets);
setActiveTargetList(activeTargets); setActiveTargetList(result.map((value) => value.original));
} } else {
}; setActiveTargetList(activeTargets);
}
},
[activeTargets]
);
const defaultValue = useMemo(getQuerySearchFilter, []);
useEffect(() => { useEffect(() => {
setTargetList(processSummary(activeTargetList, droppedTargets)); setTargetList(processSummary(activeTargetList, droppedTargets));
@ -112,7 +118,7 @@ export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, dropped
<> <>
<h2>Service Discovery</h2> <h2>Service Discovery</h2>
<Container> <Container>
<SearchBar handleChange={handleSearchChange} placeholder="Filter by labels" /> <SearchBar defaultValue={defaultValue} handleChange={handleSearchChange} placeholder="Filter by labels" />
</Container> </Container>
<ul> <ul>
{mapObjEntries(targetList, ([k, v]) => ( {mapObjEntries(targetList, ([k, v]) => (

View file

@ -4,7 +4,7 @@ import { useFetch } from '../../hooks/useFetch';
import { API_PATH } from '../../constants/constants'; import { API_PATH } from '../../constants/constants';
import { groupTargets, ScrapePool, ScrapePools, Target } from './target'; import { groupTargets, ScrapePool, ScrapePools, Target } from './target';
import { withStatusIndicator } from '../../components/withStatusIndicator'; import { withStatusIndicator } from '../../components/withStatusIndicator';
import { ChangeEvent, FC, useEffect, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Col, Collapse, Row } from 'reactstrap'; import { Col, Collapse, Row } from 'reactstrap';
import { ScrapePoolContent } from './ScrapePoolContent'; import { ScrapePoolContent } from './ScrapePoolContent';
import Filter, { Expanded, FilterData } from './Filter'; import Filter, { Expanded, FilterData } from './Filter';
@ -12,6 +12,7 @@ import { useLocalStorage } from '../../hooks/useLocalStorage';
import styles from './ScrapePoolPanel.module.css'; import styles from './ScrapePoolPanel.module.css';
import { ToggleMoreLess } from '../../components/ToggleMoreLess'; import { ToggleMoreLess } from '../../components/ToggleMoreLess';
import SearchBar from '../../components/SearchBar'; import SearchBar from '../../components/SearchBar';
import { setQuerySearchFilter, getQuerySearchFilter } from '../../utils/index';
interface ScrapePoolListProps { interface ScrapePoolListProps {
activeTargets: Target[]; activeTargets: Target[];
@ -72,14 +73,20 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
const [expanded, setExpanded] = useLocalStorage('targets-page-expansion-state', initialExpanded); const [expanded, setExpanded] = useLocalStorage('targets-page-expansion-state', initialExpanded);
const { showHealthy, showUnhealthy } = filter; const { showHealthy, showUnhealthy } = filter;
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { const handleSearchChange = useCallback(
if (e.target.value !== '') { (value: string) => {
const result = kvSearch.filter(e.target.value.trim(), activeTargets); setQuerySearchFilter(value);
setTargetList(result.map((value) => value.original)); if (value !== '') {
} else { const result = kvSearch.filter(value.trim(), activeTargets);
setTargetList(activeTargets); setTargetList(result.map((value) => value.original));
} } else {
}; setTargetList(activeTargets);
}
},
[activeTargets]
);
const defaultValue = useMemo(getQuerySearchFilter, []);
useEffect(() => { useEffect(() => {
const list = targetList.filter((t) => showHealthy || t.health.toLowerCase() !== 'up'); const list = targetList.filter((t) => showHealthy || t.health.toLowerCase() !== 'up');
@ -93,7 +100,11 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
<Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} /> <Filter filter={filter} setFilter={setFilter} expanded={expanded} setExpanded={setExpanded} />
</Col> </Col>
<Col xs="6"> <Col xs="6">
<SearchBar handleChange={handleSearchChange} placeholder="Filter by endpoint or labels" /> <SearchBar
defaultValue={defaultValue}
handleChange={handleSearchChange}
placeholder="Filter by endpoint or labels"
/>
</Col> </Col>
</Row> </Row>
{Object.keys(poolList) {Object.keys(poolList)

View file

@ -243,9 +243,21 @@ export const encodePanelOptionsToQueryString = (panels: PanelMeta[]): string =>
return `?${panels.map(toQueryString).join('&')}`; return `?${panels.map(toQueryString).join('&')}`;
}; };
export const setQuerySearchFilter = (search: string) => {
window.history.pushState({}, '', `?search=${search}`);
};
export const getQuerySearchFilter = (): string => {
const locationSearch = window.location.search;
const params = new URLSearchParams(locationSearch);
const search = params.get('search') || '';
return search;
};
export const createExpressionLink = (expr: string): string => { export const createExpressionLink = (expr: string): string => {
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.show_exemplars=0.g0.range_input=1h.`; return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.show_exemplars=0.g0.range_input=1h.`;
}; };
export const mapObjEntries = <T, key extends keyof T, Z>( export const mapObjEntries = <T, key extends keyof T, Z>(
o: T, o: T,
cb: ([k, v]: [string, T[key]], i: number, arr: [string, T[key]][]) => Z cb: ([k, v]: [string, T[key]], i: number, arr: [string, T[key]][]) => Z