Web console: Improve number alignment in tables (#10389)
* Improve tables
* removed unused state interfaces
* better copy
* one more functional component
* updated e2e tests
* extract braced text correctly
diff --git a/web-console/e2e-tests/component/datasources/datasource.ts b/web-console/e2e-tests/component/datasources/datasource.ts
index dc4ae4c..a6cdc58 100644
--- a/web-console/e2e-tests/component/datasources/datasource.ts
+++ b/web-console/e2e-tests/component/datasources/datasource.ts
@@ -28,7 +28,7 @@
interface DatasourceProps {
readonly name: string;
readonly availability: string;
- readonly numRows: number;
+ readonly totalRows: number;
}
export interface Datasource extends DatasourceProps {}
diff --git a/web-console/e2e-tests/component/datasources/overview.ts b/web-console/e2e-tests/component/datasources/overview.ts
index 4e9fb6a..5b55eed 100644
--- a/web-console/e2e-tests/component/datasources/overview.ts
+++ b/web-console/e2e-tests/component/datasources/overview.ts
@@ -29,12 +29,14 @@
NAME = 0,
AVAILABILITY,
SEGMENT_LOAD_DROP,
- RETENTION,
+ TOTAL_DATA_SIZE,
+ SEGMENT_SIZE,
+ TOTAL_ROWS,
+ AVG_ROW_SIZE,
REPLICATED_SIZE,
- SIZE,
COMPACTION,
- AVG_SEGMENT_SIZE,
- NUM_ROWS,
+ RETENTION,
+ ACTIONS,
}
/**
@@ -60,7 +62,7 @@
new Datasource({
name: row[DatasourceColumn.NAME],
availability: row[DatasourceColumn.AVAILABILITY],
- numRows: DatasourcesOverview.parseNumber(row[DatasourceColumn.NUM_ROWS]),
+ totalRows: DatasourcesOverview.parseNumber(row[DatasourceColumn.TOTAL_ROWS]),
}),
);
}
diff --git a/web-console/e2e-tests/tutorial-batch.spec.ts b/web-console/e2e-tests/tutorial-batch.spec.ts
index 12876ea..52aed72 100644
--- a/web-console/e2e-tests/tutorial-batch.spec.ts
+++ b/web-console/e2e-tests/tutorial-batch.spec.ts
@@ -156,7 +156,7 @@
const datasource = datasources.find(t => t.name === datasourceName);
expect(datasource).toBeDefined();
expect(datasource!.availability).toMatch('Fully available (1 segment)');
- expect(datasource!.numRows).toBe(39244);
+ expect(datasource!.totalRows).toBe(39244);
});
}
diff --git a/web-console/e2e-tests/util/table.ts b/web-console/e2e-tests/util/table.ts
index 77ca53b..9f0419a 100644
--- a/web-console/e2e-tests/util/table.ts
+++ b/web-console/e2e-tests/util/table.ts
@@ -39,7 +39,12 @@
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const columns = row.querySelectorAll(rowSelector);
- const values = Array.from(columns).map(c => (c as HTMLElement).innerText);
+ const values = Array.from(columns).map(c => {
+ const realTexts = Array.from(c.querySelectorAll('.real-text'));
+ return realTexts.length
+ ? (realTexts[0] as HTMLElement).innerText
+ : (c as HTMLElement).innerText;
+ });
if (!values.every(value => value === BLANK_VALUE)) {
data.push(values);
}
diff --git a/web-console/src/bootstrap/react-table-defaults.tsx b/web-console/src/bootstrap/react-table-defaults.tsx
index 67b4121..e397d3b 100644
--- a/web-console/src/bootstrap/react-table-defaults.tsx
+++ b/web-console/src/bootstrap/react-table-defaults.tsx
@@ -24,17 +24,11 @@
import { ReactTableCustomPagination } from './react-table-custom-pagination';
-/* tslint:disable:max-classes-per-file */
-
-class NoData extends React.PureComponent {
- render(): JSX.Element | null {
- const { children } = this.props;
- if (!children) return null;
- return <div className="rt-noData">{children}</div>;
- }
-}
-
-/* tslint:enable:max-classes-per-file */
+export const NoData = React.memo(function NoData(props) {
+ const { children } = props;
+ if (!children) return null;
+ return <div className="rt-noData">{children}</div>;
+});
Object.assign(ReactTableDefaults, {
className: '-striped -highlight',
diff --git a/web-console/src/components/braced-text/__snapshots__/braced-text.spec.tsx.snap b/web-console/src/components/braced-text/__snapshots__/braced-text.spec.tsx.snap
new file mode 100644
index 0000000..e46e809
--- /dev/null
+++ b/web-console/src/components/braced-text/__snapshots__/braced-text.spec.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BracedText matches snapshot 1`] = `
+<span
+ className="braced-text"
+>
+ <span
+ className="brace-text"
+ >
+ 00,000.0
+ </span>
+ <span
+ className="real-text"
+ >
+ 23.3
+ </span>
+</span>
+`;
diff --git a/web-console/src/components/braced-text/braced-text.scss b/web-console/src/components/braced-text/braced-text.scss
new file mode 100644
index 0000000..76ccb0a
--- /dev/null
+++ b/web-console/src/components/braced-text/braced-text.scss
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+.braced-text {
+ position: relative;
+ text-align: right;
+ white-space: nowrap;
+
+ .brace-text {
+ position: relative;
+ top: -50000px; // Send it into the stratosphere (get it out of the parent container to prevent the browser from adding '...')
+ opacity: 0;
+ pointer-events: none;
+ }
+
+ .real-text {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ }
+}
diff --git a/web-console/src/components/braced-text/braced-text.spec.tsx b/web-console/src/components/braced-text/braced-text.spec.tsx
new file mode 100644
index 0000000..3beb794
--- /dev/null
+++ b/web-console/src/components/braced-text/braced-text.spec.tsx
@@ -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 { shallow } from 'enzyme';
+import React from 'react';
+
+import { BracedText } from './braced-text';
+
+describe('BracedText', () => {
+ it('matches snapshot', () => {
+ const bracedText = shallow(<BracedText text="23.3" braces={['34', '23,423.4']} />);
+
+ expect(bracedText).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/components/braced-text/braced-text.tsx b/web-console/src/components/braced-text/braced-text.tsx
new file mode 100644
index 0000000..ff517ea
--- /dev/null
+++ b/web-console/src/components/braced-text/braced-text.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 React from 'react';
+
+import './braced-text.scss';
+
+export interface BracedTextProps {
+ text: string;
+ braces: string[];
+}
+
+export function findMostNumbers(strings: string[]): string {
+ let longest = '';
+ let longestNumLengthPlusOne = 1;
+ for (const s of strings) {
+ const parts = s.split(/\d/g);
+ const numLengthPlusOne = parts.length;
+ if (longestNumLengthPlusOne < numLengthPlusOne) {
+ longest = parts.join('0');
+ longestNumLengthPlusOne = numLengthPlusOne;
+ } else if (longestNumLengthPlusOne === numLengthPlusOne && longest.length < s.length) {
+ // Tie break on general length
+ longest = parts.join('0');
+ longestNumLengthPlusOne = numLengthPlusOne;
+ }
+ }
+ return longest;
+}
+
+export const BracedText = React.memo(function BracedText(props: BracedTextProps) {
+ const { text, braces } = props;
+
+ return (
+ <span className="braced-text">
+ <span className="brace-text">{findMostNumbers(braces.concat(text))}</span>
+ <span className="real-text">{text}</span>
+ </span>
+ );
+});
diff --git a/web-console/src/components/datasource-columns-table/datasource-columns-table.tsx b/web-console/src/components/datasource-columns-table/datasource-columns-table.tsx
index 635a038..865cd72 100644
--- a/web-console/src/components/datasource-columns-table/datasource-columns-table.tsx
+++ b/web-console/src/components/datasource-columns-table/datasource-columns-table.tsx
@@ -21,7 +21,7 @@
import ReactTable from 'react-table';
import { useQueryManager } from '../../hooks';
-import { ColumnMetadata, queryDruidSql, QueryState } from '../../utils';
+import { ColumnMetadata, queryDruidSql } from '../../utils';
import { Loader } from '../loader/loader';
import './datasource-columns-table.scss';
@@ -36,10 +36,6 @@
downloadFilename?: string;
}
-export interface DatasourceColumnsTableState {
- columnsState: QueryState<DatasourceColumnsTableRow[]>;
-}
-
export const DatasourceColumnsTable = React.memo(function DatasourceColumnsTable(
props: DatasourceColumnsTableProps,
) {
diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts
index a806fff..7ebc13e 100644
--- a/web-console/src/components/index.ts
+++ b/web-console/src/components/index.ts
@@ -20,6 +20,7 @@
export * from './action-icon/action-icon';
export * from './array-input/array-input';
export * from './auto-form/auto-form';
+export * from './braced-text/braced-text';
export * from './center-message/center-message';
export * from './clearable-input/clearable-input';
export * from './external-link/external-link';
diff --git a/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap b/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
index ee45126..c7a96a1 100644
--- a/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
+++ b/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
@@ -56,12 +56,12 @@
checked=""
name="Blueprint3.RadioGroup-0"
type="radio"
- value="countData"
+ value="sizeData"
/>
<span
class="bp3-control-indicator"
/>
- Segment count
+ Total size
</label>
<label
class="bp3-control bp3-radio"
@@ -69,12 +69,12 @@
<input
name="Blueprint3.RadioGroup-0"
type="radio"
- value="sizeData"
+ value="countData"
/>
<span
class="bp3-control-indicator"
/>
- Total size
+ Segment count
</label>
</div>
</div>
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index c81d686..b928d15 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -38,13 +38,15 @@
dataQueryManager?: QueryManager<{ capabilities: Capabilities; timeSpan: number }, any>;
}
+type ActiveDataType = 'sizeData' | 'countData';
+
interface SegmentTimelineState {
data?: Record<string, any>;
datasources: string[];
stackedData?: Record<string, BarUnitData[]>;
singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
activeDatasource: string | null;
- activeDataType: string; // "countData" || "sizeData"
+ activeDataType: ActiveDataType;
dataToRender: BarUnitData[];
timeSpan: number; // by months
loading: boolean;
@@ -232,7 +234,7 @@
singleDatasourceData: {},
dataToRender: [],
activeDatasource: null,
- activeDataType: 'countData',
+ activeDataType: 'sizeData',
timeSpan: DEFAULT_TIME_SPAN_MONTHS,
loading: true,
xScale: null,
@@ -517,8 +519,8 @@
onChange={(e: any) => this.setState({ activeDataType: e.target.value })}
selectedValue={activeDataType}
>
- <Radio label={'Segment count'} value={'countData'} />
<Radio label={'Total size'} value={'sizeData'} />
+ <Radio label={'Segment count'} value={'countData'} />
</RadioGroup>
</FormGroup>
diff --git a/web-console/src/components/show-history/show-history.tsx b/web-console/src/components/show-history/show-history.tsx
index 743607a..9f55d29 100644
--- a/web-console/src/components/show-history/show-history.tsx
+++ b/web-console/src/components/show-history/show-history.tsx
@@ -21,7 +21,6 @@
import React from 'react';
import { useQueryManager } from '../../hooks';
-import { QueryState } from '../../utils';
import { Loader } from '../loader/loader';
import { ShowValue } from '../show-value/show-value';
@@ -37,10 +36,6 @@
downloadFilename?: string;
}
-export interface ShowHistoryState {
- historyState: QueryState<VersionSpec[]>;
-}
-
export const ShowHistory = React.memo(function ShowHistory(props: ShowHistoryProps) {
const { downloadFilename, endpoint } = props;
diff --git a/web-console/src/components/show-json/show-json.tsx b/web-console/src/components/show-json/show-json.tsx
index f312129..c178afc 100644
--- a/web-console/src/components/show-json/show-json.tsx
+++ b/web-console/src/components/show-json/show-json.tsx
@@ -24,7 +24,7 @@
import { useQueryManager } from '../../hooks';
import { AppToaster } from '../../singletons/toaster';
import { UrlBaser } from '../../singletons/url-baser';
-import { downloadFile, QueryState } from '../../utils';
+import { downloadFile } from '../../utils';
import { Loader } from '../loader/loader';
import './show-json.scss';
@@ -35,10 +35,6 @@
downloadFilename?: string;
}
-export interface ShowJsonState {
- jsonState: QueryState<string>;
-}
-
export const ShowJson = React.memo(function ShowJson(props: ShowJsonProps) {
const { endpoint, transform, downloadFilename } = props;
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
index 76a3b59..4e98195 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
@@ -40,11 +40,6 @@
onSave: (datasource: string, newRules: Rule[], comment: string) => void;
}
-export interface RetentionDialogState {
- currentRules: Rule[];
- historyRecords: any[] | undefined;
-}
-
export const RetentionDialog = React.memo(function RetentionDialog(props: RetentionDialogProps) {
const { datasource, onCancel, onEditDefaults, rules, defaultRules, tiers } = props;
const [currentRules, setCurrentRules] = useState(props.rules);
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index ddf40a1..47c7e17 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -215,7 +215,7 @@
// ----------------------------
-export function formatNumber(n: number): string {
+export function formatInteger(n: number): string {
return numeral(n).format('0,0');
}
@@ -227,6 +227,10 @@
return numeral(n).format('0.00b');
}
+export function formatMegabytes(n: number): string {
+ return numeral(n / 1048576).format('0,0.0');
+}
+
function pad2(str: string | number): string {
return ('00' + str).substr(-2);
}
@@ -240,7 +244,7 @@
export function pluralIfNeeded(n: number, singular: string, plural?: string): string {
if (!plural) plural = singular + 's';
- return `${formatNumber(n)} ${n === 1 ? singular : plural}`;
+ return `${formatInteger(n)} ${n === 1 ? singular : plural}`;
}
// ----------------------------
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 b5c61ef..1d5ce02 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
@@ -46,15 +46,16 @@
<Memo(TableColumnSelector)
columns={
Array [
- "Datasource",
+ "Datasource name",
"Availability",
- "Segment load/drop",
- "Retention",
+ "Segment load/drop queues",
+ "Total data size",
+ "Segment size",
+ "Total rows",
+ "Avg. row size",
"Replicated size",
- "Size",
"Compaction",
- "Avg. segment size",
- "Num rows",
+ "Retention",
"Actions",
]
}
@@ -121,7 +122,11 @@
Array [
Object {
"Cell": [Function],
- "Header": "Datasource",
+ "Header": <React.Fragment>
+ Datasource
+ <br />
+ name
+ </React.Fragment>,
"accessor": "datasource",
"show": true,
"width": 150,
@@ -137,7 +142,11 @@
},
Object {
"Cell": [Function],
- "Header": "Segment load/drop",
+ "Header": <React.Fragment>
+ Segment load/drop
+ <br />
+ queues
+ </React.Fragment>,
"accessor": "num_segments_to_load",
"filterable": false,
"id": "load-drop",
@@ -145,24 +154,60 @@
},
Object {
"Cell": [Function],
- "Header": "Retention",
- "accessor": [Function],
- "filterable": false,
- "id": "retention",
- "show": true,
- },
- Object {
- "Cell": [Function],
- "Header": "Replicated size",
- "accessor": "replicated_size",
+ "Header": <React.Fragment>
+ Total
+ <br />
+ data size
+ </React.Fragment>,
+ "accessor": "total_data_size",
"filterable": false,
"show": true,
"width": 100,
},
Object {
"Cell": [Function],
- "Header": "Size",
- "accessor": "size",
+ "Header": <React.Fragment>
+ Segment size (MB)
+ <br />
+ min / avg / max
+ </React.Fragment>,
+ "accessor": "avg_segment_size",
+ "filterable": false,
+ "show": true,
+ "width": 200,
+ },
+ Object {
+ "Cell": [Function],
+ "Header": <React.Fragment>
+ Total
+ <br />
+ rows
+ </React.Fragment>,
+ "accessor": "total_rows",
+ "filterable": false,
+ "show": true,
+ "width": 100,
+ },
+ Object {
+ "Cell": [Function],
+ "Header": <React.Fragment>
+ Avg. row size
+ <br />
+ (bytes)
+ </React.Fragment>,
+ "accessor": "avg_row_size",
+ "filterable": false,
+ "show": true,
+ "width": 100,
+ },
+ Object {
+ "Cell": [Function],
+ "Header": <React.Fragment>
+ Replicated
+ <br />
+ size
+ </React.Fragment>,
+ "accessor": "replicated_size",
"filterable": false,
"show": true,
"width": 100,
@@ -177,19 +222,11 @@
},
Object {
"Cell": [Function],
- "Header": "Avg. segment size",
- "accessor": "avg_segment_size",
+ "Header": "Retention",
+ "accessor": [Function],
"filterable": false,
+ "id": "retention",
"show": true,
- "width": 100,
- },
- Object {
- "Cell": [Function],
- "Header": "Num rows",
- "accessor": "num_rows",
- "filterable": false,
- "show": true,
- "width": 100,
},
Object {
"Cell": [Function],
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx
index aacd854..20a38e7 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -30,6 +30,7 @@
ACTION_COLUMN_WIDTH,
ActionCell,
ActionIcon,
+ BracedText,
MoreButton,
RefreshButton,
SegmentTimeline,
@@ -48,7 +49,8 @@
addFilter,
countBy,
formatBytes,
- formatNumber,
+ formatInteger,
+ formatMegabytes,
getDruidErrorMessage,
LocalStorageKeys,
lookupBy,
@@ -67,35 +69,37 @@
const tableColumns: Record<CapabilitiesMode, string[]> = {
full: [
- 'Datasource',
+ 'Datasource name',
'Availability',
- 'Segment load/drop',
- 'Retention',
+ 'Segment load/drop queues',
+ 'Total data size',
+ 'Segment size',
+ 'Total rows',
+ 'Avg. row size',
'Replicated size',
- 'Size',
'Compaction',
- 'Avg. segment size',
- 'Num rows',
+ 'Retention',
ACTION_COLUMN_LABEL,
],
'no-sql': [
- 'Datasource',
+ 'Datasource name',
'Availability',
- 'Segment load/drop',
- 'Retention',
- 'Size',
+ 'Segment load/drop queues',
+ 'Total data size',
+ 'Segment size',
'Compaction',
- 'Avg. segment size',
+ 'Retention',
ACTION_COLUMN_LABEL,
],
'no-proxy': [
- 'Datasource',
+ 'Datasource name',
'Availability',
- 'Segment load/drop',
+ 'Segment load/drop queues',
+ 'Total data size',
+ 'Segment size',
+ 'Total rows',
+ 'Avg. row size',
'Replicated size',
- 'Size',
- 'Avg. segment size',
- 'Num rows',
ACTION_COLUMN_LABEL,
],
};
@@ -111,6 +115,22 @@
return loadDrop.join(', ') || 'No segments to load/drop';
}
+const formatTotalDataSize = formatBytes;
+const formatSegmentSize = formatMegabytes;
+const formatTotalRows = formatInteger;
+const formatAvgRowSize = formatInteger;
+const formatReplicatedSize = formatBytes;
+
+function twoLines(line1: string, line2: string) {
+ return (
+ <>
+ {line1}
+ <br />
+ {line2}
+ </>
+ );
+}
+
interface Datasource {
datasource: string;
rules: Rule[];
@@ -128,10 +148,13 @@
num_available_segments: number;
num_segments_to_load: number;
num_segments_to_drop: number;
+ total_data_size: number;
replicated_size: number;
- size: number;
+ min_segment_size: number;
avg_segment_size: number;
- num_rows: number;
+ max_segment_size: number;
+ total_rows: number;
+ avg_row_size: number;
}
interface RetentionDialogOpenOn {
@@ -190,13 +213,19 @@
COUNT(*) FILTER (WHERE is_available = 1 AND ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_available_segments,
COUNT(*) FILTER (WHERE is_published = 1 AND is_overshadowed = 0 AND is_available = 0) AS num_segments_to_load,
COUNT(*) FILTER (WHERE is_available = 1 AND NOT ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_segments_to_drop,
+ SUM("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS total_data_size,
SUM("size" * "num_replicas") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS replicated_size,
- SUM("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS size,
+ MIN("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS min_segment_size,
(
SUM("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) /
COUNT(*) FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)
) AS avg_segment_size,
- SUM("num_rows") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS num_rows
+ MAX("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS max_segment_size,
+ SUM("num_rows") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS total_rows,
+ (
+ SUM("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) /
+ SUM("num_rows") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)
+ ) AS avg_row_size
FROM sys.segments
GROUP BY 1`;
@@ -251,7 +280,7 @@
const loadstatus = loadstatusResp.data;
datasources = datasourcesResp.data.map(
(d: any): DatasourceQueryResultRow => {
- const size = deepGet(d, 'properties.segments.size') || -1;
+ const totalDataSize = deepGet(d, 'properties.segments.size') || -1;
const segmentsToLoad = Number(loadstatus[d.name] || 0);
const availableSegments = Number(deepGet(d, 'properties.segments.count'));
const numSegments = availableSegments + segmentsToLoad;
@@ -262,9 +291,12 @@
num_segments_to_load: segmentsToLoad,
num_segments_to_drop: 0,
replicated_size: -1,
- size,
- avg_segment_size: size / numSegments,
- num_rows: -1,
+ total_data_size: totalDataSize,
+ min_segment_size: -1,
+ avg_segment_size: totalDataSize / numSegments,
+ max_segment_size: -1,
+ total_rows: -1,
+ avg_row_size: -1,
};
},
);
@@ -773,6 +805,22 @@
datasources = datasources.filter(d => !d.unused);
}
+ // Calculate column values for bracing
+
+ const totalDataSizeValues = datasources.map(d => formatTotalDataSize(d.total_data_size));
+
+ const minSegmentSizeValues = datasources.map(d => formatSegmentSize(d.min_segment_size));
+
+ const avgSegmentSizeValues = datasources.map(d => formatSegmentSize(d.avg_segment_size));
+
+ const maxSegmentSizeValues = datasources.map(d => formatSegmentSize(d.max_segment_size));
+
+ const totalRowsValues = datasources.map(d => formatTotalRows(d.total_rows));
+
+ const avgRowSizeValues = datasources.map(d => formatAvgRowSize(d.avg_row_size));
+
+ const replicatedSizeValues = datasources.map(d => formatReplicatedSize(d.replicated_size));
+
return (
<>
<ReactTable
@@ -790,7 +838,8 @@
}}
columns={[
{
- Header: 'Datasource',
+ Header: twoLines('Datasource', 'name'),
+ show: hiddenColumns.exists('Datasource name'),
accessor: 'datasource',
width: 150,
Cell: row => {
@@ -807,10 +856,10 @@
</a>
);
},
- show: hiddenColumns.exists('Datasource'),
},
{
Header: 'Availability',
+ show: hiddenColumns.exists('Availability'),
id: 'availability',
filterable: false,
accessor: row => {
@@ -871,10 +920,10 @@
const percentAvailable2 = d2.num_available / d2.num_total;
return percentAvailable1 - percentAvailable2 || d1.num_total - d2.num_total;
},
- show: hiddenColumns.exists('Availability'),
},
{
- Header: 'Segment load/drop',
+ Header: twoLines('Segment load/drop', 'queues'),
+ show: hiddenColumns.exists('Segment load/drop queues'),
id: 'load-drop',
accessor: 'num_segments_to_load',
filterable: false,
@@ -882,10 +931,108 @@
const { num_segments_to_load, num_segments_to_drop } = row.original;
return formatLoadDrop(num_segments_to_load, num_segments_to_drop);
},
- show: hiddenColumns.exists('Segment load/drop'),
+ },
+ {
+ Header: twoLines('Total', 'data size'),
+ show: hiddenColumns.exists('Total data size'),
+ accessor: 'total_data_size',
+ filterable: false,
+ width: 100,
+ Cell: row => (
+ <BracedText text={formatTotalDataSize(row.value)} braces={totalDataSizeValues} />
+ ),
+ },
+ {
+ Header: twoLines('Segment size (MB)', 'min / avg / max'),
+ show: hiddenColumns.exists('Segment size'),
+ accessor: 'avg_segment_size',
+ filterable: false,
+ width: 200,
+ Cell: row => (
+ <>
+ <BracedText
+ text={formatSegmentSize(row.original.min_segment_size)}
+ braces={minSegmentSizeValues}
+ />{' '}
+ {' '}
+ <BracedText text={formatSegmentSize(row.value)} braces={avgSegmentSizeValues} />{' '}
+ {' '}
+ <BracedText
+ text={formatSegmentSize(row.original.max_segment_size)}
+ braces={maxSegmentSizeValues}
+ />
+ </>
+ ),
+ },
+ {
+ Header: twoLines('Total', 'rows'),
+ show: capabilities.hasSql() && hiddenColumns.exists('Total rows'),
+ accessor: 'total_rows',
+ filterable: false,
+ width: 100,
+ Cell: row => (
+ <BracedText text={formatTotalRows(row.value)} braces={totalRowsValues} />
+ ),
+ },
+ {
+ Header: twoLines('Avg. row size', '(bytes)'),
+ show: hiddenColumns.exists('Avg. row size'),
+ accessor: 'avg_row_size',
+ filterable: false,
+ width: 100,
+ Cell: row => (
+ <BracedText text={formatAvgRowSize(row.value)} braces={avgRowSizeValues} />
+ ),
+ },
+ {
+ Header: twoLines('Replicated', 'size'),
+ show: capabilities.hasSql() && hiddenColumns.exists('Replicated size'),
+ accessor: 'replicated_size',
+ filterable: false,
+ width: 100,
+ Cell: row => (
+ <BracedText text={formatReplicatedSize(row.value)} braces={replicatedSizeValues} />
+ ),
+ },
+ {
+ Header: 'Compaction',
+ show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Compaction'),
+ id: 'compaction',
+ accessor: row => Boolean(row.compaction),
+ 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';
+ }
+ return (
+ <span
+ className="clickable-cell"
+ onClick={() =>
+ this.setState({
+ compactionDialogOpenOn: {
+ datasource: row.original.datasource,
+ compactionConfig: compaction,
+ },
+ })
+ }
+ >
+ {text}
+ <ActionIcon icon={IconNames.EDIT} />
+ </span>
+ );
+ },
},
{
Header: 'Retention',
+ show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Retention'),
id: 'retention',
accessor: row => row.rules.length,
filterable: false,
@@ -915,78 +1062,10 @@
</span>
);
},
- show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Retention'),
- },
- {
- Header: 'Replicated size',
- accessor: 'replicated_size',
- filterable: false,
- width: 100,
- Cell: row => formatBytes(row.value),
- show: capabilities.hasSql() && hiddenColumns.exists('Replicated size'),
- },
- {
- Header: 'Size',
- accessor: 'size',
- filterable: false,
- width: 100,
- Cell: row => formatBytes(row.value),
- show: hiddenColumns.exists('Size'),
- },
- {
- Header: 'Compaction',
- id: 'compaction',
- accessor: row => Boolean(row.compaction),
- filterable: false,
- Cell: row => {
- const { compaction } = row.original;
- let text: string;
- if (compaction) {
- if (compaction.maxRowsPerSegment == null) {
- text = `Target: Default (${formatNumber(DEFAULT_MAX_ROWS_PER_SEGMENT)})`;
- } else {
- text = `Target: ${formatNumber(compaction.maxRowsPerSegment)}`;
- }
- } else {
- text = 'None';
- }
- return (
- <span
- className="clickable-cell"
- onClick={() =>
- this.setState({
- compactionDialogOpenOn: {
- datasource: row.original.datasource,
- compactionConfig: compaction,
- },
- })
- }
- >
- {text}
- <ActionIcon icon={IconNames.EDIT} />
- </span>
- );
- },
- show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Compaction'),
- },
- {
- Header: 'Avg. segment size',
- accessor: 'avg_segment_size',
- filterable: false,
- width: 100,
- Cell: row => formatBytes(row.value),
- show: hiddenColumns.exists('Avg. segment size'),
- },
- {
- Header: 'Num rows',
- accessor: 'num_rows',
- filterable: false,
- width: 100,
- Cell: row => formatNumber(row.value),
- show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
},
{
Header: ACTION_COLUMN_LABEL,
+ show: hiddenColumns.exists(ACTION_COLUMN_LABEL),
accessor: 'datasource',
id: ACTION_COLUMN_ID,
width: ACTION_COLUMN_WIDTH,
@@ -1012,7 +1091,6 @@
/>
);
},
- show: hiddenColumns.exists(ACTION_COLUMN_LABEL),
},
]}
defaultPageSize={50}
diff --git a/web-console/src/views/home-view/status-card/status-card.tsx b/web-console/src/views/home-view/status-card/status-card.tsx
index ed32aef..d5f3bc8 100644
--- a/web-console/src/views/home-view/status-card/status-card.tsx
+++ b/web-console/src/views/home-view/status-card/status-card.tsx
@@ -22,7 +22,7 @@
import { StatusDialog } from '../../../dialogs/status-dialog/status-dialog';
import { useQueryManager } from '../../../hooks';
-import { pluralIfNeeded, QueryState } from '../../../utils';
+import { pluralIfNeeded } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
interface StatusSummary {
@@ -32,11 +32,6 @@
export interface StatusCardProps {}
-export interface StatusCardState {
- statusSummaryState: QueryState<StatusSummary>;
- showStatusDialog: boolean;
-}
-
export const StatusCard = React.memo(function StatusCard(_props: StatusCardProps) {
const [showStatusDialog, setShowStatusDialog] = useState(false);
const [statusSummaryState] = useQueryManager<null, StatusSummary>({
diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx
index 7501a5d..0e8b489 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -32,6 +32,7 @@
import { AppToaster } from '../../singletons/toaster';
import {
BasicQueryExplanation,
+ ColumnMetadata,
downloadFile,
DruidError,
findEmptyLiteralPosition,
@@ -48,7 +49,6 @@
RowColumn,
SemiJoinQueryExplanation,
} from '../../utils';
-import { ColumnMetadata } from '../../utils/column-metadata';
import { isEmptyContext, QueryContext } from '../../utils/query-context';
import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 544e834..f9585cd 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -27,6 +27,7 @@
ACTION_COLUMN_LABEL,
ACTION_COLUMN_WIDTH,
ActionCell,
+ BracedText,
MoreButton,
RefreshButton,
TableColumnSelector,
@@ -39,7 +40,7 @@
compact,
filterMap,
formatBytes,
- formatNumber,
+ formatInteger,
LocalStorageKeys,
makeBooleanFilter,
queryDruidSql,
@@ -380,9 +381,15 @@
} = this.state;
const { capabilities } = this.props;
+ const segments = trimmedSegments || segmentsState.data || [];
+
+ const sizeValues = segments.map(d => formatBytes(d.size)).concat('(realtime)');
+
+ const numRowsValues = segments.map(d => formatInteger(d.num_rows)).concat('(unknown)');
+
return (
<ReactTable
- data={trimmedSegments || segmentsState.data || []}
+ data={segments}
pages={10000000} // Dummy, we are hiding the page selector
loading={segmentsState.loading}
noDataText={segmentsState.isEmpty() ? 'No segments' : segmentsState.getErrorMessage() || ''}
@@ -406,12 +413,13 @@
columns={[
{
Header: 'Segment ID',
+ show: hiddenColumns.exists('Segment ID'),
accessor: 'segment_id',
width: 300,
- show: hiddenColumns.exists('Segment ID'),
},
{
Header: 'Datasource',
+ show: hiddenColumns.exists('Datasource'),
accessor: 'datasource',
Cell: row => {
const value = row.value;
@@ -425,10 +433,10 @@
</a>
);
},
- show: hiddenColumns.exists('Datasource'),
},
{
Header: 'Interval',
+ show: groupByInterval,
accessor: 'interval',
width: 120,
defaultSortDesc: true,
@@ -444,10 +452,10 @@
</a>
);
},
- show: hiddenColumns.exists('interval') && groupByInterval,
},
{
Header: 'Start',
+ show: hiddenColumns.exists('Start'),
accessor: 'start',
width: 120,
defaultSortDesc: true,
@@ -463,10 +471,10 @@
</a>
);
},
- show: hiddenColumns.exists('Start'),
},
{
Header: 'End',
+ show: hiddenColumns.exists('End'),
accessor: 'end',
defaultSortDesc: true,
width: 120,
@@ -482,79 +490,90 @@
</a>
);
},
- show: hiddenColumns.exists('End'),
},
{
Header: 'Version',
+ show: hiddenColumns.exists('Version'),
accessor: 'version',
defaultSortDesc: true,
width: 120,
- show: hiddenColumns.exists('Version'),
},
{
Header: 'Partition',
+ show: hiddenColumns.exists('Partition'),
accessor: 'partition_num',
width: 60,
filterable: false,
- show: hiddenColumns.exists('Partition'),
},
{
Header: 'Size',
+ show: hiddenColumns.exists('Size'),
accessor: 'size',
filterable: false,
defaultSortDesc: true,
- Cell: row => {
- if (row.value === 0 && row.original.is_realtime === 1) return '(realtime)';
- return formatBytes(row.value);
- },
- show: hiddenColumns.exists('Size'),
+ Cell: row => (
+ <BracedText
+ text={
+ row.value === 0 && row.original.is_realtime === 1
+ ? '(realtime)'
+ : formatBytes(row.value)
+ }
+ braces={sizeValues}
+ />
+ ),
},
{
Header: 'Num rows',
+ show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
accessor: 'num_rows',
filterable: false,
defaultSortDesc: true,
- Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>),
- show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
+ Cell: row => (
+ <BracedText
+ text={row.original.is_available ? formatInteger(row.value) : '(unknown)'}
+ braces={numRowsValues}
+ />
+ ),
},
{
Header: 'Replicas',
+ show: capabilities.hasSql() && hiddenColumns.exists('Replicas'),
accessor: 'num_replicas',
width: 60,
filterable: false,
defaultSortDesc: true,
- show: capabilities.hasSql() && hiddenColumns.exists('Replicas'),
},
{
Header: 'Is published',
+ show: capabilities.hasSql() && hiddenColumns.exists('Is published'),
id: 'is_published',
accessor: row => String(Boolean(row.is_published)),
Filter: makeBooleanFilter(),
- show: capabilities.hasSql() && hiddenColumns.exists('Is published'),
},
{
Header: 'Is realtime',
+ show: capabilities.hasSql() && hiddenColumns.exists('Is realtime'),
id: 'is_realtime',
accessor: row => String(Boolean(row.is_realtime)),
Filter: makeBooleanFilter(),
- show: capabilities.hasSql() && hiddenColumns.exists('Is realtime'),
},
{
Header: 'Is available',
+ show: capabilities.hasSql() && hiddenColumns.exists('Is available'),
id: 'is_available',
accessor: row => String(Boolean(row.is_available)),
Filter: makeBooleanFilter(),
- show: capabilities.hasSql() && hiddenColumns.exists('Is available'),
},
{
Header: 'Is overshadowed',
+ show: capabilities.hasSql() && hiddenColumns.exists('Is overshadowed'),
id: 'is_overshadowed',
accessor: row => String(Boolean(row.is_overshadowed)),
Filter: makeBooleanFilter(),
- show: capabilities.hasSql() && hiddenColumns.exists('Is overshadowed'),
},
{
Header: ACTION_COLUMN_LABEL,
+ show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists(ACTION_COLUMN_LABEL),
id: ACTION_COLUMN_ID,
accessor: 'segment_id',
width: ACTION_COLUMN_WIDTH,
@@ -577,7 +596,6 @@
);
},
Aggregated: () => '',
- show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists(ACTION_COLUMN_LABEL),
},
]}
defaultPageSize={SegmentsView.PAGE_SIZE}