123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- import { Map as makeMap, Set as makeSet, List as makeList } from 'immutable';
- import { escapeRegExp } from 'lodash';
- import { isGenericTable, isPropertyList, genericTableEntryKey } from './node-details-utils';
- import { slugify } from './string-utils';
- // topolevel search fields
- const SEARCH_FIELDS = makeMap({
- label: 'label',
- labelMinor: 'labelMinor'
- });
- const COMPARISONS = makeMap({
- '<': 'lt',
- '=': 'eq',
- '>': 'gt'
- });
- const COMPARISONS_REGEX = new RegExp(`[${COMPARISONS.keySeq().toJS().join('')}]`);
- const PREFIX_DELIMITER = ':';
- /**
- * Returns a RegExp from a given string. If the string is not a valid regexp,
- * it is escaped. Returned regexp is case-insensitive.
- */
- function makeRegExp(expression, options = 'i') {
- try {
- return new RegExp(expression, options);
- } catch (e) {
- return new RegExp(escapeRegExp(expression), options);
- }
- }
- /**
- * Returns the float of a metric value string, e.g. 2 KB -> 2048
- */
- function parseValue(value) {
- let parsed = parseFloat(value);
- if ((/k/i).test(value)) {
- parsed *= 1024;
- } else if ((/m/i).test(value)) {
- parsed *= 1024 * 1024;
- } else if ((/g/i).test(value)) {
- parsed *= 1024 * 1024 * 1024;
- } else if ((/t/i).test(value)) {
- parsed *= 1024 * 1024 * 1024 * 1024;
- }
- return parsed;
- }
- /**
- * True if a prefix matches a field label
- * Slugifies the label (removes all non-alphanumerical chars).
- */
- function matchPrefix(label, prefix) {
- if (label && prefix) {
- return (makeRegExp(prefix)).test(slugify(label));
- }
- return false;
- }
- /**
- * Adds a match to nodeMatches under the keyPath. The text is matched against
- * the query. If a prefix is given, it is matched against the label (skip on
- * no match).
- * Returns a new instance of nodeMatches.
- */
- function findNodeMatch(nodeMatches, keyPath, text, query, prefix, label, truncate) {
- if (!prefix || matchPrefix(label, prefix)) {
- const queryRe = makeRegExp(query);
- const matches = text.match(queryRe);
- if (matches) {
- const firstMatch = matches[0];
- const index = text.search(queryRe);
- nodeMatches = nodeMatches.setIn(
- keyPath,
- {
- label, length: firstMatch.length, start: index, text, truncate
- }
- );
- }
- }
- return nodeMatches;
- }
- /**
- * If the metric matches the field's label and the value compares positively
- * with the comp operator, a nodeMatch is added
- */
- function findNodeMatchMetric(nodeMatches, keyPath, fieldValue, fieldLabel, metric, comp, value) {
- if (slugify(metric) === slugify(fieldLabel)) {
- let matched = false;
- switch (comp) {
- case 'gt': {
- if (fieldValue > value) {
- matched = true;
- }
- break;
- }
- case 'lt': {
- if (fieldValue < value) {
- matched = true;
- }
- break;
- }
- case 'eq': {
- if (fieldValue === value) {
- matched = true;
- }
- break;
- }
- default: {
- break;
- }
- }
- if (matched) {
- nodeMatches = nodeMatches.setIn(
- keyPath,
- {fieldLabel, metric: true}
- );
- }
- }
- return nodeMatches;
- }
- export function searchNode(node, {
- prefix, query, metric, comp, value
- }) {
- let nodeMatches = makeMap();
- if (query) {
- // top level fields
- SEARCH_FIELDS.forEach((field, label) => {
- const keyPath = [label];
- if (node.has(field)) {
- nodeMatches = findNodeMatch(
- nodeMatches, keyPath, node.get(field),
- query, prefix, label
- );
- }
- });
- // metadata
- if (node.get('metadata')) {
- node.get('metadata').forEach((field) => {
- const keyPath = ['metadata', field.get('id')];
- nodeMatches = findNodeMatch(
- nodeMatches, keyPath, field.get('value'),
- query, prefix, field.get('label'), field.get('truncate')
- );
- });
- }
- // parents and relatives
- if (node.get('parents')) {
- node.get('parents').forEach((parent) => {
- const keyPath = ['parents', parent.get('id')];
- nodeMatches = findNodeMatch(
- nodeMatches, keyPath, parent.get('label'),
- query, prefix, parent.get('topologyId')
- );
- });
- }
- // property lists
- (node.get('tables') || []).filter(isPropertyList).forEach((propertyList) => {
- (propertyList.get('rows') || []).forEach((row) => {
- const entries = row.get('entries');
- const keyPath = ['property-lists', row.get('id')];
- nodeMatches = findNodeMatch(
- nodeMatches, keyPath, entries.get('value'),
- query, prefix, entries.get('label')
- );
- });
- });
- // generic tables
- (node.get('tables') || []).filter(isGenericTable).forEach((table) => {
- (table.get('rows') || []).forEach((row) => {
- table.get('columns').forEach((column) => {
- const val = row.get('entries').get(column.get('id'));
- const keyPath = ['tables', genericTableEntryKey(row, column)];
- nodeMatches = findNodeMatch(nodeMatches, keyPath, val, query);
- });
- });
- });
- } else if (metric) {
- const metrics = node.get('metrics');
- if (metrics) {
- metrics.forEach((field) => {
- const keyPath = ['metrics', field.get('id')];
- nodeMatches = findNodeMatchMetric(
- nodeMatches, keyPath, field.get('value'),
- field.get('label'), metric, comp, value
- );
- });
- }
- }
- return nodeMatches;
- }
- export function searchTopology(nodes, parsedQuery) {
- let nodesMatches = makeMap();
- nodes.forEach((node, nodeId) => {
- const nodeMatches = searchNode(node, parsedQuery);
- if (!nodeMatches.isEmpty()) {
- nodesMatches = nodesMatches.set(nodeId, nodeMatches);
- }
- });
- return nodesMatches;
- }
- /**
- * Returns an object with fields depending on the query:
- * parseQuery('text') -> {query: 'text'}
- * parseQuery('p:text') -> {query: 'text', prefix: 'p'}
- * parseQuery('cpu > 1') -> {metric: 'cpu', value: '1', comp: 'gt'}
- */
- export function parseQuery(query) {
- if (query) {
- const prefixQuery = query.split(PREFIX_DELIMITER);
- const isPrefixQuery = prefixQuery && prefixQuery.length === 2;
- if (isPrefixQuery) {
- const prefix = prefixQuery[0].trim();
- query = prefixQuery[1].trim();
- if (prefix && query) {
- return {
- prefix,
- query
- };
- }
- } else if (COMPARISONS_REGEX.test(query)) {
- // check for comparisons
- let comparison;
- COMPARISONS.forEach((comp, delim) => {
- const comparisonQuery = query.split(delim);
- if (comparisonQuery && comparisonQuery.length === 2) {
- const value = parseValue(comparisonQuery[1]);
- const metric = comparisonQuery[0].trim();
- if (!window.isNaN(value) && metric) {
- comparison = {
- comp,
- metric,
- value
- };
- return false; // dont look further
- }
- }
- return true;
- });
- if (comparison) {
- return comparison;
- }
- } else {
- return { query };
- }
- }
- return null;
- }
- export function getSearchableFields(nodes) {
- const get = (node, key) => node.get(key) || makeList();
- const baseLabels = makeSet(nodes.size > 0 ? SEARCH_FIELDS.valueSeq() : []);
- const metadataLabels = nodes.reduce((labels, node) => (
- labels.union(get(node, 'metadata').map(f => f.get('label')))
- ), makeSet());
- const parentLabels = nodes.reduce((labels, node) => (
- labels.union(get(node, 'parents').map(p => p.get('topologyId')))
- ), makeSet());
- // Consider only property lists (and not generic tables).
- const tableRowLabels = nodes.reduce((labels, node) => (
- labels.union(get(node, 'tables').filter(isPropertyList).flatMap(t => (t.get('rows') || makeList)
- .map(f => f.getIn(['entries', 'label']))))
- ), makeSet());
- const metricLabels = nodes.reduce((labels, node) => (
- labels.union(get(node, 'metrics').map(f => f.get('label')))
- ), makeSet());
- return makeMap({
- fields: baseLabels.union(metadataLabels, parentLabels, tableRowLabels)
- .map(slugify)
- .toList()
- .sort(),
- metrics: metricLabels.toList().map(slugify).sort()
- });
- }
- /**
- * Set `filtered:true` in state's nodes if a pinned search matches
- */
- export function applyPinnedSearches(state) {
- // clear old filter state
- state = state.update(
- 'nodes',
- nodes => nodes.map(node => node.set('filtered', false))
- );
- const pinnedSearches = state.get('pinnedSearches');
- if (pinnedSearches.size > 0) {
- state.get('pinnedSearches').forEach((query) => {
- const parsed = parseQuery(query);
- if (parsed) {
- const nodeMatches = searchTopology(state.get('nodes'), parsed);
- const filteredNodes = state.get('nodes')
- .map(node => node.set(
- 'filtered',
- node.get('filtered') // matched by previous pinned search
- || nodeMatches.size === 0 // no match, filter all nodes
- || !nodeMatches.has(node.get('id'))
- )); // filter matches
- state = state.set('nodes', filteredNodes);
- }
- });
- }
- return state;
- }
- export const testable = {
- applyPinnedSearches,
- findNodeMatch,
- findNodeMatchMetric,
- makeRegExp,
- matchPrefix,
- parseQuery,
- parseValue,
- searchTopology,
- };
|