mirror of https://github.com/apache/druid.git
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:
parent
0caf383102
commit
21dcf804eb
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$bp-ns}-popover2-target {
|
.#{$bp-ns}-popover-target {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$bp-ns}-popover2-target {
|
.#{$bp-ns}-popover-target {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$bp-ns}-popover2-target {
|
.#{$bp-ns}-popover-target {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$bp-ns}-popover2-target {
|
.#{$bp-ns}-popover-target {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue