Web console: Display compaction status (#10438)
* init compaction status
* % compacted
* final UI tweaks
* extracted utils, added tests
* add tests to general foramt functions
diff --git a/web-console/src/components/json-input/json-input.tsx b/web-console/src/components/json-input/json-input.tsx
index f1dd512..eba0620 100644
--- a/web-console/src/components/json-input/json-input.tsx
+++ b/web-console/src/components/json-input/json-input.tsx
@@ -80,12 +80,11 @@
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 @@
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}
diff --git a/web-console/src/components/more-button/more-button.tsx b/web-console/src/components/more-button/more-button.tsx
index 7a161b7..4bcef07 100644
--- a/web-console/src/components/more-button/more-button.tsx
+++ b/web-console/src/components/more-button/more-button.tsx
@@ -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 @@
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>
diff --git a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
index 4ff3b51..2b1635e 100644
--- a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
@@ -38,6 +38,12 @@
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 @@
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 @@
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 @@
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 {
diff --git a/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx b/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx
index 2c0f704a..4a2611b 100644
--- a/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx
+++ b/web-console/src/dialogs/compaction-dialog/compaction-dialog.tsx
@@ -24,8 +24,6 @@
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 @@
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
diff --git a/web-console/src/utils/compaction.spec.ts b/web-console/src/utils/compaction.spec.ts
new file mode 100644
index 0000000..452cfbe
--- /dev/null
+++ b/web-console/src/utils/compaction.spec.ts
@@ -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');
+ });
+});
diff --git a/web-console/src/utils/compaction.ts b/web-console/src/utils/compaction.ts
new file mode 100644
index 0000000..34634a1
--- /dev/null
+++ b/web-console/src/utils/compaction.ts
@@ -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';
+ }
+}
diff --git a/web-console/src/utils/general.spec.ts b/web-console/src/utils/general.spec.ts
index 2a327bd..9b2398b 100644
--- a/web-console/src/utils/general.spec.ts
+++ b/web-console/src/utils/general.spec.ts
@@ -18,6 +18,11 @@
import {
alphanumericCompare,
+ formatBytes,
+ formatBytesCompact,
+ formatInteger,
+ formatMegabytes,
+ formatPercent,
sortWithPrefixSuffix,
sqlQueryCustomTableFilter,
swapElements,
@@ -83,4 +88,34 @@
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%');
+ });
+ });
});
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 47c7e17..7afe385 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -231,6 +231,10 @@
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);
}
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index b46d675..2bcf661 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -24,3 +24,4 @@
export * from './query-cursor';
export * from './local-storage-keys';
export * from './column-metadata';
+export * from './compaction';
diff --git a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
index 1d5ce02..2364a8a 100755
--- a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
+++ b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
@@ -11,7 +11,20 @@
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 @@
"Avg. row size",
"Replicated size",
"Compaction",
+ "% Compacted",
+ "Left to be compacted",
"Retention",
"Actions",
]
@@ -137,6 +152,7 @@
"accessor": [Function],
"filterable": false,
"id": "availability",
+ "minWidth": 200,
"show": true,
"sortMethod": [Function],
},
@@ -150,6 +166,7 @@
"accessor": "num_segments_to_load",
"filterable": false,
"id": "load-drop",
+ "minWidth": 100,
"show": true,
},
Object {
@@ -174,7 +191,7 @@
"accessor": "avg_segment_size",
"filterable": false,
"show": true,
- "width": 200,
+ "width": 150,
},
Object {
"Cell": [Function],
@@ -217,8 +234,35 @@
"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 @@
"accessor": [Function],
"filterable": false,
"id": "retention",
+ "minWidth": 100,
"show": true,
},
Object {
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx
index 20a38e7..59688b0 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -37,20 +37,19 @@
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 @@
queryDruidSql,
QueryManager,
QueryState,
+ zeroCompactionStatus,
} from '../../utils';
import { BasicAction } from '../../utils/basic-action';
import { Capabilities, CapabilitiesMode } from '../../utils/capabilities';
@@ -78,6 +78,8 @@
'Avg. row size',
'Replicated size',
'Compaction',
+ '% Compacted',
+ 'Left to be compacted',
'Retention',
ACTION_COLUMN_LABEL,
],
@@ -88,6 +90,8 @@
'Total data size',
'Segment size',
'Compaction',
+ '% Compacted',
+ 'Left to be compacted',
'Retention',
ACTION_COLUMN_LABEL,
],
@@ -107,10 +111,10 @@
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 formatTotalRows = formatInteger;
const formatAvgRowSize = formatInteger;
const formatReplicatedSize = formatBytes;
+const formatLeftToBeCompacted = formatBytes;
function twoLines(line1: string, line2: string) {
return (
@@ -131,9 +136,19 @@
);
}
+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 CompactionDialogOpenOn {
datasource: string;
- compactionConfig: Record<string, any>;
+ compactionConfig: CompactionConfig;
}
export interface DatasourcesViewProps {
@@ -190,6 +205,7 @@
datasourceToMarkSegmentsByIntervalIn?: string;
useUnuseAction: 'use' | 'unuse';
useUnuseInterval: string;
+ showForceCompact: boolean;
hiddenColumns: LocalStorageBackedArray<string>;
showChart: boolean;
chartWidth: number;
@@ -229,7 +245,7 @@
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 @@
showUnused: false,
useUnuseAction: 'unuse',
useUnuseInterval: '',
+ showForceCompact: false,
hiddenColumns: new LocalStorageBackedArray<string>(
LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
),
@@ -314,7 +331,7 @@
};
}
- 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 @@
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 @@
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 @@
);
}
- 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 @@
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 @@
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 @@
show: hiddenColumns.exists('Datasource name'),
accessor: 'datasource',
width: 150,
- Cell: row => {
- const value = row.value;
+ Cell: ({ value }) => {
return (
<a
onClick={() => {
@@ -862,14 +927,15 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
{this.renderKillAction()}
{this.renderRetentionDialog()}
{this.renderCompactionDialog()}
+ {this.renderForceCompactAction()}
</>
);
}
diff --git a/web-console/src/views/query-view/query-input/query-input.tsx b/web-console/src/views/query-view/query-input/query-input.tsx
index 7897953..12862a3 100644
--- a/web-console/src/views/query-view/query-input/query-input.tsx
+++ b/web-console/src/views/query-view/query-input/query-input.tsx
@@ -214,7 +214,7 @@
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