diff --git a/web/src/components/drawer/record/record-panel.tsx b/web/src/components/drawer/record/record-panel.tsx index d2a01f52a..03ad30367 100644 --- a/web/src/components/drawer/record/record-panel.tsx +++ b/web/src/components/drawer/record/record-panel.tsx @@ -235,7 +235,7 @@ export const RecordPanel: React.FC = ({ const values = [ { v: Array.isArray(value) ? value.join(value.length == 2 ? '.' : ':') : valueStr, - display: (await def.getOptions(valueStr)).find(o => o.value === valueStr)?.name + display: (await def.autocomplete(valueStr)).find(o => o.value === valueStr)?.name } ]; // TODO: is it relevant to show composed columns? diff --git a/web/src/components/toolbar/filters/autocomplete-filter.tsx b/web/src/components/toolbar/filters/autocomplete-filter.tsx index c0bc74b61..84d246188 100644 --- a/web/src/components/toolbar/filters/autocomplete-filter.tsx +++ b/web/src/components/toolbar/filters/autocomplete-filter.tsx @@ -86,7 +86,7 @@ export const AutocompleteFilter: React.FC = ({ (newValue: string) => { setCurrentValue(newValue); filterDefinition - .getOptions(newValue) + .autocomplete(newValue) .then(setOptions) .catch(err => { const errorMessage = getHTTPErrorDetails(err); @@ -147,11 +147,10 @@ export const AutocompleteFilter: React.FC = ({ return; } - createFilterValue(filterDefinition, validation.val!).then(v => { - if (addFilterParent(v)) { - resetFilterValue(); - } - }); + const fv = createFilterValue(filterDefinition, validation.val!); + if (addFilterParent(fv)) { + resetFilterValue(); + } }, [ options, filterDefinition, diff --git a/web/src/components/toolbar/filters/text-filter.tsx b/web/src/components/toolbar/filters/text-filter.tsx index 6f5ec6bf4..3f8c0e3f0 100644 --- a/web/src/components/toolbar/filters/text-filter.tsx +++ b/web/src/components/toolbar/filters/text-filter.tsx @@ -74,11 +74,10 @@ export const TextFilter: React.FC = ({ return; } - createFilterValue(filterDefinition, validation.val!).then(v => { - if (addFilter(v)) { - resetFilterValue(); - } - }); + const fv = createFilterValue(filterDefinition, validation.val!); + if (addFilter(fv)) { + resetFilterValue(); + } }, [currentValue, allowEmpty, filterDefinition, setMessageWithDelay, setIndicator, addFilter, resetFilterValue]); return ( diff --git a/web/src/model/filters.ts b/web/src/model/filters.ts index 88029af36..c60407fb3 100644 --- a/web/src/model/filters.ts +++ b/web/src/model/filters.ts @@ -67,7 +67,8 @@ export interface FilterDefinition { name: string; component: FilterComponent; category?: FilterCategory; - getOptions: (value: string) => Promise; + findOption: (value: string) => FilterOption | undefined; + autocomplete: (value: string) => Promise; validate: (value: string) => { val?: string; err?: string }; checkCompletion?: (value: string, selected: string) => { completed: boolean; option: FilterOption }; autoCompleteAddsQuotes?: boolean; @@ -101,22 +102,11 @@ export interface FilterOption { value: string; } -export const createFilterValue = (def: FilterDefinition, value: string): Promise => { - return ( - def - .getOptions(value) - .then(opts => { - const option = opts.find(opt => opt.name === value || opt.value === value); - return option - ? { v: option.value, display: option.name } - : { v: value, display: value === undefinedValue ? 'n/a' : undefined }; - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch(_ => { - // In case of error, still create the minimal possible FilterValue - return { v: value }; - }) - ); +export const createFilterValue = (def: FilterDefinition, value: string): FilterValue => { + const option = def.findOption(value); + return option + ? { v: option.value, display: option.name } + : { v: value, display: value === undefinedValue ? 'n/a' : undefined }; }; export const hasEnabledFilterValues = (filter: Filter) => { diff --git a/web/src/utils/filter-definitions.ts b/web/src/utils/filter-definitions.ts index f26ef6074..f0215fbbf 100644 --- a/web/src/utils/filter-definitions.ts +++ b/web/src/utils/filter-definitions.ts @@ -17,24 +17,30 @@ import { joinResource, SplitResource, splitResource, SplitStage } from '../model import { getPort } from '../utils/port'; import { ColumnConfigDef } from './columns'; import { + autocompleteCluster, + autocompleteDirection, + autocompleteDnsErrorCode, + autocompleteDnsResponseCode, + autocompleteDropCause, + autocompleteDropState, + autocompleteDSCP, + autocompleteEmpty, + autocompleteKind, + autocompleteNamespace, + autocompletePort, + autocompleteProtocol, + autocompleteResource, + autocompleteTCPFlags, + autocompleteUDN, + autocompleteZone, findDirectionOption, + findDnsErrorCodeOption, + findDnsResponseCodeOption, + findDropCauseOption, + findDropStateOption, + findDSCPOption, findProtocolOption, - getClusterOptions, - getDirectionOptionsAsync, - getDnsErrorCodeOptions, - getDnsResponseCodeOptions, - getDropCauseOptions, - getDropStateOptions, - getDSCPOptions, - getKindOptions, - getNamespaceOptions, - getPortOptions, - getProtocolOptions, - getResourceOptions, - getTCPFlagsOptions, - getUDNOptions, - getZoneOptions, - noOption + portValueToOption } from './filter-options'; import { validateIPFilter } from './ip'; import { validateK8SName, validateStrictK8SName } from './label'; @@ -243,7 +249,7 @@ export const getFilterDefinitions = ( return invalid(t('Value is empty')); } //allow 0 / 1 or Ingress / Egress - const found = findDirectionOption(value, t); + const found = findDirectionOption(t, true, value); if (found) { return valid(found.name); } @@ -254,7 +260,8 @@ export const getFilterDefinitions = ( const isSrc = d.id.includes('src'); const colConfig = columnsDefs.find(c => c.filter === d.id); - let getOptions: (value: string) => Promise = noOption; + let autocomplete: (value: string) => Promise = autocompleteEmpty; + let findOption: (value: string) => FilterOption | undefined = () => undefined; let validate: (value: string) => { val?: string; err?: string } = rejectEmptyValue; let encoder: FiltersEncoder = simpleFiltersEncoder(colConfig?.field as Field); let checkCompletion: @@ -262,21 +269,21 @@ export const getFilterDefinitions = ( | undefined = undefined; if (d.id.includes('namespace')) { - getOptions = getNamespaceOptions; + autocomplete = autocompleteNamespace; validate = k8sNameValidation; } else if (d.id.includes('cluster')) { - getOptions = getClusterOptions; + autocomplete = autocompleteCluster; } else if (d.id.includes('udn')) { - getOptions = getUDNOptions; + autocomplete = autocompleteUDN; } else if (d.id.includes('zone')) { - getOptions = getZoneOptions; + autocomplete = autocompleteZone; } else if (d.id.includes('name')) { validate = k8sNameValidation; } else if (d.id.includes('kind')) { - getOptions = getKindOptions; + autocomplete = autocompleteKind; encoder = kindFiltersEncoder(`${isSrc ? 'Src' : 'Dst'}K8S_Type`, `${isSrc ? 'Src' : 'Dst'}K8S_OwnerType`); } else if (d.id.includes('resource')) { - getOptions = getResourceOptions; + autocomplete = autocompleteResource; validate = k8sResourceValidation; checkCompletion = k8sResourceCompletion; encoder = k8sResourceFiltersEncoder( @@ -289,33 +296,41 @@ export const getFilterDefinitions = ( } else if (d.id.includes('address')) { validate = addressValidation; } else if (d.id.includes('port')) { - getOptions = getPortOptions; + autocomplete = autocompletePort; + findOption = portValueToOption; validate = portValidation; } else if (d.id.includes('mac')) { validate = macValidation; } else if (d.id.includes('proto')) { - getOptions = getProtocolOptions; + autocomplete = autocompleteProtocol; + findOption = findProtocolOption; validate = protoValidation; } else if (d.id.includes('direction')) { - getOptions = v => getDirectionOptionsAsync(v, t, d.id === 'nodedirection'); + autocomplete = v => autocompleteDirection(t, d.id === 'nodedirection', v); + findOption = v => findDirectionOption(t, d.id === 'nodedirection', v); validate = dirValidation; } else if (d.id.includes('drop_state')) { - getOptions = getDropStateOptions; + autocomplete = autocompleteDropState; + findOption = findDropStateOption; encoder = simpleFiltersEncoder('PktDropLatestState'); } else if (d.id.includes('drop_cause')) { - getOptions = getDropCauseOptions; + autocomplete = autocompleteDropCause; + findOption = findDropCauseOption; encoder = simpleFiltersEncoder('PktDropLatestDropCause'); } else if (d.id.includes('dns_flag_response_code')) { - getOptions = getDnsResponseCodeOptions; + autocomplete = autocompleteDnsResponseCode; + findOption = findDnsResponseCodeOption; } else if (d.id.includes('dns_errno')) { - getOptions = getDnsErrorCodeOptions; + autocomplete = autocompleteDnsErrorCode; + findOption = findDnsErrorCodeOption; } else if (d.id.includes('dscp')) { - getOptions = getDSCPOptions; + autocomplete = autocompleteDSCP; + findOption = findDSCPOption; } else if (d.id.includes('flags')) { - getOptions = getTCPFlagsOptions; + autocomplete = autocompleteTCPFlags; } - return { getOptions, validate, encoder, checkCompletion }; + return { autocomplete, findOption, validate, encoder, checkCompletion }; }; return filterDefs.map(d => { diff --git a/web/src/utils/filter-options.ts b/web/src/utils/filter-options.ts index 9d4af044f..51ffd9b7e 100644 --- a/web/src/utils/filter-options.ts +++ b/web/src/utils/filter-options.ts @@ -12,23 +12,33 @@ import { dscpValues } from './dscp'; import { dropCauses, dropStates } from './pkt-drop'; import { getPort, getService } from './port'; import { tcpFlagsList } from './tcp-flags'; +import { ReadOnlyValue, ReadOnlyValues } from './values'; -export const noOption: (value: string) => Promise = () => Promise.resolve([]); +export const autocompleteEmpty: (value: string) => Promise = () => Promise.resolve([]); const toFilterOption = (name: string): FilterOption => { return { name, value: name }; }; +const startsWithPredicate = (itemName: string, itemValue: string, search: string) => { + return itemValue.startsWith(search) || itemName.toLowerCase().startsWith(search.toLowerCase()); +}; + +const includePredicate = (itemName: string, itemValue: string, search: string) => { + return itemValue.includes(search) || itemName.toLowerCase().includes(search.toLowerCase()); +}; + +const equalPredicate = (itemName: string, itemValue: string, search: string) => { + return itemValue === search || itemName.toLowerCase() === search.toLowerCase(); +}; + const protocolOptions: FilterOption[] = Object.values(protocols) .map(proto => ({ name: proto.name, value: proto.value })) .filter(proto => !_.isEmpty(proto.name)); _.orderBy(protocolOptions, 'name'); -export const getProtocolOptions = (value: string): Promise => { - const opts = protocolOptions.filter( - opt => opt.value.startsWith(value) || opt.name.toLowerCase().startsWith(value.toLowerCase()) - ); - return Promise.resolve(opts); +export const autocompleteProtocol = (value: string): Promise => { + return Promise.resolve(protocolOptions.filter(opt => startsWithPredicate(opt.name, opt.value, value))); }; export const getDirectionOptions = (t: TFunction, allowInner: boolean): FilterOption[] => { @@ -42,7 +52,7 @@ export const getDirectionOptions = (t: TFunction, allowInner: boolean): FilterOp return directions; }; -export const getDirectionOptionsAsync = (value: string, t: TFunction, allowInner: boolean): Promise => { +export const autocompleteDirection = (t: TFunction, allowInner: boolean, value: string): Promise => { return Promise.resolve( getDirectionOptions(t, allowInner).filter( o => o.value === value || o.name.toLowerCase().includes(value.toLowerCase()) @@ -58,7 +68,7 @@ const matchOptions = (opts: FilterOption[], match: string): FilterOption[] => { } }; -export const getClusterOptions = (value: string): Promise => { +export const autocompleteCluster = (value: string): Promise => { const clusters = autoCompleteCache.getClusters(); if (clusters) { return Promise.resolve(matchOptions(clusters.map(toFilterOption), value)); @@ -69,7 +79,7 @@ export const getClusterOptions = (value: string): Promise => { }); }; -export const getUDNOptions = (value: string): Promise => { +export const autocompleteUDN = (value: string): Promise => { const clusters = autoCompleteCache.getUDNs(); if (clusters) { return Promise.resolve(matchOptions(clusters.map(toFilterOption), value)); @@ -80,7 +90,7 @@ export const getUDNOptions = (value: string): Promise => { }); }; -export const getZoneOptions = (value: string): Promise => { +export const autocompleteZone = (value: string): Promise => { const zones = autoCompleteCache.getZones(); if (zones) { return Promise.resolve(matchOptions(zones.map(toFilterOption), value)); @@ -91,7 +101,7 @@ export const getZoneOptions = (value: string): Promise => { }); }; -export const getNamespaceOptions = (value: string): Promise => { +export const autocompleteNamespace = (value: string): Promise => { const namespaces = autoCompleteCache.getNamespaces(); if (namespaces) { return Promise.resolve(matchOptions(namespaces.map(toFilterOption), value)); @@ -102,7 +112,7 @@ export const getNamespaceOptions = (value: string): Promise => { }); }; -export const getNameOptions = (kind: string, namespace: string, name: string): Promise => { +export const autocompleteName = (kind: string, namespace: string, name: string): Promise => { if (autoCompleteCache.hasNames(kind, namespace)) { const options = (autoCompleteCache.getNames(kind, namespace) || []).map(toFilterOption); return Promise.resolve(matchOptions(options, name)); @@ -113,76 +123,93 @@ export const getNameOptions = (kind: string, namespace: string, name: string): P }); }; -export const getKindOptions = (value: string): Promise => { +export const autocompleteKind = (value: string): Promise => { const options = autoCompleteCache.getKinds().map(toFilterOption); return Promise.resolve(matchOptions(options, value)); }; -export const getResourceOptions = (value: string): Promise => { +export const autocompleteResource = (value: string): Promise => { const parts = splitResource(value); switch (parts.stage) { case SplitStage.PartialKind: - return getKindOptions(parts.kind); + return autocompleteKind(parts.kind); case SplitStage.PartialNamespace: - return getNamespaceOptions(parts.namespace); + return autocompleteNamespace(parts.namespace); case SplitStage.Completed: - return getNameOptions(parts.kind, parts.namespace, parts.name); + return autocompleteName(parts.kind, parts.namespace, parts.name); } }; -export const getPortOptions = (value: string): Promise => { +export const autocompletePort = (value: string): Promise => { + const opt = portValueToOption(value); + return Promise.resolve(opt ? [opt] : []); +}; + +export const portValueToOption = (value: string): FilterOption | undefined => { const isNumber = !isNaN(Number(value)); const foundService = isNumber ? getService(Number(value)) : null; const foundPort = !isNumber ? getPort(value) : null; if (foundService) { - return Promise.resolve([{ name: foundService, value: value }]); + return { name: foundService, value: value }; } else if (foundPort) { - return Promise.resolve([{ name: value, value: foundPort }]); + return { name: value, value: foundPort }; } - return Promise.resolve([]); + return undefined; }; -export const getDropStateOptions = (value: string): Promise => { - return Promise.resolve( - dropStates - .filter(opt => String(opt.value).includes(value) || opt.name.toLowerCase().includes(value.toLowerCase())) - .map(v => ({ name: v.name.replace('TCP_', ''), value: v.name })) // map only names here since codes are stringified in storage - ); +type ROVMapping = { + rov: ReadOnlyValues; + mapper: (v: ReadOnlyValue) => FilterOption; }; -export const getDropCauseOptions = (value: string): Promise => { - return Promise.resolve( - dropCauses - .filter(opt => String(opt.value).includes(value) || opt.name.toLowerCase().includes(value.toLowerCase())) - .map(v => ({ name: v.name.replace('SKB_DROP_REASON_', ''), value: v.name })) // map only names here since codes are stringified in storage - ); +// map only names here since codes are stringified in storage +const dropStateMapping: ROVMapping = { + rov: dropStates, + mapper: (v: ReadOnlyValue): FilterOption => ({ name: v.name.replace('TCP_', ''), value: v.name }) +}; +const dropCauseMapping: ROVMapping = { + rov: dropCauses, + mapper: (v: ReadOnlyValue): FilterOption => ({ name: v.name.replace('SKB_DROP_REASON_', ''), value: v.name }) +}; +const dnsRCodeMapping: ROVMapping = { + rov: dnsRCodes, + mapper: (v: ReadOnlyValue): FilterOption => ({ name: v.name, value: v.name }) +}; +const dnsErrMapping: ROVMapping = { + rov: dnsErrors, + mapper: (v: ReadOnlyValue): FilterOption => ({ name: v.name, value: String(v.value) }) +}; +const dscpMapping: ROVMapping = { + rov: dscpValues, + mapper: (v: ReadOnlyValue): FilterOption => ({ name: v.name, value: String(v.value) }) }; -export const getDnsResponseCodeOptions = (value: string): Promise => { - return Promise.resolve( - dnsRCodes - .filter(opt => String(opt.value).includes(value) || opt.name.toLowerCase().includes(value.toLowerCase())) - .map(v => ({ name: v.name, value: v.name })) // map only names here since codes are stringified in storage - ); +const autocompleteFromROV = (value: string, mapping: ROVMapping) => { + const filtered = mapping.rov.filter(opt => includePredicate(opt.name, String(opt.value), value)).map(mapping.mapper); + return Promise.resolve(filtered); }; -export const getDnsErrorCodeOptions = (value: string): Promise => { - return Promise.resolve( - dnsErrors - .filter(opt => String(opt.value).includes(value) || opt.name.toLowerCase().includes(value.toLowerCase())) - .map(v => ({ name: v.name, value: String(v.value) })) - ); +export const autocompleteDropState = (value: string): Promise => { + return autocompleteFromROV(value, dropStateMapping); }; -export const getDSCPOptions = (value: string): Promise => { - return Promise.resolve( - dscpValues - .filter(opt => String(opt.value).includes(value) || opt.name.toLowerCase().includes(value.toLowerCase())) - .map(v => ({ name: v.name, value: String(v.value) })) - ); +export const autocompleteDropCause = (value: string): Promise => { + return autocompleteFromROV(value, dropCauseMapping); +}; + +export const autocompleteDnsResponseCode = (value: string): Promise => { + return autocompleteFromROV(value, dnsRCodeMapping); +}; + +export const autocompleteDnsErrorCode = (value: string): Promise => { + return autocompleteFromROV(value, dnsErrMapping); }; -export const getTCPFlagsOptions = (value: string): Promise => { +export const autocompleteDSCP = (value: string): Promise => { + return autocompleteFromROV(value, dscpMapping); +}; + +export const autocompleteTCPFlags = (value: string): Promise => { return Promise.resolve( tcpFlagsList .filter(opt => opt.name.toLowerCase().includes(value.toLowerCase())) @@ -190,12 +217,35 @@ export const getTCPFlagsOptions = (value: string): Promise => { ); }; -export const findProtocolOption = (nameOrVal: string) => { - return protocolOptions.find(p => p.name.toLowerCase() === nameOrVal.toLowerCase() || p.value === nameOrVal); +export const findProtocolOption = (search: string) => { + return protocolOptions.find(opt => equalPredicate(opt.name, opt.value, search)); }; -export const findDirectionOption = (nameOrVal: string, t: TFunction) => { - return getDirectionOptions(t, true).find( - o => o.name.toLowerCase() === nameOrVal.toLowerCase() || o.value === nameOrVal - ); +export const findDirectionOption = (t: TFunction, allowInner: boolean, search: string) => { + return getDirectionOptions(t, allowInner).find(opt => equalPredicate(opt.name, opt.value, search)); +}; + +const findFromROV = (search: string, mapping: ROVMapping): FilterOption | undefined => { + const rov = mapping.rov.find(opt => equalPredicate(opt.name, String(opt.value), search)); + return rov ? mapping.mapper(rov) : undefined; +}; + +export const findDropStateOption = (search: string): FilterOption | undefined => { + return findFromROV(search, dropStateMapping); +}; + +export const findDropCauseOption = (search: string): FilterOption | undefined => { + return findFromROV(search, dropCauseMapping); +}; + +export const findDnsResponseCodeOption = (search: string): FilterOption | undefined => { + return findFromROV(search, dnsRCodeMapping); +}; + +export const findDnsErrorCodeOption = (search: string): FilterOption | undefined => { + return findFromROV(search, dnsErrMapping); +}; + +export const findDSCPOption = (search: string): FilterOption | undefined => { + return findFromROV(search, dscpMapping); };