diff --git a/web-console/src/components/form-group-with-info/form-group-with-info.scss b/web-console/src/components/form-group-with-info/form-group-with-info.scss index 237225bf47c..15865eb2fb0 100644 --- a/web-console/src/components/form-group-with-info/form-group-with-info.scss +++ b/web-console/src/components/form-group-with-info/form-group-with-info.scss @@ -33,7 +33,7 @@ } } - .#{$bp-ns}-text-muted .#{$bp-ns}-popover2-target { + .#{$bp-ns}-text-muted .#{$bp-ns}-popover-target { margin-top: 0; } @@ -48,7 +48,7 @@ } } -.#{$bp-ns}-popover2-content { +.#{$bp-ns}-popover-content { .code-block { white-space: pre; overflow: auto; diff --git a/web-console/src/components/formatted-input/formatted-input.scss b/web-console/src/components/formatted-input/formatted-input.scss index 46098e694e2..547620571f6 100644 --- a/web-console/src/components/formatted-input/formatted-input.scss +++ b/web-console/src/components/formatted-input/formatted-input.scss @@ -21,7 +21,7 @@ .formatted-input { position: relative; - & > .#{$bp-ns}-popover2-target { + & > .#{$bp-ns}-popover-target { position: absolute; width: 0; right: 0; diff --git a/web-console/src/components/record-table-pane/record-table-pane.scss b/web-console/src/components/record-table-pane/record-table-pane.scss index d2d6a86232c..2e5a4cc353c 100644 --- a/web-console/src/components/record-table-pane/record-table-pane.scss +++ b/web-console/src/components/record-table-pane/record-table-pane.scss @@ -111,7 +111,7 @@ width: 100%; } - .#{$bp-ns}-popover2-target { + .#{$bp-ns}-popover-target { width: 100%; } } diff --git a/web-console/src/components/table-filterable-cell/table-filterable-cell.scss b/web-console/src/components/table-filterable-cell/table-filterable-cell.scss index 95e0d732cf6..6ebf3fee0a6 100644 --- a/web-console/src/components/table-filterable-cell/table-filterable-cell.scss +++ b/web-console/src/components/table-filterable-cell/table-filterable-cell.scss @@ -24,7 +24,7 @@ overflow: hidden; text-overflow: ellipsis; - &.#{$ns}-popover2-target { + &.#{$ns}-popover-target { display: block; // extra nesting for stronger CSS selectors } } diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 7698d3c3af8..765f7e30f64 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -381,6 +381,14 @@ export function findMap( return filterMap(xs, f)[0]; } +export function changeByIndex( + 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(xs: (T | undefined | false | null | '')[]): T[] { return xs.filter(Boolean) as T[]; } diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx index 4daeefe61c1..d9b6d62a66e 100644 --- a/web-console/src/utils/index.tsx +++ b/web-console/src/utils/index.tsx @@ -24,14 +24,12 @@ export * from './druid-lookup'; export * from './druid-query'; export * from './formatter'; export * from './general'; -export * from './intermediate-query-state'; export * from './local-storage-backed-visibility'; export * from './local-storage-keys'; export * from './null-mode-detection'; export * from './object-change'; export * from './query-action'; export * from './query-manager'; -export * from './query-state'; export * from './sanitizers'; export * from './sql'; export * from './table-helpers'; diff --git a/web-console/src/utils/query-manager/index.ts b/web-console/src/utils/query-manager/index.ts new file mode 100644 index 00000000000..cc9d0026145 --- /dev/null +++ b/web-console/src/utils/query-manager/index.ts @@ -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'; diff --git a/web-console/src/utils/intermediate-query-state.ts b/web-console/src/utils/query-manager/intermediate-query-state.ts similarity index 100% rename from web-console/src/utils/intermediate-query-state.ts rename to web-console/src/utils/query-manager/intermediate-query-state.ts diff --git a/web-console/src/utils/query-manager.tsx b/web-console/src/utils/query-manager/query-manager.ts similarity index 81% rename from web-console/src/utils/query-manager.tsx rename to web-console/src/utils/query-manager/query-manager.ts index 9391192475c..5b141c7c89d 100644 --- a/web-console/src/utils/query-manager.tsx +++ b/web-console/src/utils/query-manager/query-manager.ts @@ -20,9 +20,11 @@ import type { Canceler, CancelToken } from 'axios'; import axios from 'axios'; import debounce from 'lodash.debounce'; -import { wait } from './general'; +import { wait } from '../general'; + import { IntermediateQueryState } from './intermediate-query-state'; import { QueryState } from './query-state'; +import { ResultWithAuxiliaryWork } from './result-with-auxiliary-work'; export interface QueryManagerOptions { initState?: QueryState; @@ -30,12 +32,12 @@ export interface QueryManagerOptions { query: Q, cancelToken: CancelToken, setIntermediateQuery: (intermediateQuery: any) => void, - ) => Promise>; + ) => Promise | ResultWithAuxiliaryWork>; backgroundStatusCheck?: ( state: I, query: Q, cancelToken: CancelToken, - ) => Promise>; + ) => Promise | ResultWithAuxiliaryWork>; onStateChange?: (queryResolve: QueryState) => void; debounceIdle?: number; debounceLoading?: number; @@ -55,13 +57,13 @@ export class QueryManager { query: Q, cancelToken: CancelToken, setIntermediateQuery: (intermediateQuery: any) => void, - ) => Promise>; + ) => Promise | ResultWithAuxiliaryWork>; private readonly backgroundStatusCheck?: ( state: I, query: Q, cancelToken: CancelToken, - ) => Promise>; + ) => Promise | ResultWithAuxiliaryWork>; private readonly onStateChange?: (queryResolve: QueryState) => void; private readonly backgroundStatusCheckInitDelay: number; @@ -120,7 +122,7 @@ export class QueryManager { }); const query = this.lastQuery; - let data: R | IntermediateQueryState; + let data: R | IntermediateQueryState | ResultWithAuxiliaryWork; try { data = await this.processQuery(query, cancelToken, (intermediateQuery: any) => { this.lastIntermediateQuery = intermediateQuery; @@ -147,6 +149,7 @@ export class QueryManager { ); } cancelToken.throwIfRequested(); + if (this.currentQueryId !== myQueryId) return; this.setState( new QueryState({ @@ -166,6 +169,7 @@ export class QueryManager { if (delay) { await wait(delay); cancelToken.throwIfRequested(); + if (this.currentQueryId !== myQueryId) return; } data = await this.backgroundStatusCheck(data.state, query, cancelToken); @@ -189,12 +193,54 @@ export class QueryManager { 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({ + 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({ + data, + auxiliaryLoading: true, + lastData, + }), + ); + } + } + } catch {} + } + if (this.currentQueryId !== myQueryId) return; this.currentRunCancelFn = undefined; this.setState( new QueryState({ data, - lastData: this.state.getSomeData(), + lastData, }), ); } diff --git a/web-console/src/utils/query-state.ts b/web-console/src/utils/query-manager/query-state.ts similarity index 88% rename from web-console/src/utils/query-state.ts rename to web-console/src/utils/query-manager/query-state.ts index b4387bb9111..807646dc583 100644 --- a/web-console/src/utils/query-state.ts +++ b/web-console/src/utils/query-manager/query-state.ts @@ -20,6 +20,7 @@ export type QueryStateState = 'init' | 'loading' | 'data' | 'error'; export interface QueryStateOptions { loading?: boolean; + auxiliaryLoading?: boolean; intermediate?: I; intermediateError?: Error; error?: E; @@ -37,20 +38,19 @@ export class QueryState { public error?: E; public data?: T; public lastData?: T; + public auxiliaryLoading?: boolean; constructor(opts: QueryStateOptions) { const hasData = typeof opts.data !== 'undefined'; if (typeof opts.error !== 'undefined') { - if (hasData) { - throw new Error('can not have both error and data'); - } else { - this.state = 'error'; - this.error = opts.error; - } + if (hasData) throw new Error('can not have both error and data'); + this.state = 'error'; + this.error = opts.error; } else { if (hasData) { this.state = 'data'; this.data = opts.data; + this.auxiliaryLoading = opts.auxiliaryLoading; } else if (opts.loading) { this.state = 'loading'; this.intermediate = opts.intermediate; @@ -92,4 +92,8 @@ export class QueryState { getSomeData(): T | undefined { return this.data || this.lastData; } + + isAuxiliaryLoading(): boolean { + return Boolean(this.auxiliaryLoading); + } } diff --git a/web-console/src/utils/query-manager/result-with-auxiliary-work.ts b/web-console/src/utils/query-manager/result-with-auxiliary-work.ts new file mode 100644 index 00000000000..fae424f3b10 --- /dev/null +++ b/web-console/src/utils/query-manager/result-with-auxiliary-work.ts @@ -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 = (result: R, cancelToken: CancelToken) => Promise; +export class ResultWithAuxiliaryWork { + public readonly result: R; + public readonly auxiliaryQueries: AuxiliaryQueryFn[]; + + constructor(result: R, auxiliaryQueries: AuxiliaryQueryFn[]) { + this.result = result; + this.auxiliaryQueries = auxiliaryQueries; + } +} diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index da5f8dbbff1..69c6302c654 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -55,7 +55,7 @@ import { formatCompactionInfo, zeroCompactionStatus } from '../../druid-models'; import type { Capabilities, CapabilitiesMode } from '../../helpers'; import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table'; import { Api, AppToaster } from '../../singletons'; -import type { NumberLike } from '../../utils'; +import type { AuxiliaryQueryFn, NumberLike } from '../../utils'; import { assemble, compact, @@ -77,6 +77,7 @@ import { queryDruidSql, QueryManager, QueryState, + ResultWithAuxiliaryWork, twoLines, } from '../../utils'; import type { BasicAction } from '../../utils/basic-action'; @@ -262,8 +263,7 @@ function countRunningTasks(runningTasks: Record | undefined): nu return sum(Object.values(runningTasks)); } -function formatRunningTasks(runningTasks: Record | undefined): string { - if (!runningTasks) return 'n/a'; +function formatRunningTasks(runningTasks: Record): string { const runningTaskEntries = Object.entries(runningTasks); if (!runningTaskEntries.length) return 'No running tasks'; return moveToEnd( @@ -472,141 +472,180 @@ GROUP BY 1, 2`; throw new Error(`must have SQL or coordinator access`); } - let runningTasksByDatasource: Record> = {}; - if (visibleColumns.shown('Running tasks')) { - try { - if (capabilities.hasSql()) { - const runningTasks = await queryDruidSql({ - query: DatasourcesView.RUNNING_TASK_SQL, - }); + const auxiliaryQueries: AuxiliaryQueryFn[] = []; - runningTasksByDatasource = groupByAsMap( - runningTasks, - x => x.datasource, - xs => - groupByAsMap( - xs, - x => normalizeTaskType(x.type), - ys => sum(ys, y => y.num_running_tasks), - ), - ); - } else if (capabilities.hasOverlordAccess()) { - const taskList = (await Api.instance.get(`/druid/indexer/v1/tasks?state=running`)) - .data; - runningTasksByDatasource = groupByAsMap( - taskList, - (t: any) => t.dataSource, - xs => - groupByAsMap( - xs, - x => normalizeTaskType(x.type), - ys => ys.length, - ), - ); - } else { - throw new Error(`must have SQL or overlord access`); - } - } catch (e) { - AppToaster.show({ - icon: IconNames.ERROR, - intent: Intent.DANGER, - message: 'Could not get running task counts', + if (visibleColumns.shown('Running tasks')) { + if (capabilities.hasSql()) { + auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => { + try { + const runningTasks = await queryDruidSql( + { + 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.hasOverlordAccess()) { + auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => { + try { + const runningTasks = await queryDruidSql( + { + 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[] = []; - if (showUnused) { - try { - unused = ( - await Api.instance.get( - '/druid/coordinator/v1/metadata/datasources?includeUnused', - ) - ).data.filter(d => !seen[d]); - } catch { - AppToaster.show({ - icon: IconNames.ERROR, - intent: Intent.DANGER, - message: 'Could not get the list of unused datasources', - }); + if (capabilities.hasCoordinatorAccess()) { + // Unused + const seen = countBy(datasources, x => x.datasource); + if (showUnused) { + try { + unused = ( + await Api.instance.get( + '/druid/coordinator/v1/metadata/datasources?includeUnused', + ) + ).data.filter(d => !seen[d]); + } catch { + AppToaster.show({ + icon: IconNames.ERROR, + intent: Intent.DANGER, + message: 'Could not get the list of unused datasources', + }); + } } - } - let rules: Record = {}; - try { - rules = (await Api.instance.get>('/druid/coordinator/v1/rules')) - .data; - } catch { - AppToaster.show({ - icon: IconNames.ERROR, - intent: Intent.DANGER, - message: 'Could not get load rules', + // Rules + auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => { + try { + const rules: Record = ( + await Api.instance.get>('/druid/coordinator/v1/rules', { + cancelToken, + }) + ).data; + + 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 | undefined; - try { - const compactionConfigsResp = await Api.instance.get<{ - compactionConfigs: CompactionConfig[]; - }>('/druid/coordinator/v1/config/compaction'); - 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 | 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], - }; + return new ResultWithAuxiliaryWork( + { + datasources: datasources.concat(unused.map(makeUnusedDatasource)), + }, + auxiliaryQueries, + ); }, onStateChange: datasourcesAndDefaultRulesState => { this.setState({ @@ -1271,15 +1310,19 @@ GROUP BY 1, 2`; accessor: d => countRunningTasks(d.runningTasks), filterable: false, width: 200, - Cell: ({ original }) => ( - goToTasks(original.datasource)} - hoverIcon={IconNames.ARROW_TOP_RIGHT} - title="Go to tasks" - > - {formatRunningTasks(original.runningTasks)} - - ), + Cell: ({ original }) => { + const { runningTasks } = original; + if (!runningTasks) return; + return ( + goToTasks(original.datasource)} + hoverIcon={IconNames.ARROW_TOP_RIGHT} + title="Go to tasks" + > + {formatRunningTasks(runningTasks)} + + ); + }, }, { Header: twoLines('Segment rows', 'minimum / average / maximum'), @@ -1451,6 +1494,7 @@ GROUP BY 1, 2`; width: 180, Cell: ({ original }) => { const { datasource, compaction } = original as Datasource; + if (!compaction) return; return ( - {compaction ? formatCompactionInfo(compaction) : 'Could not get compaction info'} + {formatCompactionInfo(compaction)} ); }, @@ -1485,9 +1529,7 @@ GROUP BY 1, 2`; className: 'padded', Cell: ({ original }) => { const { compaction } = original as Datasource; - if (!compaction) { - return 'Could not get compaction info'; - } + if (!compaction) return; const { status } = compaction; if (!status || zeroCompactionStatus(status)) { @@ -1543,9 +1585,7 @@ GROUP BY 1, 2`; className: 'padded', Cell: ({ original }) => { const { compaction } = original as Datasource; - if (!compaction) { - return 'Could not get compaction info'; - } + if (!compaction) return; const { status } = compaction; if (!status) { @@ -1569,6 +1609,8 @@ GROUP BY 1, 2`; width: 200, Cell: ({ original }) => { const { datasource, rules } = original as Datasource; + if (!rules) return; + return ( - {rules?.length + {rules.length ? DatasourcesView.formatRules(rules) : defaultRules ? `Cluster default: ${DatasourcesView.formatRules(defaultRules)}` - : 'Could not get default rules'} + : ''} ); }, diff --git a/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.scss b/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.scss index ea9f6a24a9b..31733db7828 100644 --- a/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.scss +++ b/web-console/src/views/explore-view/modules/components/generic-output-table/generic-output-table.scss @@ -111,7 +111,7 @@ width: 100%; } - .#{$bp-ns}-popover2-target { + .#{$bp-ns}-popover-target { width: 100%; } diff --git a/web-console/src/views/explore-view/tile-picker/tile-picker.scss b/web-console/src/views/explore-view/tile-picker/tile-picker.scss index 95d0dbcfb46..679965670b3 100644 --- a/web-console/src/views/explore-view/tile-picker/tile-picker.scss +++ b/web-console/src/views/explore-view/tile-picker/tile-picker.scss @@ -23,7 +23,7 @@ flex: 1 1 auto; } - .more-button.#{$ns}-popover2-target { + .more-button.#{$ns}-popover-target { flex: 0; } } diff --git a/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.scss b/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.scss index 462a852e8df..a18bc291fd7 100644 --- a/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.scss +++ b/web-console/src/views/sql-data-loader-view/schema-step/preview-table/preview-table.scss @@ -152,7 +152,7 @@ width: 100%; } - .#{$bp-ns}-popover2-target { + .#{$bp-ns}-popover-target { width: 100%; } 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 4b531c0bc44..e3458f4749c 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 @@ -82,14 +82,14 @@ exports[`SupervisorsView matches snapshot 1`] = ` "label": "status API", "text": "Aggregate lag", }, - { - "label": "status API", - "text": "Recent errors", - }, { "label": "stats API", "text": "Stats", }, + { + "label": "status API", + "text": "Recent errors", + }, ] } onChange={[Function]} diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx index 648186d45bc..41d8ad2cd0f 100644 --- a/web-console/src/views/supervisors-view/supervisors-view.tsx +++ b/web-console/src/views/supervisors-view/supervisors-view.tsx @@ -61,9 +61,10 @@ import { sqlQueryCustomTableFilter, } from '../../react-table'; import { Api, AppToaster } from '../../singletons'; -import type { TableState } from '../../utils'; +import type { AuxiliaryQueryFn, TableState } from '../../utils'; import { assemble, + changeByIndex, checkedCircleIcon, deepGet, filterMap, @@ -73,6 +74,7 @@ import { formatRate, getDruidErrorMessage, hasPopoverOpen, + isNumberLike, LocalStorageBackedVisibility, LocalStorageKeys, nonEmptyArray, @@ -81,6 +83,7 @@ import { queryDruidSql, QueryManager, QueryState, + ResultWithAuxiliaryWork, sortedToOrderByClause, twoLines, } from '../../utils'; @@ -96,8 +99,8 @@ const SUPERVISOR_TABLE_COLUMNS: TableColumnSelectorColumn[] = [ 'Configured tasks', { text: 'Running tasks', label: 'status API' }, { text: 'Aggregate lag', label: 'status API' }, - { text: 'Recent errors', label: 'status API' }, { text: 'Stats', label: 'stats API' }, + { text: 'Recent errors', label: 'status API' }, ]; const ROW_STATS_KEYS: RowStatsKey[] = ['1m', '5m', '15m']; @@ -118,14 +121,14 @@ interface SupervisorQuery extends TableState { } interface SupervisorQueryResultRow { - supervisor_id: string; - type: string; - source: string; - detailed_state: string; - spec?: IngestionSpec; - suspended: boolean; - status?: SupervisorStatus; - stats?: any; + readonly supervisor_id: string; + readonly type: string; + readonly source: string; + readonly detailed_state: string; + readonly spec?: IngestionSpec; + readonly suspended: boolean; + readonly status?: SupervisorStatus; + readonly stats?: any; } export interface SupervisorsViewProps { @@ -253,19 +256,18 @@ export class SupervisorsView extends React.PureComponent< page ? `OFFSET ${page * pageSize}` : undefined, ).join('\n'); setIntermediateQuery(sqlQuery); - supervisors = await queryDruidSql( - { - query: sqlQuery, - }, - cancelToken, - ); - - for (const supervisor of supervisors) { + supervisors = ( + await queryDruidSql( + { + query: sqlQuery, + }, + cancelToken, + ) + ).map(supervisor => { const spec: any = supervisor.spec; - if (typeof spec === 'string') { - supervisor.spec = JSONBig.parse(spec); - } - } + if (typeof spec !== 'string') return supervisor; + return { ...supervisor, spec: JSONBig.parse(spec) }; + }); } else if (capabilities.hasOverlordAccess()) { const supervisorList = ( 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`); } + const auxiliaryQueries: AuxiliaryQueryFn[] = []; 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')) { - try { - for (const supervisor of supervisors) { - cancelToken.throwIfRequested(); - supervisor.status = ( - await Api.instance.get( - `/druid/indexer/v1/supervisor/${Api.encodePath( - supervisor.supervisor_id, - )}/status`, - { cancelToken, timeout: STATUS_API_TIMEOUT }, - ) - ).data; - } - } catch (e) { - showIssue('Could not get status'); - } + auxiliaryQueries.push( + ...supervisors.map( + (supervisor, i): AuxiliaryQueryFn => + async (rows, cancelToken) => { + const status = ( + await Api.instance.get( + `/druid/indexer/v1/supervisor/${Api.encodePath( + supervisor.supervisor_id, + )}/status`, + { cancelToken, timeout: STATUS_API_TIMEOUT }, + ) + ).data; + return changeByIndex(rows, i, row => ({ ...row, status })); + }, + ), + ); } if (visibleColumns.shown('Stats')) { - try { - for (const supervisor of supervisors) { - cancelToken.throwIfRequested(); - supervisor.stats = ( - await Api.instance.get( - `/druid/indexer/v1/supervisor/${Api.encodePath( - supervisor.supervisor_id, - )}/stats`, - { cancelToken, timeout: STATS_API_TIMEOUT }, - ) - ).data; - } - } catch (e) { - showIssue('Could not get stats'); - } + auxiliaryQueries.push( + ...supervisors.map( + (supervisor, i): AuxiliaryQueryFn => + async (rows, cancelToken) => { + const stats = ( + await Api.instance.get( + `/druid/indexer/v1/supervisor/${Api.encodePath( + supervisor.supervisor_id, + )}/stats`, + { cancelToken, timeout: STATS_API_TIMEOUT }, + ) + ).data; + return changeByIndex(rows, i, row => ({ ...row, stats })); + }, + ), + ); } } - return supervisors; + return new ResultWithAuxiliaryWork(supervisors, auxiliaryQueries); }, onStateChange: supervisorsState => { this.setState({ @@ -790,7 +786,7 @@ export class SupervisorsView extends React.PureComponent< ); } } else { - label = 'n/a'; + label = ''; } return ( formatInteger(value), + Cell: ({ value }) => (isNumberLike(value) ? formatInteger(value) : null), }, { Header: twoLines( @@ -899,13 +895,14 @@ export class SupervisorsView extends React.PureComponent< sortable: false, show: visibleColumns.shown('Recent errors'), Cell: ({ value, original }) => { + if (!value) return null; return ( this.onSupervisorDetail(original)} hoverIcon={IconNames.SEARCH_TEMPLATE} title="See errors" > - {pluralIfNeeded(value?.length, 'error')} + {pluralIfNeeded(value.length, 'error')} ); }, diff --git a/web-console/src/views/workbench-view/column-tree/column-tree.scss b/web-console/src/views/workbench-view/column-tree/column-tree.scss index 2371c145b59..d13431f0891 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree.scss +++ b/web-console/src/views/workbench-view/column-tree/column-tree.scss @@ -59,7 +59,7 @@ animation: druid-glow 1s infinite alternate; } - .#{$bp-ns}-popover2-target { + .#{$bp-ns}-popover-target { width: 188px; display: inline-block; cursor: pointer; diff --git a/web-console/src/views/workbench-view/result-table-pane/result-table-pane.scss b/web-console/src/views/workbench-view/result-table-pane/result-table-pane.scss index 366deb7956f..6ab4629d3ef 100644 --- a/web-console/src/views/workbench-view/result-table-pane/result-table-pane.scss +++ b/web-console/src/views/workbench-view/result-table-pane/result-table-pane.scss @@ -111,7 +111,7 @@ width: 100%; } - .#{$bp-ns}-popover2-target { + .#{$bp-ns}-popover-target { width: 100%; }