introduce codemirror editor for local query to filter targets, service discovery

Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
Augustin Husson 2022-05-17 16:12:20 +02:00
parent 070e409dba
commit 58402a741e
No known key found for this signature in database
GPG key ID: 99776005FB44BFC1
6 changed files with 177 additions and 50 deletions

View file

@ -2963,17 +2963,32 @@
"@lezer/common": "^0.15.0"
}
},
"node_modules/@nexucis/codemirror-kvsearch": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@nexucis/codemirror-kvsearch/-/codemirror-kvsearch-0.2.0.tgz",
"integrity": "sha512-6amnDba+DhQnYAjz87yj18c2co5rmEDd2XbSsIsMxuV+QRfcVOPeKtsr/pWFrVH5/7lp6cGSaWqPgqYNmjAP9g==",
"dependencies": {
"@nexucis/kvsearch": "^0.8.0"
},
"peerDependencies": {
"@codemirror/autocomplete": "^0.19.8",
"@codemirror/language": "^0.19.5",
"@codemirror/lint": "^0.19.3",
"@codemirror/state": "^0.19.6",
"@lezer/common": "^0.15.10"
}
},
"node_modules/@nexucis/fuzzy": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.4.0.tgz",
"integrity": "sha512-VZVzETBXJjFzTd5RmKR+X8V5uM9PKM2yyb7v/aygMF+1iAnZwxzQtxbFx358SNP23eid4s85TIREHw50p8Zugg=="
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.4.1.tgz",
"integrity": "sha512-oe+IW6ELwVGYL3340M+nKIT1exZizOjxdUFlTs36BqzxTENBbynG+cCWr4RNaUQF3bV78NspKwTBpTlnYADrTA=="
},
"node_modules/@nexucis/kvsearch": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@nexucis/kvsearch/-/kvsearch-0.7.0.tgz",
"integrity": "sha512-Zl1u0wUpgpfY1JmHIKyLuqDdN5iTm/wuLXbBbm//Qck/un9ivGYtePpT1/BjG/2XisBsvHb7EldMAuRQaUahtg==",
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@nexucis/kvsearch/-/kvsearch-0.8.0.tgz",
"integrity": "sha512-83m6AgFNva4+5Zoc3Z5vnk+KZBMqA6cotNIIm9Zejwp5VlhO/Pfy6RTZCB8KKKA3MRSP7EDYBMBwnOfMQKpHyg==",
"dependencies": {
"@nexucis/fuzzy": "^0.4.0"
"@nexucis/fuzzy": "^0.4.1"
}
},
"node_modules/@nodelib/fs.scandir": {
@ -17331,8 +17346,9 @@
"@fortawesome/fontawesome-svg-core": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.17",
"@nexucis/fuzzy": "^0.4.0",
"@nexucis/kvsearch": "^0.7.0",
"@nexucis/codemirror-kvsearch": "^0.2.0",
"@nexucis/fuzzy": "^0.4.1",
"@nexucis/kvsearch": "^0.8.0",
"bootstrap": "^4.6.1",
"codemirror-promql": "0.19.0",
"css.escape": "^1.5.1",
@ -19495,17 +19511,25 @@
"@lezer/common": "^0.15.0"
}
},
"@nexucis/codemirror-kvsearch": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@nexucis/codemirror-kvsearch/-/codemirror-kvsearch-0.2.0.tgz",
"integrity": "sha512-6amnDba+DhQnYAjz87yj18c2co5rmEDd2XbSsIsMxuV+QRfcVOPeKtsr/pWFrVH5/7lp6cGSaWqPgqYNmjAP9g==",
"requires": {
"@nexucis/kvsearch": "^0.8.0"
}
},
"@nexucis/fuzzy": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.4.0.tgz",
"integrity": "sha512-VZVzETBXJjFzTd5RmKR+X8V5uM9PKM2yyb7v/aygMF+1iAnZwxzQtxbFx358SNP23eid4s85TIREHw50p8Zugg=="
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@nexucis/fuzzy/-/fuzzy-0.4.1.tgz",
"integrity": "sha512-oe+IW6ELwVGYL3340M+nKIT1exZizOjxdUFlTs36BqzxTENBbynG+cCWr4RNaUQF3bV78NspKwTBpTlnYADrTA=="
},
"@nexucis/kvsearch": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@nexucis/kvsearch/-/kvsearch-0.7.0.tgz",
"integrity": "sha512-Zl1u0wUpgpfY1JmHIKyLuqDdN5iTm/wuLXbBbm//Qck/un9ivGYtePpT1/BjG/2XisBsvHb7EldMAuRQaUahtg==",
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@nexucis/kvsearch/-/kvsearch-0.8.0.tgz",
"integrity": "sha512-83m6AgFNva4+5Zoc3Z5vnk+KZBMqA6cotNIIm9Zejwp5VlhO/Pfy6RTZCB8KKKA3MRSP7EDYBMBwnOfMQKpHyg==",
"requires": {
"@nexucis/fuzzy": "^0.4.0"
"@nexucis/fuzzy": "^0.4.1"
}
},
"@nodelib/fs.scandir": {
@ -23951,8 +23975,9 @@
"@fortawesome/fontawesome-svg-core": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.17",
"@nexucis/fuzzy": "^0.4.0",
"@nexucis/kvsearch": "^0.7.0",
"@nexucis/codemirror-kvsearch": "^0.2.0",
"@nexucis/fuzzy": "^0.4.1",
"@nexucis/kvsearch": "^0.8.0",
"@testing-library/react-hooks": "^7.0.1",
"@types/enzyme": "^3.10.10",
"@types/flot": "0.0.32",

View file

@ -19,8 +19,9 @@
"@fortawesome/fontawesome-svg-core": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.17",
"@nexucis/fuzzy": "^0.4.0",
"@nexucis/kvsearch": "^0.7.0",
"@nexucis/fuzzy": "^0.4.1",
"@nexucis/kvsearch": "^0.8.0",
"@nexucis/codemirror-kvsearch": "^0.2.0",
"bootstrap": "^4.6.1",
"codemirror-promql": "0.19.0",
"css.escape": "^1.5.1",

View file

@ -1,34 +1,110 @@
import React, { ChangeEvent, FC, useEffect } from 'react';
import { Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import React, { FC, useEffect, useRef } from 'react';
import { InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import { EditorState } from '@codemirror/state';
import { baseTheme, lightTheme, promqlHighlighter } from '../pages/graph/CMTheme';
import { EditorView, highlightSpecialChars, keymap, placeholder as placeholderFunc, ViewUpdate } from '@codemirror/view';
import { history, historyKeymap } from '@codemirror/history';
import { indentOnInput } from '@codemirror/language';
import { bracketMatching } from '@codemirror/matchbrackets';
import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets';
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
import { highlightSelectionMatches } from '@codemirror/search';
import { defaultKeymap } from '@codemirror/commands';
import { commentKeymap } from '@codemirror/comment';
import { lintKeymap } from '@codemirror/lint';
import { KVSearchExtension } from '@nexucis/codemirror-kvsearch';
export interface SearchBarProps {
handleChange: (e: string) => void;
handleChange: (state: EditorState) => void;
placeholder: string;
defaultValue: string;
objects?: Record<string, unknown>[];
}
const SearchBar: FC<SearchBarProps> = ({ handleChange, placeholder, defaultValue }) => {
let filterTimeout: NodeJS.Timeout;
const handleSearchChange = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
clearTimeout(filterTimeout);
filterTimeout = setTimeout(() => {
handleChange(e.target.value);
}, 300);
};
const SearchBar: FC<SearchBarProps> = ({ handleChange, placeholder, defaultValue, objects }) => {
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
const filterTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
handleChange(defaultValue);
}, [defaultValue, handleChange]);
const kvsearchExtension = new KVSearchExtension(objects);
const handleSearchChange = (state: EditorState) => {
let filterTimeout = filterTimeoutRef.current;
if (filterTimeout !== null) {
clearTimeout(filterTimeout);
}
filterTimeout = setTimeout(() => {
handleChange(state);
}, 300);
filterTimeoutRef.current = filterTimeout;
};
// Create or reconfigure the editor.
let view = viewRef.current;
if (view === null) {
// If the editor does not exist yet, create it.
if (!containerRef.current) {
throw new Error('expected CodeMirror container element to exist');
}
const startState = EditorState.create({
doc: defaultValue,
extensions: [
baseTheme,
promqlHighlighter,
lightTheme,
highlightSpecialChars(),
history(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
highlightSelectionMatches(),
EditorView.lineWrapping,
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...commentKeymap,
...completionKeymap,
...lintKeymap,
]),
placeholderFunc(placeholder),
keymap.of([
{
key: 'Escape',
run: (v: EditorView): boolean => {
v.contentDOM.blur();
return false;
},
},
]),
EditorView.updateListener.of((update: ViewUpdate): void => {
handleSearchChange(update.state);
}),
kvsearchExtension.asExtension(),
],
});
view = new EditorView({
state: startState,
parent: containerRef.current,
});
viewRef.current = view;
view.focus();
}
handleChange(view.state);
}, [defaultValue, handleChange, objects, placeholder]);
return (
<InputGroup>
<InputGroupAddon addonType="prepend">
<InputGroupText>{<FontAwesomeIcon icon={faSearch} />}</InputGroupText>
</InputGroupAddon>
<Input autoFocus onChange={handleSearchChange} placeholder={placeholder} defaultValue={defaultValue} />
<div ref={containerRef} className="cm-expression-input" />
</InputGroup>
);
};

View file

@ -8,6 +8,8 @@ import { useLocalStorage } from '../../hooks/useLocalStorage';
import CustomInfiniteScroll, { InfiniteScrollItemsProps } from '../../components/CustomInfiniteScroll';
import { KVSearch } from '@nexucis/kvsearch';
import SearchBar from '../../components/SearchBar';
import { EditorState } from '@codemirror/state';
import { translate } from '@nexucis/codemirror-kvsearch';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RuleState = keyof RuleStatus<any>;
@ -79,13 +81,15 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
};
const handleSearchChange = useCallback(
(value: string) => {
setQuerySearchFilter(value);
if (value !== '') {
const pattern = value.trim();
(state: EditorState) => {
const pattern = state.doc.toString().trim();
setQuerySearchFilter(pattern);
if (pattern !== '') {
const query = translate(state);
const result: RuleGroup[] = [];
for (const group of groups) {
const ruleFilterList = kvSearchRule.filter(pattern, group.rules);
const ruleFilterList =
query !== null ? kvSearchRule.filterWithQuery(query, group.rules) : kvSearchRule.filter(pattern, group.rules);
if (ruleFilterList.length > 0) {
result.push({
file: group.file,

View file

@ -10,6 +10,8 @@ import { API_PATH } from '../../constants/constants';
import { KVSearch } from '@nexucis/kvsearch';
import { Container } from 'reactstrap';
import SearchBar from '../../components/SearchBar';
import { EditorState } from '@codemirror/state';
import { translate } from '@nexucis/codemirror-kvsearch';
interface ServiceMap {
activeTargets: Target[];
@ -101,11 +103,19 @@ export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, dropped
const [labelList, setLabelList] = useState(processTargets(activeTargets, droppedTargets));
const handleSearchChange = useCallback(
(value: string) => {
setQuerySearchFilter(value);
if (value !== '') {
const activeTargetResult = activeTargetKVSearch.filter(value.trim(), activeTargets);
const droppedTargetResult = droppedTargetKVSearch.filter(value.trim(), droppedTargets);
(state: EditorState) => {
const pattern = state.doc.toString().trim();
setQuerySearchFilter(pattern);
if (pattern !== '') {
const query = translate(state);
const activeTargetResult =
query !== null
? activeTargetKVSearch.filterWithQuery(query, activeTargets)
: activeTargetKVSearch.filter(pattern, activeTargets);
const droppedTargetResult =
query !== null
? droppedTargetKVSearch.filterWithQuery(query, droppedTargets)
: droppedTargetKVSearch.filter(pattern, droppedTargets);
setActiveTargetList(activeTargetResult.map((value) => value.original));
setDroppedTargetList(droppedTargetResult.map((value) => value.original));
} else {
@ -126,7 +136,12 @@ export const ServiceDiscoveryContent: FC<ServiceMap> = ({ activeTargets, dropped
<>
<h2>Service Discovery</h2>
<Container>
<SearchBar defaultValue={defaultValue} handleChange={handleSearchChange} placeholder="Filter by labels" />
<SearchBar
defaultValue={defaultValue}
handleChange={handleSearchChange}
placeholder="Filter by labels"
objects={activeTargets}
/>
</Container>
<ul>
{mapObjEntries(targetList, ([k, v]) => (

View file

@ -13,6 +13,8 @@ import styles from './ScrapePoolPanel.module.css';
import { ToggleMoreLess } from '../../components/ToggleMoreLess';
import SearchBar from '../../components/SearchBar';
import { setQuerySearchFilter, getQuerySearchFilter } from '../../utils/index';
import { EditorState } from '@codemirror/state';
import { translate } from '@nexucis/codemirror-kvsearch';
interface ScrapePoolListProps {
activeTargets: Target[];
@ -74,10 +76,13 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
const { showHealthy, showUnhealthy } = filter;
const handleSearchChange = useCallback(
(value: string) => {
setQuerySearchFilter(value);
if (value !== '') {
const result = kvSearch.filter(value.trim(), activeTargets);
(state: EditorState) => {
const pattern = state.doc.toString().trim();
setQuerySearchFilter(pattern);
if (pattern !== '') {
const query = translate(state);
const result =
query !== null ? kvSearch.filterWithQuery(query, activeTargets) : kvSearch.filter(pattern, activeTargets);
setTargetList(result.map((value) => value.original));
} else {
setTargetList(activeTargets);
@ -104,6 +109,7 @@ const ScrapePoolListContent: FC<ScrapePoolListProps> = ({ activeTargets }) => {
defaultValue={defaultValue}
handleChange={handleSearchChange}
placeholder="Filter by endpoint or labels"
objects={activeTargets}
/>
</Col>
</Row>