Web console: add ability to issue auxiliary queries to speed up data views (#16952)

* Add ability to issue auxiliary queries

* readonly supervisor

* return

* update snapshot

* fix classes
This commit is contained in:
Vadim Ogievetsky 2024-08-27 13:38:30 -07:00 committed by GitHub
parent 0caf383102
commit 21dcf804eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 387 additions and 240 deletions

View File

@ -33,7 +33,7 @@
} }
} }
.#{$bp-ns}-text-muted .#{$bp-ns}-popover2-target { .#{$bp-ns}-text-muted .#{$bp-ns}-popover-target {
margin-top: 0; margin-top: 0;
} }
@ -48,7 +48,7 @@
} }
} }
.#{$bp-ns}-popover2-content { .#{$bp-ns}-popover-content {
.code-block { .code-block {
white-space: pre; white-space: pre;
overflow: auto; overflow: auto;

View File

@ -21,7 +21,7 @@
.formatted-input { .formatted-input {
position: relative; position: relative;
& > .#{$bp-ns}-popover2-target { & > .#{$bp-ns}-popover-target {
position: absolute; position: absolute;
width: 0; width: 0;
right: 0; right: 0;

View File

@ -111,7 +111,7 @@
width: 100%; width: 100%;
} }
.#{$bp-ns}-popover2-target { .#{$bp-ns}-popover-target {
width: 100%; width: 100%;
} }
} }

View File

@ -24,7 +24,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
&.#{$ns}-popover2-target { &.#{$ns}-popover-target {
display: block; // extra nesting for stronger CSS selectors display: block; // extra nesting for stronger CSS selectors
} }
} }

View File

@ -381,6 +381,14 @@ export function findMap<T, Q>(
return filterMap(xs, f)[0]; return filterMap(xs, f)[0];
} }
export function changeByIndex<T>(
xs: readonly T[],
i: number,
f: (x: T, i: number) => T | undefined,
): T[] {
return filterMap(xs, (x, j) => (j === i ? f(x, i) : x));
}
export function compact<T>(xs: (T | undefined | false | null | '')[]): T[] { export function compact<T>(xs: (T | undefined | false | null | '')[]): T[] {
return xs.filter(Boolean) as T[]; return xs.filter(Boolean) as T[];
} }

View File

@ -24,14 +24,12 @@ export * from './druid-lookup';
export * from './druid-query'; export * from './druid-query';
export * from './formatter'; export * from './formatter';
export * from './general'; export * from './general';
export * from './intermediate-query-state';
export * from './local-storage-backed-visibility'; export * from './local-storage-backed-visibility';
export * from './local-storage-keys'; export * from './local-storage-keys';
export * from './null-mode-detection'; export * from './null-mode-detection';
export * from './object-change'; export * from './object-change';
export * from './query-action'; export * from './query-action';
export * from './query-manager'; export * from './query-manager';
export * from './query-state';
export * from './sanitizers'; export * from './sanitizers';
export * from './sql'; export * from './sql';
export * from './table-helpers'; export * from './table-helpers';

View File

@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './intermediate-query-state';
export * from './query-manager';
export * from './query-state';
export * from './result-with-auxiliary-work';

View File

@ -20,9 +20,11 @@ import type { Canceler, CancelToken } from 'axios';
import axios from 'axios'; import axios from 'axios';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { wait } from './general'; import { wait } from '../general';
import { IntermediateQueryState } from './intermediate-query-state'; import { IntermediateQueryState } from './intermediate-query-state';
import { QueryState } from './query-state'; import { QueryState } from './query-state';
import { ResultWithAuxiliaryWork } from './result-with-auxiliary-work';
export interface QueryManagerOptions<Q, R, I = never, E extends Error = Error> { export interface QueryManagerOptions<Q, R, I = never, E extends Error = Error> {
initState?: QueryState<R, E, I>; initState?: QueryState<R, E, I>;
@ -30,12 +32,12 @@ export interface QueryManagerOptions<Q, R, I = never, E extends Error = Error> {
query: Q, query: Q,
cancelToken: CancelToken, cancelToken: CancelToken,
setIntermediateQuery: (intermediateQuery: any) => void, setIntermediateQuery: (intermediateQuery: any) => void,
) => Promise<R | IntermediateQueryState<I>>; ) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
backgroundStatusCheck?: ( backgroundStatusCheck?: (
state: I, state: I,
query: Q, query: Q,
cancelToken: CancelToken, cancelToken: CancelToken,
) => Promise<R | IntermediateQueryState<I>>; ) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
onStateChange?: (queryResolve: QueryState<R, E, I>) => void; onStateChange?: (queryResolve: QueryState<R, E, I>) => void;
debounceIdle?: number; debounceIdle?: number;
debounceLoading?: number; debounceLoading?: number;
@ -55,13 +57,13 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
query: Q, query: Q,
cancelToken: CancelToken, cancelToken: CancelToken,
setIntermediateQuery: (intermediateQuery: any) => void, setIntermediateQuery: (intermediateQuery: any) => void,
) => Promise<R | IntermediateQueryState<I>>; ) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
private readonly backgroundStatusCheck?: ( private readonly backgroundStatusCheck?: (
state: I, state: I,
query: Q, query: Q,
cancelToken: CancelToken, cancelToken: CancelToken,
) => Promise<R | IntermediateQueryState<I>>; ) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
private readonly onStateChange?: (queryResolve: QueryState<R, E, I>) => void; private readonly onStateChange?: (queryResolve: QueryState<R, E, I>) => void;
private readonly backgroundStatusCheckInitDelay: number; private readonly backgroundStatusCheckInitDelay: number;
@ -120,7 +122,7 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
}); });
const query = this.lastQuery; const query = this.lastQuery;
let data: R | IntermediateQueryState<I>; let data: R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>;
try { try {
data = await this.processQuery(query, cancelToken, (intermediateQuery: any) => { data = await this.processQuery(query, cancelToken, (intermediateQuery: any) => {
this.lastIntermediateQuery = intermediateQuery; this.lastIntermediateQuery = intermediateQuery;
@ -147,6 +149,7 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
); );
} }
cancelToken.throwIfRequested(); cancelToken.throwIfRequested();
if (this.currentQueryId !== myQueryId) return;
this.setState( this.setState(
new QueryState<R, E, I>({ new QueryState<R, E, I>({
@ -166,6 +169,7 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
if (delay) { if (delay) {
await wait(delay); await wait(delay);
cancelToken.throwIfRequested(); cancelToken.throwIfRequested();
if (this.currentQueryId !== myQueryId) return;
} }
data = await this.backgroundStatusCheck(data.state, query, cancelToken); data = await this.backgroundStatusCheck(data.state, query, cancelToken);
@ -189,12 +193,54 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
backgroundChecks++; backgroundChecks++;
} }
if (this.currentQueryId !== myQueryId) return;
if (data instanceof ResultWithAuxiliaryWork && !data.auxiliaryQueries.length) {
data = data.result;
}
const lastData = this.state.getSomeData();
if (data instanceof ResultWithAuxiliaryWork) {
const auxiliaryQueries = data.auxiliaryQueries;
const numAuxiliaryQueries = auxiliaryQueries.length;
data = data.result;
this.setState(
new QueryState<R, E>({
data,
auxiliaryLoading: true,
lastData,
}),
);
try {
for (let i = 0; i < numAuxiliaryQueries; i++) {
cancelToken.throwIfRequested();
if (this.currentQueryId !== myQueryId) return;
data = await auxiliaryQueries[i](data, cancelToken);
if (this.currentQueryId !== myQueryId) return;
if (i < numAuxiliaryQueries - 1) {
// Update data in intermediate state
this.setState(
new QueryState<R, E>({
data,
auxiliaryLoading: true,
lastData,
}),
);
}
}
} catch {}
}
if (this.currentQueryId !== myQueryId) return; if (this.currentQueryId !== myQueryId) return;
this.currentRunCancelFn = undefined; this.currentRunCancelFn = undefined;
this.setState( this.setState(
new QueryState<R, E>({ new QueryState<R, E>({
data, data,
lastData: this.state.getSomeData(), lastData,
}), }),
); );
} }

View File

@ -20,6 +20,7 @@ export type QueryStateState = 'init' | 'loading' | 'data' | 'error';
export interface QueryStateOptions<T, E extends Error = Error, I = never> { export interface QueryStateOptions<T, E extends Error = Error, I = never> {
loading?: boolean; loading?: boolean;
auxiliaryLoading?: boolean;
intermediate?: I; intermediate?: I;
intermediateError?: Error; intermediateError?: Error;
error?: E; error?: E;
@ -37,20 +38,19 @@ export class QueryState<T, E extends Error = Error, I = never> {
public error?: E; public error?: E;
public data?: T; public data?: T;
public lastData?: T; public lastData?: T;
public auxiliaryLoading?: boolean;
constructor(opts: QueryStateOptions<T, E, I>) { constructor(opts: QueryStateOptions<T, E, I>) {
const hasData = typeof opts.data !== 'undefined'; const hasData = typeof opts.data !== 'undefined';
if (typeof opts.error !== 'undefined') { if (typeof opts.error !== 'undefined') {
if (hasData) { if (hasData) throw new Error('can not have both error and data');
throw new Error('can not have both error and data'); this.state = 'error';
} else { this.error = opts.error;
this.state = 'error';
this.error = opts.error;
}
} else { } else {
if (hasData) { if (hasData) {
this.state = 'data'; this.state = 'data';
this.data = opts.data; this.data = opts.data;
this.auxiliaryLoading = opts.auxiliaryLoading;
} else if (opts.loading) { } else if (opts.loading) {
this.state = 'loading'; this.state = 'loading';
this.intermediate = opts.intermediate; this.intermediate = opts.intermediate;
@ -92,4 +92,8 @@ export class QueryState<T, E extends Error = Error, I = never> {
getSomeData(): T | undefined { getSomeData(): T | undefined {
return this.data || this.lastData; return this.data || this.lastData;
} }
isAuxiliaryLoading(): boolean {
return Boolean(this.auxiliaryLoading);
}
} }

View File

@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CancelToken } from 'axios';
export type AuxiliaryQueryFn<R> = (result: R, cancelToken: CancelToken) => Promise<R>;
export class ResultWithAuxiliaryWork<R> {
public readonly result: R;
public readonly auxiliaryQueries: AuxiliaryQueryFn<R>[];
constructor(result: R, auxiliaryQueries: AuxiliaryQueryFn<R>[]) {
this.result = result;
this.auxiliaryQueries = auxiliaryQueries;
}
}

View File

@ -55,7 +55,7 @@ import { formatCompactionInfo, zeroCompactionStatus } from '../../druid-models';
import type { Capabilities, CapabilitiesMode } from '../../helpers'; import type { Capabilities, CapabilitiesMode } from '../../helpers';
import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table'; import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
import { Api, AppToaster } from '../../singletons'; import { Api, AppToaster } from '../../singletons';
import type { NumberLike } from '../../utils'; import type { AuxiliaryQueryFn, NumberLike } from '../../utils';
import { import {
assemble, assemble,
compact, compact,
@ -77,6 +77,7 @@ import {
queryDruidSql, queryDruidSql,
QueryManager, QueryManager,
QueryState, QueryState,
ResultWithAuxiliaryWork,
twoLines, twoLines,
} from '../../utils'; } from '../../utils';
import type { BasicAction } from '../../utils/basic-action'; import type { BasicAction } from '../../utils/basic-action';
@ -262,8 +263,7 @@ function countRunningTasks(runningTasks: Record<string, number> | undefined): nu
return sum(Object.values(runningTasks)); return sum(Object.values(runningTasks));
} }
function formatRunningTasks(runningTasks: Record<string, number> | undefined): string { function formatRunningTasks(runningTasks: Record<string, number>): string {
if (!runningTasks) return 'n/a';
const runningTaskEntries = Object.entries(runningTasks); const runningTaskEntries = Object.entries(runningTasks);
if (!runningTaskEntries.length) return 'No running tasks'; if (!runningTaskEntries.length) return 'No running tasks';
return moveToEnd( return moveToEnd(
@ -472,141 +472,180 @@ GROUP BY 1, 2`;
throw new Error(`must have SQL or coordinator access`); throw new Error(`must have SQL or coordinator access`);
} }
let runningTasksByDatasource: Record<string, Record<string, number>> = {}; const auxiliaryQueries: AuxiliaryQueryFn<DatasourcesAndDefaultRules>[] = [];
if (visibleColumns.shown('Running tasks')) {
try {
if (capabilities.hasSql()) {
const runningTasks = await queryDruidSql<RunningTaskRow>({
query: DatasourcesView.RUNNING_TASK_SQL,
});
runningTasksByDatasource = groupByAsMap( if (visibleColumns.shown('Running tasks')) {
runningTasks, if (capabilities.hasSql()) {
x => x.datasource, auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
xs => try {
groupByAsMap( const runningTasks = await queryDruidSql<RunningTaskRow>(
xs, {
x => normalizeTaskType(x.type), query: DatasourcesView.RUNNING_TASK_SQL,
ys => sum(ys, y => y.num_running_tasks), },
), cancelToken,
); );
} else if (capabilities.hasOverlordAccess()) {
const taskList = (await Api.instance.get(`/druid/indexer/v1/tasks?state=running`)) const runningTasksByDatasource = groupByAsMap(
.data; runningTasks,
runningTasksByDatasource = groupByAsMap( x => x.datasource,
taskList, xs =>
(t: any) => t.dataSource, groupByAsMap(
xs => xs,
groupByAsMap( x => normalizeTaskType(x.type),
xs, ys => sum(ys, y => y.num_running_tasks),
x => normalizeTaskType(x.type), ),
ys => ys.length, );
),
); return {
} else { ...datasourcesAndDefaultRules,
throw new Error(`must have SQL or overlord access`); datasources: datasourcesAndDefaultRules.datasources.map(ds => ({
} ...ds,
} catch (e) { runningTasks: runningTasksByDatasource[ds.datasource] || {},
AppToaster.show({ })),
icon: IconNames.ERROR, };
intent: Intent.DANGER, } catch {
message: 'Could not get running task counts', AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get running task counts',
});
return datasourcesAndDefaultRules;
}
});
}
if (capabilities.hasOverlordAccess()) {
auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
try {
const runningTasks = await queryDruidSql<RunningTaskRow>(
{
query: DatasourcesView.RUNNING_TASK_SQL,
},
cancelToken,
);
const runningTasksByDatasource = groupByAsMap(
runningTasks,
x => x.datasource,
xs =>
groupByAsMap(
xs,
x => normalizeTaskType(x.type),
ys => sum(ys, y => y.num_running_tasks),
),
);
return {
...datasourcesAndDefaultRules,
datasources: datasourcesAndDefaultRules.datasources.map(ds => ({
...ds,
runningTasks: runningTasksByDatasource[ds.datasource] || {},
})),
};
} catch {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get running task counts',
});
return datasourcesAndDefaultRules;
}
}); });
} }
} }
if (!capabilities.hasCoordinatorAccess()) {
return {
datasources: datasources.map(ds => ({ ...ds, rules: [] })),
defaultRules: [],
};
}
const seen = countBy(datasources, x => x.datasource);
let unused: string[] = []; let unused: string[] = [];
if (showUnused) { if (capabilities.hasCoordinatorAccess()) {
try { // Unused
unused = ( const seen = countBy(datasources, x => x.datasource);
await Api.instance.get<string[]>( if (showUnused) {
'/druid/coordinator/v1/metadata/datasources?includeUnused', try {
) unused = (
).data.filter(d => !seen[d]); await Api.instance.get<string[]>(
} catch { '/druid/coordinator/v1/metadata/datasources?includeUnused',
AppToaster.show({ )
icon: IconNames.ERROR, ).data.filter(d => !seen[d]);
intent: Intent.DANGER, } catch {
message: 'Could not get the list of unused datasources', AppToaster.show({
}); icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get the list of unused datasources',
});
}
} }
}
let rules: Record<string, Rule[]> = {}; // Rules
try { auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
rules = (await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules')) try {
.data; const rules: Record<string, Rule[]> = (
} catch { await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules', {
AppToaster.show({ cancelToken,
icon: IconNames.ERROR, })
intent: Intent.DANGER, ).data;
message: 'Could not get load rules',
return {
datasources: datasourcesAndDefaultRules.datasources.map(ds => ({
...ds,
rules: rules[ds.datasource] || [],
})),
defaultRules: rules[DEFAULT_RULES_KEY],
};
} catch {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get load rules',
});
return datasourcesAndDefaultRules;
}
});
// Compaction
auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
try {
const compactionConfigsResp = await Api.instance.get<{
compactionConfigs: CompactionConfig[];
}>('/druid/coordinator/v1/config/compaction', { cancelToken });
const compactionConfigs = lookupBy(
compactionConfigsResp.data.compactionConfigs || [],
c => c.dataSource,
);
const compactionStatusesResp = await Api.instance.get<{
latestStatus: CompactionStatus[];
}>('/druid/coordinator/v1/compaction/status', { cancelToken });
const compactionStatuses = lookupBy(
compactionStatusesResp.data.latestStatus || [],
c => c.dataSource,
);
return {
...datasourcesAndDefaultRules,
datasources: datasourcesAndDefaultRules.datasources.map(ds => ({
...ds,
compaction: {
config: compactionConfigs[ds.datasource],
status: compactionStatuses[ds.datasource],
},
})),
};
} catch {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get compaction information',
});
return datasourcesAndDefaultRules;
}
}); });
} }
let compactionConfigs: Record<string, CompactionConfig> | undefined; return new ResultWithAuxiliaryWork(
try { {
const compactionConfigsResp = await Api.instance.get<{ datasources: datasources.concat(unused.map(makeUnusedDatasource)),
compactionConfigs: CompactionConfig[]; },
}>('/druid/coordinator/v1/config/compaction'); auxiliaryQueries,
compactionConfigs = lookupBy( );
compactionConfigsResp.data.compactionConfigs || [],
c => c.dataSource,
);
} catch {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get compaction configs',
});
}
let compactionStatuses: Record<string, CompactionStatus> | undefined;
if (compactionConfigs) {
// Don't bother getting the statuses if we can not even get the configs
try {
const compactionStatusesResp = await Api.instance.get<{
latestStatus: CompactionStatus[];
}>('/druid/coordinator/v1/compaction/status');
compactionStatuses = lookupBy(
compactionStatusesResp.data.latestStatus || [],
c => c.dataSource,
);
} catch {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message: 'Could not get compaction statuses',
});
}
}
return {
datasources: datasources.concat(unused.map(makeUnusedDatasource)).map(ds => {
return {
...ds,
runningTasks: runningTasksByDatasource[ds.datasource] || {},
rules: rules[ds.datasource],
compaction:
compactionConfigs && compactionStatuses
? {
config: compactionConfigs[ds.datasource],
status: compactionStatuses[ds.datasource],
}
: undefined,
};
}),
defaultRules: rules[DEFAULT_RULES_KEY],
};
}, },
onStateChange: datasourcesAndDefaultRulesState => { onStateChange: datasourcesAndDefaultRulesState => {
this.setState({ this.setState({
@ -1271,15 +1310,19 @@ GROUP BY 1, 2`;
accessor: d => countRunningTasks(d.runningTasks), accessor: d => countRunningTasks(d.runningTasks),
filterable: false, filterable: false,
width: 200, width: 200,
Cell: ({ original }) => ( Cell: ({ original }) => {
<TableClickableCell const { runningTasks } = original;
onClick={() => goToTasks(original.datasource)} if (!runningTasks) return;
hoverIcon={IconNames.ARROW_TOP_RIGHT} return (
title="Go to tasks" <TableClickableCell
> onClick={() => goToTasks(original.datasource)}
{formatRunningTasks(original.runningTasks)} hoverIcon={IconNames.ARROW_TOP_RIGHT}
</TableClickableCell> title="Go to tasks"
), >
{formatRunningTasks(runningTasks)}
</TableClickableCell>
);
},
}, },
{ {
Header: twoLines('Segment rows', 'minimum / average / maximum'), Header: twoLines('Segment rows', 'minimum / average / maximum'),
@ -1451,6 +1494,7 @@ GROUP BY 1, 2`;
width: 180, width: 180,
Cell: ({ original }) => { Cell: ({ original }) => {
const { datasource, compaction } = original as Datasource; const { datasource, compaction } = original as Datasource;
if (!compaction) return;
return ( return (
<TableClickableCell <TableClickableCell
disabled={!compaction} disabled={!compaction}
@ -1465,7 +1509,7 @@ GROUP BY 1, 2`;
}} }}
hoverIcon={IconNames.EDIT} hoverIcon={IconNames.EDIT}
> >
{compaction ? formatCompactionInfo(compaction) : 'Could not get compaction info'} {formatCompactionInfo(compaction)}
</TableClickableCell> </TableClickableCell>
); );
}, },
@ -1485,9 +1529,7 @@ GROUP BY 1, 2`;
className: 'padded', className: 'padded',
Cell: ({ original }) => { Cell: ({ original }) => {
const { compaction } = original as Datasource; const { compaction } = original as Datasource;
if (!compaction) { if (!compaction) return;
return 'Could not get compaction info';
}
const { status } = compaction; const { status } = compaction;
if (!status || zeroCompactionStatus(status)) { if (!status || zeroCompactionStatus(status)) {
@ -1543,9 +1585,7 @@ GROUP BY 1, 2`;
className: 'padded', className: 'padded',
Cell: ({ original }) => { Cell: ({ original }) => {
const { compaction } = original as Datasource; const { compaction } = original as Datasource;
if (!compaction) { if (!compaction) return;
return 'Could not get compaction info';
}
const { status } = compaction; const { status } = compaction;
if (!status) { if (!status) {
@ -1569,6 +1609,8 @@ GROUP BY 1, 2`;
width: 200, width: 200,
Cell: ({ original }) => { Cell: ({ original }) => {
const { datasource, rules } = original as Datasource; const { datasource, rules } = original as Datasource;
if (!rules) return;
return ( return (
<TableClickableCell <TableClickableCell
disabled={!defaultRules} disabled={!defaultRules}
@ -1577,17 +1619,17 @@ GROUP BY 1, 2`;
this.setState({ this.setState({
retentionDialogOpenOn: { retentionDialogOpenOn: {
datasource, datasource,
rules: rules || [], rules,
}, },
}); });
}} }}
hoverIcon={IconNames.EDIT} hoverIcon={IconNames.EDIT}
> >
{rules?.length {rules.length
? DatasourcesView.formatRules(rules) ? DatasourcesView.formatRules(rules)
: defaultRules : defaultRules
? `Cluster default: ${DatasourcesView.formatRules(defaultRules)}` ? `Cluster default: ${DatasourcesView.formatRules(defaultRules)}`
: 'Could not get default rules'} : ''}
</TableClickableCell> </TableClickableCell>
); );
}, },

View File

@ -111,7 +111,7 @@
width: 100%; width: 100%;
} }
.#{$bp-ns}-popover2-target { .#{$bp-ns}-popover-target {
width: 100%; width: 100%;
} }

View File

@ -23,7 +23,7 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.more-button.#{$ns}-popover2-target { .more-button.#{$ns}-popover-target {
flex: 0; flex: 0;
} }
} }

View File

@ -152,7 +152,7 @@
width: 100%; width: 100%;
} }
.#{$bp-ns}-popover2-target { .#{$bp-ns}-popover-target {
width: 100%; width: 100%;
} }

View File

@ -82,14 +82,14 @@ exports[`SupervisorsView matches snapshot 1`] = `
"label": "status API", "label": "status API",
"text": "Aggregate lag", "text": "Aggregate lag",
}, },
{
"label": "status API",
"text": "Recent errors",
},
{ {
"label": "stats API", "label": "stats API",
"text": "Stats", "text": "Stats",
}, },
{
"label": "status API",
"text": "Recent errors",
},
] ]
} }
onChange={[Function]} onChange={[Function]}

View File

@ -61,9 +61,10 @@ import {
sqlQueryCustomTableFilter, sqlQueryCustomTableFilter,
} from '../../react-table'; } from '../../react-table';
import { Api, AppToaster } from '../../singletons'; import { Api, AppToaster } from '../../singletons';
import type { TableState } from '../../utils'; import type { AuxiliaryQueryFn, TableState } from '../../utils';
import { import {
assemble, assemble,
changeByIndex,
checkedCircleIcon, checkedCircleIcon,
deepGet, deepGet,
filterMap, filterMap,
@ -73,6 +74,7 @@ import {
formatRate, formatRate,
getDruidErrorMessage, getDruidErrorMessage,
hasPopoverOpen, hasPopoverOpen,
isNumberLike,
LocalStorageBackedVisibility, LocalStorageBackedVisibility,
LocalStorageKeys, LocalStorageKeys,
nonEmptyArray, nonEmptyArray,
@ -81,6 +83,7 @@ import {
queryDruidSql, queryDruidSql,
QueryManager, QueryManager,
QueryState, QueryState,
ResultWithAuxiliaryWork,
sortedToOrderByClause, sortedToOrderByClause,
twoLines, twoLines,
} from '../../utils'; } from '../../utils';
@ -96,8 +99,8 @@ const SUPERVISOR_TABLE_COLUMNS: TableColumnSelectorColumn[] = [
'Configured tasks', 'Configured tasks',
{ text: 'Running tasks', label: 'status API' }, { text: 'Running tasks', label: 'status API' },
{ text: 'Aggregate lag', label: 'status API' }, { text: 'Aggregate lag', label: 'status API' },
{ text: 'Recent errors', label: 'status API' },
{ text: 'Stats', label: 'stats API' }, { text: 'Stats', label: 'stats API' },
{ text: 'Recent errors', label: 'status API' },
]; ];
const ROW_STATS_KEYS: RowStatsKey[] = ['1m', '5m', '15m']; const ROW_STATS_KEYS: RowStatsKey[] = ['1m', '5m', '15m'];
@ -118,14 +121,14 @@ interface SupervisorQuery extends TableState {
} }
interface SupervisorQueryResultRow { interface SupervisorQueryResultRow {
supervisor_id: string; readonly supervisor_id: string;
type: string; readonly type: string;
source: string; readonly source: string;
detailed_state: string; readonly detailed_state: string;
spec?: IngestionSpec; readonly spec?: IngestionSpec;
suspended: boolean; readonly suspended: boolean;
status?: SupervisorStatus; readonly status?: SupervisorStatus;
stats?: any; readonly stats?: any;
} }
export interface SupervisorsViewProps { export interface SupervisorsViewProps {
@ -253,19 +256,18 @@ export class SupervisorsView extends React.PureComponent<
page ? `OFFSET ${page * pageSize}` : undefined, page ? `OFFSET ${page * pageSize}` : undefined,
).join('\n'); ).join('\n');
setIntermediateQuery(sqlQuery); setIntermediateQuery(sqlQuery);
supervisors = await queryDruidSql<SupervisorQueryResultRow>( supervisors = (
{ await queryDruidSql<SupervisorQueryResultRow>(
query: sqlQuery, {
}, query: sqlQuery,
cancelToken, },
); cancelToken,
)
for (const supervisor of supervisors) { ).map(supervisor => {
const spec: any = supervisor.spec; const spec: any = supervisor.spec;
if (typeof spec === 'string') { if (typeof spec !== 'string') return supervisor;
supervisor.spec = JSONBig.parse(spec); return { ...supervisor, spec: JSONBig.parse(spec) };
} });
}
} else if (capabilities.hasOverlordAccess()) { } else if (capabilities.hasOverlordAccess()) {
const supervisorList = ( const supervisorList = (
await Api.instance.get('/druid/indexer/v1/supervisor?full', { cancelToken }) await Api.instance.get('/druid/indexer/v1/supervisor?full', { cancelToken })
@ -302,54 +304,48 @@ export class SupervisorsView extends React.PureComponent<
throw new Error(`must have SQL or overlord access`); throw new Error(`must have SQL or overlord access`);
} }
const auxiliaryQueries: AuxiliaryQueryFn<SupervisorQueryResultRow[]>[] = [];
if (capabilities.hasOverlordAccess()) { if (capabilities.hasOverlordAccess()) {
let showIssue = (message: string) => {
showIssue = () => {}; // Only show once
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
message,
});
};
if (visibleColumns.shown('Running tasks', 'Aggregate lag', 'Recent errors')) { if (visibleColumns.shown('Running tasks', 'Aggregate lag', 'Recent errors')) {
try { auxiliaryQueries.push(
for (const supervisor of supervisors) { ...supervisors.map(
cancelToken.throwIfRequested(); (supervisor, i): AuxiliaryQueryFn<SupervisorQueryResultRow[]> =>
supervisor.status = ( async (rows, cancelToken) => {
await Api.instance.get( const status = (
`/druid/indexer/v1/supervisor/${Api.encodePath( await Api.instance.get(
supervisor.supervisor_id, `/druid/indexer/v1/supervisor/${Api.encodePath(
)}/status`, supervisor.supervisor_id,
{ cancelToken, timeout: STATUS_API_TIMEOUT }, )}/status`,
) { cancelToken, timeout: STATUS_API_TIMEOUT },
).data; )
} ).data;
} catch (e) { return changeByIndex(rows, i, row => ({ ...row, status }));
showIssue('Could not get status'); },
} ),
);
} }
if (visibleColumns.shown('Stats')) { if (visibleColumns.shown('Stats')) {
try { auxiliaryQueries.push(
for (const supervisor of supervisors) { ...supervisors.map(
cancelToken.throwIfRequested(); (supervisor, i): AuxiliaryQueryFn<SupervisorQueryResultRow[]> =>
supervisor.stats = ( async (rows, cancelToken) => {
await Api.instance.get( const stats = (
`/druid/indexer/v1/supervisor/${Api.encodePath( await Api.instance.get(
supervisor.supervisor_id, `/druid/indexer/v1/supervisor/${Api.encodePath(
)}/stats`, supervisor.supervisor_id,
{ cancelToken, timeout: STATS_API_TIMEOUT }, )}/stats`,
) { cancelToken, timeout: STATS_API_TIMEOUT },
).data; )
} ).data;
} catch (e) { return changeByIndex(rows, i, row => ({ ...row, stats }));
showIssue('Could not get stats'); },
} ),
);
} }
} }
return supervisors; return new ResultWithAuxiliaryWork(supervisors, auxiliaryQueries);
}, },
onStateChange: supervisorsState => { onStateChange: supervisorsState => {
this.setState({ this.setState({
@ -790,7 +786,7 @@ export class SupervisorsView extends React.PureComponent<
); );
} }
} else { } else {
label = 'n/a'; label = '';
} }
return ( return (
<TableClickableCell <TableClickableCell
@ -812,7 +808,7 @@ export class SupervisorsView extends React.PureComponent<
sortable: false, sortable: false,
className: 'padded', className: 'padded',
show: visibleColumns.shown('Aggregate lag'), show: visibleColumns.shown('Aggregate lag'),
Cell: ({ value }) => formatInteger(value), Cell: ({ value }) => (isNumberLike(value) ? formatInteger(value) : null),
}, },
{ {
Header: twoLines( Header: twoLines(
@ -899,13 +895,14 @@ export class SupervisorsView extends React.PureComponent<
sortable: false, sortable: false,
show: visibleColumns.shown('Recent errors'), show: visibleColumns.shown('Recent errors'),
Cell: ({ value, original }) => { Cell: ({ value, original }) => {
if (!value) return null;
return ( return (
<TableClickableCell <TableClickableCell
onClick={() => this.onSupervisorDetail(original)} onClick={() => this.onSupervisorDetail(original)}
hoverIcon={IconNames.SEARCH_TEMPLATE} hoverIcon={IconNames.SEARCH_TEMPLATE}
title="See errors" title="See errors"
> >
{pluralIfNeeded(value?.length, 'error')} {pluralIfNeeded(value.length, 'error')}
</TableClickableCell> </TableClickableCell>
); );
}, },

View File

@ -59,7 +59,7 @@
animation: druid-glow 1s infinite alternate; animation: druid-glow 1s infinite alternate;
} }
.#{$bp-ns}-popover2-target { .#{$bp-ns}-popover-target {
width: 188px; width: 188px;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;

View File

@ -111,7 +111,7 @@
width: 100%; width: 100%;
} }
.#{$bp-ns}-popover2-target { .#{$bp-ns}-popover-target {
width: 100%; width: 100%;
} }