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 8232fb519d2 Web console: fix supervisor actions menu not working well
with lots of incremental loading (#18023)
8232fb519d2 is described below
commit 8232fb519d22da583fc4a0b95c17988d6ee9b583
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri May 30 16:49:58 2025 +0100
Web console: fix supervisor actions menu not working well with lots of
incremental loading (#18023)
* fix supervisor action menu
* refactor service view to get load queues though context
* always load segment_id
* add 'Go to segments' action
* work correctly with slashes in path
* cleanup
* fix tests
* jump to first page when filter changes
* fix same issue in segments view
* more paginator display flexibility
* show a count in supervisors view
* show a count in segments view
* cleanup
---
web-console/package.json | 1 +
.../table-filterable-cell.tsx | 13 +-
web-console/src/console-application.tsx | 2 +-
.../src/dialogs/status-dialog/status-dialog.tsx | 4 +-
.../src/react-table/react-table-filters.spec.ts | 8 +-
web-console/src/react-table/react-table-filters.ts | 13 +-
.../react-table-pagination.tsx | 4 +-
.../views/datasources-view/datasources-view.tsx | 10 +-
.../src/views/lookups-view/lookups-view.tsx | 4 +-
.../__snapshots__/segments-view.spec.tsx.snap | 2 +-
.../src/views/segments-view/segments-view.tsx | 84 ++-
.../__snapshots__/services-view.spec.tsx.snap | 544 +++++++--------
.../fill-indicator.scss} | 52 +-
.../fill-indicator.tsx} | 55 +-
.../src/views/services-view/services-view.scss | 24 -
.../src/views/services-view/services-view.spec.tsx | 66 +-
.../src/views/services-view/services-view.tsx | 635 +++++++++---------
.../__snapshots__/supervisors-view.spec.tsx.snap | 575 ++++++++--------
.../views/supervisors-view/supervisors-view.tsx | 728 ++++++++++++---------
19 files changed, 1434 insertions(+), 1390 deletions(-)
diff --git a/web-console/package.json b/web-console/package.json
index ca9c78c0538..7a30c853ec9 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -21,6 +21,7 @@
"coverage": "jest --coverage src",
"update-snapshots": "jest -u --config jest.config.js",
"autofix": "npm run eslint-fix && npm run sasslint-fix && npm run
prettify",
+ "typecheck": "tsc --noEmit",
"eslint": "eslint",
"eslint-fix": "npm run eslint -- --fix",
"eslint-changed-only": "git diff --diff-filter=ACMR --cached --name-only |
grep -E \\.tsx\\?$ | xargs ./node_modules/.bin/eslint",
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 c03f0038cc7..80d9cc83d59 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
@@ -22,7 +22,7 @@ import React from 'react';
import type { Filter } from 'react-table';
import type { FilterMode } from '../../react-table';
-import { addFilter, filterModeToIcon } from '../../react-table';
+import { addOrUpdateFilter, combineModeAndNeedle, filterModeToIcon } from
'../../react-table';
import { Deferred } from '../deferred/deferred';
import './table-filterable-cell.scss';
@@ -57,7 +57,14 @@ export const TableFilterableCell = React.memo(function
TableFilterableCell(
key={i}
icon={filterModeToIcon(mode)}
text={value}
- onClick={() => onFiltersChange(addFilter(filters, field,
mode, value))}
+ onClick={() =>
+ onFiltersChange(
+ addOrUpdateFilter(filters, {
+ id: field,
+ value: combineModeAndNeedle(mode, value),
+ }),
+ )
+ }
/>
))}
</Menu>
@@ -65,7 +72,7 @@ export const TableFilterableCell = React.memo(function
TableFilterableCell(
/>
}
>
- {children}
+ {children ?? value}
</Popover>
);
});
diff --git a/web-console/src/console-application.tsx
b/web-console/src/console-application.tsx
index 5dfbba77101..dc858cd3111 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -61,7 +61,7 @@ function viewFilterChange(tab: HeaderActiveTab) {
}
function pathWithFilter(tab: HeaderActiveTab) {
- return [`/${tab}/:filters`, `/${tab}`];
+ return `/${tab}/:filters?`;
}
function switchTab(tab: HeaderActiveTab) {
diff --git a/web-console/src/dialogs/status-dialog/status-dialog.tsx
b/web-console/src/dialogs/status-dialog/status-dialog.tsx
index e87eb64a58c..e07a3c2e325 100644
--- a/web-console/src/dialogs/status-dialog/status-dialog.tsx
+++ b/web-console/src/dialogs/status-dialog/status-dialog.tsx
@@ -72,9 +72,7 @@ export const StatusDialog = React.memo(function
StatusDialog(props: StatusDialog
value={row.value}
filters={moduleFilter}
onFiltersChange={setModuleFilter}
- >
- {row.value}
- </TableFilterableCell>
+ />
);
};
};
diff --git a/web-console/src/react-table/react-table-filters.spec.ts
b/web-console/src/react-table/react-table-filters.spec.ts
index 03737825cbd..c0a5476fcde 100644
--- a/web-console/src/react-table/react-table-filters.spec.ts
+++ b/web-console/src/react-table/react-table-filters.spec.ts
@@ -62,18 +62,18 @@ describe('react-table-utils', () => {
expect(
tableFiltersToString([
{ id: 'x', value: '~y' },
- { id: 'z', value: '=w&' },
+ { id: 'z', value: '=w&%/' },
]),
- ).toEqual('x~y&z=w%26');
+ ).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')).toEqual([
+ expect(stringToTableFilters('x~y&z=w%26%25%2F')).toEqual([
{ id: 'x', value: '~y' },
- { id: 'z', value: '=w&' },
+ { id: 'z', value: '=w&%/' },
]);
expect(stringToTableFilters('x<3&y<=3')).toEqual([
{ id: 'x', value: '<3' },
diff --git a/web-console/src/react-table/react-table-filters.ts
b/web-console/src/react-table/react-table-filters.ts
index bb19dddae2d..3d446a210e7 100644
--- a/web-console/src/react-table/react-table-filters.ts
+++ b/web-console/src/react-table/react-table-filters.ts
@@ -84,15 +84,6 @@ interface FilterModeAndNeedle {
needleParts: string[];
}
-export function addFilter(
- filters: readonly Filter[],
- id: string,
- mode: FilterMode,
- needle: string,
-): Filter[] {
- return addOrUpdateFilter(filters, { id, value: combineModeAndNeedle(mode,
needle) });
-}
-
export function parseFilterModeAndNeedle(
filter: Filter,
loose = false,
@@ -187,7 +178,7 @@ export function sqlQueryCustomTableFilters(filters:
Filter[]): SqlExpression {
export function tableFiltersToString(tableFilters: Filter[]): string {
return tableFilters
- .map(({ id, value }) => `${id}${value.replace(/[&%]/g,
encodeURIComponent)}`)
+ .map(({ id, value }) => `${id}${value.replace(/[&%/]/g,
encodeURIComponent)}`)
.join('&');
}
@@ -196,7 +187,7 @@ export function stringToTableFilters(str: string |
undefined): Filter[] {
// '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
return filterMap(str.split('&'), clause => {
const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec(
- clause.replace(/%2[56]/g, decodeURIComponent),
+ 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-pagination/react-table-pagination.tsx
b/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx
index 634747dcd8b..2a21b0d61f1 100644
---
a/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx
+++
b/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx
@@ -109,8 +109,10 @@ export const ReactTablePagination = React.memo(function
ReactTablePagination(
} else {
pageInfo += '...';
}
- if (ofText && nonEmptyArray(sortedData)) {
+ if (ofText === 'of' && nonEmptyArray(sortedData)) {
pageInfo += ` ${ofText} ${formatInteger(sortedData.length)}`;
+ } else if (ofText) {
+ pageInfo += ` ${ofText}`;
}
const pageJumpMenuItem = renderPageJumpMenuItem();
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index 8ff4ec9f423..bf6ca1950d3 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -1004,7 +1004,7 @@ GROUP BY 1, 2`;
rules: Rule[] | undefined,
compactionInfo: CompactionInfo | undefined,
): BasicAction[] {
- const { goToQuery, capabilities } = this.props;
+ const { goToQuery, goToSegments, capabilities } = this.props;
const goToActions: BasicAction[] = [];
@@ -1016,6 +1016,14 @@ GROUP BY 1, 2`;
});
}
+ goToActions.push({
+ icon: IconNames.STACKED_CHART,
+ title: 'Go to segments',
+ onAction: () => {
+ goToSegments({ datasource });
+ },
+ });
+
if (!capabilities.hasCoordinatorAccess()) {
return goToActions;
}
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx
b/web-console/src/views/lookups-view/lookups-view.tsx
index 39b7a920f1c..384305744e0 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -339,9 +339,7 @@ export class LookupsView extends
React.PureComponent<LookupsViewProps, LookupsVi
value={row.value}
filters={filters}
onFiltersChange={onFiltersChange}
- >
- {row.value}
- </TableFilterableCell>
+ />
);
};
}
diff --git
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index e0b2847e2f7..0e6b64e844d 100755
---
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -442,7 +442,7 @@ exports[`SegmentsView matches snapshot 1`] = `
rowsText="rows"
showPageJump={false}
showPageSizeOptions={true}
- showPagination={false}
+ showPagination={true}
showPaginationBottom={true}
showPaginationTop={false}
sortable={true}
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index c60911a5e0c..4baebd6ffc2 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -56,9 +56,10 @@ import {
STANDARD_TABLE_PAGE_SIZE_OPTIONS,
} from '../../react-table';
import { Api } from '../../singletons';
-import type { NumberLike, TableState } from '../../utils';
+import type { AuxiliaryQueryFn, NumberLike, TableState } from '../../utils';
import {
applySorting,
+ assemble,
compact,
countBy,
filterMap,
@@ -74,6 +75,7 @@ import {
queryDruidSql,
QueryManager,
QueryState,
+ ResultWithAuxiliaryWork,
sortedToOrderByClause,
twoLines,
} from '../../utils';
@@ -217,6 +219,11 @@ interface SegmentQueryResultRow {
is_overshadowed: number;
}
+interface SegmentsWithAuxiliaryInfo {
+ readonly segments: SegmentQueryResultRow[];
+ readonly count: number;
+}
+
export interface SegmentsViewProps {
filters: Filter[];
onFiltersChange(filters: Filter[]): void;
@@ -225,7 +232,7 @@ export interface SegmentsViewProps {
}
export interface SegmentsViewState {
- segmentsState: QueryState<SegmentQueryResultRow[]>;
+ segmentsState: QueryState<SegmentsWithAuxiliaryInfo>;
segmentTableActionDialogId?: string;
datasourceTableActionDialogId?: string;
actions: BasicAction[];
@@ -245,7 +252,7 @@ export interface SegmentsViewState {
export class SegmentsView extends React.PureComponent<SegmentsViewProps,
SegmentsViewState> {
static baseQuery(visibleColumns: LocalStorageBackedVisibility) {
const columns = compact([
- visibleColumns.shown('Segment ID') && `"segment_id"`,
+ `"segment_id"`,
visibleColumns.shown('Datasource') && `"datasource"`,
`"start"`,
`"end"`,
@@ -268,7 +275,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
return `WITH s AS (SELECT\n${columns.join(',\n')}\nFROM sys.segments)`;
}
- private readonly segmentsQueryManager: QueryManager<SegmentsQuery,
SegmentQueryResultRow[]>;
+ private readonly segmentsQueryManager: QueryManager<SegmentsQuery,
SegmentsWithAuxiliaryInfo>;
constructor(props: SegmentsViewProps) {
super(props);
@@ -296,6 +303,10 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
const { page, pageSize, filtered, sorted, visibleColumns,
capabilities, groupByInterval } =
query;
+ let segments: SegmentQueryResultRow[];
+ let count = -1;
+ const auxiliaryQueries: AuxiliaryQueryFn<SegmentsWithAuxiliaryInfo>[]
= [];
+
if (capabilities.hasSql()) {
const whereExpression = segmentFiltersToExpression(filtered);
@@ -374,7 +385,27 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
}));
}
- return result as SegmentQueryResultRow[];
+ segments = result as SegmentQueryResultRow[];
+
+ auxiliaryQueries.push(async (segmentsWithAuxiliaryInfo, cancelToken)
=> {
+ const sqlQuery = assemble(
+ 'SELECT COUNT(*) AS "cnt"',
+ 'FROM "sys"."segments"',
+ filterClause ? `WHERE ${filterClause}` : undefined,
+ ).join('\n');
+ const cnt: any = (
+ await queryDruidSql<{ cnt: number }>(
+ {
+ query: sqlQuery,
+ },
+ cancelToken,
+ )
+ )[0].cnt;
+ return {
+ ...segmentsWithAuxiliaryInfo,
+ count: typeof cnt === 'number' ? cnt : -1,
+ };
+ });
} else if (capabilities.hasCoordinatorAccess()) {
let datasourceList: string[] = [];
const datasourceFilter = filtered.find(({ id }) => id ===
'datasource');
@@ -428,11 +459,17 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
});
}
+ count = results.length;
const maxResults = (page + 1) * pageSize;
- return applySorting(results, sorted).slice(page * pageSize,
maxResults);
+ segments = applySorting(results, sorted).slice(page * pageSize,
maxResults);
} else {
throw new Error('must have SQL or coordinator access to load this
view');
}
+
+ return new ResultWithAuxiliaryWork<SegmentsWithAuxiliaryInfo>(
+ { segments, count },
+ auxiliaryQueries,
+ );
},
onStateChange: segmentsState => {
this.setState({
@@ -497,6 +534,17 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
});
};
+ private readonly handleFilterChange = (filters: Filter[]) => {
+ this.goToFirstPage();
+ this.props.onFiltersChange(filters);
+ };
+
+ private goToFirstPage() {
+ if (this.state.page) {
+ this.setState({ page: 0 });
+ }
+ }
+
private getSegmentActions(id: string, datasource: string): BasicAction[] {
const actions: BasicAction[] = [];
actions.push({
@@ -521,7 +569,8 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
enableComparisons = false,
valueFn: (value: string) => ReactNode = String,
) {
- const { filters, onFiltersChange } = this.props;
+ const { filters } = this.props;
+ const { handleFilterChange } = this;
return function FilterableCell(row: { value: any }) {
return (
@@ -529,7 +578,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
field={field}
value={row.value}
filters={filters}
- onFiltersChange={onFiltersChange}
+ onFiltersChange={handleFilterChange}
enableComparisons={enableComparisons}
>
{valueFn(row.value)}
@@ -539,7 +588,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
}
renderSegmentsTable() {
- const { capabilities, filters, onFiltersChange } = this.props;
+ const { capabilities, filters } = this.props;
const {
segmentsState,
visibleColumns,
@@ -550,7 +599,10 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
showSegmentTimeline,
} = this.state;
- const segments = segmentsState.data || [];
+ const { segments, count } = segmentsState.data || {
+ segments: [],
+ count: -1,
+ };
const sizeValues = segments.map(d =>
formatBytes(d.size)).concat('(realtime)');
@@ -570,7 +622,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
return (
<ReactTable
data={segments}
- pages={10000000} // Dummy, we are hiding the page selector
+ pages={count >= 0 ? Math.ceil(count / pageSize) : 10000000}
loading={segmentsState.loading}
noDataText={
segmentsState.isEmpty()
@@ -580,7 +632,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
manual
filterable
filtered={filters}
- onFilteredChange={onFiltersChange}
+ onFilteredChange={this.handleFilterChange}
sorted={sorted}
onSortedChange={sorted => this.setState({ sorted })}
page={page}
@@ -588,9 +640,9 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
pageSize={pageSize}
onPageSizeChange={pageSize => this.setState({ pageSize })}
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
- showPagination={segments.length >= STANDARD_TABLE_PAGE_SIZE || page >
0}
+ showPagination
showPageJump={false}
- ofText=""
+ ofText={count >= 0 ? `of ${formatInteger(count)}` : ''}
pivotBy={groupByInterval ? ['interval'] : []}
columns={[
{
@@ -1013,7 +1065,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
}
render() {
- const { capabilities, filters, onFiltersChange } = this.props;
+ const { capabilities, filters } = this.props;
const {
segmentTableActionDialogId,
datasourceTableActionDialogId,
@@ -1104,7 +1156,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
small
rightIcon={IconNames.ARROW_DOWN}
onClick={() =>
- onFiltersChange(
+ this.handleFilterChange(
compact([
start && { id: 'start', value:
`>=${start.toISOString()}` },
end && { id: 'end', value: `<${end.toISOString()}` },
diff --git
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
index 2cf28b8b22c..329a7fd3562 100644
---
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
+++
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
@@ -66,283 +66,287 @@ exports[`ServicesView renders data 1`] = `
tableColumnsHidden={[]}
/>
</Memo(ViewControlBar)>
- <ReactTable
- AggregatedComponent={[Function]}
- ExpanderComponent={[Function]}
- FilterComponent={[Function]}
- LoadingComponent={[Function]}
- NoDataComponent={[Function]}
- PadRowComponent={[Function]}
- PaginationComponent={[Function]}
- PivotValueComponent={[Function]}
- ResizerComponent={[Function]}
- TableComponent={[Function]}
- TbodyComponent={[Function]}
- TdComponent={[Function]}
- TfootComponent={[Function]}
- ThComponent={[Function]}
- TheadComponent={[Function]}
- TrComponent={[Function]}
- TrGroupComponent={[Function]}
- aggregatedKey="_aggregated"
- className=""
- collapseOnDataChange={true}
- collapseOnPageChange={true}
- collapseOnSortingChange={true}
- column={
- {
- "Aggregated": undefined,
- "Cell": undefined,
- "Expander": undefined,
- "Filter": undefined,
- "Footer": undefined,
- "Header": undefined,
- "Pivot": undefined,
- "PivotValue": undefined,
- "Placeholder": undefined,
- "aggregate": undefined,
- "className": "",
- "filterAll": false,
- "filterMethod": undefined,
- "filterable": undefined,
- "footerClassName": "",
- "footerStyle": {},
- "getFooterProps": [Function],
- "getHeaderProps": [Function],
- "getProps": [Function],
- "headerClassName": "",
- "headerStyle": {},
- "minResizeWidth": 11,
- "minWidth": 100,
- "resizable": undefined,
- "show": true,
- "sortMethod": undefined,
- "sortable": undefined,
- "style": {},
- }
- }
- columns={
- [
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Service",
- "accessor": "service",
- "show": true,
- "width": 300,
- },
- {
- "Cell": [Function],
- "Filter": [Function],
- "Header": "Type",
- "accessor": "service_type",
- "show": true,
- "width": 150,
- },
- {
- "Cell": [Function],
- "Header": "Tier",
- "accessor": [Function],
- "id": "tier",
- "show": true,
- "width": 180,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Host",
- "accessor": "host",
- "show": true,
- "width": 200,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Port",
- "accessor": [Function],
- "id": "port",
- "show": true,
- "width": 100,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Current size",
- "accessor": "curr_size",
- "className": "padded",
- "filterable": false,
- "id": "curr_size",
- "show": true,
- "width": 100,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Max size",
- "accessor": "max_size",
- "className": "padded",
- "filterable": false,
- "id": "max_size",
- "show": true,
- "width": 100,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Usage",
- "accessor": [Function],
- "className": "padded",
- "filterable": false,
- "id": "usage",
- "show": true,
- "width": 140,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Start time",
- "accessor": "start_time",
- "show": true,
- "width": 200,
- },
- {
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Detail",
- "accessor": [Function],
- "className": "padded",
- "filterable": false,
- "id": "queue",
- "show": true,
- "width": 400,
- },
+ <Context.Provider
+ value={{}}
+ >
+ <ReactTable
+ AggregatedComponent={[Function]}
+ ExpanderComponent={[Function]}
+ FilterComponent={[Function]}
+ LoadingComponent={[Function]}
+ NoDataComponent={[Function]}
+ PadRowComponent={[Function]}
+ PaginationComponent={[Function]}
+ PivotValueComponent={[Function]}
+ ResizerComponent={[Function]}
+ TableComponent={[Function]}
+ TbodyComponent={[Function]}
+ TdComponent={[Function]}
+ TfootComponent={[Function]}
+ ThComponent={[Function]}
+ TheadComponent={[Function]}
+ TrComponent={[Function]}
+ TrGroupComponent={[Function]}
+ aggregatedKey="_aggregated"
+ className=""
+ collapseOnDataChange={true}
+ collapseOnPageChange={true}
+ collapseOnSortingChange={true}
+ column={
{
- "Aggregated": [Function],
- "Cell": [Function],
- "Header": "Actions",
- "accessor": [Function],
- "filterable": false,
- "id": "actions",
+ "Aggregated": undefined,
+ "Cell": undefined,
+ "Expander": undefined,
+ "Filter": undefined,
+ "Footer": undefined,
+ "Header": undefined,
+ "Pivot": undefined,
+ "PivotValue": undefined,
+ "Placeholder": undefined,
+ "aggregate": undefined,
+ "className": "",
+ "filterAll": false,
+ "filterMethod": undefined,
+ "filterable": undefined,
+ "footerClassName": "",
+ "footerStyle": {},
+ "getFooterProps": [Function],
+ "getHeaderProps": [Function],
+ "getProps": [Function],
+ "headerClassName": "",
+ "headerStyle": {},
+ "minResizeWidth": 11,
+ "minWidth": 100,
+ "resizable": undefined,
"show": true,
- "sortable": false,
- "width": 70,
- },
- ]
- }
- data={
- [
+ "sortMethod": undefined,
+ "sortable": undefined,
+ "style": {},
+ }
+ }
+ columns={
[
{
- "curr_size": 0,
- "host": "localhost",
- "is_leader": 0,
- "max_size": 0,
- "plaintext_port": 8082,
- "service": "localhost:8082",
- "service_type": "broker",
- "start_time": 0,
- "tier": null,
- "tls_port": -1,
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Service",
+ "accessor": "service",
+ "show": true,
+ "width": 300,
},
{
- "curr_size": 179744287,
- "host": "localhost",
- "is_leader": 0,
- "max_size": 3000000000n,
- "plaintext_port": 8083,
- "segmentsToDrop": 0,
- "segmentsToDropSize": 0,
- "segmentsToLoad": 0,
- "segmentsToLoadSize": 0,
- "service": "localhost:8083",
- "service_type": "historical",
- "start_time": 0,
- "tier": "_default_tier",
- "tls_port": -1,
+ "Cell": [Function],
+ "Filter": [Function],
+ "Header": "Type",
+ "accessor": "service_type",
+ "show": true,
+ "width": 150,
},
- ],
- ]
- }
- defaultExpanded={{}}
- defaultFilterMethod={[Function]}
- defaultFiltered={[]}
- defaultPage={0}
- defaultPageSize={50}
- defaultResized={[]}
- defaultSortDesc={false}
- defaultSortMethod={[Function]}
- defaultSorted={[]}
- expanderDefaults={
- {
- "filterable": false,
- "resizable": false,
- "sortable": false,
- "width": 35,
+ {
+ "Cell": [Function],
+ "Header": "Tier",
+ "accessor": [Function],
+ "id": "tier",
+ "show": true,
+ "width": 180,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Host",
+ "accessor": "host",
+ "show": true,
+ "width": 200,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Port",
+ "accessor": [Function],
+ "id": "port",
+ "show": true,
+ "width": 100,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Current size",
+ "accessor": "curr_size",
+ "className": "padded",
+ "filterable": false,
+ "id": "curr_size",
+ "show": true,
+ "width": 100,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Max size",
+ "accessor": "max_size",
+ "className": "padded",
+ "filterable": false,
+ "id": "max_size",
+ "show": true,
+ "width": 100,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Usage",
+ "accessor": [Function],
+ "className": "padded",
+ "filterable": false,
+ "id": "usage",
+ "show": true,
+ "width": 140,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Start time",
+ "accessor": "start_time",
+ "show": true,
+ "width": 200,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Detail",
+ "accessor": "service",
+ "className": "padded",
+ "filterable": false,
+ "id": "queue",
+ "show": true,
+ "width": 400,
+ },
+ {
+ "Aggregated": [Function],
+ "Cell": [Function],
+ "Header": "Actions",
+ "accessor": "service",
+ "filterable": false,
+ "id": "actions",
+ "show": true,
+ "sortable": false,
+ "width": 70,
+ },
+ ]
}
- }
- filterable={true}
- filtered={[]}
- freezeWhenExpanded={false}
- getLoadingProps={[Function]}
- getNoDataProps={[Function]}
- getPaginationProps={[Function]}
- getProps={[Function]}
- getResizerProps={[Function]}
- getTableProps={[Function]}
- getTbodyProps={[Function]}
- getTdProps={[Function]}
- getTfootProps={[Function]}
- getTfootTdProps={[Function]}
- getTfootTrProps={[Function]}
- getTheadFilterProps={[Function]}
- getTheadFilterThProps={[Function]}
- getTheadFilterTrProps={[Function]}
- getTheadGroupProps={[Function]}
- getTheadGroupThProps={[Function]}
- getTheadGroupTrProps={[Function]}
- getTheadProps={[Function]}
- getTheadThProps={[Function]}
- getTheadTrProps={[Function]}
- getTrGroupProps={[Function]}
- getTrProps={[Function]}
- groupedByPivotKey="_groupedByPivot"
- indexKey="_index"
- loading={false}
- loadingText="Loading..."
- multiSort={true}
- nestingLevelKey="_nestingLevel"
- nextText="Next"
- noDataText=""
- ofText="of"
- onFetchData={[Function]}
- onFilteredChange={[Function]}
- originalKey="_original"
- pageJumpText="jump to page"
- pageSizeOptions={
- [
- 50,
- 100,
- 200,
- ]
- }
- pageText="Page"
- pivotBy={[]}
- pivotDefaults={{}}
- pivotIDKey="_pivotID"
- pivotValKey="_pivotVal"
- previousText="Previous"
- resizable={true}
- resolveData={[Function]}
- rowsSelectorText="rows per page"
- rowsText="rows"
- showPageJump={true}
- showPageSizeOptions={true}
- showPagination={false}
- showPaginationBottom={true}
- showPaginationTop={false}
- sortable={true}
- style={{}}
- subRowsKey="_subRows"
- />
+ data={
+ [
+ [
+ {
+ "curr_size": 0,
+ "host": "localhost",
+ "is_leader": 0,
+ "max_size": 0,
+ "plaintext_port": 8082,
+ "service": "localhost:8082",
+ "service_type": "broker",
+ "start_time": 0,
+ "tier": null,
+ "tls_port": -1,
+ },
+ {
+ "curr_size": 179744287,
+ "host": "localhost",
+ "is_leader": 0,
+ "max_size": 3000000000n,
+ "plaintext_port": 8083,
+ "segmentsToDrop": 0,
+ "segmentsToDropSize": 0,
+ "segmentsToLoad": 0,
+ "segmentsToLoadSize": 0,
+ "service": "localhost:8083",
+ "service_type": "historical",
+ "start_time": 0,
+ "tier": "_default_tier",
+ "tls_port": -1,
+ },
+ ],
+ ]
+ }
+ defaultExpanded={{}}
+ defaultFilterMethod={[Function]}
+ defaultFiltered={[]}
+ defaultPage={0}
+ defaultPageSize={50}
+ defaultResized={[]}
+ defaultSortDesc={false}
+ defaultSortMethod={[Function]}
+ defaultSorted={[]}
+ expanderDefaults={
+ {
+ "filterable": false,
+ "resizable": false,
+ "sortable": false,
+ "width": 35,
+ }
+ }
+ filterable={true}
+ filtered={[]}
+ freezeWhenExpanded={false}
+ getLoadingProps={[Function]}
+ getNoDataProps={[Function]}
+ getPaginationProps={[Function]}
+ getProps={[Function]}
+ getResizerProps={[Function]}
+ getTableProps={[Function]}
+ getTbodyProps={[Function]}
+ getTdProps={[Function]}
+ getTfootProps={[Function]}
+ getTfootTdProps={[Function]}
+ getTfootTrProps={[Function]}
+ getTheadFilterProps={[Function]}
+ getTheadFilterThProps={[Function]}
+ getTheadFilterTrProps={[Function]}
+ getTheadGroupProps={[Function]}
+ getTheadGroupThProps={[Function]}
+ getTheadGroupTrProps={[Function]}
+ getTheadProps={[Function]}
+ getTheadThProps={[Function]}
+ getTheadTrProps={[Function]}
+ getTrGroupProps={[Function]}
+ getTrProps={[Function]}
+ groupedByPivotKey="_groupedByPivot"
+ indexKey="_index"
+ loading={false}
+ loadingText="Loading..."
+ multiSort={true}
+ nestingLevelKey="_nestingLevel"
+ nextText="Next"
+ noDataText=""
+ ofText="of"
+ onFetchData={[Function]}
+ onFilteredChange={[Function]}
+ originalKey="_original"
+ pageJumpText="jump to page"
+ pageSizeOptions={
+ [
+ 50,
+ 100,
+ 200,
+ ]
+ }
+ pageText="Page"
+ pivotBy={[]}
+ pivotDefaults={{}}
+ pivotIDKey="_pivotID"
+ pivotValKey="_pivotVal"
+ previousText="Previous"
+ resizable={true}
+ resolveData={[Function]}
+ rowsSelectorText="rows per page"
+ rowsText="rows"
+ showPageJump={true}
+ showPageSizeOptions={true}
+ showPagination={false}
+ showPaginationBottom={true}
+ showPaginationTop={false}
+ sortable={true}
+ style={{}}
+ subRowsKey="_subRows"
+ />
+ </Context.Provider>
</div>
`;
diff --git a/web-console/src/views/services-view/services-view.scss
b/web-console/src/views/services-view/fill-indicator/fill-indicator.scss
similarity index 58%
copy from web-console/src/views/services-view/services-view.scss
copy to web-console/src/views/services-view/fill-indicator/fill-indicator.scss
index 6d1dcd0ef65..2ef5c9a40a6 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/fill-indicator/fill-indicator.scss
@@ -16,48 +16,26 @@
* limitations under the License.
*/
-@import '../../variables';
-
-.services-view {
- height: 100%;
+.fill-indicator {
+ position: relative;
width: 100%;
+ height: 18px;
+ background-color: #dadada;
+ border-radius: 2px;
- .view-control-bar {
- margin-bottom: $standard-padding;
+ .bar {
+ height: 100%;
+ background-color: #85cc00;
+ border-radius: 2px;
+ transition: all 0.2s ease-out;
}
- .ReactTable {
+ .label {
position: absolute;
- top: $view-control-bar-height + $standard-padding;
- bottom: 0;
- width: 100%;
- }
-
- ul {
- line-height: 20px;
- }
-
- .fill-indicator {
- position: relative;
+ height: 100%;
width: 100%;
- height: 18px;
- background-color: #dadada;
- border-radius: 2px;
-
- .bar {
- height: 100%;
- background-color: #85cc00;
- border-radius: 2px;
- transition: all 0.2s ease-out;
- }
-
- .label {
- position: absolute;
- height: 100%;
- width: 100%;
- text-align: center;
- top: 0;
- color: #421890;
- }
+ text-align: center;
+ top: 0;
+ color: #421890;
}
}
diff --git a/web-console/src/views/services-view/services-view.scss
b/web-console/src/views/services-view/fill-indicator/fill-indicator.tsx
similarity index 52%
copy from web-console/src/views/services-view/services-view.scss
copy to web-console/src/views/services-view/fill-indicator/fill-indicator.tsx
index 6d1dcd0ef65..9403c8a32f1 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/fill-indicator/fill-indicator.tsx
@@ -16,48 +16,19 @@
* limitations under the License.
*/
-@import '../../variables';
+import './fill-indicator.scss';
-.services-view {
- height: 100%;
- width: 100%;
-
- .view-control-bar {
- margin-bottom: $standard-padding;
- }
-
- .ReactTable {
- position: absolute;
- top: $view-control-bar-height + $standard-padding;
- bottom: 0;
- width: 100%;
- }
-
- ul {
- line-height: 20px;
- }
-
- .fill-indicator {
- position: relative;
- width: 100%;
- height: 18px;
- background-color: #dadada;
- border-radius: 2px;
-
- .bar {
- height: 100%;
- background-color: #85cc00;
- border-radius: 2px;
- transition: all 0.2s ease-out;
- }
+export interface FillIndicatorProps {
+ value: number;
+}
- .label {
- position: absolute;
- height: 100%;
- width: 100%;
- text-align: center;
- top: 0;
- color: #421890;
- }
- }
+export function FillIndicator({ value }: FillIndicatorProps) {
+ let formattedValue = (value * 100).toFixed(1);
+ if (formattedValue === '0.0' && value > 0) formattedValue = '~' +
formattedValue;
+ return (
+ <div className="fill-indicator">
+ <div className="bar" style={{ width: `${value * 100}%` }} />
+ <div className="label">{formattedValue + '%'}</div>
+ </div>
+ );
}
diff --git a/web-console/src/views/services-view/services-view.scss
b/web-console/src/views/services-view/services-view.scss
index 6d1dcd0ef65..642b65c6683 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/services-view.scss
@@ -36,28 +36,4 @@
ul {
line-height: 20px;
}
-
- .fill-indicator {
- position: relative;
- width: 100%;
- height: 18px;
- background-color: #dadada;
- border-radius: 2px;
-
- .bar {
- height: 100%;
- background-color: #85cc00;
- border-radius: 2px;
- transition: all 0.2s ease-out;
- }
-
- .label {
- position: absolute;
- height: 100%;
- width: 100%;
- text-align: center;
- top: 0;
- color: #421890;
- }
- }
}
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 0b2ff752576..35a147e6a2e 100644
--- a/web-console/src/views/services-view/services-view.spec.tsx
+++ b/web-console/src/views/services-view/services-view.spec.tsx
@@ -35,38 +35,42 @@ jest.mock('../../utils', () => {
public runQuery() {
this.onStateChange(
new QueryState({
- data: [
- [
- {
- service: 'localhost:8082',
- service_type: 'broker',
- tier: null,
- host: 'localhost',
- plaintext_port: 8082,
- tls_port: -1,
- curr_size: 0,
- max_size: 0,
- is_leader: 0,
- start_time: 0,
- },
- {
- service: 'localhost:8083',
- service_type: 'historical',
- tier: '_default_tier',
- host: 'localhost',
- plaintext_port: 8083,
- tls_port: -1,
- curr_size: 179744287,
- max_size: BigInt(3000000000),
- is_leader: 0,
- segmentsToLoad: 0,
- segmentsToDrop: 0,
- segmentsToLoadSize: 0,
- segmentsToDropSize: 0,
- start_time: 0,
- },
+ data: {
+ services: [
+ [
+ {
+ service: 'localhost:8082',
+ service_type: 'broker',
+ tier: null,
+ host: 'localhost',
+ plaintext_port: 8082,
+ tls_port: -1,
+ curr_size: 0,
+ max_size: 0,
+ is_leader: 0,
+ start_time: 0,
+ },
+ {
+ service: 'localhost:8083',
+ service_type: 'historical',
+ tier: '_default_tier',
+ host: 'localhost',
+ plaintext_port: 8083,
+ tls_port: -1,
+ curr_size: 179744287,
+ max_size: BigInt(3000000000),
+ is_leader: 0,
+ segmentsToLoad: 0,
+ segmentsToDrop: 0,
+ segmentsToLoadSize: 0,
+ segmentsToDropSize: 0,
+ start_time: 0,
+ },
+ ],
],
- ],
+ loadQueueInfo: {},
+ workerInfo: {},
+ },
}) as any,
);
}
diff --git a/web-console/src/views/services-view/services-view.tsx
b/web-console/src/views/services-view/services-view.tsx
index 58dca141034..ca962c545ee 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -19,8 +19,9 @@
import { Button, ButtonGroup, Intent, Label, MenuItem, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { max, sum } from 'd3-array';
-import React from 'react';
-import type { Filter } from 'react-table';
+import memoize from 'memoize-one';
+import React, { createContext, useContext } from 'react';
+import type { Column, Filter } from 'react-table';
import ReactTable from 'react-table';
import {
@@ -67,6 +68,8 @@ import {
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
+import { FillIndicator } from './fill-indicator/fill-indicator';
+
import './services-view.scss';
const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode,
TableColumnSelectorColumn[]> = {
@@ -119,7 +122,7 @@ export interface ServicesViewProps {
}
export interface ServicesViewState {
- servicesState: QueryState<ServiceResultRow[]>;
+ servicesState: QueryState<ServicesWithAuxiliaryInfo>;
groupServicesBy?: 'service_type' | 'tier';
middleManagerDisableWorkerHost?: string;
@@ -139,10 +142,16 @@ interface ServiceResultRow {
readonly plaintext_port: number;
readonly tls_port: number;
readonly start_time: string;
- readonly loadQueueInfo?: LoadQueueInfo;
- readonly workerInfo?: WorkerInfo;
}
+interface ServicesWithAuxiliaryInfo {
+ readonly services: ServiceResultRow[];
+ readonly loadQueueInfo: Record<string, LoadQueueInfo>;
+ readonly workerInfo: Record<string, WorkerInfo>;
+}
+
+export const LoadQueueInfoContext = createContext<Record<string,
LoadQueueInfo>>({});
+
interface LoadQueueInfo {
readonly segmentsToDrop: NumberLike;
readonly segmentsToDropSize: NumberLike;
@@ -206,7 +215,7 @@ interface WorkerInfo {
}
export class ServicesView extends React.PureComponent<ServicesViewProps,
ServicesViewState> {
- private readonly serviceQueryManager: QueryManager<ServicesQuery,
ServiceResultRow[]>;
+ private readonly serviceQueryManager: QueryManager<ServicesQuery,
ServicesWithAuxiliaryInfo>;
// Ranking
// coordinator => 8
@@ -284,10 +293,10 @@ ORDER BY
throw new Error(`must have SQL or coordinator access`);
}
- const auxiliaryQueries: AuxiliaryQueryFn<ServiceResultRow[]>[] = [];
+ const auxiliaryQueries: AuxiliaryQueryFn<ServicesWithAuxiliaryInfo>[]
= [];
- if (capabilities.hasCoordinatorAccess() &&
visibleColumns.shown('Details')) {
- auxiliaryQueries.push(async (services, cancelToken) => {
+ if (capabilities.hasCoordinatorAccess() &&
visibleColumns.shown('Detail')) {
+ auxiliaryQueries.push(async (servicesWithAuxiliaryInfo, cancelToken)
=> {
try {
const loadQueueInfos = (
await Api.instance.get<Record<string, LoadQueueInfo>>(
@@ -295,23 +304,23 @@ ORDER BY
{ cancelToken },
)
).data;
- return services.map(s => ({
- ...s,
- loadQueueInfo: loadQueueInfos[s.service],
- }));
+ return {
+ ...servicesWithAuxiliaryInfo,
+ loadQueueInfo: loadQueueInfos,
+ };
} catch {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'There was an error getting the load queue info',
});
- return services;
+ return servicesWithAuxiliaryInfo;
}
});
}
if (capabilities.hasOverlordAccess()) {
- auxiliaryQueries.push(async (services, cancelToken) => {
+ auxiliaryQueries.push(async (servicesWithAuxiliaryInfo, cancelToken)
=> {
try {
const workerInfos = await getApiArray<WorkerInfo>(
'/druid/indexer/v1/workers',
@@ -323,10 +332,10 @@ ORDER BY
m => m.worker?.host,
);
- return services.map(s => ({
- ...s,
- workerInfo: workerInfoLookup[s.service],
- }));
+ return {
+ ...servicesWithAuxiliaryInfo,
+ workerInfo: workerInfoLookup,
+ };
} catch (e) {
// Swallow this error because it simply a reflection of a local
task runner.
if (
@@ -338,12 +347,15 @@ ORDER BY
message: 'There was an error getting the worker info',
});
}
- return services;
+ return servicesWithAuxiliaryInfo;
}
});
}
- return new ResultWithAuxiliaryWork(services, auxiliaryQueries);
+ return new ResultWithAuxiliaryWork<ServicesWithAuxiliaryInfo>(
+ { services, loadQueueInfo: {}, workerInfo: {} },
+ auxiliaryQueries,
+ );
},
onStateChange: servicesState => {
this.setState({
@@ -377,325 +389,324 @@ ORDER BY
value={row.value}
filters={filters}
onFiltersChange={onFiltersChange}
- >
- {row.value}
- </TableFilterableCell>
+ />
);
};
}
renderServicesTable() {
- const { capabilities, filters, onFiltersChange } = this.props;
+ const { filters, onFiltersChange } = this.props;
const { servicesState, groupServicesBy, visibleColumns } = this.state;
- const fillIndicator = (value: number) => {
- let formattedValue = (value * 100).toFixed(1);
- if (formattedValue === '0.0' && value > 0) formattedValue = '~' +
formattedValue;
- return (
- <div className="fill-indicator">
- <div className="bar" style={{ width: `${value * 100}%` }} />
- <div className="label">{formattedValue + '%'}</div>
- </div>
- );
+ const { services, loadQueueInfo, workerInfo } = servicesState.data || {
+ services: [],
+ loadQueueInfo: {},
+ workerInfo: {},
};
- const services = servicesState.data || [];
return (
- <ReactTable
- data={services}
- loading={servicesState.loading}
- noDataText={
- servicesState.isEmpty() ? 'No historicals' :
servicesState.getErrorMessage() || ''
- }
- filterable
- filtered={filters}
- onFilteredChange={onFiltersChange}
- pivotBy={groupServicesBy ? [groupServicesBy] : []}
- defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
- pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
- showPagination={services.length > STANDARD_TABLE_PAGE_SIZE}
- columns={[
- {
- Header: 'Service',
- show: visibleColumns.shown('Service'),
- accessor: 'service',
- width: 300,
- Cell: this.renderFilterableCell('service'),
- Aggregated: () => '',
+ <LoadQueueInfoContext.Provider value={loadQueueInfo}>
+ <ReactTable
+ data={services}
+ loading={servicesState.loading}
+ noDataText={
+ servicesState.isEmpty() ? 'No services' :
servicesState.getErrorMessage() || ''
+ }
+ filterable
+ filtered={filters}
+ onFilteredChange={onFiltersChange}
+ pivotBy={groupServicesBy ? [groupServicesBy] : []}
+ defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
+ pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
+ showPagination={services.length > STANDARD_TABLE_PAGE_SIZE}
+ columns={this.getTableColumns(visibleColumns, filters,
onFiltersChange, workerInfo)}
+ />
+ </LoadQueueInfoContext.Provider>
+ );
+ }
+
+ private readonly getTableColumns = memoize(
+ (
+ visibleColumns: LocalStorageBackedVisibility,
+ _filters: Filter[],
+ _onFiltersChange: (filters: Filter[]) => void,
+ workerInfoLookup: Record<string, WorkerInfo>,
+ ): Column<ServiceResultRow>[] => {
+ const { capabilities } = this.props;
+ return [
+ {
+ Header: 'Service',
+ show: visibleColumns.shown('Service'),
+ accessor: 'service',
+ width: 300,
+ Cell: this.renderFilterableCell('service'),
+ Aggregated: () => '',
+ },
+ {
+ Header: 'Type',
+ show: visibleColumns.shown('Type'),
+ Filter: suggestibleFilterInput([
+ 'coordinator',
+ 'overlord',
+ 'router',
+ 'broker',
+ 'historical',
+ 'indexer',
+ 'middle_manager',
+ 'peon',
+ ]),
+ accessor: 'service_type',
+ width: 150,
+ Cell: this.renderFilterableCell('service_type'),
+ },
+ {
+ Header: 'Tier',
+ show: visibleColumns.shown('Tier'),
+ id: 'tier',
+ width: 180,
+ accessor: row => {
+ if (row.tier) return row.tier;
+ return workerInfoLookup[row.service]?.worker?.category;
},
- {
- Header: 'Type',
- show: visibleColumns.shown('Type'),
- Filter: suggestibleFilterInput([
- 'coordinator',
- 'overlord',
- 'router',
- 'broker',
- 'historical',
- 'indexer',
- 'middle_manager',
- 'peon',
- ]),
- accessor: 'service_type',
- width: 150,
- Cell: this.renderFilterableCell('service_type'),
+ Cell: this.renderFilterableCell('tier'),
+ },
+ {
+ Header: 'Host',
+ show: visibleColumns.shown('Host'),
+ accessor: 'host',
+ width: 200,
+ Cell: this.renderFilterableCell('host'),
+ Aggregated: () => '',
+ },
+ {
+ Header: 'Port',
+ show: visibleColumns.shown('Port'),
+ id: 'port',
+ width: 100,
+ accessor: row => {
+ const ports: string[] = [];
+ if (row.plaintext_port !== -1) {
+ ports.push(`${row.plaintext_port} (plain)`);
+ }
+ if (row.tls_port !== -1) {
+ ports.push(`${row.tls_port} (TLS)`);
+ }
+ return ports.join(', ') || 'No port';
},
- {
- Header: 'Tier',
- show: visibleColumns.shown('Tier'),
- id: 'tier',
- width: 180,
- accessor: row => {
- if (row.tier) return row.tier;
- return deepGet(row, 'workerInfo.worker.category');
- },
- Cell: this.renderFilterableCell('tier'),
+ Cell: this.renderFilterableCell('port'),
+ Aggregated: () => '',
+ },
+ {
+ Header: 'Current size',
+ show: visibleColumns.shown('Current size'),
+ id: 'curr_size',
+ width: 100,
+ filterable: false,
+ accessor: 'curr_size',
+ className: 'padded',
+ Aggregated: ({ subRows }) => {
+ const originalRows = subRows.map(r => r._original);
+ if (!originalRows.some(r => r.service_type === 'historical'))
return '';
+ const totalCurr = sum(originalRows, s => s.curr_size);
+ return formatBytes(totalCurr);
},
- {
- Header: 'Host',
- show: visibleColumns.shown('Host'),
- accessor: 'host',
- width: 200,
- Cell: this.renderFilterableCell('host'),
- Aggregated: () => '',
+ Cell: ({ value, aggregated, original }) => {
+ if (aggregated || original.service_type !== 'historical') return
'';
+ if (value === null) return '';
+ return formatBytes(value);
},
- {
- Header: 'Port',
- show: visibleColumns.shown('Port'),
- id: 'port',
- width: 100,
- accessor: row => {
- const ports: string[] = [];
- if (row.plaintext_port !== -1) {
- ports.push(`${row.plaintext_port} (plain)`);
- }
- if (row.tls_port !== -1) {
- ports.push(`${row.tls_port} (TLS)`);
- }
- return ports.join(', ') || 'No port';
- },
- Cell: this.renderFilterableCell('port'),
- Aggregated: () => '',
+ },
+ {
+ Header: 'Max size',
+ show: visibleColumns.shown('Max size'),
+ id: 'max_size',
+ width: 100,
+ filterable: false,
+ accessor: 'max_size',
+ className: 'padded',
+ Aggregated: ({ subRows }) => {
+ const originalRows = subRows.map(r => r._original);
+ if (!originalRows.some(r => r.service_type === 'historical'))
return '';
+ const totalMax = sum(originalRows, s => s.max_size);
+ return formatBytes(totalMax);
},
- {
- Header: 'Current size',
- show: visibleColumns.shown('Current size'),
- id: 'curr_size',
- width: 100,
- filterable: false,
- accessor: 'curr_size',
- className: 'padded',
- Aggregated: ({ subRows }) => {
- const originalRows = subRows.map(r => r._original);
- if (!originalRows.some(r => r.service_type === 'historical'))
return '';
- const totalCurr = sum(originalRows, s => s.curr_size);
- return formatBytes(totalCurr);
- },
- Cell: ({ value, aggregated, original }) => {
- if (aggregated || original.service_type !== 'historical') return
'';
- if (value === null) return '';
- return formatBytes(value);
- },
+ Cell: ({ value, aggregated, original }) => {
+ if (aggregated || original.service_type !== 'historical') return
'';
+ if (value === null) return '';
+ return formatBytes(value);
},
- {
- Header: 'Max size',
- show: visibleColumns.shown('Max size'),
- id: 'max_size',
- width: 100,
- filterable: false,
- accessor: 'max_size',
- className: 'padded',
- Aggregated: ({ subRows }) => {
- const originalRows = subRows.map(r => r._original);
- if (!originalRows.some(r => r.service_type === 'historical'))
return '';
- const totalMax = sum(originalRows, s => s.max_size);
- return formatBytes(totalMax);
- },
- Cell: ({ value, aggregated, original }) => {
- if (aggregated || original.service_type !== 'historical') return
'';
- if (value === null) return '';
- return formatBytes(value);
- },
+ },
+ {
+ Header: 'Usage',
+ show: visibleColumns.shown('Usage'),
+ id: 'usage',
+ width: 140,
+ filterable: false,
+ className: 'padded',
+ accessor: row => {
+ if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
+ const workerInfo = workerInfoLookup[row.service];
+ if (!workerInfo) return 0;
+ return (
+ (Number(workerInfo.currCapacityUsed) || 0) /
Number(workerInfo.worker?.capacity)
+ );
+ } else {
+ return row.max_size ? Number(row.curr_size) /
Number(row.max_size) : null;
+ }
},
- {
- Header: 'Usage',
- show: visibleColumns.shown('Usage'),
- id: 'usage',
- width: 140,
- filterable: false,
- className: 'padded',
- accessor: row => {
- if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
- const { workerInfo } = row;
- if (!workerInfo) return 0;
- return (
- (Number(workerInfo.currCapacityUsed) || 0) /
Number(workerInfo.worker?.capacity)
- );
- } else {
- return row.max_size ? Number(row.curr_size) /
Number(row.max_size) : null;
- }
- },
- Aggregated: ({ subRows }) => {
- const originalRows = subRows.map(r => r._original);
-
- if (originalRows.some(r => r.service_type === 'historical')) {
- const totalCurr = sum(originalRows, s => Number(s.curr_size));
- const totalMax = sum(originalRows, s => Number(s.max_size));
- return fillIndicator(totalCurr / totalMax);
- } else if (
- originalRows.some(
- r => r.service_type === 'indexer' || r.service_type ===
'middle_manager',
- )
- ) {
- const workerInfos: WorkerInfo[] = filterMap(originalRows, r =>
r.workerInfo);
- if (!workerInfos.length) return '';
-
- const totalCurrCapacityUsed = sum(
- workerInfos,
- w => Number(w.currCapacityUsed) || 0,
- );
- const totalWorkerCapacity = sum(
- workerInfos,
- s => deepGet(s, 'worker.capacity') || 0,
- );
- return `Slots used: ${totalCurrCapacityUsed} of
${totalWorkerCapacity}`;
- } else {
- return '';
- }
- },
- Cell: ({ value, aggregated, original }) => {
- if (aggregated) return '';
- const { service_type } = original;
- switch (service_type) {
- case 'historical':
- return fillIndicator(value);
-
- case 'indexer':
- case 'middle_manager': {
- if (!deepGet(original, 'workerInfo')) return '';
- const currCapacityUsed = deepGet(original,
'workerInfo.currCapacityUsed') || 0;
- const capacity = deepGet(original,
'workerInfo.worker.capacity');
- if (typeof capacity === 'number') {
- return `Slots used: ${currCapacityUsed} of ${capacity}`;
- } else {
- return 'Slots used: -';
- }
- }
+ Aggregated: ({ subRows }) => {
+ const originalRows = subRows.map(r => r._original);
+
+ if (originalRows.some(r => r.service_type === 'historical')) {
+ const totalCurr = sum(originalRows, s => Number(s.curr_size));
+ const totalMax = sum(originalRows, s => Number(s.max_size));
+ return <FillIndicator value={totalCurr / totalMax} />;
+ } else if (
+ originalRows.some(
+ r => r.service_type === 'indexer' || r.service_type ===
'middle_manager',
+ )
+ ) {
+ const workerInfos: WorkerInfo[] = filterMap(
+ originalRows,
+ r => workerInfoLookup[r.service],
+ );
- default:
- return '';
- }
- },
- },
- {
- Header: 'Start time',
- show: visibleColumns.shown('Start time'),
- accessor: 'start_time',
- width: 200,
- Cell: this.renderFilterableCell('start_time'),
- Aggregated: () => '',
+ if (!workerInfos.length) return '';
+
+ const totalCurrCapacityUsed = sum(workerInfos, w =>
Number(w.currCapacityUsed) || 0);
+ const totalWorkerCapacity = sum(workerInfos, s => deepGet(s,
'worker.capacity') || 0);
+ return `Slots used: ${totalCurrCapacityUsed} of
${totalWorkerCapacity}`;
+ } else {
+ return '';
+ }
},
- {
- Header: 'Detail',
- show: visibleColumns.shown('Detail'),
- id: 'queue',
- width: 400,
- filterable: false,
- className: 'padded',
- accessor: row => {
- switch (row.service_type) {
- case 'middle_manager':
- case 'indexer': {
- const { workerInfo } = row;
- if (!workerInfo) return '';
-
- if (workerInfo.worker.version === '') return 'Disabled';
-
- const details: string[] = [];
- if (workerInfo.lastCompletedTaskTime) {
- details.push(
- `Last completed task:
${prettyFormatIsoDateWithMsIfNeeded(
- workerInfo.lastCompletedTaskTime,
- )}`,
- );
- }
- if (workerInfo.blacklistedUntil) {
- details.push(
- `Blacklisted until: ${prettyFormatIsoDateWithMsIfNeeded(
- workerInfo.blacklistedUntil,
- )}`,
- );
- }
- return details.join(' ');
+ Cell: ({ value, aggregated, original }) => {
+ if (aggregated) return '';
+ const { service_type } = original;
+
+ switch (service_type) {
+ case 'historical':
+ return <FillIndicator value={value} />;
+
+ case 'indexer':
+ case 'middle_manager': {
+ const workerInfo = workerInfoLookup[original.service];
+ if (!workerInfo) return '';
+
+ const currCapacityUsed = workerInfo.currCapacityUsed || 0;
+ const capacity = deepGet(workerInfo, 'worker.capacity');
+ if (typeof capacity === 'number') {
+ return `Slots used: ${currCapacityUsed} of ${capacity}`;
+ } else {
+ return 'Slots used: -';
}
+ }
- case 'coordinator':
- case 'overlord':
- return row.is_leader === 1 ? 'Leader' : '';
-
- case 'historical': {
- const { loadQueueInfo } = row;
- if (!loadQueueInfo) return 0;
- return (
- (Number(loadQueueInfo.segmentsToLoad) || 0) +
- (Number(loadQueueInfo.segmentsToDrop) || 0)
+ default:
+ return '';
+ }
+ },
+ },
+ {
+ Header: 'Start time',
+ show: visibleColumns.shown('Start time'),
+ accessor: 'start_time',
+ width: 200,
+ Cell: this.renderFilterableCell('start_time'),
+ Aggregated: () => '',
+ },
+ {
+ Header: 'Detail',
+ show: visibleColumns.shown('Detail'),
+ id: 'queue',
+ width: 400,
+ filterable: false,
+ className: 'padded',
+ accessor: 'service',
+ Cell: ({ original }) => {
+ const { service_type, service, is_leader } = original;
+ const loadQueueInfoContext = useContext(LoadQueueInfoContext);
+
+ switch (service_type) {
+ case 'middle_manager':
+ case 'indexer': {
+ const workerInfo = workerInfoLookup[service];
+ if (!workerInfo) return null;
+
+ if (workerInfo.worker.version === '') return 'Disabled';
+
+ const details: string[] = [];
+ if (workerInfo.lastCompletedTaskTime) {
+ details.push(
+ `Last completed task: ${prettyFormatIsoDateWithMsIfNeeded(
+ workerInfo.lastCompletedTaskTime,
+ )}`,
);
}
-
- default:
- return 0;
- }
- },
- Cell: ({ value, aggregated, original }) => {
- if (aggregated) return '';
- const { service_type } = original;
- switch (service_type) {
- case 'middle_manager':
- case 'indexer':
- case 'coordinator':
- case 'overlord':
- return value;
-
- case 'historical': {
- const { loadQueueInfo } = original;
- return loadQueueInfo ? formatLoadQueueInfo(loadQueueInfo) :
'';
+ if (workerInfo.blacklistedUntil) {
+ details.push(
+ `Blacklisted until: ${prettyFormatIsoDateWithMsIfNeeded(
+ workerInfo.blacklistedUntil,
+ )}`,
+ );
}
+ return details.join(' ') || null;
+ }
- default:
- return '';
+ case 'coordinator':
+ case 'overlord':
+ return is_leader === 1 ? 'Leader' : '';
+
+ case 'historical': {
+ const loadQueueInfo = loadQueueInfoContext[service];
+ if (!loadQueueInfo) return null;
+
+ return formatLoadQueueInfo(loadQueueInfo);
}
- },
- Aggregated: ({ subRows }) => {
- const originalRows = subRows.map(r => r._original);
- if (!originalRows.some(r => r.service_type === 'historical'))
return '';
- const loadQueueInfos: LoadQueueInfo[] = filterMap(originalRows,
r => r.loadQueueInfo);
- return loadQueueInfos.length
- ? formatLoadQueueInfo(aggregateLoadQueueInfos(loadQueueInfos))
- : '';
- },
+
+ default:
+ return null;
+ }
},
- {
- Header: ACTION_COLUMN_LABEL,
- show: capabilities.hasOverlordAccess(),
- id: ACTION_COLUMN_ID,
- width: ACTION_COLUMN_WIDTH,
- accessor: row => row.workerInfo,
- filterable: false,
- sortable: false,
- Cell: ({ value, aggregated }) => {
- if (aggregated) return '';
- if (!value) return null;
- const { worker } = value;
- const disabled = worker.version === '';
- const workerActions = this.getWorkerActions(worker.host,
disabled);
- return <ActionCell actions={workerActions}
menuTitle={worker.host} />;
- },
- Aggregated: () => '',
+ Aggregated: ({ subRows }) => {
+ const loadQueueInfoContext = useContext(LoadQueueInfoContext);
+ const originalRows = subRows.map(r => r._original);
+ if (!originalRows.some(r => r.service_type === 'historical'))
return '';
+
+ const loadQueueInfos: LoadQueueInfo[] = filterMap(
+ originalRows,
+ r => loadQueueInfoContext[r.service],
+ );
+
+ return loadQueueInfos.length
+ ? formatLoadQueueInfo(aggregateLoadQueueInfos(loadQueueInfos))
+ : '';
},
- ]}
- />
- );
- }
+ },
+ {
+ Header: ACTION_COLUMN_LABEL,
+ show: capabilities.hasOverlordAccess(),
+ id: ACTION_COLUMN_ID,
+ width: ACTION_COLUMN_WIDTH,
+ accessor: 'service',
+ filterable: false,
+ sortable: false,
+ Cell: ({ value, aggregated }) => {
+ if (aggregated) return '';
+
+ const workerInfo = workerInfoLookup[value];
+ if (!workerInfo) return null;
+
+ const { worker } = workerInfo;
+ const disabled = worker.version === '';
+ const workerActions = this.getWorkerActions(worker.host, disabled);
+ return <ActionCell actions={workerActions} menuTitle={worker.host}
/>;
+ },
+ Aggregated: () => '',
+ },
+ ];
+ },
+ );
private getWorkerActions(workerHost: string, disabled: boolean):
BasicAction[] {
if (disabled) {
diff --git
a/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
b/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
index 84b5cbe6e34..b94559423b6 100644
---
a/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
+++
b/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
@@ -97,321 +97,270 @@ exports[`SupervisorsView matches snapshot 1`] = `
tableColumnsHidden={[]}
/>
</Memo(ViewControlBar)>
- <ReactTable
- AggregatedComponent={[Function]}
- ExpanderComponent={[Function]}
- FilterComponent={[Function]}
- LoadingComponent={[Function]}
- NoDataComponent={[Function]}
- PadRowComponent={[Function]}
- PaginationComponent={[Function]}
- PivotValueComponent={[Function]}
- ResizerComponent={[Function]}
- TableComponent={[Function]}
- TbodyComponent={[Function]}
- TdComponent={[Function]}
- TfootComponent={[Function]}
- ThComponent={[Function]}
- TheadComponent={[Function]}
- TrComponent={[Function]}
- TrGroupComponent={[Function]}
- aggregatedKey="_aggregated"
- className=""
- collapseOnDataChange={true}
- collapseOnPageChange={true}
- collapseOnSortingChange={true}
- column={
- {
- "Aggregated": undefined,
- "Cell": undefined,
- "Expander": undefined,
- "Filter": undefined,
- "Footer": undefined,
- "Header": undefined,
- "Pivot": undefined,
- "PivotValue": undefined,
- "Placeholder": undefined,
- "aggregate": undefined,
- "className": "",
- "filterAll": false,
- "filterMethod": undefined,
- "filterable": undefined,
- "footerClassName": "",
- "footerStyle": {},
- "getFooterProps": [Function],
- "getHeaderProps": [Function],
- "getProps": [Function],
- "headerClassName": "",
- "headerStyle": {},
- "minResizeWidth": 11,
- "minWidth": 100,
- "resizable": undefined,
- "show": true,
- "sortMethod": undefined,
- "sortable": undefined,
- "style": {},
- }
- }
- columns={
- [
- {
- "Cell": [Function],
- "Header": <React.Fragment>
- Supervisor ID
- <br />
- <i>
- (datasource)
- </i>
- </React.Fragment>,
- "accessor": "supervisor_id",
- "id": "supervisor_id",
- "show": true,
- "width": 280,
- },
- {
- "Cell": [Function],
- "Header": "Type",
- "accessor": "type",
- "show": true,
- "width": 120,
- },
- {
- "Cell": [Function],
- "Header": "Topic/Stream",
- "accessor": "source",
- "show": true,
- "width": 200,
- },
- {
- "Cell": [Function],
- "Filter": [Function],
- "Header": "Status",
- "accessor": "detailed_state",
- "id": "detailed_state",
- "show": true,
- "width": 170,
- },
- {
- "Cell": [Function],
- "Header": "Configured tasks",
- "accessor": "spec",
- "className": "padded",
- "filterable": false,
- "id": "configured_tasks",
- "show": true,
- "sortable": false,
- "width": 130,
- },
- {
- "Cell": [Function],
- "Header": "Running tasks",
- "accessor": "status.payload",
- "filterable": false,
- "id": "running_tasks",
- "show": true,
- "sortable": false,
- "width": 150,
- },
- {
- "Cell": [Function],
- "Header": "Aggregate lag",
- "accessor": "status.payload.aggregateLag",
- "className": "padded",
- "filterable": false,
- "show": true,
- "sortable": false,
- "width": 200,
- },
- {
- "Cell": [Function],
- "Header": <React.Fragment>
- Stats
- <br />
- <Blueprint5.Popover
- boundary="clippingParents"
- captureDismiss={false}
- content={
- <Blueprint5.Menu>
- <Blueprint5.MenuItem
- active={false}
- disabled={false}
- icon="circle"
- multiline={false}
- onClick={[Function]}
- popoverProps={{}}
- shouldDismissPopover={true}
- text="Rate over past 1 minute"
- />
- <Blueprint5.MenuItem
- active={false}
- disabled={false}
- icon="tick-circle"
- multiline={false}
- onClick={[Function]}
- popoverProps={{}}
- shouldDismissPopover={true}
- text="Rate over past 5 minutes"
- />
- <Blueprint5.MenuItem
- active={false}
- disabled={false}
- icon="circle"
- multiline={false}
- onClick={[Function]}
- popoverProps={{}}
- shouldDismissPopover={true}
- text="Rate over past 15 minutes"
- />
- </Blueprint5.Menu>
- }
- defaultIsOpen={false}
- disabled={false}
- fill={false}
- hasBackdrop={false}
- hoverCloseDelay={300}
- hoverOpenDelay={150}
- inheritDarkTheme={true}
- interactionKind="click"
- matchTargetWidth={false}
- minimal={false}
- openOnTargetFocus={true}
- position="bottom"
- positioningStrategy="absolute"
- shouldReturnFocusOnClose={false}
- targetTagName="span"
- transitionDuration={300}
- usePortal={true}
- >
- <i
- className="title-button"
- >
- Rate over past 5 minutes
-
- <Blueprint5.Icon
- autoLoad={true}
- icon="caret-down"
- tagName="span"
- />
- </i>
- </Blueprint5.Popover>
- </React.Fragment>,
- "accessor": "stats",
- "className": "padded",
- "filterable": false,
- "id": "stats",
- "show": true,
- "sortable": false,
- "width": 300,
- },
- {
- "Cell": [Function],
- "Header": "Recent errors",
- "accessor": "status.payload.recentErrors",
- "filterable": false,
- "show": true,
- "sortable": false,
- "width": 150,
- },
+ <Context.Provider
+ value={{}}
+ >
+ <Context.Provider
+ value={
{
- "Cell": [Function],
- "Header": "Actions",
- "accessor": "supervisor_id",
- "filterable": false,
- "id": "actions",
- "sortable": false,
- "width": 70,
- },
- ]
- }
- data={[]}
- defaultExpanded={{}}
- defaultFilterMethod={[Function]}
- defaultFiltered={[]}
- defaultPage={0}
- defaultPageSize={20}
- defaultResized={[]}
- defaultSortDesc={false}
- defaultSortMethod={[Function]}
- defaultSorted={[]}
- expanderDefaults={
- {
- "filterable": false,
- "resizable": false,
- "sortable": false,
- "width": 35,
+ "stats": {},
+ "statsKey": "5m",
+ }
}
- }
- filterable={true}
- filtered={[]}
- freezeWhenExpanded={false}
- getLoadingProps={[Function]}
- getNoDataProps={[Function]}
- getPaginationProps={[Function]}
- getProps={[Function]}
- getResizerProps={[Function]}
- getTableProps={[Function]}
- getTbodyProps={[Function]}
- getTdProps={[Function]}
- getTfootProps={[Function]}
- getTfootTdProps={[Function]}
- getTfootTrProps={[Function]}
- getTheadFilterProps={[Function]}
- getTheadFilterThProps={[Function]}
- getTheadFilterTrProps={[Function]}
- getTheadGroupProps={[Function]}
- getTheadGroupThProps={[Function]}
- getTheadGroupTrProps={[Function]}
- getTheadProps={[Function]}
- getTheadThProps={[Function]}
- getTheadTrProps={[Function]}
- getTrGroupProps={[Function]}
- getTrProps={[Function]}
- groupedByPivotKey="_groupedByPivot"
- indexKey="_index"
- loading={true}
- loadingText="Loading..."
- manual={true}
- multiSort={true}
- nestingLevelKey="_nestingLevel"
- nextText="Next"
- noDataText=""
- ofText=""
- onFetchData={[Function]}
- onFilteredChange={[Function]}
- onPageChange={[Function]}
- onPageSizeChange={[Function]}
- onSortedChange={[Function]}
- originalKey="_original"
- page={0}
- pageJumpText="jump to page"
- pageSize={25}
- pageSizeOptions={
- [
- 25,
- 50,
- 100,
- ]
- }
- pageText="Page"
- pages={10000000}
- pivotDefaults={{}}
- pivotIDKey="_pivotID"
- pivotValKey="_pivotVal"
- previousText="Previous"
- resizable={true}
- resolveData={[Function]}
- rowsSelectorText="rows per page"
- rowsText="rows"
- showPageJump={false}
- showPageSizeOptions={true}
- showPagination={false}
- showPaginationBottom={true}
- showPaginationTop={false}
- sortable={true}
- sorted={[]}
- style={{}}
- subRowsKey="_subRows"
- />
+ >
+ <ReactTable
+ AggregatedComponent={[Function]}
+ ExpanderComponent={[Function]}
+ FilterComponent={[Function]}
+ LoadingComponent={[Function]}
+ NoDataComponent={[Function]}
+ PadRowComponent={[Function]}
+ PaginationComponent={[Function]}
+ PivotValueComponent={[Function]}
+ ResizerComponent={[Function]}
+ TableComponent={[Function]}
+ TbodyComponent={[Function]}
+ TdComponent={[Function]}
+ TfootComponent={[Function]}
+ ThComponent={[Function]}
+ TheadComponent={[Function]}
+ TrComponent={[Function]}
+ TrGroupComponent={[Function]}
+ aggregatedKey="_aggregated"
+ className=""
+ collapseOnDataChange={true}
+ collapseOnPageChange={true}
+ collapseOnSortingChange={true}
+ column={
+ {
+ "Aggregated": undefined,
+ "Cell": undefined,
+ "Expander": undefined,
+ "Filter": undefined,
+ "Footer": undefined,
+ "Header": undefined,
+ "Pivot": undefined,
+ "PivotValue": undefined,
+ "Placeholder": undefined,
+ "aggregate": undefined,
+ "className": "",
+ "filterAll": false,
+ "filterMethod": undefined,
+ "filterable": undefined,
+ "footerClassName": "",
+ "footerStyle": {},
+ "getFooterProps": [Function],
+ "getHeaderProps": [Function],
+ "getProps": [Function],
+ "headerClassName": "",
+ "headerStyle": {},
+ "minResizeWidth": 11,
+ "minWidth": 100,
+ "resizable": undefined,
+ "show": true,
+ "sortMethod": undefined,
+ "sortable": undefined,
+ "style": {},
+ }
+ }
+ columns={
+ [
+ {
+ "Cell": [Function],
+ "Header": <React.Fragment>
+ Supervisor ID
+ <br />
+ <i>
+ (datasource)
+ </i>
+ </React.Fragment>,
+ "accessor": "supervisor_id",
+ "id": "supervisor_id",
+ "show": true,
+ "width": 280,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Type",
+ "accessor": "type",
+ "show": true,
+ "width": 120,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Topic/Stream",
+ "accessor": "source",
+ "show": true,
+ "width": 200,
+ },
+ {
+ "Cell": [Function],
+ "Filter": [Function],
+ "Header": "Status",
+ "accessor": "detailed_state",
+ "id": "detailed_state",
+ "show": true,
+ "width": 170,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Configured tasks",
+ "accessor": "spec",
+ "className": "padded",
+ "filterable": false,
+ "id": "configured_tasks",
+ "show": true,
+ "sortable": false,
+ "width": 130,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Running tasks",
+ "accessor": "supervisor_id",
+ "filterable": false,
+ "id": "running_tasks",
+ "show": true,
+ "sortable": false,
+ "width": 150,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Aggregate lag",
+ "accessor": "supervisor_id",
+ "className": "padded",
+ "filterable": false,
+ "show": true,
+ "sortable": false,
+ "width": 200,
+ },
+ {
+ "Cell": [Function],
+ "Header": <React.Fragment>
+ Stats
+ <br />
+ <HeaderStatsKeySelector
+ changeStatsKey={[Function]}
+ />
+ </React.Fragment>,
+ "accessor": "supervisor_id",
+ "className": "padded",
+ "filterable": false,
+ "id": "stats",
+ "show": true,
+ "sortable": false,
+ "width": 300,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Recent errors",
+ "accessor": "supervisor_id",
+ "filterable": false,
+ "show": true,
+ "sortable": false,
+ "width": 150,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Actions",
+ "accessor": "supervisor_id",
+ "filterable": false,
+ "id": "actions",
+ "sortable": false,
+ "width": 70,
+ },
+ ]
+ }
+ data={[]}
+ defaultExpanded={{}}
+ defaultFilterMethod={[Function]}
+ defaultFiltered={[]}
+ defaultPage={0}
+ defaultPageSize={20}
+ defaultResized={[]}
+ defaultSortDesc={false}
+ defaultSortMethod={[Function]}
+ defaultSorted={[]}
+ expanderDefaults={
+ {
+ "filterable": false,
+ "resizable": false,
+ "sortable": false,
+ "width": 35,
+ }
+ }
+ filterable={true}
+ filtered={[]}
+ freezeWhenExpanded={false}
+ getLoadingProps={[Function]}
+ getNoDataProps={[Function]}
+ getPaginationProps={[Function]}
+ getProps={[Function]}
+ getResizerProps={[Function]}
+ getTableProps={[Function]}
+ getTbodyProps={[Function]}
+ getTdProps={[Function]}
+ getTfootProps={[Function]}
+ getTfootTdProps={[Function]}
+ getTfootTrProps={[Function]}
+ getTheadFilterProps={[Function]}
+ getTheadFilterThProps={[Function]}
+ getTheadFilterTrProps={[Function]}
+ getTheadGroupProps={[Function]}
+ getTheadGroupThProps={[Function]}
+ getTheadGroupTrProps={[Function]}
+ getTheadProps={[Function]}
+ getTheadThProps={[Function]}
+ getTheadTrProps={[Function]}
+ getTrGroupProps={[Function]}
+ getTrProps={[Function]}
+ groupedByPivotKey="_groupedByPivot"
+ indexKey="_index"
+ loading={true}
+ loadingText="Loading..."
+ manual={true}
+ multiSort={true}
+ nestingLevelKey="_nestingLevel"
+ nextText="Next"
+ noDataText=""
+ ofText=""
+ onFetchData={[Function]}
+ onFilteredChange={[Function]}
+ onPageChange={[Function]}
+ onPageSizeChange={[Function]}
+ onSortedChange={[Function]}
+ originalKey="_original"
+ page={0}
+ pageJumpText="jump to page"
+ pageSize={25}
+ pageSizeOptions={
+ [
+ 25,
+ 50,
+ 100,
+ ]
+ }
+ pageText="Page"
+ pages={10000000}
+ pivotDefaults={{}}
+ pivotIDKey="_pivotID"
+ pivotValKey="_pivotVal"
+ previousText="Previous"
+ resizable={true}
+ resolveData={[Function]}
+ rowsSelectorText="rows per page"
+ rowsText="rows"
+ showPageJump={false}
+ showPageSizeOptions={true}
+ showPagination={true}
+ showPaginationBottom={true}
+ showPaginationTop={false}
+ sortable={true}
+ sorted={[]}
+ style={{}}
+ subRowsKey="_subRows"
+ />
+ </Context.Provider>
+ </Context.Provider>
<Memo(AlertDialog)
confirmButtonText="OK"
icon="error"
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx
b/web-console/src/views/supervisors-view/supervisors-view.tsx
index e8576865964..382066a7014 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -19,9 +19,10 @@
import { Icon, Intent, Menu, MenuItem, Popover, Position, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import * as JSONBig from 'json-bigint-native';
+import memoize from 'memoize-one';
import type { JSX } from 'react';
-import React from 'react';
-import type { Filter, SortingRule } from 'react-table';
+import React, { createContext, useContext } from 'react';
+import type { Column, Filter, SortingRule } from 'react-table';
import ReactTable from 'react-table';
import type { TableColumnSelectorColumn } from '../../components';
@@ -41,14 +42,15 @@ import {
AlertDialog,
AsyncActionDialog,
SpecDialog,
+ SupervisorResetOffsetsDialog,
SupervisorTableActionDialog,
+ TaskGroupHandoffDialog,
} from '../../dialogs';
-import { SupervisorResetOffsetsDialog } from
'../../dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog';
-import { TaskGroupHandoffDialog } from
'../../dialogs/task-group-handoff-dialog/task-group-handoff-dialog';
import type {
IngestionSpec,
QueryWithContext,
RowStatsKey,
+ SupervisorStats,
SupervisorStatus,
SupervisorStatusTask,
} from '../../druid-models';
@@ -64,7 +66,6 @@ import { Api, AppToaster } from '../../singletons';
import type { AuxiliaryQueryFn, TableState } from '../../utils';
import {
assemble,
- changeByIndex,
checkedCircleIcon,
deepGet,
filterMap,
@@ -128,8 +129,48 @@ interface SupervisorQueryResultRow {
readonly detailed_state: string;
readonly spec?: IngestionSpec;
readonly suspended: boolean;
- readonly status?: SupervisorStatus;
- readonly stats?: any;
+}
+
+interface SupervisorsWithAuxiliaryInfo {
+ readonly supervisors: SupervisorQueryResultRow[];
+ readonly count: number;
+ readonly status: Record<string, SupervisorStatus>;
+ readonly stats: Record<string, SupervisorStats>;
+}
+
+export const StatusContext = createContext<Record<string,
SupervisorStatus>>({});
+export const StatsContext = createContext<{
+ stats: Record<string, SupervisorStats>;
+ statsKey: RowStatsKey;
+}>({ stats: {}, statsKey: '5m' });
+
+interface HeaderStatsKeySelectorProps {
+ changeStatsKey(statsKey: RowStatsKey): void;
+}
+
+function HeaderStatsKeySelector({ changeStatsKey }:
HeaderStatsKeySelectorProps) {
+ const { statsKey } = useContext(StatsContext);
+ return (
+ <Popover
+ position={Position.BOTTOM}
+ content={
+ <Menu>
+ {ROW_STATS_KEYS.map(k => (
+ <MenuItem
+ key={k}
+ icon={checkedCircleIcon(k === statsKey)}
+ text={getRowStatsKeyTitle(k)}
+ onClick={() => changeStatsKey(k)}
+ />
+ ))}
+ </Menu>
+ }
+ >
+ <i className="title-button">
+ {getRowStatsKeyTitle(statsKey)} <Icon icon={IconNames.CARET_DOWN} />
+ </i>
+ </Popover>
+ );
}
export interface SupervisorsViewProps {
@@ -144,7 +185,7 @@ export interface SupervisorsViewProps {
}
export interface SupervisorsViewState {
- supervisorsState: QueryState<SupervisorQueryResultRow[]>;
+ supervisorsState: QueryState<SupervisorsWithAuxiliaryInfo>;
statsKey: RowStatsKey;
resumeSupervisorId?: string;
@@ -209,7 +250,7 @@ export class SupervisorsView extends React.PureComponent<
> {
private readonly supervisorQueryManager: QueryManager<
SupervisorQuery,
- SupervisorQueryResultRow[]
+ SupervisorsWithAuxiliaryInfo
>;
constructor(props: SupervisorsViewProps) {
@@ -242,6 +283,8 @@ export class SupervisorsView extends React.PureComponent<
setIntermediateQuery,
) => {
let supervisors: SupervisorQueryResultRow[];
+ let count = -1;
+ const auxiliaryQueries:
AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo>[] = [];
if (capabilities.hasSql()) {
const whereExpression = sqlQueryCustomTableFilters(filtered);
@@ -279,6 +322,26 @@ export class SupervisorsView extends React.PureComponent<
if (typeof spec !== 'string') return supervisor;
return { ...supervisor, spec: JSONBig.parse(spec) };
});
+
+ auxiliaryQueries.push(async (supervisorsWithAuxiliaryInfo,
cancelToken) => {
+ const sqlQuery = assemble(
+ 'SELECT COUNT(*) AS "cnt"',
+ 'FROM "sys"."supervisors"',
+ filterClause ? `WHERE ${filterClause}` : undefined,
+ ).join('\n');
+ const cnt: any = (
+ await queryDruidSql<{ cnt: number }>(
+ {
+ query: sqlQuery,
+ },
+ cancelToken,
+ )
+ )[0].cnt;
+ return {
+ ...supervisorsWithAuxiliaryInfo,
+ count: typeof cnt === 'number' ? cnt : -1,
+ };
+ });
} else if (capabilities.hasOverlordAccess()) {
supervisors = (await
getApiArray('/druid/indexer/v1/supervisor?full', cancelToken)).map(
(sup: any) => {
@@ -296,6 +359,7 @@ export class SupervisorsView extends React.PureComponent<
};
},
);
+ count = supervisors.length;
const firstSorted = sorted[0];
if (firstSorted) {
@@ -311,13 +375,12 @@ export class SupervisorsView extends React.PureComponent<
throw new Error(`must have SQL or overlord access`);
}
- const auxiliaryQueries: AuxiliaryQueryFn<SupervisorQueryResultRow[]>[]
= [];
if (capabilities.hasOverlordAccess()) {
if (visibleColumns.shown('Running tasks', 'Aggregate lag', 'Recent
errors')) {
auxiliaryQueries.push(
...supervisors.map(
- (supervisor, i): AuxiliaryQueryFn<SupervisorQueryResultRow[]>
=>
- async (rows, cancelToken) => {
+ (supervisor): AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo> =>
+ async (supervisorsWithAuxiliaryInfo, cancelToken) => {
const status = (
await Api.instance.get(
`/druid/indexer/v1/supervisor/${Api.encodePath(
@@ -326,7 +389,13 @@ export class SupervisorsView extends React.PureComponent<
{ cancelToken, timeout: STATUS_API_TIMEOUT },
)
).data;
- return changeByIndex(rows, i, row => ({ ...row, status }));
+ return {
+ ...supervisorsWithAuxiliaryInfo,
+ status: {
+ ...supervisorsWithAuxiliaryInfo.status,
+ [supervisor.supervisor_id]: status,
+ },
+ };
},
),
);
@@ -336,9 +405,9 @@ export class SupervisorsView extends React.PureComponent<
auxiliaryQueries.push(
...filterMap(
supervisors,
- (supervisor, i): AuxiliaryQueryFn<SupervisorQueryResultRow[]>
| undefined => {
+ (supervisor): AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo> |
undefined => {
if (oneOf(supervisor.type, 'autocompact',
'scheduled_batch')) return; // These supervisors do not report stats
- return async (rows, cancelToken) => {
+ return async (supervisorsWithAuxiliaryInfo, cancelToken) => {
const stats = (
await Api.instance.get(
`/druid/indexer/v1/supervisor/${Api.encodePath(
@@ -347,7 +416,13 @@ export class SupervisorsView extends React.PureComponent<
{ cancelToken, timeout: STATS_API_TIMEOUT },
)
).data;
- return changeByIndex(rows, i, row => ({ ...row, stats }));
+ return {
+ ...supervisorsWithAuxiliaryInfo,
+ stats: {
+ ...supervisorsWithAuxiliaryInfo.stats,
+ [supervisor.supervisor_id]: stats,
+ },
+ };
};
},
),
@@ -355,7 +430,10 @@ export class SupervisorsView extends React.PureComponent<
}
}
- return new ResultWithAuxiliaryWork(supervisors, auxiliaryQueries);
+ return new ResultWithAuxiliaryWork<SupervisorsWithAuxiliaryInfo>(
+ { supervisors, count, status: {}, stats: {} },
+ auxiliaryQueries,
+ );
},
onStateChange: supervisorsState => {
this.setState({
@@ -402,6 +480,17 @@ export class SupervisorsView extends React.PureComponent<
});
};
+ private readonly handleFilterChange = (filters: Filter[]) => {
+ this.goToFirstPage();
+ this.props.onFiltersChange(filters);
+ };
+
+ private goToFirstPage() {
+ if (this.state.page) {
+ this.setState({ page: 0 });
+ }
+ }
+
private readonly closeSpecDialogs = () => {
this.setState({
supervisorSpecDialogOpen: false,
@@ -662,7 +751,8 @@ export class SupervisorsView extends React.PureComponent<
}
private renderSupervisorFilterableCell(field: string) {
- const { filters, onFiltersChange } = this.props;
+ const { filters } = this.props;
+ const { handleFilterChange } = this;
return function SupervisorFilterableCell(row: { value: any }) {
return (
@@ -670,10 +760,8 @@ export class SupervisorsView extends React.PureComponent<
field={field}
value={row.value}
filters={filters}
- onFiltersChange={onFiltersChange}
- >
- {row.value}
- </TableFilterableCell>
+ onFiltersChange={handleFilterChange}
+ />
);
};
}
@@ -711,314 +799,320 @@ export class SupervisorsView extends
React.PureComponent<
}
private renderSupervisorTable() {
- const { filters, onFiltersChange } = this.props;
- const { supervisorsState, statsKey, visibleColumns, page, pageSize, sorted
} = this.state;
+ const { filters } = this.props;
+ const { supervisorsState, page, pageSize, sorted, statsKey, visibleColumns
} = this.state;
- const supervisors = supervisorsState.data || [];
+ const { supervisors, count, status, stats } = supervisorsState.data || {
+ supervisors: [],
+ count: -1,
+ status: {},
+ stats: {},
+ };
return (
- <ReactTable
- data={supervisors}
- pages={10000000} // Dummy, we are hiding the page selector
- loading={supervisorsState.loading}
- noDataText={
- supervisorsState.isEmpty() ? 'No supervisors' :
supervisorsState.getErrorMessage() || ''
- }
- manual
- filterable
- filtered={filters}
- onFilteredChange={onFiltersChange}
- sorted={sorted}
- onSortedChange={sorted => this.setState({ sorted })}
- page={page}
- onPageChange={page => this.setState({ page })}
- pageSize={pageSize}
- onPageSizeChange={pageSize => this.setState({ pageSize })}
- pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
- showPagination={supervisors.length >= SMALL_TABLE_PAGE_SIZE || page >
0}
- showPageJump={false}
- ofText=""
- columns={[
- {
- Header: twoLines('Supervisor ID', <i>(datasource)</i>),
- id: 'supervisor_id',
- accessor: 'supervisor_id',
- width: 280,
- show: visibleColumns.shown('Supervisor ID'),
- Cell: ({ value, original }) => (
- <TableClickableCell
- tooltip="Show detail"
- onClick={() => this.onSupervisorDetail(original)}
- hoverIcon={IconNames.SEARCH_TEMPLATE}
- >
+ <StatusContext.Provider value={status}>
+ <StatsContext.Provider value={{ stats, statsKey }}>
+ <ReactTable
+ data={supervisors}
+ pages={count >= 0 ? Math.ceil(count / pageSize) : 10000000} // We
are hiding the page selector
+ loading={supervisorsState.loading}
+ noDataText={
+ supervisorsState.isEmpty()
+ ? 'No supervisors'
+ : supervisorsState.getErrorMessage() || ''
+ }
+ manual
+ filterable
+ filtered={filters}
+ onFilteredChange={this.handleFilterChange}
+ sorted={sorted}
+ onSortedChange={sorted => this.setState({ sorted })}
+ page={page}
+ onPageChange={page => this.setState({ page })}
+ pageSize={pageSize}
+ onPageSizeChange={pageSize => this.setState({ pageSize })}
+ pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
+ showPagination
+ showPageJump={false}
+ ofText={count >= 0 ? `of ${formatInteger(count)}` : ''}
+ columns={this.getTableColumns(visibleColumns, filters)}
+ />
+ </StatsContext.Provider>
+ </StatusContext.Provider>
+ );
+ }
+
+ private readonly getTableColumns = memoize(
+ (
+ visibleColumns: LocalStorageBackedVisibility,
+ filters: Filter[],
+ ): Column<SupervisorQueryResultRow>[] => {
+ return [
+ {
+ Header: twoLines('Supervisor ID', <i>(datasource)</i>),
+ id: 'supervisor_id',
+ accessor: 'supervisor_id',
+ width: 280,
+ show: visibleColumns.shown('Supervisor ID'),
+ Cell: ({ value, original }) => (
+ <TableClickableCell
+ tooltip="Show detail"
+ onClick={() => this.onSupervisorDetail(original)}
+ hoverIcon={IconNames.SEARCH_TEMPLATE}
+ >
+ {value}
+ </TableClickableCell>
+ ),
+ },
+ {
+ Header: 'Type',
+ accessor: 'type',
+ width: 120,
+ Cell: this.renderSupervisorFilterableCell('type'),
+ show: visibleColumns.shown('Type'),
+ },
+ {
+ Header: 'Topic/Stream',
+ accessor: 'source',
+ width: 200,
+ Cell: this.renderSupervisorFilterableCell('source'),
+ show: visibleColumns.shown('Topic/Stream'),
+ },
+ {
+ Header: 'Status',
+ id: 'detailed_state',
+ width: 170,
+ Filter: suggestibleFilterInput([
+ 'CONNECTING_TO_STREAM',
+ 'CREATING_TASKS',
+ 'DISCOVERING_INITIAL_TASKS',
+ 'IDLE',
+ 'INVALID_SPEC',
+ 'LOST_CONTACT_WITH_STREAM',
+ 'PENDING',
+ 'RUNNING',
+ 'SCHEDULER_STOPPED',
+ 'STOPPING',
+ 'SUSPENDED',
+ 'UNABLE_TO_CONNECT_TO_STREAM',
+ 'UNHEALTHY_SUPERVISOR',
+ 'UNHEALTHY_TASKS',
+ ]),
+ accessor: 'detailed_state',
+ Cell: ({ value }) => (
+ <TableFilterableCell
+ field="detailed_state"
+ value={value}
+ filters={filters}
+ onFiltersChange={this.handleFilterChange}
+ >
+ <span>
+ <span style={{ color: detailedStateToColor(value)
}}>● </span>
{value}
- </TableClickableCell>
- ),
- },
- {
- Header: 'Type',
- accessor: 'type',
- width: 120,
- Cell: this.renderSupervisorFilterableCell('type'),
- show: visibleColumns.shown('Type'),
- },
- {
- Header: 'Topic/Stream',
- accessor: 'source',
- width: 200,
- Cell: this.renderSupervisorFilterableCell('source'),
- show: visibleColumns.shown('Topic/Stream'),
- },
- {
- Header: 'Status',
- id: 'detailed_state',
- width: 170,
- Filter: suggestibleFilterInput([
- 'CONNECTING_TO_STREAM',
- 'CREATING_TASKS',
- 'DISCOVERING_INITIAL_TASKS',
- 'IDLE',
- 'INVALID_SPEC',
- 'LOST_CONTACT_WITH_STREAM',
- 'PENDING',
- 'RUNNING',
- 'SCHEDULER_STOPPED',
- 'STOPPING',
- 'SUSPENDED',
- 'UNABLE_TO_CONNECT_TO_STREAM',
- 'UNHEALTHY_SUPERVISOR',
- 'UNHEALTHY_TASKS',
- ]),
- accessor: 'detailed_state',
- Cell: ({ value }) => (
- <TableFilterableCell
- field="detailed_state"
- value={value}
- filters={filters}
- onFiltersChange={onFiltersChange}
- >
- <span>
- <span style={{ color: detailedStateToColor(value)
}}>● </span>
- {value}
- </span>
- </TableFilterableCell>
- ),
- show: visibleColumns.shown('Status'),
- },
- {
- Header: 'Configured tasks',
- id: 'configured_tasks',
- width: 130,
- accessor: 'spec',
- filterable: false,
- sortable: false,
- className: 'padded',
- Cell: ({ value }) => {
- if (!value) return null;
- const taskCount = deepGet(value, 'spec.ioConfig.taskCount');
- const replicas = deepGet(value, 'spec.ioConfig.replicas');
- if (typeof taskCount !== 'number' || typeof replicas !==
'number') return null;
- return (
- <div>
- <div>{formatInteger(taskCount * replicas)}</div>
- <div className="detail-line">
- {replicas === 1
- ? '(no replication)'
- : `(${pluralIfNeeded(taskCount, 'task')} ×
${pluralIfNeeded(
- replicas,
- 'replica',
- )})`}
- </div>
+ </span>
+ </TableFilterableCell>
+ ),
+ show: visibleColumns.shown('Status'),
+ },
+ {
+ Header: 'Configured tasks',
+ id: 'configured_tasks',
+ width: 130,
+ accessor: 'spec',
+ filterable: false,
+ sortable: false,
+ className: 'padded',
+ Cell: ({ value }) => {
+ if (!value) return null;
+ const taskCount = deepGet(value, 'spec.ioConfig.taskCount');
+ const replicas = deepGet(value, 'spec.ioConfig.replicas');
+ if (typeof taskCount !== 'number' || typeof replicas !== 'number')
return null;
+ return (
+ <div>
+ <div>{formatInteger(taskCount * replicas)}</div>
+ <div className="detail-line">
+ {replicas === 1
+ ? '(no replication)'
+ : `(${pluralIfNeeded(taskCount, 'task')} ×
${pluralIfNeeded(
+ replicas,
+ 'replica',
+ )})`}
</div>
- );
- },
- show: visibleColumns.shown('Configured tasks'),
+ </div>
+ );
},
- {
- Header: 'Running tasks',
- id: 'running_tasks',
- width: 150,
- accessor: 'status.payload',
- filterable: false,
- sortable: false,
- Cell: ({ value, original }) => {
- if (original.suspended) return;
- let label: string | JSX.Element;
- const { activeTasks, publishingTasks } = value || {};
- if (Array.isArray(activeTasks)) {
- label = pluralIfNeeded(activeTasks.length, 'active task');
- if (nonEmptyArray(publishingTasks)) {
- label = (
- <>
- <div>{label}</div>
- <div>{pluralIfNeeded(publishingTasks.length, 'publishing
task')}</div>
- </>
- );
- }
- } else {
- label = '';
+ show: visibleColumns.shown('Configured tasks'),
+ },
+ {
+ Header: 'Running tasks',
+ id: 'running_tasks',
+ accessor: 'supervisor_id',
+ width: 150,
+ filterable: false,
+ sortable: false,
+ Cell: ({ value, original }) => {
+ const status = useContext(StatusContext);
+ if (original.suspended) return;
+ const { activeTasks, publishingTasks } = status[value]?.payload ||
{};
+ let label: string | JSX.Element;
+ if (Array.isArray(activeTasks)) {
+ label = pluralIfNeeded(activeTasks.length, 'active task');
+ if (nonEmptyArray(publishingTasks)) {
+ label = (
+ <>
+ <div>{label}</div>
+ <div>{pluralIfNeeded(publishingTasks.length, 'publishing
task')}</div>
+ </>
+ );
}
+ } else {
+ label = '';
+ }
- return (
- <TableClickableCell
- tooltip="Go to tasks"
- onClick={() => this.goToTasksForSupervisor(original)}
- hoverIcon={IconNames.ARROW_TOP_RIGHT}
- >
- {label}
- </TableClickableCell>
- );
- },
- show: visibleColumns.shown('Running tasks'),
+ return (
+ <TableClickableCell
+ tooltip="Go to tasks"
+ onClick={() => this.goToTasksForSupervisor(original)}
+ hoverIcon={IconNames.ARROW_TOP_RIGHT}
+ >
+ {label}
+ </TableClickableCell>
+ );
},
- {
- Header: 'Aggregate lag',
- accessor: 'status.payload.aggregateLag',
- width: 200,
- filterable: false,
- sortable: false,
- className: 'padded',
- show: visibleColumns.shown('Aggregate lag'),
- Cell: ({ value }) => (isNumberLike(value) ? formatInteger(value) :
null),
+ show: visibleColumns.shown('Running tasks'),
+ },
+ {
+ Header: 'Aggregate lag',
+ accessor: 'supervisor_id',
+ width: 200,
+ filterable: false,
+ sortable: false,
+ className: 'padded',
+ show: visibleColumns.shown('Aggregate lag'),
+ Cell: ({ value }) => {
+ const status = useContext(StatusContext);
+ const aggregateLag = status[value]?.payload?.aggregateLag;
+ return isNumberLike(aggregateLag) ? formatInteger(aggregateLag) :
null;
},
- {
- Header: twoLines(
- 'Stats',
- <Popover
- position={Position.BOTTOM}
- content={
- <Menu>
- {ROW_STATS_KEYS.map(k => (
- <MenuItem
- key={k}
- icon={checkedCircleIcon(k === statsKey)}
- text={getRowStatsKeyTitle(k)}
- onClick={() => {
- this.setState({ statsKey: k });
- }}
- />
- ))}
- </Menu>
- }
- >
- <i className="title-button">
- {getRowStatsKeyTitle(statsKey)} <Icon
icon={IconNames.CARET_DOWN} />
- </i>
- </Popover>,
- ),
- id: 'stats',
- width: 300,
- filterable: false,
- sortable: false,
- className: 'padded',
- accessor: 'stats',
- Cell: ({ value, original }) => {
- if (!value) return;
- const activeTasks: SupervisorStatusTask[] | undefined = deepGet(
- original,
- 'status.payload.activeTasks',
- );
- const activeTaskIds: string[] | undefined =
Array.isArray(activeTasks)
- ? activeTasks.map((t: SupervisorStatusTask) => t.id)
- : undefined;
-
- const c = getTotalSupervisorStats(value, statsKey,
activeTaskIds);
- const seconds = getRowStatsKeySeconds(statsKey);
- const issues =
- (c.processedWithError || 0) + (c.thrownAway || 0) +
(c.unparseable || 0);
- const totalLabel = `Total (past ${statsKey}): `;
- return issues ? (
- <div>
- <div
- data-tooltip={`${totalLabel}${formatBytes(c.processedBytes
* seconds)}`}
- >{`Input: ${formatByteRate(c.processedBytes)}`}</div>
- {Boolean(c.processed) && (
- <div
- data-tooltip={`${totalLabel}${formatInteger(c.processed
* seconds)} events`}
- >{`Processed: ${formatRate(c.processed)}`}</div>
- )}
- {Boolean(c.processedWithError) && (
- <div
- className="warning-line"
- data-tooltip={`${totalLabel}${formatInteger(
- c.processedWithError * seconds,
- )} events`}
- >
- Processed with error: {formatRate(c.processedWithError)}
- </div>
- )}
- {Boolean(c.thrownAway) && (
- <div
- className="warning-line"
- data-tooltip={`${totalLabel}${formatInteger(c.thrownAway
* seconds)} events`}
- >
- Thrown away: {formatRate(c.thrownAway)}
- </div>
- )}
- {Boolean(c.unparseable) && (
- <div
- className="warning-line"
-
data-tooltip={`${totalLabel}${formatInteger(c.unparseable * seconds)} events`}
- >
- Unparseable: {formatRate(c.unparseable)}
- </div>
- )}
- </div>
- ) : c.processedBytes ? (
+ },
+ {
+ Header: twoLines(
+ 'Stats',
+ <HeaderStatsKeySelector changeStatsKey={statsKey =>
this.setState({ statsKey })} />,
+ ),
+ id: 'stats',
+ accessor: 'supervisor_id',
+ width: 300,
+ filterable: false,
+ sortable: false,
+ className: 'padded',
+ show: visibleColumns.shown('Stats'),
+ Cell: ({ value, original }) => {
+ const { stats, statsKey } = useContext(StatsContext);
+ const s = stats[value];
+ if (!s) return;
+ const activeTasks: SupervisorStatusTask[] | undefined = deepGet(
+ original,
+ 'status.payload.activeTasks',
+ );
+ const activeTaskIds: string[] | undefined =
Array.isArray(activeTasks)
+ ? activeTasks.map((t: SupervisorStatusTask) => t.id)
+ : undefined;
+
+ const c = getTotalSupervisorStats(s, statsKey, activeTaskIds);
+ const seconds = getRowStatsKeySeconds(statsKey);
+ const issues = (c.processedWithError || 0) + (c.thrownAway || 0) +
(c.unparseable || 0);
+ const totalLabel = `Total (past ${statsKey}): `;
+ return issues ? (
+ <div>
<div
- data-tooltip={`${totalLabel}${formatInteger(
- c.processed * seconds,
- )} events, ${formatBytes(c.processedBytes * seconds)}`}
- >{`Processed: ${formatRate(c.processed)} (${formatByteRate(
- c.processedBytes,
- )})`}</div>
- ) : (
- <div>No activity</div>
- );
- },
- show: visibleColumns.shown('Stats'),
+ data-tooltip={`${totalLabel}${formatBytes(c.processedBytes *
seconds)}`}
+ >{`Input: ${formatByteRate(c.processedBytes)}`}</div>
+ {Boolean(c.processed) && (
+ <div
+ data-tooltip={`${totalLabel}${formatInteger(c.processed *
seconds)} events`}
+ >{`Processed: ${formatRate(c.processed)}`}</div>
+ )}
+ {Boolean(c.processedWithError) && (
+ <div
+ className="warning-line"
+ data-tooltip={`${totalLabel}${formatInteger(
+ c.processedWithError * seconds,
+ )} events`}
+ >
+ Processed with error: {formatRate(c.processedWithError)}
+ </div>
+ )}
+ {Boolean(c.thrownAway) && (
+ <div
+ className="warning-line"
+ data-tooltip={`${totalLabel}${formatInteger(c.thrownAway *
seconds)} events`}
+ >
+ Thrown away: {formatRate(c.thrownAway)}
+ </div>
+ )}
+ {Boolean(c.unparseable) && (
+ <div
+ className="warning-line"
+ data-tooltip={`${totalLabel}${formatInteger(c.unparseable
* seconds)} events`}
+ >
+ Unparseable: {formatRate(c.unparseable)}
+ </div>
+ )}
+ </div>
+ ) : c.processedBytes ? (
+ <div
+ data-tooltip={`${totalLabel}${formatInteger(
+ c.processed * seconds,
+ )} events, ${formatBytes(c.processedBytes * seconds)}`}
+ >{`Processed: ${formatRate(c.processed)}
(${formatByteRate(c.processedBytes)})`}</div>
+ ) : (
+ <div>No activity</div>
+ );
},
- {
- Header: 'Recent errors',
- accessor: 'status.payload.recentErrors',
- width: 150,
- filterable: false,
- sortable: false,
- show: visibleColumns.shown('Recent errors'),
- Cell: ({ value, original }) => {
- if (!value) return null;
- return (
- <TableClickableCell
- tooltip="Show errors"
- onClick={() => this.onSupervisorDetail(original)}
- hoverIcon={IconNames.SEARCH_TEMPLATE}
- >
- {pluralIfNeeded(value.length, 'error')}
- </TableClickableCell>
- );
- },
+ },
+ {
+ Header: 'Recent errors',
+ accessor: 'supervisor_id',
+ width: 150,
+ filterable: false,
+ sortable: false,
+ show: visibleColumns.shown('Recent errors'),
+ Cell: ({ value, original }) => {
+ const status = useContext(StatusContext);
+ const recentErrors = status[value]?.payload?.recentErrors;
+ if (!recentErrors) return null;
+ return (
+ <TableClickableCell
+ tooltip="Show errors"
+ onClick={() => this.onSupervisorDetail(original)}
+ hoverIcon={IconNames.SEARCH_TEMPLATE}
+ >
+ {pluralIfNeeded(recentErrors.length, 'error')}
+ </TableClickableCell>
+ );
},
- {
- Header: ACTION_COLUMN_LABEL,
- id: ACTION_COLUMN_ID,
- accessor: 'supervisor_id',
- width: ACTION_COLUMN_WIDTH,
- filterable: false,
- sortable: false,
- Cell: ({ value: id, original }) => {
- const supervisorActions = this.getSupervisorActions(original);
- return (
- <ActionCell
- onDetail={() => this.onSupervisorDetail(original)}
- actions={supervisorActions}
- menuTitle={id}
- />
- );
- },
+ },
+ {
+ Header: ACTION_COLUMN_LABEL,
+ id: ACTION_COLUMN_ID,
+ accessor: 'supervisor_id',
+ width: ACTION_COLUMN_WIDTH,
+ filterable: false,
+ sortable: false,
+ Cell: ({ value: id, original }) => {
+ const supervisorActions = this.getSupervisorActions(original);
+ return (
+ <ActionCell
+ onDetail={() => this.onSupervisorDetail(original)}
+ actions={supervisorActions}
+ menuTitle={id}
+ />
+ );
},
- ]}
- />
- );
- }
+ },
+ ];
+ },
+ );
renderBulkSupervisorActions() {
const { capabilities, goToQuery } = this.props;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]