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}
+                  />{' '}
+                  &nbsp;{' '}
+                  <BracedText text={formatSegmentSize(row.value)} braces={avgSegmentSizeValues} />{' '}
+                  &nbsp;{' '}
+                  <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}&nbsp;
+                    <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}&nbsp;
-                    <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}