From ca64167548f381c18e996913585199e15e790770 Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Tue, 18 Nov 2025 13:47:08 -0800 Subject: [PATCH 1/3] Make TableFilters class instead of using the interface from react-table --- .../src/bootstrap/react-table-defaults.tsx | 13 +- .../table-filterable-cell.tsx | 18 +- web-console/src/console-application.tsx | 111 +++------ .../dialogs/status-dialog/status-dialog.tsx | 8 +- web-console/src/react-table/constants.ts | 25 ++ web-console/src/react-table/index.ts | 2 +- .../react-table/react-table-filters.spec.ts | 83 ------- .../src/react-table/react-table-filters.ts | 195 ---------------- .../src/react-table/react-table-inputs.tsx | 42 ++-- web-console/src/utils/table-filters/index.ts | 20 ++ .../utils/table-filters/table-filter.spec.ts | 96 ++++++++ .../src/utils/table-filters/table-filter.ts | 214 ++++++++++++++++++ .../utils/table-filters/table-filters.spec.ts | 94 ++++++++ .../src/utils/table-filters/table-filters.ts | 107 +++++++++ web-console/src/utils/table-helpers.ts | 5 +- .../datasources-view.spec.tsx | 6 +- .../datasources-view/datasources-view.tsx | 60 +++-- .../load-data-view/load-data-view.spec.tsx | 6 +- .../views/load-data-view/load-data-view.tsx | 13 +- .../views/lookups-view/lookups-view.spec.tsx | 5 +- .../src/views/lookups-view/lookups-view.tsx | 10 +- .../segments-view/segments-view.spec.tsx | 3 +- .../src/views/segments-view/segments-view.tsx | 133 ++++++----- .../services-view/services-view.spec.tsx | 3 +- .../src/views/services-view/services-view.tsx | 33 +-- .../ingestion-progress-dialog.tsx | 20 +- .../sql-data-loader-view.tsx | 11 +- .../supervisors-view.spec.tsx | 6 +- .../supervisors-view/supervisors-view.tsx | 60 +++-- .../__snapshots__/tasks-view.spec.tsx.snap | 1 + .../src/views/tasks-view/tasks-view.spec.tsx | 5 +- .../src/views/tasks-view/tasks-view.tsx | 40 ++-- .../views/workbench-view/workbench-view.tsx | 12 +- 33 files changed, 856 insertions(+), 604 deletions(-) create mode 100644 web-console/src/react-table/constants.ts delete mode 100644 web-console/src/react-table/react-table-filters.spec.ts delete mode 100644 web-console/src/react-table/react-table-filters.ts create mode 100644 web-console/src/utils/table-filters/index.ts create mode 100644 web-console/src/utils/table-filters/table-filter.spec.ts create mode 100644 web-console/src/utils/table-filters/table-filter.ts create mode 100644 web-console/src/utils/table-filters/table-filters.spec.ts create mode 100644 web-console/src/utils/table-filters/table-filters.ts diff --git a/web-console/src/bootstrap/react-table-defaults.tsx b/web-console/src/bootstrap/react-table-defaults.tsx index 531bc8127bce..fe5cfe99f5aa 100644 --- a/web-console/src/bootstrap/react-table-defaults.tsx +++ b/web-console/src/bootstrap/react-table-defaults.tsx @@ -21,13 +21,9 @@ import type { Filter } from 'react-table'; import { ReactTableDefaults } from 'react-table'; import { Loader } from '../components'; -import { - booleanCustomTableFilter, - DEFAULT_TABLE_CLASS_NAME, - GenericFilterInput, - ReactTablePagination, -} from '../react-table'; +import { DEFAULT_TABLE_CLASS_NAME, GenericFilterInput, ReactTablePagination } from '../react-table'; import { countBy } from '../utils'; +import { TableFilter } from '../utils/table-filters'; const NoData = React.memo(function NoData(props: { children?: React.ReactNode }) { const { children } = props; @@ -41,10 +37,11 @@ export function bootstrapReactTable() { defaultFilterMethod: (filter: Filter, row: any) => { const id = filter.pivotId || filter.id; const subRows = row._subRows; + const tableFilter = TableFilter.fromFilter(filter); if (Array.isArray(subRows)) { - return subRows.some(r => booleanCustomTableFilter(filter, r[id])); + return subRows.some(r => tableFilter.matches(r[id])); } else { - return booleanCustomTableFilter(filter, row[id]); + return tableFilter.matches(row[id]); } }, LoadingComponent: Loader, diff --git a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx index 1e2f3094ff39..4e27307a1d2f 100644 --- a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx +++ b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx @@ -19,10 +19,9 @@ import { Menu, MenuDivider, MenuItem, Popover } from '@blueprintjs/core'; import type { ReactNode } from 'react'; import React from 'react'; -import type { Filter } from 'react-table'; -import type { FilterMode } from '../../react-table'; -import { addOrUpdateFilter, combineModeAndNeedle, filterModeToIcon } from '../../react-table'; +import type { FilterMode, TableFilters } from '../../utils/table-filters'; +import { TableFilter } from '../../utils/table-filters'; import { Deferred } from '../deferred/deferred'; import './table-filterable-cell.scss'; @@ -33,8 +32,8 @@ const FILTER_MODES_NO_COMPARISONS: FilterMode[] = ['=', '!=']; export interface TableFilterableCellProps { field: string; value: string; - filters: Filter[]; - onFiltersChange(filters: Filter[]): void; + filters: TableFilters; + onFiltersChange(filters: TableFilters): void; enableComparisons?: boolean; children?: ReactNode; displayValue?: string; @@ -57,15 +56,10 @@ export const TableFilterableCell = React.memo(function TableFilterableCell( {(enableComparisons ? FILTER_MODES : FILTER_MODES_NO_COMPARISONS).map((mode, i) => ( - onFiltersChange( - addOrUpdateFilter(filters, { - id: field, - value: combineModeAndNeedle(mode, value), - }), - ) + onFiltersChange(filters.addOrUpdate(new TableFilter(field, mode, value))) } /> ))} diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx index 822429582c90..3c838c25feb8 100644 --- a/web-console/src/console-application.tsx +++ b/web-console/src/console-application.tsx @@ -24,7 +24,6 @@ import React from 'react'; import type { RouteComponentProps } from 'react-router'; import { Redirect } from 'react-router'; import { HashRouter, Route, Switch } from 'react-router-dom'; -import type { Filter } from 'react-table'; import { initAceDsqlMode } from './ace-modes/dsql'; import { initAceHjsonMode } from './ace-modes/hjson'; @@ -33,9 +32,9 @@ import { SqlFunctionsProvider } from './contexts/sql-functions-context'; import type { ConsoleViewId, QueryContext, QueryWithContext } from './druid-models'; import type { AvailableFunctions } from './helpers'; import { Capabilities, maybeGetClusterCapacity } from './helpers'; -import { stringToTableFilters, tableFiltersToString } from './react-table'; import { AppToaster } from './singletons'; -import { compact, localStorageGetJson, LocalStorageKeys, QueryManager } from './utils'; +import { localStorageGetJson, LocalStorageKeys, QueryManager } from './utils'; +import { TableFilters } from './utils/table-filters'; import { DatasourcesView, ExploreView, @@ -54,23 +53,23 @@ import './console-application.scss'; type FiltersRouteMatch = RouteComponentProps<{ filters?: string }>; -function changeTabWithFilter(tab: ConsoleViewId, filters: Filter[]) { - const filterString = tableFiltersToString(filters); +function goToView(tab: ConsoleViewId, filters?: TableFilters) { + if (!filters || filters.isEmpty()) { + location.hash = tab; + return; + } + const filterString = filters.toString(); location.hash = tab + (filterString ? `/${filterString}` : ''); } function viewFilterChange(tab: ConsoleViewId) { - return (filters: Filter[]) => changeTabWithFilter(tab, filters); + return (filters: TableFilters) => goToView(tab, filters); } function pathWithFilter(tab: ConsoleViewId) { return `/${tab}/:filters?`; } -function switchTab(tab: ConsoleViewId) { - location.hash = tab; -} - function switchToWorkbenchTab(tabId: string) { location.hash = `workbench/${tabId}`; } @@ -183,79 +182,31 @@ export class ConsoleApplication extends React.PureComponent< private readonly goToStreamingDataLoader = (supervisorId?: string) => { if (supervisorId) this.supervisorId = supervisorId; - switchTab('streaming-data-loader'); + goToView('streaming-data-loader'); this.resetInitialsWithDelay(); }; private readonly goToClassicBatchDataLoader = (taskId?: string) => { if (taskId) this.taskId = taskId; - switchTab('classic-batch-data-loader'); + goToView('classic-batch-data-loader'); this.resetInitialsWithDelay(); }; - private readonly goToDatasources = (datasource: string) => { - changeTabWithFilter('datasources', [{ id: 'datasource', value: `=${datasource}` }]); - }; - - private readonly goToSegments = ({ - start, - end, - datasource, - realtime, - }: { - start?: Date; - end?: Date; - datasource?: string; - realtime?: boolean; - }) => { - changeTabWithFilter( - 'segments', - compact([ - start && { id: 'start', value: `>=${start.toISOString()}` }, - end && { id: 'end', value: `<${end.toISOString()}` }, - datasource && { id: 'datasource', value: `=${datasource}` }, - typeof realtime === 'boolean' ? { id: 'is_realtime', value: `=${realtime}` } : undefined, - ]), - ); - }; - - private readonly goToSupervisor = (supervisorId: string) => { - changeTabWithFilter('supervisors', [{ id: 'supervisor_id', value: `=${supervisorId}` }]); - }; - - private readonly goToTasksWithTaskId = (taskId: string) => { - changeTabWithFilter('tasks', [{ id: 'task_id', value: `=${taskId}` }]); - }; - - private readonly goToTasksWithTaskGroupId = (taskGroupId: string) => { - changeTabWithFilter('tasks', [{ id: 'group_id', value: `=${taskGroupId}` }]); - }; - - private readonly goToTasksWithDatasource = (datasource: string, type?: string) => { - changeTabWithFilter( - 'tasks', - compact([ - { id: 'datasource', value: `=${datasource}` }, - type ? { id: 'type', value: `=${type}` } : undefined, - ]), - ); - }; - private readonly openSupervisorSubmit = () => { this.openSupervisorDialog = true; - switchTab('supervisors'); + goToView('supervisors'); this.resetInitialsWithDelay(); }; private readonly openTaskSubmit = () => { this.openTaskDialog = true; - switchTab('tasks'); + goToView('tasks'); this.resetInitialsWithDelay(); }; private readonly goToQuery = (queryWithContext: QueryWithContext) => { this.queryWithContext = queryWithContext; - switchTab('workbench'); + goToView('workbench'); this.resetInitialsWithDelay(); }; @@ -290,8 +241,7 @@ export class ConsoleApplication extends React.PureComponent< mode="all" initTaskId={this.taskId} initSupervisorId={this.supervisorId} - goToSupervisor={this.goToSupervisor} - goToTasks={this.goToTasksWithTaskGroupId} + goToView={goToView} openSupervisorSubmit={this.openSupervisorSubmit} openTaskSubmit={this.openTaskSubmit} />, @@ -305,8 +255,7 @@ export class ConsoleApplication extends React.PureComponent< , @@ -320,8 +269,7 @@ export class ConsoleApplication extends React.PureComponent< , @@ -346,7 +294,7 @@ export class ConsoleApplication extends React.PureComponent< baseQueryContext={baseQueryContext} serverQueryContext={serverQueryContext} queryEngines={capabilities.getSupportedQueryEngines()} - goToTask={this.goToTasksWithTaskId} + goToView={goToView} getClusterCapacity={maybeGetClusterCapacity} />, 'thin', @@ -361,8 +309,7 @@ export class ConsoleApplication extends React.PureComponent< , @@ -374,11 +321,10 @@ export class ConsoleApplication extends React.PureComponent< return this.wrapInViewContainer( 'datasources', , ); @@ -389,7 +335,7 @@ export class ConsoleApplication extends React.PureComponent< return this.wrapInViewContainer( 'segments', , ); @@ -419,10 +364,10 @@ export class ConsoleApplication extends React.PureComponent< return this.wrapInViewContainer( 'tasks', , ); diff --git a/web-console/src/dialogs/status-dialog/status-dialog.tsx b/web-console/src/dialogs/status-dialog/status-dialog.tsx index babc5e8acf12..de9d4b679f7b 100644 --- a/web-console/src/dialogs/status-dialog/status-dialog.tsx +++ b/web-console/src/dialogs/status-dialog/status-dialog.tsx @@ -18,13 +18,13 @@ import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import React, { useState } from 'react'; -import type { Filter } from 'react-table'; import ReactTable from 'react-table'; import { Loader, TableFilterableCell } from '../../components'; import { useQueryManager } from '../../hooks'; import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table'; import { Api, UrlBaser } from '../../singletons'; +import { TableFilters } from '../../utils/table-filters'; import './status-dialog.scss'; @@ -45,7 +45,7 @@ interface StatusDialogProps { export const StatusDialog = React.memo(function StatusDialog(props: StatusDialogProps) { const { onClose } = props; - const [moduleFilter, setModuleFilter] = useState([]); + const [moduleFilter, setModuleFilter] = useState(TableFilters.empty()); const [responseState] = useQueryManager({ initQuery: null, @@ -86,8 +86,8 @@ export const StatusDialog = React.memo(function StatusDialog(props: StatusDialog data={response.modules} loading={responseState.loading} filterable - filtered={moduleFilter} - onFilteredChange={setModuleFilter} + filtered={moduleFilter.toFilters()} + onFilteredChange={filters => setModuleFilter(TableFilters.fromFilters(filters))} defaultPageSize={SMALL_TABLE_PAGE_SIZE} pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS} showPagination={response.modules.length > SMALL_TABLE_PAGE_SIZE} diff --git a/web-console/src/react-table/constants.ts b/web-console/src/react-table/constants.ts new file mode 100644 index 000000000000..aad0e6f1170d --- /dev/null +++ b/web-console/src/react-table/constants.ts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const DEFAULT_TABLE_CLASS_NAME = '-striped -highlight padded-header'; + +export const STANDARD_TABLE_PAGE_SIZE = 50; +export const STANDARD_TABLE_PAGE_SIZE_OPTIONS = [50, 100, 200]; + +export const SMALL_TABLE_PAGE_SIZE = 25; +export const SMALL_TABLE_PAGE_SIZE_OPTIONS = [25, 50, 100]; diff --git a/web-console/src/react-table/index.ts b/web-console/src/react-table/index.ts index 65b62cff5b4d..ce293570ad4b 100644 --- a/web-console/src/react-table/index.ts +++ b/web-console/src/react-table/index.ts @@ -16,6 +16,6 @@ * limitations under the License. */ -export * from './react-table-filters'; +export * from './constants'; export * from './react-table-inputs'; export * from './react-table-pagination/react-table-pagination'; diff --git a/web-console/src/react-table/react-table-filters.spec.ts b/web-console/src/react-table/react-table-filters.spec.ts deleted file mode 100644 index c0a5476fcde0..000000000000 --- a/web-console/src/react-table/react-table-filters.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - sqlQueryCustomTableFilter, - stringToTableFilters, - tableFiltersToString, -} from './react-table-filters'; - -describe('react-table-utils', () => { - describe('sqlQueryCustomTableFilter', () => { - it('works with contains', () => { - expect( - String( - sqlQueryCustomTableFilter({ - id: 'datasource', - value: `Hello`, - }), - ), - ).toEqual(`LOWER("datasource") LIKE '%hello%'`); - }); - - it('works with exact', () => { - expect( - String( - sqlQueryCustomTableFilter({ - id: 'datasource', - value: `=Hello`, - }), - ), - ).toEqual(`"datasource" = 'Hello'`); - }); - - it('works with less than or equal', () => { - expect( - String( - sqlQueryCustomTableFilter({ - id: 'datasource', - value: `<=Hello`, - }), - ), - ).toEqual(`"datasource" <= 'Hello'`); - }); - }); - - it('tableFiltersToString', () => { - expect( - tableFiltersToString([ - { id: 'x', value: '~y' }, - { id: 'z', value: '=w&%/' }, - ]), - ).toEqual('x~y&z=w%26%25%2F'); - }); - - it('stringToTableFilters', () => { - expect(stringToTableFilters(undefined)).toEqual([]); - expect(stringToTableFilters('')).toEqual([]); - expect(stringToTableFilters('x~y')).toEqual([{ id: 'x', value: '~y' }]); - expect(stringToTableFilters('x~y&z=w%26%25%2F')).toEqual([ - { id: 'x', value: '~y' }, - { id: 'z', value: '=w&%/' }, - ]); - expect(stringToTableFilters('x<3&y<=3')).toEqual([ - { id: 'x', value: '<3' }, - { id: 'y', value: '<=3' }, - ]); - }); -}); diff --git a/web-console/src/react-table/react-table-filters.ts b/web-console/src/react-table/react-table-filters.ts deleted file mode 100644 index 3d446a210e7a..000000000000 --- a/web-console/src/react-table/react-table-filters.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IconName } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { C, F, SqlExpression } from 'druid-query-toolkit'; -import type { Filter } from 'react-table'; - -import { addOrUpdate, caseInsensitiveContains, filterMap } from '../utils'; - -export const DEFAULT_TABLE_CLASS_NAME = '-striped -highlight padded-header'; - -export const STANDARD_TABLE_PAGE_SIZE = 50; -export const STANDARD_TABLE_PAGE_SIZE_OPTIONS = [50, 100, 200]; - -export const SMALL_TABLE_PAGE_SIZE = 25; -export const SMALL_TABLE_PAGE_SIZE_OPTIONS = [25, 50, 100]; - -export type FilterMode = '~' | '=' | '!=' | '<' | '<=' | '>' | '>='; - -export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<', '<=', '>', '>=']; -export const FILTER_MODES_NO_COMPARISON: FilterMode[] = ['~', '=', '!=']; - -export function filterModeToIcon(mode: FilterMode): IconName { - switch (mode) { - case '~': - return IconNames.SEARCH; - case '=': - return IconNames.EQUALS; - case '!=': - return IconNames.NOT_EQUAL_TO; - case '<': - return IconNames.LESS_THAN; - case '<=': - return IconNames.LESS_THAN_OR_EQUAL_TO; - case '>': - return IconNames.GREATER_THAN; - case '>=': - return IconNames.GREATER_THAN_OR_EQUAL_TO; - default: - return IconNames.BLANK; - } -} - -export function filterModeToTitle(mode: FilterMode): string { - switch (mode) { - case '~': - return 'Search'; - case '=': - return 'Equals'; - case '!=': - return 'Not equals'; - case '<': - return 'Less than'; - case '<=': - return 'Less than or equal'; - case '>': - return 'Greater than'; - case '>=': - return 'Greater than or equal'; - default: - return '?'; - } -} - -interface FilterModeAndNeedle { - mode: FilterMode; - needle: string; - needleParts: string[]; -} - -export function parseFilterModeAndNeedle( - filter: Filter, - loose = false, -): FilterModeAndNeedle | undefined { - const m = /^(~|=|!=|<(?!=)|<=|>(?!=)|>=)?(.*)$/.exec(String(filter.value)); - if (!m) return; - if (!loose && !m[2]) return; - const mode = (m[1] as FilterMode) || '~'; - const needle = m[2] || ''; - return { - mode, - needle, - needleParts: needle.split('|'), - }; -} - -export function combineModeAndNeedle(mode: FilterMode, needle: string, cleanup = false): string { - if (cleanup && needle === '') return ''; - return `${mode}${needle}`; -} - -export function addOrUpdateFilter(filters: readonly Filter[], filter: Filter): Filter[] { - return addOrUpdate(filters, filter, f => f.id); -} - -export function booleanCustomTableFilter(filter: Filter, value: unknown): boolean { - if (value == null) return false; - const modeAndNeedles = parseFilterModeAndNeedle(filter); - if (!modeAndNeedles) return true; - const { mode, needleParts } = modeAndNeedles; - const strValue = String(value); - switch (mode) { - case '=': - return needleParts.some(needle => strValue === needle); - - case '!=': - return needleParts.every(needle => strValue !== needle); - - case '<': - return needleParts.some(needle => strValue < needle); - - case '<=': - return needleParts.some(needle => strValue <= needle); - - case '>': - return needleParts.some(needle => strValue > needle); - - case '>=': - return needleParts.some(needle => strValue >= needle); - - default: - return needleParts.some(needle => caseInsensitiveContains(strValue, needle)); - } -} - -export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression | undefined { - const modeAndNeedles = parseFilterModeAndNeedle(filter); - if (!modeAndNeedles) return; - const { mode, needleParts } = modeAndNeedles; - const column = C(filter.id); - switch (mode) { - case '=': { - return SqlExpression.or(...needleParts.map(needle => column.equal(needle))); - } - - case '!=': { - return SqlExpression.and(...needleParts.map(needle => column.unequal(needle))); - } - - case '<': - return SqlExpression.or(...needleParts.map(needle => column.lessThan(needle))); - - case '<=': - return SqlExpression.or(...needleParts.map(needle => column.lessThanOrEqual(needle))); - - case '>': - return SqlExpression.or(...needleParts.map(needle => column.greaterThan(needle))); - - case '>=': - return SqlExpression.or(...needleParts.map(needle => column.greaterThanOrEqual(needle))); - - default: - return SqlExpression.or( - ...needleParts.map(needle => F('LOWER', column).like(`%${needle.toLowerCase()}%`)), - ); - } -} - -export function sqlQueryCustomTableFilters(filters: Filter[]): SqlExpression { - return SqlExpression.and(...filterMap(filters, sqlQueryCustomTableFilter)); -} - -export function tableFiltersToString(tableFilters: Filter[]): string { - return tableFilters - .map(({ id, value }) => `${id}${value.replace(/[&%/]/g, encodeURIComponent)}`) - .join('&'); -} - -export function stringToTableFilters(str: string | undefined): Filter[] { - if (!str) return []; - // '~' | '=' | '!=' | '<' | '<=' | '>' | '>='; - return filterMap(str.split('&'), clause => { - const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec( - clause.replace(/%2[56F]/g, decodeURIComponent), - ); - if (!m) return; - return { id: m[1], value: m[2] }; - }); -} diff --git a/web-console/src/react-table/react-table-inputs.tsx b/web-console/src/react-table/react-table-inputs.tsx index 6794cff42822..59662e509e8b 100644 --- a/web-console/src/react-table/react-table-inputs.tsx +++ b/web-console/src/react-table/react-table-inputs.tsx @@ -23,15 +23,7 @@ import { useEffect, useState } from 'react'; import type { Column, ReactTableFunction } from 'react-table'; import { filterMap, toggle } from '../utils'; - -import { - combineModeAndNeedle, - FILTER_MODES, - FILTER_MODES_NO_COMPARISON, - filterModeToIcon, - filterModeToTitle, - parseFilterModeAndNeedle, -} from './react-table-filters'; +import { TableFilter } from '../utils/table-filters'; interface FilterRendererProps { column: Column; @@ -48,7 +40,7 @@ export function GenericFilterInput({ column, filter, onChange, key }: FilterRend const enableComparisons = String(column.headerClassName).includes('enable-comparisons'); - const { mode, needle } = (filter ? parseFilterModeAndNeedle(filter, true) : undefined) || { + const { mode, needle } = (filter ? TableFilter.parseModeAndNeedle(filter, true) : undefined) || { mode: '~', needle: '', }; @@ -56,7 +48,7 @@ export function GenericFilterInput({ column, filter, onChange, key }: FilterRend useEffect(() => { const handler = setTimeout(() => { if (focusedText !== undefined && focusedText !== debouncedValue) { - onChange(combineModeAndNeedle(mode, focusedText)); + onChange(TableFilter.combineModeAndNeedle(mode, focusedText)); setDebouncedValue(focusedText); } }, INPUT_DEBOUNCE_TIME_IN_MILLISECONDS); @@ -80,19 +72,21 @@ export function GenericFilterInput({ column, filter, onChange, key }: FilterRend onInteraction={setMenuOpen} content={ - {(enableComparisons ? FILTER_MODES : FILTER_MODES_NO_COMPARISON).map((m, i) => ( - onChange(combineModeAndNeedle(m, needle))} - labelElement={m === mode ? : undefined} - /> - ))} + {(enableComparisons ? TableFilter.MODES : TableFilter.MODES_NO_COMPARISON).map( + (m, i) => ( + onChange(TableFilter.combineModeAndNeedle(m, needle))} + labelElement={m === mode ? : undefined} + /> + ), + )} } > -