mirror of
https://github.com/apache/druid.git
synced 2025-02-24 19:55:03 +00:00
Web console: Display compaction status (#10438)
* init compaction status * % compacted * final UI tweaks * extracted utils, added tests * add tests to general foramt functions
This commit is contained in:
parent
599aacce0f
commit
729bcba7ac
@ -80,12 +80,11 @@ export const JsonInput = React.memo(function JsonInput(props: JsonInputProps) {
|
||||
const aceEditor = useRef<Editor | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!deepEqual(value, internalValue.value)) {
|
||||
setInternalValue({
|
||||
value,
|
||||
stringified: stringifyJson(value),
|
||||
});
|
||||
}
|
||||
if (deepEqual(value, internalValue.value)) return;
|
||||
setInternalValue({
|
||||
value,
|
||||
stringified: stringifyJson(value),
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
const internalValueError = internalValue.error;
|
||||
@ -149,8 +148,8 @@ export const JsonInput = React.memo(function JsonInput(props: JsonInputProps) {
|
||||
const rc = extractRowColumnFromHjsonError(internalValueError);
|
||||
if (!rc) return;
|
||||
|
||||
aceEditor.current.focus(); // Grab the focus
|
||||
aceEditor.current.getSelection().moveCursorTo(rc.row, rc.column);
|
||||
aceEditor.current.focus(); // Grab the focus also
|
||||
}}
|
||||
>
|
||||
{internalValueError.message}
|
||||
|
@ -18,14 +18,19 @@
|
||||
|
||||
import { Button, Menu, Popover, Position } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type OpenState = 'open' | 'alt-open';
|
||||
|
||||
export interface MoreButtonProps {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
altExtra?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const MoreButton = React.memo(function MoreButton(props: MoreButtonProps) {
|
||||
const { children } = props;
|
||||
const { children, altExtra } = props;
|
||||
|
||||
const [openState, setOpenState] = useState<OpenState | undefined>();
|
||||
|
||||
let childCount = 0;
|
||||
// Sadly React.Children.count does not ignore nulls correctly
|
||||
@ -36,8 +41,18 @@ export const MoreButton = React.memo(function MoreButton(props: MoreButtonProps)
|
||||
return (
|
||||
<Popover
|
||||
className="more-button"
|
||||
content={<Menu>{children}</Menu>}
|
||||
isOpen={Boolean(openState)}
|
||||
content={
|
||||
<Menu>
|
||||
{children}
|
||||
{openState === 'alt-open' && altExtra}
|
||||
</Menu>
|
||||
}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
onInteraction={(nextOpenState, e: any) => {
|
||||
if (!e) return; // For some reason this function is always called twice once with e and once without
|
||||
setOpenState(nextOpenState ? (e.altKey ? 'alt-open' : 'open') : undefined);
|
||||
}}
|
||||
>
|
||||
<Button icon={IconNames.MORE} disabled={!childCount} />
|
||||
</Popover>
|
||||
|
@ -38,6 +38,12 @@ exports[`CompactionDialog matches snapshot with compactionConfig (dynamic partit
|
||||
The offset for searching segments to be compacted. Strongly recommended to set for realtime dataSources.
|
||||
</p>,
|
||||
"name": "skipOffsetFromLatest",
|
||||
"suggestions": Array [
|
||||
"PT0H",
|
||||
"PT1H",
|
||||
"P1D",
|
||||
"P3D",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
@ -264,6 +270,12 @@ exports[`CompactionDialog matches snapshot with compactionConfig (hashed partiti
|
||||
The offset for searching segments to be compacted. Strongly recommended to set for realtime dataSources.
|
||||
</p>,
|
||||
"name": "skipOffsetFromLatest",
|
||||
"suggestions": Array [
|
||||
"PT0H",
|
||||
"PT1H",
|
||||
"P1D",
|
||||
"P3D",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
@ -490,6 +502,12 @@ exports[`CompactionDialog matches snapshot with compactionConfig (single_dim par
|
||||
The offset for searching segments to be compacted. Strongly recommended to set for realtime dataSources.
|
||||
</p>,
|
||||
"name": "skipOffsetFromLatest",
|
||||
"suggestions": Array [
|
||||
"PT0H",
|
||||
"PT1H",
|
||||
"P1D",
|
||||
"P3D",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
@ -716,6 +734,12 @@ exports[`CompactionDialog matches snapshot without compactionConfig 1`] = `
|
||||
The offset for searching segments to be compacted. Strongly recommended to set for realtime dataSources.
|
||||
</p>,
|
||||
"name": "skipOffsetFromLatest",
|
||||
"suggestions": Array [
|
||||
"PT0H",
|
||||
"PT1H",
|
||||
"P1D",
|
||||
"P3D",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
|
@ -24,8 +24,6 @@ import { deepGet, deepSet } from '../../utils/object-change';
|
||||
|
||||
import './compaction-dialog.scss';
|
||||
|
||||
export const DEFAULT_MAX_ROWS_PER_SEGMENT = 5000000;
|
||||
|
||||
type Tabs = 'form' | 'json';
|
||||
|
||||
type CompactionConfig = Record<string, any>;
|
||||
@ -35,6 +33,7 @@ const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
|
||||
name: 'skipOffsetFromLatest',
|
||||
type: 'string',
|
||||
defaultValue: 'P1D',
|
||||
suggestions: ['PT0H', 'PT1H', 'P1D', 'P3D'],
|
||||
info: (
|
||||
<p>
|
||||
The offset for searching segments to be compacted. Strongly recommended to set for realtime
|
||||
|
87
web-console/src/utils/compaction.spec.ts
Normal file
87
web-console/src/utils/compaction.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 {
|
||||
CompactionConfig,
|
||||
CompactionStatus,
|
||||
formatCompactionConfigAndStatus,
|
||||
zeroCompactionStatus,
|
||||
} from './compaction';
|
||||
|
||||
describe('compaction', () => {
|
||||
const BASIC_CONFIG: CompactionConfig = {};
|
||||
const ZERO_STATUS: CompactionStatus = {
|
||||
dataSource: 'tbl',
|
||||
scheduleStatus: 'RUNNING',
|
||||
bytesAwaitingCompaction: 0,
|
||||
bytesCompacted: 0,
|
||||
bytesSkipped: 0,
|
||||
segmentCountAwaitingCompaction: 0,
|
||||
segmentCountCompacted: 0,
|
||||
segmentCountSkipped: 0,
|
||||
intervalCountAwaitingCompaction: 0,
|
||||
intervalCountCompacted: 0,
|
||||
intervalCountSkipped: 0,
|
||||
};
|
||||
|
||||
it('zeroCompactionStatus', () => {
|
||||
expect(zeroCompactionStatus(ZERO_STATUS)).toEqual(true);
|
||||
|
||||
expect(
|
||||
zeroCompactionStatus({
|
||||
dataSource: 'tbl',
|
||||
scheduleStatus: 'RUNNING',
|
||||
bytesAwaitingCompaction: 1,
|
||||
bytesCompacted: 0,
|
||||
bytesSkipped: 0,
|
||||
segmentCountAwaitingCompaction: 0,
|
||||
segmentCountCompacted: 0,
|
||||
segmentCountSkipped: 0,
|
||||
intervalCountAwaitingCompaction: 0,
|
||||
intervalCountCompacted: 0,
|
||||
intervalCountSkipped: 0,
|
||||
}),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('formatCompactionConfigAndStatus', () => {
|
||||
expect(formatCompactionConfigAndStatus(undefined, undefined)).toEqual('Not enabled');
|
||||
|
||||
expect(formatCompactionConfigAndStatus(BASIC_CONFIG, undefined)).toEqual('Awaiting first run');
|
||||
|
||||
expect(formatCompactionConfigAndStatus(undefined, ZERO_STATUS)).toEqual('Running');
|
||||
|
||||
expect(formatCompactionConfigAndStatus(BASIC_CONFIG, ZERO_STATUS)).toEqual('Running');
|
||||
|
||||
expect(
|
||||
formatCompactionConfigAndStatus(BASIC_CONFIG, {
|
||||
dataSource: 'tbl',
|
||||
scheduleStatus: 'RUNNING',
|
||||
bytesAwaitingCompaction: 0,
|
||||
bytesCompacted: 100,
|
||||
bytesSkipped: 0,
|
||||
segmentCountAwaitingCompaction: 0,
|
||||
segmentCountCompacted: 10,
|
||||
segmentCountSkipped: 0,
|
||||
intervalCountAwaitingCompaction: 0,
|
||||
intervalCountCompacted: 10,
|
||||
intervalCountSkipped: 0,
|
||||
}),
|
||||
).toEqual('Fully compacted');
|
||||
});
|
||||
});
|
68
web-console/src/utils/compaction.ts
Normal file
68
web-console/src/utils/compaction.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function capitalizeFirst(str: string): string {
|
||||
return str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
export interface CompactionStatus {
|
||||
dataSource: string;
|
||||
scheduleStatus: string;
|
||||
bytesAwaitingCompaction: number;
|
||||
bytesCompacted: number;
|
||||
bytesSkipped: number;
|
||||
segmentCountAwaitingCompaction: number;
|
||||
segmentCountCompacted: number;
|
||||
segmentCountSkipped: number;
|
||||
intervalCountAwaitingCompaction: number;
|
||||
intervalCountCompacted: number;
|
||||
intervalCountSkipped: number;
|
||||
}
|
||||
|
||||
export type CompactionConfig = Record<string, any>;
|
||||
|
||||
export function zeroCompactionStatus(compactionStatus: CompactionStatus): boolean {
|
||||
return (
|
||||
!compactionStatus.bytesAwaitingCompaction &&
|
||||
!compactionStatus.bytesCompacted &&
|
||||
!compactionStatus.bytesSkipped &&
|
||||
!compactionStatus.segmentCountAwaitingCompaction &&
|
||||
!compactionStatus.segmentCountCompacted &&
|
||||
!compactionStatus.segmentCountSkipped &&
|
||||
!compactionStatus.intervalCountAwaitingCompaction &&
|
||||
!compactionStatus.intervalCountCompacted &&
|
||||
!compactionStatus.intervalCountSkipped
|
||||
);
|
||||
}
|
||||
|
||||
export function formatCompactionConfigAndStatus(
|
||||
compactionConfig: CompactionConfig | undefined,
|
||||
compactionStatus: CompactionStatus | undefined,
|
||||
) {
|
||||
if (compactionStatus) {
|
||||
if (compactionStatus.bytesAwaitingCompaction === 0 && !zeroCompactionStatus(compactionStatus)) {
|
||||
return 'Fully compacted';
|
||||
} else {
|
||||
return capitalizeFirst(compactionStatus.scheduleStatus);
|
||||
}
|
||||
} else if (compactionConfig) {
|
||||
return 'Awaiting first run';
|
||||
} else {
|
||||
return 'Not enabled';
|
||||
}
|
||||
}
|
@ -18,6 +18,11 @@
|
||||
|
||||
import {
|
||||
alphanumericCompare,
|
||||
formatBytes,
|
||||
formatBytesCompact,
|
||||
formatInteger,
|
||||
formatMegabytes,
|
||||
formatPercent,
|
||||
sortWithPrefixSuffix,
|
||||
sqlQueryCustomTableFilter,
|
||||
swapElements,
|
||||
@ -83,4 +88,34 @@ describe('general', () => {
|
||||
expect(swapElements(array, 2, 4)).toEqual(['a', 'b', 'e', 'd', 'c']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatInteger', () => {
|
||||
it('works', () => {
|
||||
expect(formatInteger(10000)).toEqual('10,000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('works', () => {
|
||||
expect(formatBytes(10000)).toEqual('10.00 KB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBytesCompact', () => {
|
||||
it('works', () => {
|
||||
expect(formatBytesCompact(10000)).toEqual('10.00KB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMegabytes', () => {
|
||||
it('works', () => {
|
||||
expect(formatMegabytes(30000000)).toEqual('28.6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPercent', () => {
|
||||
it('works', () => {
|
||||
expect(formatPercent(2 / 3)).toEqual('66.67%');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -231,6 +231,10 @@ export function formatMegabytes(n: number): string {
|
||||
return numeral(n / 1048576).format('0,0.0');
|
||||
}
|
||||
|
||||
export function formatPercent(n: number): string {
|
||||
return (n * 100).toFixed(2) + '%';
|
||||
}
|
||||
|
||||
function pad2(str: string | number): string {
|
||||
return ('00' + str).substr(-2);
|
||||
}
|
||||
|
@ -24,3 +24,4 @@ export * from './query-manager';
|
||||
export * from './query-cursor';
|
||||
export * from './local-storage-keys';
|
||||
export * from './column-metadata';
|
||||
export * from './compaction';
|
||||
|
@ -11,7 +11,20 @@ exports[`data source view matches snapshot 1`] = `
|
||||
localStorageKey="datasources-refresh-rate"
|
||||
onRefresh={[Function]}
|
||||
/>
|
||||
<Memo(MoreButton)>
|
||||
<Memo(MoreButton)
|
||||
altExtra={
|
||||
<Blueprint3.MenuItem
|
||||
disabled={false}
|
||||
icon="compressed"
|
||||
intent="danger"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
shouldDismissPopover={true}
|
||||
text="Force compaction run (debug)"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Blueprint3.MenuItem
|
||||
disabled={false}
|
||||
icon="application"
|
||||
@ -55,6 +68,8 @@ exports[`data source view matches snapshot 1`] = `
|
||||
"Avg. row size",
|
||||
"Replicated size",
|
||||
"Compaction",
|
||||
"% Compacted",
|
||||
"Left to be compacted",
|
||||
"Retention",
|
||||
"Actions",
|
||||
]
|
||||
@ -137,6 +152,7 @@ exports[`data source view matches snapshot 1`] = `
|
||||
"accessor": [Function],
|
||||
"filterable": false,
|
||||
"id": "availability",
|
||||
"minWidth": 200,
|
||||
"show": true,
|
||||
"sortMethod": [Function],
|
||||
},
|
||||
@ -150,6 +166,7 @@ exports[`data source view matches snapshot 1`] = `
|
||||
"accessor": "num_segments_to_load",
|
||||
"filterable": false,
|
||||
"id": "load-drop",
|
||||
"minWidth": 100,
|
||||
"show": true,
|
||||
},
|
||||
Object {
|
||||
@ -174,7 +191,7 @@ exports[`data source view matches snapshot 1`] = `
|
||||
"accessor": "avg_segment_size",
|
||||
"filterable": false,
|
||||
"show": true,
|
||||
"width": 200,
|
||||
"width": 150,
|
||||
},
|
||||
Object {
|
||||
"Cell": [Function],
|
||||
@ -217,8 +234,35 @@ exports[`data source view matches snapshot 1`] = `
|
||||
"Header": "Compaction",
|
||||
"accessor": [Function],
|
||||
"filterable": false,
|
||||
"id": "compaction",
|
||||
"id": "compactionStatus",
|
||||
"show": true,
|
||||
"width": 150,
|
||||
},
|
||||
Object {
|
||||
"Cell": [Function],
|
||||
"Header": <React.Fragment>
|
||||
% Compacted
|
||||
<br />
|
||||
bytes / segments / intervals
|
||||
</React.Fragment>,
|
||||
"accessor": [Function],
|
||||
"filterable": false,
|
||||
"id": "percentCompacted",
|
||||
"show": true,
|
||||
"width": 200,
|
||||
},
|
||||
Object {
|
||||
"Cell": [Function],
|
||||
"Header": <React.Fragment>
|
||||
Left to be
|
||||
<br />
|
||||
compacted
|
||||
</React.Fragment>,
|
||||
"accessor": [Function],
|
||||
"filterable": false,
|
||||
"id": "leftToBeCompacted",
|
||||
"show": true,
|
||||
"width": 100,
|
||||
},
|
||||
Object {
|
||||
"Cell": [Function],
|
||||
@ -226,6 +270,7 @@ exports[`data source view matches snapshot 1`] = `
|
||||
"accessor": [Function],
|
||||
"filterable": false,
|
||||
"id": "retention",
|
||||
"minWidth": 100,
|
||||
"show": true,
|
||||
},
|
||||
Object {
|
||||
|
@ -37,20 +37,19 @@ import {
|
||||
TableColumnSelector,
|
||||
ViewControlBar,
|
||||
} from '../../components';
|
||||
import {
|
||||
AsyncActionDialog,
|
||||
CompactionDialog,
|
||||
DEFAULT_MAX_ROWS_PER_SEGMENT,
|
||||
RetentionDialog,
|
||||
} from '../../dialogs';
|
||||
import { AsyncActionDialog, CompactionDialog, RetentionDialog } from '../../dialogs';
|
||||
import { DatasourceTableActionDialog } from '../../dialogs/datasource-table-action-dialog/datasource-table-action-dialog';
|
||||
import { AppToaster } from '../../singletons/toaster';
|
||||
import {
|
||||
addFilter,
|
||||
CompactionConfig,
|
||||
CompactionStatus,
|
||||
countBy,
|
||||
formatBytes,
|
||||
formatCompactionConfigAndStatus,
|
||||
formatInteger,
|
||||
formatMegabytes,
|
||||
formatPercent,
|
||||
getDruidErrorMessage,
|
||||
LocalStorageKeys,
|
||||
lookupBy,
|
||||
@ -58,6 +57,7 @@ import {
|
||||
queryDruidSql,
|
||||
QueryManager,
|
||||
QueryState,
|
||||
zeroCompactionStatus,
|
||||
} from '../../utils';
|
||||
import { BasicAction } from '../../utils/basic-action';
|
||||
import { Capabilities, CapabilitiesMode } from '../../utils/capabilities';
|
||||
@ -78,6 +78,8 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
|
||||
'Avg. row size',
|
||||
'Replicated size',
|
||||
'Compaction',
|
||||
'% Compacted',
|
||||
'Left to be compacted',
|
||||
'Retention',
|
||||
ACTION_COLUMN_LABEL,
|
||||
],
|
||||
@ -88,6 +90,8 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
|
||||
'Total data size',
|
||||
'Segment size',
|
||||
'Compaction',
|
||||
'% Compacted',
|
||||
'Left to be compacted',
|
||||
'Retention',
|
||||
ACTION_COLUMN_LABEL,
|
||||
],
|
||||
@ -107,10 +111,10 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
|
||||
function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
|
||||
const loadDrop: string[] = [];
|
||||
if (segmentsToLoad) {
|
||||
loadDrop.push(`${segmentsToLoad} segments to load`);
|
||||
loadDrop.push(`${pluralIfNeeded(segmentsToLoad, 'segment')} to load`);
|
||||
}
|
||||
if (segmentsToDrop) {
|
||||
loadDrop.push(`${segmentsToDrop} segments to drop`);
|
||||
loadDrop.push(`${pluralIfNeeded(segmentsToDrop, 'segment')} to drop`);
|
||||
}
|
||||
return loadDrop.join(', ') || 'No segments to load/drop';
|
||||
}
|
||||
@ -120,6 +124,7 @@ const formatSegmentSize = formatMegabytes;
|
||||
const formatTotalRows = formatInteger;
|
||||
const formatAvgRowSize = formatInteger;
|
||||
const formatReplicatedSize = formatBytes;
|
||||
const formatLeftToBeCompacted = formatBytes;
|
||||
|
||||
function twoLines(line1: string, line2: string) {
|
||||
return (
|
||||
@ -131,9 +136,19 @@ function twoLines(line1: string, line2: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function progress(done: number, awaiting: number): number {
|
||||
const d = done + awaiting;
|
||||
if (!d) return 0;
|
||||
return done / d;
|
||||
}
|
||||
|
||||
const PERCENT_BRACES = [formatPercent(1)];
|
||||
|
||||
interface Datasource {
|
||||
datasource: string;
|
||||
rules: Rule[];
|
||||
compactionConfig?: CompactionConfig;
|
||||
compactionStatus?: CompactionStatus;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -164,7 +179,7 @@ interface RetentionDialogOpenOn {
|
||||
|
||||
interface CompactionDialogOpenOn {
|
||||
datasource: string;
|
||||
compactionConfig: Record<string, any>;
|
||||
compactionConfig: CompactionConfig;
|
||||
}
|
||||
|
||||
export interface DatasourcesViewProps {
|
||||
@ -190,6 +205,7 @@ export interface DatasourcesViewState {
|
||||
datasourceToMarkSegmentsByIntervalIn?: string;
|
||||
useUnuseAction: 'use' | 'unuse';
|
||||
useUnuseInterval: string;
|
||||
showForceCompact: boolean;
|
||||
hiddenColumns: LocalStorageBackedArray<string>;
|
||||
showChart: boolean;
|
||||
chartWidth: number;
|
||||
@ -229,7 +245,7 @@ export class DatasourcesView extends React.PureComponent<
|
||||
FROM sys.segments
|
||||
GROUP BY 1`;
|
||||
|
||||
static formatRules(rules: any[]): string {
|
||||
static formatRules(rules: Rule[]): string {
|
||||
if (rules.length === 0) {
|
||||
return 'No rules';
|
||||
} else if (rules.length <= 2) {
|
||||
@ -259,6 +275,7 @@ GROUP BY 1`;
|
||||
showUnused: false,
|
||||
useUnuseAction: 'unuse',
|
||||
useUnuseInterval: '',
|
||||
showForceCompact: false,
|
||||
hiddenColumns: new LocalStorageBackedArray<string>(
|
||||
LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
|
||||
),
|
||||
@ -314,7 +331,7 @@ GROUP BY 1`;
|
||||
};
|
||||
}
|
||||
|
||||
const seen = countBy(datasources, (x: any) => x.datasource);
|
||||
const seen = countBy(datasources, x => x.datasource);
|
||||
|
||||
let unused: string[] = [];
|
||||
if (this.state.showUnused) {
|
||||
@ -329,18 +346,25 @@ GROUP BY 1`;
|
||||
const rulesResp = await axios.get('/druid/coordinator/v1/rules');
|
||||
const rules = rulesResp.data;
|
||||
|
||||
const compactionResp = await axios.get('/druid/coordinator/v1/config/compaction');
|
||||
const compaction = lookupBy(
|
||||
compactionResp.data.compactionConfigs,
|
||||
(c: any) => c.dataSource,
|
||||
const compactionConfigsResp = await axios.get('/druid/coordinator/v1/config/compaction');
|
||||
const compactionConfigs = lookupBy(
|
||||
compactionConfigsResp.data.compactionConfigs || [],
|
||||
(c: CompactionConfig) => c.dataSource,
|
||||
);
|
||||
|
||||
const compactionStatusesResp = await axios.get('/druid/coordinator/v1/compaction/status');
|
||||
const compactionStatuses = lookupBy(
|
||||
compactionStatusesResp.data.latestStatus || [],
|
||||
(c: CompactionStatus) => c.dataSource,
|
||||
);
|
||||
|
||||
const allDatasources = (datasources as any).concat(
|
||||
unused.map(d => ({ datasource: d, unused: true })),
|
||||
);
|
||||
allDatasources.forEach((ds: any) => {
|
||||
allDatasources.forEach((ds: Datasource) => {
|
||||
ds.rules = rules[ds.datasource] || [];
|
||||
ds.compaction = compaction[ds.datasource];
|
||||
ds.compactionConfig = compactionConfigs[ds.datasource];
|
||||
ds.compactionStatus = compactionStatuses[ds.datasource];
|
||||
});
|
||||
|
||||
return {
|
||||
@ -535,7 +559,18 @@ GROUP BY 1`;
|
||||
const { goToQuery, capabilities } = this.props;
|
||||
|
||||
return (
|
||||
<MoreButton>
|
||||
<MoreButton
|
||||
altExtra={
|
||||
<MenuItem
|
||||
icon={IconNames.COMPRESSED}
|
||||
text="Force compaction run (debug)"
|
||||
intent={Intent.DANGER}
|
||||
onClick={() => {
|
||||
this.setState({ showForceCompact: true });
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{capabilities.hasSql() && (
|
||||
<MenuItem
|
||||
icon={IconNames.APPLICATION}
|
||||
@ -552,7 +587,32 @@ GROUP BY 1`;
|
||||
);
|
||||
}
|
||||
|
||||
private saveRules = async (datasource: string, rules: any[], comment: string) => {
|
||||
renderForceCompactAction() {
|
||||
const { showForceCompact } = this.state;
|
||||
if (!showForceCompact) return;
|
||||
|
||||
return (
|
||||
<AsyncActionDialog
|
||||
action={async () => {
|
||||
const resp = await axios.post(`/druid/coordinator/v1/compaction/compact`, {});
|
||||
return resp.data;
|
||||
}}
|
||||
confirmButtonText="Force compaction run"
|
||||
successText="Out of band compaction run has been initiated"
|
||||
failText="Could not force compaction"
|
||||
intent={Intent.DANGER}
|
||||
onClose={() => {
|
||||
this.setState({ showForceCompact: false });
|
||||
}}
|
||||
>
|
||||
<p>Are you sure you want to force a compaction run?</p>
|
||||
<p>This functionality only exists for debugging and testing reasons.</p>
|
||||
<p>If you are running it in production you are doing something wrong.</p>
|
||||
</AsyncActionDialog>
|
||||
);
|
||||
}
|
||||
|
||||
private saveRules = async (datasource: string, rules: Rule[], comment: string) => {
|
||||
try {
|
||||
await axios.post(`/druid/coordinator/v1/rules/${datasource}`, rules, {
|
||||
headers: {
|
||||
@ -642,8 +702,8 @@ GROUP BY 1`;
|
||||
getDatasourceActions(
|
||||
datasource: string,
|
||||
unused: boolean,
|
||||
rules: any[],
|
||||
compactionConfig: Record<string, any>,
|
||||
rules: Rule[],
|
||||
compactionConfig: CompactionConfig,
|
||||
): BasicAction[] {
|
||||
const { goToQuery, goToTask, capabilities } = this.props;
|
||||
|
||||
@ -821,6 +881,12 @@ GROUP BY 1`;
|
||||
|
||||
const replicatedSizeValues = datasources.map(d => formatReplicatedSize(d.replicated_size));
|
||||
|
||||
const leftToBeCompactedValues = datasources.map(d =>
|
||||
d.compactionStatus
|
||||
? formatLeftToBeCompacted(d.compactionStatus.bytesAwaitingCompaction)
|
||||
: '-',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactTable
|
||||
@ -842,8 +908,7 @@ GROUP BY 1`;
|
||||
show: hiddenColumns.exists('Datasource name'),
|
||||
accessor: 'datasource',
|
||||
width: 150,
|
||||
Cell: row => {
|
||||
const value = row.value;
|
||||
Cell: ({ value }) => {
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
@ -862,14 +927,15 @@ GROUP BY 1`;
|
||||
show: hiddenColumns.exists('Availability'),
|
||||
id: 'availability',
|
||||
filterable: false,
|
||||
minWidth: 200,
|
||||
accessor: row => {
|
||||
return {
|
||||
num_available: row.num_available_segments,
|
||||
num_total: row.num_segments,
|
||||
};
|
||||
},
|
||||
Cell: row => {
|
||||
const { datasource, num_available_segments, num_segments, unused } = row.original;
|
||||
Cell: ({ original }) => {
|
||||
const { datasource, num_available_segments, num_segments, unused } = original;
|
||||
|
||||
if (unused) {
|
||||
return (
|
||||
@ -927,8 +993,9 @@ GROUP BY 1`;
|
||||
id: 'load-drop',
|
||||
accessor: 'num_segments_to_load',
|
||||
filterable: false,
|
||||
Cell: row => {
|
||||
const { num_segments_to_load, num_segments_to_drop } = row.original;
|
||||
minWidth: 100,
|
||||
Cell: ({ original }) => {
|
||||
const { num_segments_to_load, num_segments_to_drop } = original;
|
||||
return formatLoadDrop(num_segments_to_load, num_segments_to_drop);
|
||||
},
|
||||
},
|
||||
@ -938,8 +1005,8 @@ GROUP BY 1`;
|
||||
accessor: 'total_data_size',
|
||||
filterable: false,
|
||||
width: 100,
|
||||
Cell: row => (
|
||||
<BracedText text={formatTotalDataSize(row.value)} braces={totalDataSizeValues} />
|
||||
Cell: ({ value }) => (
|
||||
<BracedText text={formatTotalDataSize(value)} braces={totalDataSizeValues} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -947,18 +1014,18 @@ GROUP BY 1`;
|
||||
show: hiddenColumns.exists('Segment size'),
|
||||
accessor: 'avg_segment_size',
|
||||
filterable: false,
|
||||
width: 200,
|
||||
Cell: row => (
|
||||
width: 150,
|
||||
Cell: ({ value, original }) => (
|
||||
<>
|
||||
<BracedText
|
||||
text={formatSegmentSize(row.original.min_segment_size)}
|
||||
text={formatSegmentSize(original.min_segment_size)}
|
||||
braces={minSegmentSizeValues}
|
||||
/>{' '}
|
||||
{' '}
|
||||
<BracedText text={formatSegmentSize(row.value)} braces={avgSegmentSizeValues} />{' '}
|
||||
<BracedText text={formatSegmentSize(value)} braces={avgSegmentSizeValues} />{' '}
|
||||
{' '}
|
||||
<BracedText
|
||||
text={formatSegmentSize(row.original.max_segment_size)}
|
||||
text={formatSegmentSize(original.max_segment_size)}
|
||||
braces={maxSegmentSizeValues}
|
||||
/>
|
||||
</>
|
||||
@ -970,8 +1037,8 @@ GROUP BY 1`;
|
||||
accessor: 'total_rows',
|
||||
filterable: false,
|
||||
width: 100,
|
||||
Cell: row => (
|
||||
<BracedText text={formatTotalRows(row.value)} braces={totalRowsValues} />
|
||||
Cell: ({ value }) => (
|
||||
<BracedText text={formatTotalRows(value)} braces={totalRowsValues} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -980,8 +1047,8 @@ GROUP BY 1`;
|
||||
accessor: 'avg_row_size',
|
||||
filterable: false,
|
||||
width: 100,
|
||||
Cell: row => (
|
||||
<BracedText text={formatAvgRowSize(row.value)} braces={avgRowSizeValues} />
|
||||
Cell: ({ value }) => (
|
||||
<BracedText text={formatAvgRowSize(value)} braces={avgRowSizeValues} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -990,74 +1057,145 @@ GROUP BY 1`;
|
||||
accessor: 'replicated_size',
|
||||
filterable: false,
|
||||
width: 100,
|
||||
Cell: row => (
|
||||
<BracedText text={formatReplicatedSize(row.value)} braces={replicatedSizeValues} />
|
||||
Cell: ({ value }) => (
|
||||
<BracedText text={formatReplicatedSize(value)} braces={replicatedSizeValues} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Compaction',
|
||||
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Compaction'),
|
||||
id: 'compaction',
|
||||
accessor: row => Boolean(row.compaction),
|
||||
id: 'compactionStatus',
|
||||
accessor: row => Boolean(row.compactionStatus),
|
||||
filterable: false,
|
||||
Cell: row => {
|
||||
const { compaction } = row.original;
|
||||
let text: string;
|
||||
if (compaction) {
|
||||
if (compaction.maxRowsPerSegment == null) {
|
||||
text = `Target: Default (${formatInteger(DEFAULT_MAX_ROWS_PER_SEGMENT)})`;
|
||||
} else {
|
||||
text = `Target: ${formatInteger(compaction.maxRowsPerSegment)}`;
|
||||
}
|
||||
} else {
|
||||
text = 'Not enabled';
|
||||
}
|
||||
width: 150,
|
||||
Cell: ({ original }) => {
|
||||
const { datasource, compactionConfig, compactionStatus } = original;
|
||||
return (
|
||||
<span
|
||||
className="clickable-cell"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
compactionDialogOpenOn: {
|
||||
datasource: row.original.datasource,
|
||||
compactionConfig: compaction,
|
||||
datasource,
|
||||
compactionConfig,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{text}
|
||||
{formatCompactionConfigAndStatus(compactionConfig, compactionStatus)}
|
||||
<ActionIcon icon={IconNames.EDIT} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: twoLines('% Compacted', 'bytes / segments / intervals'),
|
||||
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('% Compacted'),
|
||||
id: 'percentCompacted',
|
||||
width: 200,
|
||||
accessor: ({ compactionStatus }) =>
|
||||
compactionStatus && compactionStatus.bytesCompacted
|
||||
? compactionStatus.bytesCompacted /
|
||||
(compactionStatus.bytesAwaitingCompaction + compactionStatus.bytesCompacted)
|
||||
: 0,
|
||||
filterable: false,
|
||||
Cell: ({ original }) => {
|
||||
const { compactionStatus } = original;
|
||||
|
||||
if (!compactionStatus || zeroCompactionStatus(compactionStatus)) {
|
||||
return (
|
||||
<>
|
||||
<BracedText text="-" braces={PERCENT_BRACES} /> {' '}
|
||||
<BracedText text="-" braces={PERCENT_BRACES} /> {' '}
|
||||
<BracedText text="-" braces={PERCENT_BRACES} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BracedText
|
||||
text={formatPercent(
|
||||
progress(
|
||||
compactionStatus.bytesCompacted,
|
||||
compactionStatus.bytesAwaitingCompaction,
|
||||
),
|
||||
)}
|
||||
braces={PERCENT_BRACES}
|
||||
/>{' '}
|
||||
{' '}
|
||||
<BracedText
|
||||
text={formatPercent(
|
||||
progress(
|
||||
compactionStatus.segmentCountCompacted,
|
||||
compactionStatus.segmentCountAwaitingCompaction,
|
||||
),
|
||||
)}
|
||||
braces={PERCENT_BRACES}
|
||||
/>{' '}
|
||||
{' '}
|
||||
<BracedText
|
||||
text={formatPercent(
|
||||
progress(
|
||||
compactionStatus.intervalCountCompacted,
|
||||
compactionStatus.intervalCountAwaitingCompaction,
|
||||
),
|
||||
)}
|
||||
braces={PERCENT_BRACES}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: twoLines('Left to be', 'compacted'),
|
||||
show:
|
||||
capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Left to be compacted'),
|
||||
id: 'leftToBeCompacted',
|
||||
width: 100,
|
||||
accessor: ({ compactionStatus }) =>
|
||||
(compactionStatus && compactionStatus.bytesAwaitingCompaction) || 0,
|
||||
filterable: false,
|
||||
Cell: ({ original }) => {
|
||||
const { compactionStatus } = original;
|
||||
|
||||
if (!compactionStatus) {
|
||||
return <BracedText text="-" braces={leftToBeCompactedValues} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BracedText
|
||||
text={formatLeftToBeCompacted(compactionStatus.bytesAwaitingCompaction)}
|
||||
braces={leftToBeCompactedValues}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: 'Retention',
|
||||
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Retention'),
|
||||
id: 'retention',
|
||||
accessor: row => row.rules.length,
|
||||
filterable: false,
|
||||
Cell: row => {
|
||||
const { rules } = row.original;
|
||||
let text: string;
|
||||
if (rules.length === 0) {
|
||||
text = 'Cluster default: ' + DatasourcesView.formatRules(defaultRules);
|
||||
} else {
|
||||
text = DatasourcesView.formatRules(rules);
|
||||
}
|
||||
|
||||
minWidth: 100,
|
||||
Cell: ({ original }) => {
|
||||
const { datasource, rules } = original;
|
||||
return (
|
||||
<span
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
retentionDialogOpenOn: {
|
||||
datasource: row.original.datasource,
|
||||
rules: row.original.rules,
|
||||
datasource,
|
||||
rules,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="clickable-cell"
|
||||
>
|
||||
{text}
|
||||
{rules.length
|
||||
? DatasourcesView.formatRules(rules)
|
||||
: `Cluster default: ${DatasourcesView.formatRules(defaultRules)}`}
|
||||
|
||||
<ActionIcon icon={IconNames.EDIT} />
|
||||
</span>
|
||||
);
|
||||
@ -1070,9 +1208,8 @@ GROUP BY 1`;
|
||||
id: ACTION_COLUMN_ID,
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
filterable: false,
|
||||
Cell: row => {
|
||||
const datasource = row.value;
|
||||
const { unused, rules, compaction } = row.original;
|
||||
Cell: ({ value: datasource, original }) => {
|
||||
const { unused, rules, compaction } = original;
|
||||
const datasourceActions = this.getDatasourceActions(
|
||||
datasource,
|
||||
unused,
|
||||
@ -1101,6 +1238,7 @@ GROUP BY 1`;
|
||||
{this.renderKillAction()}
|
||||
{this.renderRetentionDialog()}
|
||||
{this.renderCompactionDialog()}
|
||||
{this.renderForceCompactAction()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
|
||||
public goToPosition(rowColumn: RowColumn) {
|
||||
const { aceEditor } = this;
|
||||
if (!aceEditor) return;
|
||||
aceEditor.focus(); // Grab the focus also
|
||||
aceEditor.focus(); // Grab the focus
|
||||
aceEditor.getSelection().moveCursorTo(rowColumn.row, rowColumn.column);
|
||||
if (rowColumn.endRow && rowColumn.endColumn) {
|
||||
aceEditor
|
||||
|
Loading…
x
Reference in New Issue
Block a user