This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new bbcccd2e1f8 Web console: refactor table filters, show inactive MSQ
worker count (#18768)
bbcccd2e1f8 is described below
commit bbcccd2e1f8bb6ca58b10e12e4e4f95d78648007
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri Nov 21 23:47:02 2025 +0000
Web console: refactor table filters, show inactive MSQ worker count (#18768)
* Make TableFilters class instead of using the interface from react-table
* Add refresh button to ShowJson and ShowJsonOrStages
* Show inactive workers
---
web-console/src/bootstrap/react-table-defaults.tsx | 13 +-
.../show-json-or-stages.spec.tsx.snap | 12 ++
.../show-json-or-stages/show-json-or-stages.tsx | 8 +-
.../__snapshots__/show-json.spec.tsx.snap | 12 ++
web-console/src/components/show-json/show-json.tsx | 8 +-
.../table-filterable-cell.tsx | 18 +-
web-console/src/console-application.tsx | 111 +++--------
.../segment-table-action-dialog.spec.tsx.snap | 12 ++
.../src/dialogs/status-dialog/status-dialog.tsx | 8 +-
.../supervisor-table-action-dialog.spec.tsx.snap | 12 ++
.../task-table-action-dialog.spec.tsx.snap | 12 ++
web-console/src/druid-models/stages/stages.ts | 23 +++
.../src/react-table/{index.ts => constants.ts} | 10 +-
web-console/src/react-table/index.ts | 2 +-
.../src/react-table/react-table-filters.spec.ts | 83 --------
web-console/src/react-table/react-table-filters.ts | 195 -------------------
web-console/src/react-table/react-table-inputs.tsx | 42 ++--
.../{react-table => utils/table-filters}/index.ts | 5 +-
.../src/utils/table-filters/table-filter.spec.ts | 96 +++++++++
.../src/utils/table-filters/table-filter.ts | 214 +++++++++++++++++++++
.../src/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/datasources-view.spec.tsx | 6 +-
.../views/datasources-view/datasources-view.tsx | 60 ++++--
.../views/load-data-view/load-data-view.spec.tsx | 6 +-
.../src/views/load-data-view/load-data-view.tsx | 13 +-
.../src/views/lookups-view/lookups-view.spec.tsx | 5 +-
.../src/views/lookups-view/lookups-view.tsx | 10 +-
.../src/views/segments-view/segments-view.spec.tsx | 3 +-
.../src/views/segments-view/segments-view.tsx | 133 ++++++-------
.../src/views/services-view/services-view.spec.tsx | 3 +-
.../src/views/services-view/services-view.tsx | 33 ++--
.../ingestion-progress-dialog.tsx | 20 +-
.../sql-data-loader-view/sql-data-loader-view.tsx | 11 +-
.../supervisors-view/supervisors-view.spec.tsx | 6 +-
.../views/supervisors-view/supervisors-view.tsx | 60 +++---
.../__snapshots__/tasks-view.spec.tsx.snap | 1 +
.../src/views/tasks-view/tasks-view.spec.tsx | 5 +-
web-console/src/views/tasks-view/tasks-view.tsx | 40 ++--
.../execution-stages-pane.spec.tsx.snap | 1 +
.../execution-stages-pane.tsx | 16 ++
.../src/views/workbench-view/workbench-view.tsx | 12 +-
43 files changed, 934 insertions(+), 612 deletions(-)
diff --git a/web-console/src/bootstrap/react-table-defaults.tsx
b/web-console/src/bootstrap/react-table-defaults.tsx
index 531bc8127bc..fe5cfe99f5a 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/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap
b/web-console/src/components/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap
index 052bb7e9d6c..5110584105c 100644
---
a/web-console/src/components/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap
+++
b/web-console/src/components/show-json-or-stages/__snapshots__/show-json-or-stages.spec.tsx.snap
@@ -10,6 +10,18 @@ exports[`ShowJsonOrStages matches snapshot 1`] = `
<div
class="bp5-button-group right-buttons"
>
+ <button
+ class="bp5-button bp5-disabled bp5-minimal"
+ disabled=""
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="bp5-button-text"
+ >
+ Refesh
+ </span>
+ </button>
<button
class="bp5-button bp5-disabled bp5-minimal"
disabled=""
diff --git
a/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx
b/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx
index 8010104bcee..6d15330c431 100644
--- a/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx
+++ b/web-console/src/components/show-json-or-stages/show-json-or-stages.tsx
@@ -40,7 +40,7 @@ export interface ShowJsonOrStagesProps {
export const ShowJsonOrStages = React.memo(function ShowJsonOrStages(props:
ShowJsonOrStagesProps) {
const { endpoint, transform, downloadFilename } = props;
- const [jsonState] = useQueryManager<null, [string, Execution | undefined]>({
+ const [jsonState, queryManager] = useQueryManager<null, [string, Execution |
undefined]>({
processQuery: async (_, signal) => {
const resp = await Api.instance.get(endpoint, { signal });
let data = resp.data;
@@ -68,6 +68,12 @@ export const ShowJsonOrStages = React.memo(function
ShowJsonOrStages(props: Show
<div className="show-json-or-stages">
<div className="top-actions">
<ButtonGroup className="right-buttons">
+ <Button
+ disabled={jsonState.loading}
+ text="Refesh"
+ minimal
+ onClick={() => queryManager.rerunLastQuery()}
+ />
{downloadFilename && (
<Button
disabled={jsonState.loading}
diff --git
a/web-console/src/components/show-json/__snapshots__/show-json.spec.tsx.snap
b/web-console/src/components/show-json/__snapshots__/show-json.spec.tsx.snap
index a5f40157462..a52424f9cba 100644
--- a/web-console/src/components/show-json/__snapshots__/show-json.spec.tsx.snap
+++ b/web-console/src/components/show-json/__snapshots__/show-json.spec.tsx.snap
@@ -10,6 +10,18 @@ exports[`ShowJson matches snapshot 1`] = `
<div
class="bp5-button-group right-buttons"
>
+ <button
+ class="bp5-button bp5-disabled bp5-minimal"
+ disabled=""
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="bp5-button-text"
+ >
+ Refesh
+ </span>
+ </button>
<button
class="bp5-button bp5-disabled bp5-minimal"
disabled=""
diff --git a/web-console/src/components/show-json/show-json.tsx
b/web-console/src/components/show-json/show-json.tsx
index 3340e5818ec..c54e8938591 100644
--- a/web-console/src/components/show-json/show-json.tsx
+++ b/web-console/src/components/show-json/show-json.tsx
@@ -38,7 +38,7 @@ export interface ShowJsonProps {
export const ShowJson = React.memo(function ShowJson(props: ShowJsonProps) {
const { endpoint, transform, downloadFilename } = props;
- const [jsonState] = useQueryManager<null, string>({
+ const [jsonState, queryManager] = useQueryManager<null, string>({
processQuery: async (_, signal) => {
const resp = await Api.instance.get(endpoint, { signal });
let data = resp.data;
@@ -53,6 +53,12 @@ export const ShowJson = React.memo(function ShowJson(props:
ShowJsonProps) {
<div className="show-json">
<div className="top-actions">
<ButtonGroup className="right-buttons">
+ <Button
+ disabled={jsonState.loading}
+ text="Refesh"
+ minimal
+ onClick={() => queryManager.rerunLastQuery()}
+ />
{downloadFilename && (
<Button
disabled={jsonState.loading}
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 1e2f3094ff3..4e27307a1d2 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) => (
<MenuItem
key={i}
- icon={filterModeToIcon(mode)}
+ icon={TableFilter.modeToIcon(mode)}
text={displayValue ?? value}
onClick={() =>
- 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 822429582c9..3c838c25feb 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<
<LoadDataView
mode="streaming"
initSupervisorId={this.supervisorId}
- goToSupervisor={this.goToSupervisor}
- goToTasks={this.goToTasksWithTaskGroupId}
+ goToView={goToView}
openSupervisorSubmit={this.openSupervisorSubmit}
openTaskSubmit={this.openTaskSubmit}
/>,
@@ -320,8 +269,7 @@ export class ConsoleApplication extends React.PureComponent<
<LoadDataView
mode="batch"
initTaskId={this.taskId}
- goToSupervisor={this.goToSupervisor}
- goToTasks={this.goToTasksWithTaskGroupId}
+ goToView={goToView}
openSupervisorSubmit={this.openSupervisorSubmit}
openTaskSubmit={this.openTaskSubmit}
/>,
@@ -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<
<SqlDataLoaderView
capabilities={capabilities}
goToQuery={this.goToQuery}
- goToTask={this.goToTasksWithTaskId}
- goToTaskGroup={this.goToTasksWithTaskGroupId}
+ goToView={goToView}
getClusterCapacity={maybeGetClusterCapacity}
serverQueryContext={serverQueryContext}
/>,
@@ -374,11 +321,10 @@ export class ConsoleApplication extends
React.PureComponent<
return this.wrapInViewContainer(
'datasources',
<DatasourcesView
- filters={stringToTableFilters(p.match.params.filters)}
+ filters={TableFilters.fromString(p.match.params.filters)}
onFiltersChange={viewFilterChange('datasources')}
goToQuery={this.goToQuery}
- goToTasks={this.goToTasksWithDatasource}
- goToSegments={this.goToSegments}
+ goToView={goToView}
capabilities={capabilities}
/>,
);
@@ -389,7 +335,7 @@ export class ConsoleApplication extends React.PureComponent<
return this.wrapInViewContainer(
'segments',
<SegmentsView
- filters={stringToTableFilters(p.match.params.filters)}
+ filters={TableFilters.fromString(p.match.params.filters)}
onFiltersChange={viewFilterChange('segments')}
goToQuery={this.goToQuery}
capabilities={capabilities}
@@ -402,13 +348,12 @@ export class ConsoleApplication extends
React.PureComponent<
return this.wrapInViewContainer(
'supervisors',
<SupervisorsView
- filters={stringToTableFilters(p.match.params.filters)}
+ filters={TableFilters.fromString(p.match.params.filters)}
onFiltersChange={viewFilterChange('supervisors')}
openSupervisorDialog={this.openSupervisorDialog}
- goToDatasource={this.goToDatasources}
+ goToView={goToView}
goToQuery={this.goToQuery}
goToStreamingDataLoader={this.goToStreamingDataLoader}
- goToTasks={this.goToTasksWithTaskGroupId}
capabilities={capabilities}
/>,
);
@@ -419,10 +364,10 @@ export class ConsoleApplication extends
React.PureComponent<
return this.wrapInViewContainer(
'tasks',
<TasksView
- filters={stringToTableFilters(p.match.params.filters)}
+ filters={TableFilters.fromString(p.match.params.filters)}
onFiltersChange={viewFilterChange('tasks')}
openTaskDialog={this.openTaskDialog}
- goToDatasource={this.goToDatasources}
+ goToView={goToView}
goToQuery={this.goToQuery}
goToClassicBatchDataLoader={this.goToClassicBatchDataLoader}
capabilities={capabilities}
@@ -435,7 +380,7 @@ export class ConsoleApplication extends React.PureComponent<
return this.wrapInViewContainer(
'services',
<ServicesView
- filters={stringToTableFilters(p.match.params.filters)}
+ filters={TableFilters.fromString(p.match.params.filters)}
onFiltersChange={viewFilterChange('services')}
goToQuery={this.goToQuery}
capabilities={capabilities}
@@ -447,7 +392,7 @@ export class ConsoleApplication extends React.PureComponent<
return this.wrapInViewContainer(
'lookups',
<LookupsView
- filters={stringToTableFilters(p.match.params.filters)}
+ filters={TableFilters.fromString(p.match.params.filters)}
onFiltersChange={viewFilterChange('lookups')}
/>,
);
diff --git
a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
index b5e9297210f..5926054f5c9 100644
---
a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
+++
b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
@@ -133,6 +133,18 @@ exports[`SegmentTableActionDialog matches snapshot 1`] = `
<div
class="bp5-button-group right-buttons"
>
+ <button
+ class="bp5-button bp5-disabled bp5-minimal"
+ disabled=""
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="bp5-button-text"
+ >
+ Refesh
+ </span>
+ </button>
<button
class="bp5-button bp5-disabled bp5-minimal"
disabled=""
diff --git a/web-console/src/dialogs/status-dialog/status-dialog.tsx
b/web-console/src/dialogs/status-dialog/status-dialog.tsx
index babc5e8acf1..de9d4b679f7 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<Filter[]>([]);
+ const [moduleFilter, setModuleFilter] =
useState<TableFilters>(TableFilters.empty());
const [responseState] = useQueryManager<null, StatusResponse>({
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/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
index 84e7b4d3a5a..b35c08f23cc 100755
---
a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
+++
b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
@@ -187,6 +187,18 @@ exports[`SupervisorTableActionDialog matches snapshot 1`]
= `
<div
class="bp5-button-group right-buttons"
>
+ <button
+ class="bp5-button bp5-disabled bp5-minimal"
+ disabled=""
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="bp5-button-text"
+ >
+ Refesh
+ </span>
+ </button>
<button
class="bp5-button bp5-disabled bp5-minimal"
disabled=""
diff --git
a/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
b/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
index 28fa10bb145..0e92f7194af 100644
---
a/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
+++
b/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
@@ -187,6 +187,18 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
<div
class="bp5-button-group right-buttons"
>
+ <button
+ class="bp5-button bp5-disabled bp5-minimal"
+ disabled=""
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="bp5-button-text"
+ >
+ Refesh
+ </span>
+ </button>
<button
class="bp5-button bp5-disabled bp5-minimal"
disabled=""
diff --git a/web-console/src/druid-models/stages/stages.ts
b/web-console/src/druid-models/stages/stages.ts
index 8f9638e4e5b..65818c80467 100644
--- a/web-console/src/druid-models/stages/stages.ts
+++ b/web-console/src/druid-models/stages/stages.ts
@@ -617,6 +617,29 @@ export class Stages {
});
}
+ getInactiveWorkerCount(stage: StageDefinition): number | undefined {
+ const { counters } = this;
+ const { stageNumber, definition } = stage;
+ const forStageCounters = counters?.[stageNumber];
+ if (!forStageCounters) return;
+
+ const inputChannelCounters = definition.input.map((_, i) => `input${i}` as
ChannelCounterName);
+
+ // Calculate and return the number of workers that have zero count across
all inputChannelCounters
+ return sum(
+ Object.values(forStageCounters).map(stageCounters =>
+ Number(
+ inputChannelCounters.every(channel => {
+ const c = stageCounters[channel];
+ if (!c) return true;
+ const totalRows = sum(c.rows || []);
+ return totalRows === 0;
+ }),
+ ),
+ ),
+ );
+ }
+
getPartitionChannelCounterNamesForStage(
stage: StageDefinition,
inOut: InOut,
diff --git a/web-console/src/react-table/index.ts
b/web-console/src/react-table/constants.ts
similarity index 73%
copy from web-console/src/react-table/index.ts
copy to web-console/src/react-table/constants.ts
index 65b62cff5b4..aad0e6f1170 100644
--- a/web-console/src/react-table/index.ts
+++ b/web-console/src/react-table/constants.ts
@@ -16,6 +16,10 @@
* limitations under the License.
*/
-export * from './react-table-filters';
-export * from './react-table-inputs';
-export * from './react-table-pagination/react-table-pagination';
+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 65b62cff5b4..ce293570ad4 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 c0a5476fcde..00000000000
--- 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 3d446a210e7..00000000000
--- 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 6794cff4282..59662e509e8 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={
<Menu>
- {(enableComparisons ? FILTER_MODES :
FILTER_MODES_NO_COMPARISON).map((m, i) => (
- <MenuItem
- key={i}
- icon={filterModeToIcon(m)}
- text={filterModeToTitle(m)}
- onClick={() => onChange(combineModeAndNeedle(m, needle))}
- labelElement={m === mode ? <Icon icon={IconNames.TICK} /> :
undefined}
- />
- ))}
+ {(enableComparisons ? TableFilter.MODES :
TableFilter.MODES_NO_COMPARISON).map(
+ (m, i) => (
+ <MenuItem
+ key={i}
+ icon={TableFilter.modeToIcon(m)}
+ text={TableFilter.modeToTitle(m)}
+ onClick={() =>
onChange(TableFilter.combineModeAndNeedle(m, needle))}
+ labelElement={m === mode ? <Icon icon={IconNames.TICK} />
: undefined}
+ />
+ ),
+ )}
</Menu>
}
>
- <Button className="filter-mode-button" icon={filterModeToIcon(mode)}
minimal />
+ <Button className="filter-mode-button"
icon={TableFilter.modeToIcon(mode)} minimal />
</Popover>
}
value={focusedText ?? needle}
@@ -101,7 +95,7 @@ export function GenericFilterInput({ column, filter,
onChange, key }: FilterRend
if (e.key === 'Enter') {
const inputValue = (e.target as HTMLInputElement).value;
setDebouncedValue(undefined); // Reset debounce to avoid duplicate
triggers
- onChange(combineModeAndNeedle(mode, inputValue));
+ onChange(TableFilter.combineModeAndNeedle(mode, inputValue));
}
}}
rightElement={
@@ -119,7 +113,7 @@ export function suggestibleFilterInput(suggestions:
string[]) {
return function SuggestibleFilterInput({ filter, onChange, key, ...rest }:
FilterRendererProps) {
let valuesFilteredOn: string[] | undefined;
if (filter) {
- const modeAndNeedle = parseFilterModeAndNeedle(filter, true);
+ const modeAndNeedle = TableFilter.parseModeAndNeedle(filter, true);
if (modeAndNeedle && modeAndNeedle.mode === '=') {
valuesFilteredOn = modeAndNeedle.needleParts;
}
@@ -145,7 +139,7 @@ export function suggestibleFilterInput(suggestions:
string[]) {
text={suggestion}
onClick={() =>
onChange(
- combineModeAndNeedle(
+ TableFilter.combineModeAndNeedle(
'=',
valuesFilteredOn
? toggle(valuesFilteredOn, suggestion).join('|')
diff --git a/web-console/src/react-table/index.ts
b/web-console/src/utils/table-filters/index.ts
similarity index 85%
copy from web-console/src/react-table/index.ts
copy to web-console/src/utils/table-filters/index.ts
index 65b62cff5b4..c41a8055052 100644
--- a/web-console/src/react-table/index.ts
+++ b/web-console/src/utils/table-filters/index.ts
@@ -16,6 +16,5 @@
* limitations under the License.
*/
-export * from './react-table-filters';
-export * from './react-table-inputs';
-export * from './react-table-pagination/react-table-pagination';
+export * from './table-filter';
+export * from './table-filters';
diff --git a/web-console/src/utils/table-filters/table-filter.spec.ts
b/web-console/src/utils/table-filters/table-filter.spec.ts
new file mode 100644
index 00000000000..59fd22ec05a
--- /dev/null
+++ b/web-console/src/utils/table-filters/table-filter.spec.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 { TableFilter } from './table-filter';
+
+describe('TableFilter', () => {
+ describe('sqlQueryCustomTableFilter', () => {
+ it('works with contains', () => {
+ expect(String(new TableFilter('datasource', '~',
'Hello').toSqlExpression())).toEqual(
+ `LOWER("datasource") LIKE '%hello%'`,
+ );
+ });
+
+ it('works with exact', () => {
+ expect(String(new TableFilter('datasource', '=',
'Hello').toSqlExpression())).toEqual(
+ `"datasource" = 'Hello'`,
+ );
+ });
+
+ it('works with less than or equal', () => {
+ expect(String(new TableFilter('datasource', '<=',
'Hello').toSqlExpression())).toEqual(
+ `"datasource" <= 'Hello'`,
+ );
+ });
+ });
+
+ describe('toFilter', () => {
+ it('converts to react-table Filter format', () => {
+ const filter = new TableFilter('datasource', '=', 'test');
+ expect(filter.toFilter()).toEqual({ id: 'datasource', value: '=test' });
+ });
+ });
+
+ describe('fromFilter', () => {
+ it('converts from react-table Filter format', () => {
+ const filter = TableFilter.fromFilter({ id: 'datasource', value: '=test'
});
+ expect(filter.key).toBe('datasource');
+ expect(filter.mode).toBe('=');
+ expect(filter.value).toBe('test');
+ });
+
+ it('defaults to contains mode when no mode specified', () => {
+ const filter = TableFilter.fromFilter({ id: 'datasource', value: 'test'
});
+ expect(filter.key).toBe('datasource');
+ expect(filter.mode).toBe('~');
+ expect(filter.value).toBe('test');
+ });
+ });
+
+ describe('matches', () => {
+ it('works with equals', () => {
+ const filter = new TableFilter('status', '=', 'active');
+ expect(filter.matches('active')).toBe(true);
+ expect(filter.matches('inactive')).toBe(false);
+ });
+
+ it('works with contains', () => {
+ const filter = new TableFilter('name', '~', 'test');
+ expect(filter.matches('testing')).toBe(true);
+ expect(filter.matches('TEST')).toBe(true);
+ expect(filter.matches('notest')).toBe(true);
+ expect(filter.matches('other')).toBe(false);
+ });
+
+ it('works with comparisons', () => {
+ const filter = new TableFilter('count', '<', '10');
+ expect(filter.matches('05')).toBe(true); // String comparison: '05' <
'10'
+ expect(filter.matches('2')).toBe(false); // String comparison: '2' > '10'
+ });
+ });
+
+ describe('equals', () => {
+ it('compares two filters', () => {
+ const filter1 = new TableFilter('x', '=', 'y');
+ const filter2 = new TableFilter('x', '=', 'y');
+ const filter3 = new TableFilter('x', '=', 'z');
+ expect(filter1.equals(filter2)).toBe(true);
+ expect(filter1.equals(filter3)).toBe(false);
+ });
+ });
+});
diff --git a/web-console/src/utils/table-filters/table-filter.ts
b/web-console/src/utils/table-filters/table-filter.ts
new file mode 100644
index 00000000000..f29e4d94258
--- /dev/null
+++ b/web-console/src/utils/table-filters/table-filter.ts
@@ -0,0 +1,214 @@
+/*
+ * 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 { caseInsensitiveContains } from '../index';
+
+export type FilterMode = '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
+
+export class TableFilter {
+ static readonly MODES: FilterMode[] = ['~', '=', '!=', '<', '<=', '>', '>='];
+ static readonly MODES_NO_COMPARISON: FilterMode[] = ['~', '=', '!='];
+
+ public readonly key: string;
+ public readonly mode: FilterMode;
+ public readonly values: string[];
+
+ static fromSingleTableFilterString(str: string): TableFilter | undefined {
+ const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec(
+ str.replace(/%2[56F]/g, decodeURIComponent),
+ );
+ if (!m) return;
+
+ const key = m[1];
+ const modeAndValue = m[2];
+
+ // Parse mode from the value
+ const modeMatch = /^(~|=|!=|<(?!=)|<=|>(?!=)|>=)?(.*)$/.exec(modeAndValue);
+ if (!modeMatch) return;
+
+ const mode = (modeMatch[1] as FilterMode) || '~';
+ const value = modeMatch[2] || '';
+
+ return new TableFilter(key, mode, value);
+ }
+
+ static fromFilter(filter: Filter): TableFilter {
+ const modeMatch =
/^(~|=|!=|<(?!=)|<=|>(?!=)|>=)?(.*)$/.exec(String(filter.value));
+ if (!modeMatch) {
+ return new TableFilter(filter.id, '~', String(filter.value));
+ }
+
+ const mode = (modeMatch[1] as FilterMode) || '~';
+ const value = modeMatch[2] || '';
+
+ return new TableFilter(filter.id, mode, value);
+ }
+
+ constructor(key: string, mode: FilterMode, value: string | string[]) {
+ this.key = key;
+ this.mode = mode;
+ this.values = typeof value === 'string' ? value.split('|') : value;
+ }
+
+ public get value(): string {
+ return this.values.join('|');
+ }
+
+ public toFilter(): Filter {
+ return {
+ id: this.key,
+ value: `${this.mode}${this.value}`,
+ };
+ }
+
+ public equals(other: TableFilter): boolean {
+ return (
+ this.key === other.key &&
+ this.mode === other.mode &&
+ this.values.length === other.values.length &&
+ this.values.every((v, i) => v === other.values[i])
+ );
+ }
+
+ public matches(value: unknown): boolean {
+ if (value == null) return false;
+ const strValue = String(value);
+
+ switch (this.mode) {
+ case '=':
+ return this.values.some(needle => strValue === needle);
+
+ case '!=':
+ return this.values.every(needle => strValue !== needle);
+
+ case '<':
+ return this.values.some(needle => strValue < needle);
+
+ case '<=':
+ return this.values.some(needle => strValue <= needle);
+
+ case '>':
+ return this.values.some(needle => strValue > needle);
+
+ case '>=':
+ return this.values.some(needle => strValue >= needle);
+
+ default:
+ return this.values.some(needle => caseInsensitiveContains(strValue,
needle));
+ }
+ }
+
+ public toSqlExpression(): SqlExpression {
+ const column = C(this.key);
+
+ switch (this.mode) {
+ case '=': {
+ return SqlExpression.or(...this.values.map(needle =>
column.equal(needle)));
+ }
+
+ case '!=': {
+ return SqlExpression.and(...this.values.map(needle =>
column.unequal(needle)));
+ }
+
+ case '<':
+ return SqlExpression.or(...this.values.map(needle =>
column.lessThan(needle)));
+
+ case '<=':
+ return SqlExpression.or(...this.values.map(needle =>
column.lessThanOrEqual(needle)));
+
+ case '>':
+ return SqlExpression.or(...this.values.map(needle =>
column.greaterThan(needle)));
+
+ case '>=':
+ return SqlExpression.or(...this.values.map(needle =>
column.greaterThanOrEqual(needle)));
+
+ default:
+ return SqlExpression.or(
+ ...this.values.map(needle => F('LOWER',
column).like(`%${needle.toLowerCase()}%`)),
+ );
+ }
+ }
+
+ static modeToIcon(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;
+ }
+ }
+
+ static modeToTitle(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 '?';
+ }
+ }
+
+ static combineModeAndNeedle(mode: FilterMode, needle: string, cleanup =
false): string {
+ if (cleanup && needle === '') return '';
+ return `${mode}${needle}`;
+ }
+
+ static parseModeAndNeedle(
+ filter: Filter,
+ loose = false,
+ ): { mode: FilterMode; needle: string; needleParts: string[] } | 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('|'),
+ };
+ }
+}
diff --git a/web-console/src/utils/table-filters/table-filters.spec.ts
b/web-console/src/utils/table-filters/table-filters.spec.ts
new file mode 100644
index 00000000000..1a9dfa2ef77
--- /dev/null
+++ b/web-console/src/utils/table-filters/table-filters.spec.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { TableFilter } from './table-filter';
+import { TableFilters } from './table-filters';
+
+describe('TableFilters', () => {
+ describe('toString', () => {
+ it('converts filters to string', () => {
+ const filters = new TableFilters([
+ new TableFilter('x', '~', 'y'),
+ new TableFilter('z', '=', 'w&%/'),
+ ]);
+ expect(filters.toString()).toEqual('x~y&z=w%26%25%2F');
+ });
+ });
+
+ describe('fromString', () => {
+ it('handles empty strings', () => {
+ expect(TableFilters.fromString(undefined).isEmpty()).toBe(true);
+ expect(TableFilters.fromString('').isEmpty()).toBe(true);
+ });
+
+ it('parses single filter', () => {
+ const filters = TableFilters.fromString('x~y');
+ expect(filters.size()).toBe(1);
+ const filterArray = filters.toArray();
+ expect(filterArray[0].key).toBe('x');
+ expect(filterArray[0].mode).toBe('~');
+ expect(filterArray[0].value).toBe('y');
+ });
+
+ it('parses multiple filters with encoded characters', () => {
+ const filters = TableFilters.fromString('x~y&z=w%26%25%2F');
+ expect(filters.size()).toBe(2);
+ const filterArray = filters.toArray();
+ expect(filterArray[0].key).toBe('x');
+ expect(filterArray[0].mode).toBe('~');
+ expect(filterArray[0].value).toBe('y');
+ expect(filterArray[1].key).toBe('z');
+ expect(filterArray[1].mode).toBe('=');
+ expect(filterArray[1].value).toBe('w&%/');
+ });
+
+ it('parses comparison filters', () => {
+ const filters = TableFilters.fromString('x<3&y<=3');
+ expect(filters.size()).toBe(2);
+ const filterArray = filters.toArray();
+ expect(filterArray[0].key).toBe('x');
+ expect(filterArray[0].mode).toBe('<');
+ expect(filterArray[0].value).toBe('3');
+ expect(filterArray[1].key).toBe('y');
+ expect(filterArray[1].mode).toBe('<=');
+ expect(filterArray[1].value).toBe('3');
+ });
+ });
+
+ describe('eq', () => {
+ it('creates filters from key-value pairs', () => {
+ const filters = TableFilters.eq({ datasource: 'test', type: 'index' });
+ expect(filters.size()).toBe(2);
+ const filterArray = filters.toArray();
+ expect(filterArray[0].key).toBe('datasource');
+ expect(filterArray[0].mode).toBe('=');
+ expect(filterArray[0].value).toBe('test');
+ expect(filterArray[1].key).toBe('type');
+ expect(filterArray[1].mode).toBe('=');
+ expect(filterArray[1].value).toBe('index');
+ });
+ });
+
+ describe('empty', () => {
+ it('creates an empty TableFilters', () => {
+ const filters = TableFilters.empty();
+ expect(filters.isEmpty()).toBe(true);
+ expect(filters.size()).toBe(0);
+ });
+ });
+});
diff --git a/web-console/src/utils/table-filters/table-filters.ts
b/web-console/src/utils/table-filters/table-filters.ts
new file mode 100644
index 00000000000..d219e927978
--- /dev/null
+++ b/web-console/src/utils/table-filters/table-filters.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 { SqlExpression } from 'druid-query-toolkit';
+import type { Filter } from 'react-table';
+
+import { addOrUpdate, filterMap } from '../index';
+
+import { TableFilter } from './table-filter';
+
+export class TableFilters {
+ private readonly filters: readonly TableFilter[];
+
+ static fromString(str: string | undefined): TableFilters {
+ if (!str) return new TableFilters([]);
+ const filters = filterMap(str.split('&'), clause => {
+ return TableFilter.fromSingleTableFilterString(clause);
+ });
+ return new TableFilters(filters);
+ }
+
+ static fromFilters(filters: Filter[]): TableFilters {
+ return new TableFilters(filters.map(TableFilter.fromFilter));
+ }
+
+ static eq(keyValue: Record<string, string>): TableFilters {
+ const filters = Object.entries(keyValue).map(
+ ([key, value]) => new TableFilter(key, '=', value),
+ );
+ return new TableFilters(filters);
+ }
+
+ static empty(): TableFilters {
+ return new TableFilters([]);
+ }
+
+ constructor(filters?: TableFilter | TableFilter[] | TableFilters) {
+ if (!filters) {
+ this.filters = [];
+ } else if (filters instanceof TableFilters) {
+ this.filters = filters.filters;
+ } else if (Array.isArray(filters)) {
+ this.filters = filters;
+ } else {
+ this.filters = [filters];
+ }
+ }
+
+ toString(): string {
+ return this.filters
+ .map(
+ filter =>
+ `${filter.key}${filter.mode}${filter.values
+ .join('|')
+ .replace(/[&%/]/g, encodeURIComponent)}`,
+ )
+ .join('&');
+ }
+
+ toFilters(): Filter[] {
+ return this.filters.map(f => f.toFilter());
+ }
+
+ addOrUpdate(filter: TableFilter): TableFilters {
+ return new TableFilters(addOrUpdate(this.filters, filter, f => f.key));
+ }
+
+ toSqlExpression(): SqlExpression {
+ return SqlExpression.and(
+ ...filterMap(this.filters, function (filter: TableFilter): SqlExpression
| undefined {
+ return filter.toSqlExpression();
+ }),
+ );
+ }
+
+ toArray(): readonly TableFilter[] {
+ return this.filters;
+ }
+
+ isEmpty(): boolean {
+ return this.filters.length === 0;
+ }
+
+ size(): number {
+ return this.filters.length;
+ }
+
+ equals(other: TableFilters): boolean {
+ if (this.filters.length !== other.filters.length) return false;
+ return this.filters.every((f, i) => f.equals(other.filters[i]));
+ }
+}
diff --git a/web-console/src/utils/table-helpers.ts
b/web-console/src/utils/table-helpers.ts
index a4ba4fce7dd..30328f32752 100644
--- a/web-console/src/utils/table-helpers.ts
+++ b/web-console/src/utils/table-helpers.ts
@@ -19,10 +19,11 @@
import { ascending, descending, sort } from 'd3-array';
import type { QueryResult, SqlExpression } from 'druid-query-toolkit';
import { C } from 'druid-query-toolkit';
-import type { Filter, SortingRule } from 'react-table';
+import type { SortingRule } from 'react-table';
import { filterMap, formatNumber, isNumberLike, oneOf } from './general';
import { deepSet } from './object-change';
+import type { TableFilters } from './table-filters';
export interface Pagination {
page: number;
@@ -71,7 +72,7 @@ export function getNumericColumnBraces(
export interface TableState {
page: number;
pageSize: number;
- filtered: Filter[];
+ filtered: TableFilters;
sorted: SortingRule[];
}
diff --git a/web-console/src/views/datasources-view/datasources-view.spec.tsx
b/web-console/src/views/datasources-view/datasources-view.spec.tsx
index 196085369a8..f915a3dc7cd 100644
--- a/web-console/src/views/datasources-view/datasources-view.spec.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.spec.tsx
@@ -18,6 +18,7 @@
import { Capabilities } from '../../helpers';
import { shallow } from '../../utils/shallow-renderer';
+import { TableFilters } from '../../utils/table-filters';
import { DatasourcesView } from './datasources-view';
@@ -25,11 +26,10 @@ describe('DatasourcesView', () => {
it('matches snapshot', () => {
const dataSourceView = shallow(
<DatasourcesView
- filters={[]}
+ filters={TableFilters.empty()}
onFiltersChange={() => {}}
goToQuery={() => {}}
- goToTasks={() => null}
- goToSegments={() => {}}
+ goToView={() => {}}
capabilities={Capabilities.FULL}
/>,
);
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index b1d08522ee3..dc3f9ef45b4 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -21,7 +21,6 @@ import { IconNames } from '@blueprintjs/icons';
import { sum } from 'd3-array';
import { SqlQuery, T } from 'druid-query-toolkit';
import React from 'react';
-import type { Filter } from 'react-table';
import ReactTable from 'react-table';
import {
@@ -51,6 +50,7 @@ import type {
CompactionConfigs,
CompactionInfo,
CompactionStatus,
+ ConsoleViewId,
QueryWithContext,
Rule,
} from '../../druid-models';
@@ -94,6 +94,7 @@ import {
twoLines,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { TableFilter, TableFilters } from '../../utils/table-filters';
import './datasources-view.scss';
@@ -304,16 +305,10 @@ function normalizeTaskType(taskType: string): string {
}
export interface DatasourcesViewProps {
- filters: Filter[];
- onFiltersChange(filters: Filter[]): void;
+ filters: TableFilters;
+ onFiltersChange(filters: TableFilters): void;
goToQuery(queryWithContext: QueryWithContext): void;
- goToTasks(datasource?: string): void;
- goToSegments(options: {
- start?: Date;
- end?: Date;
- datasource?: string;
- realtime?: boolean;
- }): void;
+ goToView(tab: ConsoleViewId, filters?: TableFilters): void;
capabilities: Capabilities;
}
@@ -1002,7 +997,7 @@ GROUP BY 1, 2`;
rules: Rule[] | undefined,
compactionInfo: CompactionInfo | undefined,
): BasicAction[] {
- const { goToQuery, goToSegments, capabilities } = this.props;
+ const { goToQuery, goToView, capabilities } = this.props;
if (unused) {
if (!capabilities.hasOverlordAccess()) return [];
@@ -1045,7 +1040,7 @@ GROUP BY 1, 2`;
icon: getConsoleViewIcon('segments'),
title: 'Go to segments',
onAction: () => {
- goToSegments({ datasource });
+ goToView('segments', TableFilters.eq({ datasource }));
},
},
capabilities.hasCoordinatorAccess()
@@ -1174,7 +1169,7 @@ GROUP BY 1, 2`;
}
private renderDatasourcesTable() {
- const { goToTasks, capabilities, filters, onFiltersChange } = this.props;
+ const { goToView, capabilities, filters, onFiltersChange } = this.props;
const { datasourcesAndDefaultRulesState, showUnused, visibleColumns,
showSegmentTimeline } =
this.state;
@@ -1219,8 +1214,8 @@ GROUP BY 1, 2`;
: '')
}
filterable
- filtered={filters}
- onFilteredChange={onFiltersChange}
+ filtered={filters.toFilters()}
+ onFilteredChange={filters =>
onFiltersChange(TableFilters.fromFilters(filters))}
defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
showPagination={datasources.length > STANDARD_TABLE_PAGE_SIZE}
@@ -1364,7 +1359,9 @@ GROUP BY 1, 2`;
if (!runningTasks) return;
return (
<TableClickableCell
- onClick={() => goToTasks(original.datasource)}
+ onClick={() =>
+ goToView('tasks', TableFilters.eq({ datasource:
original.datasource }))
+ }
hoverIcon={IconNames.ARROW_TOP_RIGHT}
tooltip="Go to tasks"
>
@@ -1719,7 +1716,7 @@ GROUP BY 1, 2`;
}
render() {
- const { capabilities, filters, goToSegments } = this.props;
+ const { capabilities, filters, goToView } = this.props;
const {
showUnused,
visibleColumns,
@@ -1751,9 +1748,9 @@ GROUP BY 1, 2`;
? undefined
: {
capabilities,
- datasource: findMap(filters, filter =>
- filter.id === 'datasource' &&
/^=[^=|]+$/.exec(String(filter.value))
- ? filter.value.slice(1)
+ datasource: findMap(filters.toArray(), filter =>
+ filter.key === 'datasource' && filter.mode === '='
+ ? filter.value
: undefined,
),
},
@@ -1791,7 +1788,28 @@ GROUP BY 1, 2`;
text="Open in segments view"
small
rightIcon={IconNames.ARROW_TOP_RIGHT}
- onClick={() => goToSegments({ start, end, datasource,
realtime })}
+ onClick={() => {
+ let filters = TableFilters.empty();
+ if (datasource) {
+ filters = filters.addOrUpdate(
+ new TableFilter('datasource', '=', datasource),
+ );
+ }
+ if (realtime !== undefined) {
+ filters = filters.addOrUpdate(
+ new TableFilter('is_realtime', '=', String(realtime
? 1 : 0)),
+ );
+ }
+ if (start && end) {
+ filters = filters.addOrUpdate(
+ new TableFilter('start', '>=', start.toISOString()),
+ );
+ filters = filters.addOrUpdate(
+ new TableFilter('end', '<=', end.toISOString()),
+ );
+ }
+ goToView('segments', filters);
+ }}
/>
);
}}
diff --git a/web-console/src/views/load-data-view/load-data-view.spec.tsx
b/web-console/src/views/load-data-view/load-data-view.spec.tsx
index 3c7e443a9cb..8e808046081 100644
--- a/web-console/src/views/load-data-view/load-data-view.spec.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.spec.tsx
@@ -25,8 +25,7 @@ describe('LoadDataView', () => {
const loadDataView = shallow(
<LoadDataView
mode="streaming"
- goToSupervisor={() => {}}
- goToTasks={() => {}}
+ goToView={() => {}}
openSupervisorSubmit={() => {}}
openTaskSubmit={() => {}}
/>,
@@ -38,8 +37,7 @@ describe('LoadDataView', () => {
const loadDataView = shallow(
<LoadDataView
mode="batch"
- goToSupervisor={() => {}}
- goToTasks={() => {}}
+ goToView={() => {}}
openSupervisorSubmit={() => {}}
openTaskSubmit={() => {}}
/>,
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx
b/web-console/src/views/load-data-view/load-data-view.tsx
index cb42359878f..88694367baa 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -59,6 +59,7 @@ import {
import { AlertDialog, AsyncActionDialog, DiffDialog } from '../../dialogs';
import type {
ArrayIngestMode,
+ ConsoleViewId,
DimensionSpec,
DruidFilter,
FlattenField,
@@ -186,6 +187,7 @@ import {
sampleForTimestamp,
sampleForTransform,
} from '../../utils/sampler';
+import { TableFilters } from '../../utils/table-filters';
import { ExamplePicker } from './example-picker/example-picker';
import { EXAMPLE_SPECS } from './example-specs';
@@ -389,9 +391,8 @@ export interface LoadDataViewProps {
mode: LoadDataViewMode;
initSupervisorId?: string;
initTaskId?: string;
- goToSupervisor: (supervisorId: string) => void;
+ goToView: (tab: ConsoleViewId, filters?: TableFilters) => void;
openSupervisorSubmit: () => void;
- goToTasks: (taskGroupId: string) => void;
openTaskSubmit: () => void;
}
@@ -3704,7 +3705,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
}
private readonly handleSubmitSupervisor = async () => {
- const { goToSupervisor } = this.props;
+ const { goToView } = this.props;
const { spec, submitting } = this.state;
if (submitting) return;
@@ -3728,13 +3729,13 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
const supervisorId = getSpecDatasourceName(spec);
if (supervisorId) {
setTimeout(() => {
- goToSupervisor(supervisorId);
+ goToView('supervisors', TableFilters.eq({ supervisor_id: supervisorId
}));
}, 1000);
}
};
private readonly handleSubmitTask = async () => {
- const { goToTasks } = this.props;
+ const { goToView } = this.props;
const { spec, submitting } = this.state;
if (submitting) return;
@@ -3757,7 +3758,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
});
setTimeout(() => {
- goToTasks(taskResp.data.task);
+ goToView('tasks', TableFilters.eq({ task_id: taskResp.data.task }));
}, 1000);
};
}
diff --git a/web-console/src/views/lookups-view/lookups-view.spec.tsx
b/web-console/src/views/lookups-view/lookups-view.spec.tsx
index b4f38247cc3..f71047d4ee0 100644
--- a/web-console/src/views/lookups-view/lookups-view.spec.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.spec.tsx
@@ -17,12 +17,15 @@
*/
import { shallow } from '../../utils/shallow-renderer';
+import { TableFilters } from '../../utils/table-filters';
import { LookupsView } from './lookups-view';
describe('LookupsView', () => {
it('matches snapshot', () => {
- const lookupsView = shallow(<LookupsView filters={[]} onFiltersChange={()
=> {}} />);
+ const lookupsView = shallow(
+ <LookupsView filters={TableFilters.empty()} onFiltersChange={() => {}}
/>,
+ );
expect(lookupsView).toMatchSnapshot();
});
});
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx
b/web-console/src/views/lookups-view/lookups-view.tsx
index e66efbed062..e0dffa50c3a 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -19,7 +19,6 @@
import { Button, Icon, Intent, Tag } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import React from 'react';
-import type { Filter } from 'react-table';
import ReactTable from 'react-table';
import {
@@ -51,6 +50,7 @@ import {
QueryState,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { TableFilters } from '../../utils/table-filters';
import './lookups-view.scss';
@@ -89,8 +89,8 @@ export interface LookupEditInfo {
}
export interface LookupsViewProps {
- filters: Filter[];
- onFiltersChange(filters: Filter[]): void;
+ filters: TableFilters;
+ onFiltersChange(filters: TableFilters): void;
}
export interface LookupsViewState {
@@ -370,8 +370,8 @@ export class LookupsView extends
React.PureComponent<LookupsViewProps, LookupsVi
loading={lookupEntriesAndTiersState.loading}
noDataText={lookupEntriesAndTiersState.getErrorMessage() || 'No
lookups'}
filterable
- filtered={filters}
- onFilteredChange={onFiltersChange}
+ filtered={filters.toFilters()}
+ onFilteredChange={filters =>
onFiltersChange(TableFilters.fromFilters(filters))}
defaultSorted={[{ id: 'lookup_name', desc: false }]}
defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
diff --git a/web-console/src/views/segments-view/segments-view.spec.tsx
b/web-console/src/views/segments-view/segments-view.spec.tsx
index 7f78bb6d53f..3b30275e1d1 100644
--- a/web-console/src/views/segments-view/segments-view.spec.tsx
+++ b/web-console/src/views/segments-view/segments-view.spec.tsx
@@ -18,13 +18,14 @@
import { Capabilities } from '../../helpers';
import { shallow } from '../../utils/shallow-renderer';
+import { TableFilters } from '../../utils/table-filters';
import { SegmentsView } from '../segments-view/segments-view';
describe('SegmentsView', () => {
it('matches snapshot', () => {
const segmentsView = shallow(
<SegmentsView
- filters={[]}
+ filters={TableFilters.empty()}
onFiltersChange={() => {}}
goToQuery={() => {}}
capabilities={Capabilities.FULL}
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index 33510736c5c..770577a2ec0 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -23,7 +23,7 @@ import { C, L, SqlComparison, SqlExpression } from
'druid-query-toolkit';
import * as JSONBig from 'json-bigint-native';
import type { ReactNode } from 'react';
import React from 'react';
-import type { Filter, SortingRule } from 'react-table';
+import type { SortingRule } from 'react-table';
import ReactTable from 'react-table';
import {
@@ -49,11 +49,7 @@ import type { QueryContext, QueryWithContext, ShardSpec }
from '../../druid-mode
import { computeSegmentTimeSpan, getConsoleViewIcon, getDatasourceColor } from
'../../druid-models';
import type { Capabilities, CapabilitiesMode } from '../../helpers';
import {
- booleanCustomTableFilter,
BooleanFilterInput,
- combineModeAndNeedle,
- parseFilterModeAndNeedle,
- sqlQueryCustomTableFilter,
STANDARD_TABLE_PAGE_SIZE,
STANDARD_TABLE_PAGE_SIZE_OPTIONS,
} from '../../react-table';
@@ -83,6 +79,7 @@ import {
twoLines,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { TableFilter, TableFilters } from '../../utils/table-filters';
import './segments-view.scss';
@@ -158,56 +155,47 @@ function formatRangeDimensionValue(dimension: any, value:
any): string {
return `${C(String(dimension))}=${L(String(value))}`;
}
-function segmentFiltersToExpression(filters: Filter[]): SqlExpression {
+function segmentFiltersToExpression(filters: TableFilters): SqlExpression {
return SqlExpression.and(
- ...filterMap(filters, filter => {
- if (filter.id === 'start' || filter.id === 'end') {
+ ...filterMap(filters.toArray(), filter => {
+ if (filter.key === 'start' || filter.key === 'end') {
// Dates need to be converted to ISO string for the SQL query
- const modeAndNeedle = parseFilterModeAndNeedle(filter);
- if (!modeAndNeedle) return;
- if (modeAndNeedle.mode === '~') {
- return sqlQueryCustomTableFilter(filter);
+ if (filter.mode === '~') {
+ return filter.toSqlExpression();
}
try {
- const internalFilter = { ...filter };
- const formattedDate = formatDate(modeAndNeedle.needle);
+ const formattedDate = formatDate(filter.value);
const filterDate = dayjs(formattedDate).toISOString();
- filter.value = combineModeAndNeedle(modeAndNeedle.mode,
formattedDate);
- internalFilter.value = combineModeAndNeedle(modeAndNeedle.mode,
filterDate);
- return sqlQueryCustomTableFilter(internalFilter);
+ const internalFilter = new TableFilter(filter.key, filter.mode,
filterDate);
+ return internalFilter.toSqlExpression();
} catch {
- return sqlQueryCustomTableFilter(filter);
+ return filter.toSqlExpression();
}
}
- if (filter.id === 'shard_type') {
+ if (filter.key === 'shard_type') {
// Special handling for shard_type that needs to be searched for in
the shard_spec
// Creates filters like `shard_spec LIKE '%"type":"numbered"%'`
- const modeAndNeedle = parseFilterModeAndNeedle(filter);
- if (!modeAndNeedle) return;
const shardSpecColumn = C('shard_spec');
- switch (modeAndNeedle.mode) {
+ switch (filter.mode) {
case '=':
- return SqlComparison.like(shardSpecColumn,
`%"type":"${modeAndNeedle.needle}"%`);
+ return SqlComparison.like(shardSpecColumn,
`%"type":"${filter.value}"%`);
case '!=':
- return SqlComparison.notLike(shardSpecColumn,
`%"type":"${modeAndNeedle.needle}"%`);
+ return SqlComparison.notLike(shardSpecColumn,
`%"type":"${filter.value}"%`);
default:
- return SqlComparison.like(shardSpecColumn,
`%"type":"${modeAndNeedle.needle}%`);
+ return SqlComparison.like(shardSpecColumn,
`%"type":"${filter.value}%`);
}
- } else if (filter.id.startsWith('is_')) {
- switch (filter.value) {
- case '=false':
- return C(filter.id).equal(0);
-
- case '=true':
- return C(filter.id).equal(1);
-
- default:
- return;
+ } else if (filter.key.startsWith('is_')) {
+ if (filter.mode === '=' && filter.value === 'false') {
+ return C(filter.key).equal(0);
+ } else if (filter.mode === '=' && filter.value === 'true') {
+ return C(filter.key).equal(1);
+ } else {
+ return;
}
} else {
- return sqlQueryCustomTableFilter(filter);
+ return filter.toSqlExpression();
}
}),
);
@@ -246,8 +234,8 @@ interface SegmentsWithAuxiliaryInfo {
}
export interface SegmentsViewProps {
- filters: Filter[];
- onFiltersChange(filters: Filter[]): void;
+ filters: TableFilters;
+ onFiltersChange(filters: TableFilters): void;
goToQuery(queryWithContext: QueryWithContext): void;
capabilities: Capabilities;
}
@@ -426,13 +414,11 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
});
} else if (capabilities.hasCoordinatorAccess()) {
let datasourceList: string[] = [];
- const datasourceFilter = filtered.find(({ id }) => id ===
'datasource');
+ const datasourceFilter = filtered.toArray().find(({ key }) => key
=== 'datasource');
if (datasourceFilter) {
datasourceList = (
await getApiArray('/druid/coordinator/v1/metadata/datasources',
signal)
- ).filter((datasource: string) =>
- booleanCustomTableFilter(datasourceFilter, datasource),
- );
+ ).filter((datasource: string) =>
datasourceFilter.matches(datasource));
}
let results = (
@@ -466,13 +452,10 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
};
});
- if (filtered.length) {
+ if (filtered.toArray().length) {
results = results.filter((d: SegmentQueryResultRow) => {
- return filtered.every(filter => {
- return booleanCustomTableFilter(
- filter,
- d[filter.id as keyof SegmentQueryResultRow],
- );
+ return filtered.toArray().every(filter => {
+ return filter.matches(d[filter.key as keyof
SegmentQueryResultRow]);
});
});
}
@@ -552,7 +535,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
});
};
- private readonly handleFilterChange = (filters: Filter[]) => {
+ private readonly handleFilterChange = (filters: TableFilters) => {
this.goToFirstPage();
this.props.onFiltersChange(filters);
};
@@ -641,9 +624,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
// Only allow filtering of columns other than datasource if in SQL mode,
or if we are filtering on an exact datasource
const allowGeneralFilter =
hasSql ||
- filters.some(
- filter => filter.id === 'datasource' &&
parseFilterModeAndNeedle(filter)?.mode === '=',
- );
+ filters.toArray().some(filter => filter.key === 'datasource' &&
filter.mode === '=');
return (
<ReactTable
@@ -652,13 +633,13 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
loading={segmentsState.loading}
noDataText={
segmentsState.isEmpty()
- ? `No segments${filters.length ? ' matching filter' : ''}`
+ ? `No segments${filters.toArray().length ? ' matching filter' :
''}`
: segmentsState.getErrorMessage() || ''
}
manual
filterable
- filtered={filters}
- onFilteredChange={this.handleFilterChange}
+ filtered={filters.toFilters()}
+ onFilteredChange={filters =>
this.handleFilterChange(TableFilters.fromFilters(filters))}
sorted={sorted}
onSortedChange={sorted => this.setState({ sorted })}
page={page}
@@ -1144,9 +1125,9 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
? undefined
: {
capabilities,
- datasource: findMap(filters, filter =>
- filter.id === 'datasource' &&
/^=[^=|]+$/.exec(String(filter.value))
- ? filter.value.slice(1)
+ datasource: findMap(filters.toArray(), filter =>
+ filter.key === 'datasource' && filter.mode === '='
+ ? filter.value
: undefined,
),
},
@@ -1187,18 +1168,30 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
text="Apply fitler to table"
small
rightIcon={IconNames.ARROW_DOWN}
- onClick={() =>
- this.handleFilterChange(
- 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,
- ]),
- )
- }
+ onClick={() => {
+ let filters = TableFilters.empty();
+ if (start) {
+ filters = filters.addOrUpdate(
+ new TableFilter('start', '>=', start.toISOString()),
+ );
+ }
+ if (end) {
+ filters = filters.addOrUpdate(
+ new TableFilter('end', '<', end.toISOString()),
+ );
+ }
+ if (datasource) {
+ filters = filters.addOrUpdate(
+ new TableFilter('datasource', '=', datasource),
+ );
+ }
+ if (typeof realtime === 'boolean') {
+ filters = filters.addOrUpdate(
+ new TableFilter('is_realtime', '=',
String(realtime)),
+ );
+ }
+ this.handleFilterChange(filters);
+ }}
/>
);
}}
diff --git a/web-console/src/views/services-view/services-view.spec.tsx
b/web-console/src/views/services-view/services-view.spec.tsx
index 35a147e6a2e..c05ab10e358 100644
--- a/web-console/src/views/services-view/services-view.spec.tsx
+++ b/web-console/src/views/services-view/services-view.spec.tsx
@@ -19,6 +19,7 @@
import { Capabilities } from '../../helpers';
import { QueryState } from '../../utils';
import { shallow } from '../../utils/shallow-renderer';
+import { TableFilters } from '../../utils/table-filters';
import { ServicesView } from './services-view';
@@ -88,7 +89,7 @@ describe('ServicesView', () => {
it('renders data', () => {
const comp = (
<ServicesView
- filters={[]}
+ filters={TableFilters.empty()}
onFiltersChange={() => {}}
goToQuery={() => {}}
capabilities={Capabilities.FULL}
diff --git a/web-console/src/views/services-view/services-view.tsx
b/web-console/src/views/services-view/services-view.tsx
index f0d97f35612..7eec92ef5e0 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -41,10 +41,7 @@ import type { QueryWithContext } from '../../druid-models';
import { getConsoleViewIcon } from '../../druid-models';
import type { Capabilities, CapabilitiesMode } from '../../helpers';
import {
- booleanCustomTableFilter,
- combineModeAndNeedle,
DEFAULT_TABLE_CLASS_NAME,
- parseFilterModeAndNeedle,
STANDARD_TABLE_PAGE_SIZE,
STANDARD_TABLE_PAGE_SIZE_OPTIONS,
suggestibleFilterInput,
@@ -72,6 +69,7 @@ import {
ResultWithAuxiliaryWork,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { TableFilter, TableFilters } from '../../utils/table-filters';
import { FillIndicator } from './fill-indicator/fill-indicator';
@@ -123,8 +121,8 @@ interface ServicesQuery {
}
export interface ServicesViewProps {
- filters: Filter[];
- onFiltersChange(filters: Filter[]): void;
+ filters: TableFilters;
+ onFiltersChange(filters: TableFilters): void;
goToQuery(queryWithContext: QueryWithContext): void;
capabilities: Capabilities;
}
@@ -438,9 +436,9 @@ ORDER BY
servicesState.isEmpty() ? 'No services' :
servicesState.getErrorMessage() || ''
}
filterable
- filtered={filters}
+ filtered={filters.toFilters()}
className={`centered-table ${DEFAULT_TABLE_CLASS_NAME}`}
- onFilteredChange={onFiltersChange}
+ onFilteredChange={filters =>
onFiltersChange(TableFilters.fromFilters(filters))}
pivotBy={groupServicesBy ? [groupServicesBy] : []}
defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
@@ -454,8 +452,8 @@ ORDER BY
private readonly getTableColumns = memoize(
(
visibleColumns: LocalStorageBackedVisibility,
- _filters: Filter[],
- _onFiltersChange: (filters: Filter[]) => void,
+ _filters: TableFilters,
+ _onFiltersChange: (filters: TableFilters) => void,
workerInfoLookup: Record<string, WorkerInfo>,
): Column<ServiceResultRow>[] => {
const { capabilities } = this.props;
@@ -643,15 +641,18 @@ ORDER BY
Cell: this.renderFilterableCell('start_time', formatDate),
Aggregated: () => '',
filterMethod: (filter: Filter, row: ServiceResultRow) => {
- const modeAndNeedle = parseFilterModeAndNeedle(filter);
- if (!modeAndNeedle) return true;
+ const tableFilter = TableFilter.fromFilter(filter);
const parsedRowTime = formatDate(row.start_time);
- if (modeAndNeedle.mode === '~') {
- return booleanCustomTableFilter(filter, parsedRowTime);
+ if (tableFilter.mode === '~') {
+ return tableFilter.matches(parsedRowTime);
}
- const parsedFilterTime = formatDate(modeAndNeedle.needle);
- filter.value = combineModeAndNeedle(modeAndNeedle.mode,
parsedFilterTime);
- return booleanCustomTableFilter(filter, parsedRowTime);
+ const parsedFilterTime = formatDate(tableFilter.value);
+ const updatedFilter = new TableFilter(
+ tableFilter.key,
+ tableFilter.mode,
+ parsedFilterTime,
+ );
+ return updatedFilter.matches(parsedRowTime);
},
},
{
diff --git
a/web-console/src/views/sql-data-loader-view/ingestion-progress-dialog/ingestion-progress-dialog.tsx
b/web-console/src/views/sql-data-loader-view/ingestion-progress-dialog/ingestion-progress-dialog.tsx
index d383a5d3459..6167af06433 100644
---
a/web-console/src/views/sql-data-loader-view/ingestion-progress-dialog/ingestion-progress-dialog.tsx
+++
b/web-console/src/views/sql-data-loader-view/ingestion-progress-dialog/ingestion-progress-dialog.tsx
@@ -22,10 +22,11 @@ import classNames from 'classnames';
import { T } from 'druid-query-toolkit';
import React, { useState } from 'react';
-import type { Execution, QueryWithContext } from '../../../druid-models';
+import type { ConsoleViewId, Execution, QueryWithContext } from
'../../../druid-models';
import { getConsoleViewIcon } from '../../../druid-models';
import { executionBackgroundStatusCheck, reattachTaskExecution } from
'../../../helpers';
import { useQueryManager } from '../../../hooks';
+import { TableFilters } from '../../../utils/table-filters';
import { ExecutionProgressBarPane } from
'../../workbench-view/execution-progress-bar-pane/execution-progress-bar-pane';
import { ExecutionStagesPane } from
'../../workbench-view/execution-stages-pane/execution-stages-pane';
@@ -34,8 +35,7 @@ import './ingestion-progress-dialog.scss';
interface IngestionProgressDialogProps {
taskId: string;
goToQuery(queryWithContext: QueryWithContext): void;
- goToTask(taskGroupId: string): void;
- goToTaskGroup(taskGroupId: string): void;
+ goToView(tab: ConsoleViewId, filters?: TableFilters): void;
onReset(): void;
onClose(): void;
}
@@ -43,7 +43,7 @@ interface IngestionProgressDialogProps {
export const IngestionProgressDialog = React.memo(function
IngestionProgressDialog(
props: IngestionProgressDialogProps,
) {
- const { taskId, goToQuery, goToTask, goToTaskGroup, onReset, onClose } =
props;
+ const { taskId, goToQuery, goToView, onReset, onClose } = props;
const [showLiveReports, setShowLiveReports] = useState(false);
const [insertResultState, ingestQueryManager] = useQueryManager<string,
Execution, Execution>({
@@ -80,7 +80,12 @@ export const IngestionProgressDialog = React.memo(function
IngestionProgressDial
showLiveReports={showLiveReports}
/>
{insertResultState.intermediate?.stages && showLiveReports && (
- <ExecutionStagesPane execution={insertResultState.intermediate}
goToTask={goToTask} />
+ <ExecutionStagesPane
+ execution={insertResultState.intermediate}
+ goToTask={(taskId: string) =>
+ goToView('tasks', TableFilters.eq({ task_id: taskId }))
+ }
+ />
)}
</>
)}
@@ -106,7 +111,10 @@ export const IngestionProgressDialog = React.memo(function
IngestionProgressDial
rightIcon={IconNames.ARROW_TOP_RIGHT}
onClick={() => {
if (!insertResultState.intermediate) return;
- goToTaskGroup(insertResultState.intermediate.id);
+ goToView(
+ 'tasks',
+ TableFilters.eq({ group_id:
insertResultState.intermediate.id }),
+ );
}}
/>
</>
diff --git
a/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
b/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
index d692c229441..2ce732cd5ba 100644
--- a/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
+++ b/web-console/src/views/sql-data-loader-view/sql-data-loader-view.tsx
@@ -25,6 +25,7 @@ import React, { useState } from 'react';
import type {
CapacityInfo,
+ ConsoleViewId,
ExternalConfig,
QueryContext,
QueryWithContext,
@@ -40,6 +41,7 @@ import { submitTaskQuery } from '../../helpers';
import { useLocalStorageState } from '../../hooks';
import { AppToaster } from '../../singletons';
import { deepDelete, LocalStorageKeys } from '../../utils';
+import type { TableFilters } from '../../utils/table-filters';
import { CapacityAlert } from
'../workbench-view/capacity-alert/capacity-alert';
import { InputFormatStep } from
'../workbench-view/input-format-step/input-format-step';
import { InputSourceStep } from
'../workbench-view/input-source-step/input-source-step';
@@ -58,8 +60,7 @@ interface LoaderContent extends QueryWithContext {
export interface SqlDataLoaderViewProps {
capabilities: Capabilities;
goToQuery(queryWithContext: QueryWithContext): void;
- goToTask(taskId: string): void;
- goToTaskGroup(taskGroupId: string): void;
+ goToView(tab: ConsoleViewId, filters?: TableFilters): void;
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
serverQueryContext?: QueryContext;
}
@@ -70,8 +71,7 @@ export const SqlDataLoaderView = React.memo(function
SqlDataLoaderView(
const {
capabilities,
goToQuery,
- goToTask,
- goToTaskGroup,
+ goToView,
getClusterCapacity,
serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
} = props;
@@ -246,8 +246,7 @@ export const SqlDataLoaderView = React.memo(function
SqlDataLoaderView(
<IngestionProgressDialog
taskId={content.id}
goToQuery={goToQuery}
- goToTask={goToTask}
- goToTaskGroup={goToTaskGroup}
+ goToView={goToView}
onReset={() => {
setExternalConfigStep({});
setContent(undefined);
diff --git a/web-console/src/views/supervisors-view/supervisors-view.spec.tsx
b/web-console/src/views/supervisors-view/supervisors-view.spec.tsx
index 66c518b9100..9a03fca45b8 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.spec.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.spec.tsx
@@ -18,6 +18,7 @@
import { Capabilities } from '../../helpers';
import { shallow } from '../../utils/shallow-renderer';
+import { TableFilters } from '../../utils/table-filters';
import { SupervisorsView } from './supervisors-view';
@@ -25,13 +26,12 @@ describe('SupervisorsView', () => {
it('matches snapshot', () => {
const taskView = shallow(
<SupervisorsView
- filters={[]}
+ filters={TableFilters.empty()}
onFiltersChange={() => {}}
openSupervisorDialog={undefined}
- goToDatasource={() => {}}
+ goToView={() => {}}
goToQuery={() => {}}
goToStreamingDataLoader={() => {}}
- goToTasks={() => {}}
capabilities={Capabilities.FULL}
/>,
);
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx
b/web-console/src/views/supervisors-view/supervisors-view.tsx
index 3da20ee8ec3..a56ac816c38 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -22,7 +22,7 @@ import * as JSONBig from 'json-bigint-native';
import memoize from 'memoize-one';
import type { JSX } from 'react';
import React, { createContext, useContext } from 'react';
-import type { Column, Filter, SortingRule } from 'react-table';
+import type { Column, SortingRule } from 'react-table';
import ReactTable from 'react-table';
import type { TableColumnSelectorColumn } from '../../components';
@@ -47,6 +47,7 @@ import {
TaskGroupHandoffDialog,
} from '../../dialogs';
import type {
+ ConsoleViewId,
IngestionSpec,
QueryWithContext,
RowStatsKey,
@@ -59,7 +60,6 @@ import type { Capabilities } from '../../helpers';
import {
SMALL_TABLE_PAGE_SIZE,
SMALL_TABLE_PAGE_SIZE_OPTIONS,
- sqlQueryCustomTableFilters,
suggestibleFilterInput,
} from '../../react-table';
import { Api, AppToaster } from '../../singletons';
@@ -90,6 +90,7 @@ import {
twoLines,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { TableFilters } from '../../utils/table-filters';
import './supervisors-view.scss';
@@ -176,13 +177,12 @@ function HeaderStatsKeySelector({ changeStatsKey }:
HeaderStatsKeySelectorProps)
}
export interface SupervisorsViewProps {
- filters: Filter[];
- onFiltersChange(filters: Filter[]): void;
+ filters: TableFilters;
+ onFiltersChange(filters: TableFilters): void;
openSupervisorDialog: boolean | undefined;
- goToDatasource(datasource: string): void;
+ goToView(tab: ConsoleViewId, filters?: TableFilters): void;
goToQuery(queryWithContext: QueryWithContext): void;
goToStreamingDataLoader(supervisorId: string): void;
- goToTasks(supervisorId: string, type: string | undefined): void;
capabilities: Capabilities;
}
@@ -288,7 +288,7 @@ export class SupervisorsView extends React.PureComponent<
let count = -1;
const auxiliaryQueries:
AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo>[] = [];
if (capabilities.hasSql()) {
- const whereExpression = sqlQueryCustomTableFilters(filtered);
+ const whereExpression = filtered.toSqlExpression();
let filterClause = '';
if (whereExpression.toString() !== 'TRUE') {
@@ -462,7 +462,7 @@ export class SupervisorsView extends React.PureComponent<
const { filters } = this.props;
const { page, pageSize, sorted } = this.state;
if (
-
!sqlQueryCustomTableFilters(filters).equals(sqlQueryCustomTableFilters(prevProps.filters))
||
+ !filters.toSqlExpression().equals(prevProps.filters.toSqlExpression()) ||
page !== prevState.page ||
pageSize !== prevState.pageSize ||
sortedToOrderByClause(sorted) !== sortedToOrderByClause(prevState.sorted)
@@ -484,7 +484,7 @@ export class SupervisorsView extends React.PureComponent<
});
};
- private readonly handleFilterChange = (filters: Filter[]) => {
+ private readonly handleFilterChange = (filters: TableFilters) => {
this.goToFirstPage();
this.props.onFiltersChange(filters);
};
@@ -521,7 +521,7 @@ export class SupervisorsView extends React.PureComponent<
private getSupervisorActions(supervisor: SupervisorQueryResultRow):
BasicAction[] {
const { supervisor_id, datasource, suspended, type } = supervisor;
- const { goToDatasource, goToStreamingDataLoader } = this.props;
+ const { goToView, goToStreamingDataLoader } = this.props;
const actions: BasicAction[] = [];
if (oneOf(type, 'kafka', 'kinesis')) {
@@ -529,7 +529,7 @@ export class SupervisorsView extends React.PureComponent<
{
icon: IconNames.MULTI_SELECT,
title: 'Go to datasource',
- onAction: () => goToDatasource(datasource),
+ onAction: () => goToView('datasources', TableFilters.eq({ datasource
})),
},
{
icon: IconNames.CLOUD_UPLOAD,
@@ -778,29 +778,43 @@ export class SupervisorsView extends React.PureComponent<
}
private goToTasksForSupervisor(supervisor: SupervisorQueryResultRow) {
- const { goToTasks } = this.props;
+ const { goToView } = this.props;
switch (supervisor.type) {
case 'kafka':
case 'kinesis':
- goToTasks(
- `index_${supervisor.type}_${supervisor.supervisor_id}`,
- `index_${supervisor.type}`,
+ goToView(
+ 'tasks',
+ TableFilters.eq({
+ group_id: `index_${supervisor.type}_${supervisor.supervisor_id}`,
+ type: `index_${supervisor.type}`,
+ }),
);
return;
case 'autocompact':
- goToTasks(supervisor.supervisor_id.replace(/^autocompact__/, ''),
'compact');
+ goToView(
+ 'tasks',
+ TableFilters.eq({
+ datasource: supervisor.supervisor_id.replace(/^autocompact__/, ''),
+ type: 'compact',
+ }),
+ );
return;
case 'scheduled_batch':
- goToTasks(
- supervisor.supervisor_id.replace(/^scheduled_batch__/,
'').replace(/__[0-9a-f-]+$/, ''),
- 'query_controller',
+ goToView(
+ 'tasks',
+ TableFilters.eq({
+ datasource: supervisor.supervisor_id
+ .replace(/^scheduled_batch__/, '')
+ .replace(/__[0-9a-f-]+$/, ''),
+ type: 'query_controller',
+ }),
);
return;
default:
- goToTasks(supervisor.supervisor_id, undefined);
+ goToView('tasks', TableFilters.eq({ group_id: supervisor.supervisor_id
}));
return;
}
}
@@ -829,8 +843,8 @@ export class SupervisorsView extends React.PureComponent<
}
manual
filterable
- filtered={filters}
- onFilteredChange={this.handleFilterChange}
+ filtered={filters.toFilters()}
+ onFilteredChange={filters =>
this.handleFilterChange(TableFilters.fromFilters(filters))}
sorted={sorted}
onSortedChange={sorted => this.setState({ sorted })}
page={page}
@@ -851,7 +865,7 @@ export class SupervisorsView extends React.PureComponent<
private readonly getTableColumns = memoize(
(
visibleColumns: LocalStorageBackedVisibility,
- filters: Filter[],
+ filters: TableFilters,
): Column<SupervisorQueryResultRow>[] => {
return [
{
diff --git
a/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
b/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
index 87744c0ac83..5ab6f043c73 100644
--- a/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -202,6 +202,7 @@ exports[`TasksView matches snapshot 1`] = `
"Header": "Created time",
"accessor": "created_time",
"filterMethod": [Function],
+ "headerClassName": "enable-comparisons",
"show": true,
"width": 220,
},
diff --git a/web-console/src/views/tasks-view/tasks-view.spec.tsx
b/web-console/src/views/tasks-view/tasks-view.spec.tsx
index 88759d9e76c..84d93e2078e 100644
--- a/web-console/src/views/tasks-view/tasks-view.spec.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.spec.tsx
@@ -18,6 +18,7 @@
import { Capabilities } from '../../helpers';
import { shallow } from '../../utils/shallow-renderer';
+import { TableFilters } from '../../utils/table-filters';
import { TasksView } from './tasks-view';
@@ -25,10 +26,10 @@ describe('TasksView', () => {
it('matches snapshot', () => {
const taskView = shallow(
<TasksView
- filters={[]}
+ filters={TableFilters.empty()}
onFiltersChange={() => {}}
openTaskDialog={undefined}
- goToDatasource={() => {}}
+ goToView={() => {}}
goToQuery={() => {}}
goToClassicBatchDataLoader={() => {}}
capabilities={Capabilities.FULL}
diff --git a/web-console/src/views/tasks-view/tasks-view.tsx
b/web-console/src/views/tasks-view/tasks-view.tsx
index 543a746d51e..23a8d2d52f0 100644
--- a/web-console/src/views/tasks-view/tasks-view.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.tsx
@@ -37,7 +37,7 @@ import {
ViewControlBar,
} from '../../components';
import { AlertDialog, AsyncActionDialog, SpecDialog, TaskTableActionDialog }
from '../../dialogs';
-import type { QueryWithContext } from '../../druid-models';
+import type { ConsoleViewId, QueryWithContext } from '../../druid-models';
import {
getConsoleViewIcon,
TASK_CANCELED_ERROR_MESSAGES,
@@ -45,9 +45,6 @@ import {
} from '../../druid-models';
import type { Capabilities } from '../../helpers';
import {
- booleanCustomTableFilter,
- combineModeAndNeedle,
- parseFilterModeAndNeedle,
SMALL_TABLE_PAGE_SIZE,
SMALL_TABLE_PAGE_SIZE_OPTIONS,
suggestibleFilterInput,
@@ -68,6 +65,7 @@ import {
QueryState,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { TableFilter, TableFilters } from '../../utils/table-filters';
import { ExecutionDetailsDialog } from
'../workbench-view/execution-details-dialog/execution-details-dialog';
import './tasks-view.scss';
@@ -99,10 +97,10 @@ interface TaskQueryResultRow {
}
export interface TasksViewProps {
- filters: Filter[];
- onFiltersChange(filters: Filter[]): void;
+ filters: TableFilters;
+ onFiltersChange(filters: TableFilters): void;
openTaskDialog: boolean | undefined;
- goToDatasource(datasource: string): void;
+ goToView(tab: ConsoleViewId, filters?: TableFilters): void;
goToQuery(queryWithContext: QueryWithContext): void;
goToClassicBatchDataLoader(taskId?: string): void;
capabilities: Capabilities;
@@ -260,7 +258,7 @@ ORDER BY
type: string,
fromTable?: boolean,
): BasicAction[] {
- const { goToDatasource, goToClassicBatchDataLoader } = this.props;
+ const { goToView, goToClassicBatchDataLoader } = this.props;
const actions: BasicAction[] = [];
if (fromTable) {
@@ -282,7 +280,7 @@ ORDER BY
actions.push({
icon: IconNames.MULTI_SELECT,
title: 'Go to datasource',
- onAction: () => goToDatasource(datasource),
+ onAction: () => goToView('datasources', TableFilters.eq({ datasource
})),
});
}
if (oneOf(type, 'index', 'index_parallel')) {
@@ -385,8 +383,8 @@ ORDER BY
loading={tasksState.loading}
noDataText={tasksState.isEmpty() ? 'No tasks' :
tasksState.getErrorMessage() || ''}
filterable
- filtered={filters}
- onFilteredChange={onFiltersChange}
+ filtered={filters.toFilters()}
+ onFilteredChange={filters =>
onFiltersChange(TableFilters.fromFilters(filters))}
defaultSorted={[{ id: 'status', desc: true }]}
pivotBy={groupTasksBy ? [groupTasksBy] : []}
defaultPageSize={SMALL_TABLE_PAGE_SIZE}
@@ -504,6 +502,7 @@ ORDER BY
{
Header: 'Created time',
accessor: 'created_time',
+ headerClassName: 'enable-comparisons',
width: 220,
Cell: this.renderTaskFilterableCell(
'created_time',
@@ -521,15 +520,18 @@ ORDER BY
Aggregated: () => '',
show: visibleColumns.shown('Created time'),
filterMethod: (filter: Filter, row: TaskQueryResultRow) => {
- const modeAndNeedle = parseFilterModeAndNeedle(filter);
- if (!modeAndNeedle) return true;
+ const tableFilter = TableFilter.fromFilter(filter);
const parsedRowDate = formatDate(row.created_time);
- if (modeAndNeedle.mode === '~') {
- return booleanCustomTableFilter(filter, parsedRowDate);
+ if (tableFilter.mode === '~') {
+ return tableFilter.matches(parsedRowDate);
}
- const parsedFilterDate = formatDate(modeAndNeedle.needle);
- filter.value = combineModeAndNeedle(modeAndNeedle.mode,
parsedFilterDate);
- return booleanCustomTableFilter(filter, parsedRowDate);
+ const parsedFilterDate = formatDate(tableFilter.value);
+ const updatedFilter = new TableFilter(
+ tableFilter.key,
+ tableFilter.mode,
+ parsedFilterDate,
+ );
+ return updatedFilter.matches(parsedRowDate);
},
},
{
@@ -714,7 +716,7 @@ ORDER BY
<ExecutionDetailsDialog
id={executionDialogOpen}
goToTask={taskId => {
- onFiltersChange([{ id: 'task_id', value: `=${taskId}` }]);
+ onFiltersChange(TableFilters.eq({ task_id: taskId }));
this.setState({ executionDialogOpen: undefined });
}}
onClose={() => this.setState({ executionDialogOpen: undefined })}
diff --git
a/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap
b/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap
index 331e8235998..0faac636e38 100644
---
a/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap
+++
b/web-console/src/views/workbench-view/execution-stages-pane/__snapshots__/execution-stages-pane.spec.tsx.snap
@@ -150,6 +150,7 @@ exports[`ExecutionStagesPane matches snapshot 1`] = `
"width": 240,
},
{
+ "Cell": [Function],
"Header": <React.Fragment>
Num
<br />
diff --git
a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
index e4717c6d691..8dfa73f1c29 100644
---
a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
+++
b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
@@ -917,6 +917,22 @@ ${title} uncompressed size: ${formatBytesCompact(
accessor: 'workerCount',
className: 'padded',
width: 75,
+ Cell({ value, original }) {
+ const inactiveWorkers = stages.getInactiveWorkerCount(original);
+ if (inactiveWorkers) {
+ return (
+ <div>
+ <div>{formatInteger(value)}</div>
+ <div
+ className="detail-line"
+ data-tooltip="Workers are counted as inactive until they
reprot starting to read rows from their input."
+ >{`${formatInteger(inactiveWorkers)} inactive`}</div>
+ </div>
+ );
+ } else {
+ return formatInteger(value);
+ }
+ },
},
{
Header: 'Output',
diff --git a/web-console/src/views/workbench-view/workbench-view.tsx
b/web-console/src/views/workbench-view/workbench-view.tsx
index 0f4f02edfc1..fe7bf1c6a71 100644
--- a/web-console/src/views/workbench-view/workbench-view.tsx
+++ b/web-console/src/views/workbench-view/workbench-view.tsx
@@ -36,6 +36,7 @@ import { MenuCheckbox, SplitterLayout } from
'../../components';
import { SpecDialog, StringInputDialog } from '../../dialogs';
import type {
CapacityInfo,
+ ConsoleViewId,
DruidEngine,
Execution,
QueryContext,
@@ -67,6 +68,7 @@ import {
QueryManager,
QueryState,
} from '../../utils';
+import { TableFilters } from '../../utils/table-filters';
import { ColumnTree } from './column-tree/column-tree';
import { ConnectExternalDataDialog } from
'./connect-external-data-dialog/connect-external-data-dialog';
@@ -131,7 +133,7 @@ export interface WorkbenchViewProps
serverQueryContext?: QueryContext;
queryEngines: DruidEngine[];
hiddenMoreMenuItems?: MoreMenuItem[] | ((engine: DruidEngine) =>
MoreMenuItem[]);
- goToTask(taskId: string): void;
+ goToView(tab: ConsoleViewId, filters?: TableFilters): void;
getClusterCapacity: (() => Promise<CapacityInfo | undefined>) | undefined;
hideToolbar?: boolean;
maxTasksOptions?:
@@ -334,7 +336,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
}
private renderExecutionDetailsDialog() {
- const { goToTask } = this.props;
+ const { goToView } = this.props;
const { details } = this.state;
if (!details) return;
@@ -343,7 +345,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
id={details.id}
initTab={details.initTab}
initExecution={details.initExecution}
- goToTask={goToTask}
+ goToTask={(taskId: string) => goToView('tasks', TableFilters.eq({
task_id: taskId }))}
onClose={() => this.setState({ details: undefined })}
/>
);
@@ -734,7 +736,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
baseQueryContext,
serverQueryContext = DEFAULT_SERVER_QUERY_CONTEXT,
queryEngines,
- goToTask,
+ goToView,
getClusterCapacity,
maxTasksMenuHeader,
enginesLabelFn,
@@ -771,7 +773,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
onDetails={this.handleDetailsWithExecution}
queryEngines={queryEngines}
clusterCapacity={capabilities.getMaxTaskSlots()}
- goToTask={goToTask}
+ goToTask={(taskId: string) => goToView('tasks', TableFilters.eq({
task_id: taskId }))}
getClusterCapacity={getClusterCapacity}
maxTasksMenuHeader={maxTasksMenuHeader}
enginesLabelFn={enginesLabelFn}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]