diff --git a/frontend/src/metabase/css/core/scroll.css b/frontend/src/metabase/css/core/scroll.css index 12fccedf309f12c5fae4b8165ebb6f78ca64419d..97100825e9dbf59af557e271900d059a7f94521e 100644 --- a/frontend/src/metabase/css/core/scroll.css +++ b/frontend/src/metabase/css/core/scroll.css @@ -67,10 +67,12 @@ display: none; /* Safari and Chrome */ } -.scroll-hide-all, .scroll-hide-all * { +.scroll-hide-all, +.scroll-hide-all * { -ms-overflow-style: none; /* IE 10+ */ overflow: -moz-scrollbars-none; /* Firefox */ } +.scroll-hide-all::-webkit-scrollbar, .scroll-hide-all *::-webkit-scrollbar { display: none; /* Safari and Chrome */ } diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.css b/frontend/src/metabase/visualizations/components/TableInteractive.css index fd07f661939a3007812ce5b59f3484223ede6b6a..42faf55c441d46e64c8f14bf33ccd062d370078f 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive.css +++ b/frontend/src/metabase/visualizations/components/TableInteractive.css @@ -6,13 +6,11 @@ font-weight: 700; } -.TableInteractive-headerCellData:hover { - cursor: pointer; +.TableInteractive-headerCellData .Icon { + opacity: 0; } -.TableInteractive-headerCellData .Icon { opacity: 0; } -.TableInteractive-headerCellData:hover .Icon, -.TableInteractive-headerCellData--sorted .Icon { +.TableInteractive-headerCellData--sorted .Icon { opacity: 1; transition: opacity .3s linear; } diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx index 32e26d8220e2f1f81555286712350f57ff0cf300..c16ab22c635083c10a7a7db982a678d11a27cc83 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx +++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx @@ -8,11 +8,9 @@ import "./TableInteractive.css"; import Icon from "metabase/components/Icon.jsx"; -import Value from "metabase/components/Value.jsx"; - -import { capitalize } from "metabase/lib/formatting"; +import { formatValue, capitalize } from "metabase/lib/formatting"; import { getFriendlyName } from "metabase/visualizations/lib/utils"; -import { getTableCellClickedObject } from "metabase/visualizations/lib/table"; +import { getTableCellClickedObject, isColumnRightAligned } from "metabase/visualizations/lib/table"; import _ from "underscore"; import cx from "classnames"; @@ -21,8 +19,8 @@ import ExplicitSize from "metabase/components/ExplicitSize.jsx"; import { Grid, ScrollSync } from "react-virtualized"; import Draggable from "react-draggable"; -const HEADER_HEIGHT = 50; -const ROW_HEIGHT = 35; +const HEADER_HEIGHT = 36; +const ROW_HEIGHT = 30; const MIN_COLUMN_WIDTH = ROW_HEIGHT; const RESIZE_HANDLE_WIDTH = 5; @@ -226,21 +224,23 @@ export default class TableInteractive extends Component<*, Props, State> { return ( <div key={key} style={style} - className={cx("TableInteractive-cellWrapper cellData", { + className={cx("TableInteractive-cellWrapper", { "TableInteractive-cellWrapper--firstColumn": columnIndex === 0, - "cursor-pointer": isClickable + "cursor-pointer": isClickable, + "justify-end": isColumnRightAligned(column) })} onClick={isClickable && ((e) => { onVisualizationClick({ ...clicked, element: e.currentTarget }); })} > - <Value - className="link" - type="cell" - value={value} - column={column} - onResize={this.onCellResize.bind(this, columnIndex)} - /> + <div className="cellData"> + {/* using formatValue instead of <Value> here for performance. The later wraps in an extra <span> */} + {formatValue(value, { + column: column, + type: "cell", + jsx: true + })} + </div> </div> ); } @@ -271,6 +271,10 @@ export default class TableInteractive extends Component<*, Props, State> { const isClickable = onVisualizationClick && visualizationIsClickable(clicked); const isSortable = isClickable && column.source; + const isRightAligned = isColumnRightAligned(column); + + const isSorted = sort && sort[0] && sort[0][0] === column.id; + const isAscending = sort && sort[0] && sort[0][1] === "ascending"; return ( <div @@ -278,22 +282,21 @@ export default class TableInteractive extends Component<*, Props, State> { style={{ ...style, overflow: "visible" /* ensure resize handle is visible */ }} className={cx("TableInteractive-cellWrapper TableInteractive-headerCellData", { "TableInteractive-cellWrapper--firstColumn": columnIndex === 0, - "TableInteractive-headerCellData--sorted": (sort && sort[0] && sort[0][0] === column.id), + "TableInteractive-headerCellData--sorted": isSorted, + "cursor-pointer": isClickable, + "justify-end": isRightAligned + })} + onClick={isClickable && ((e) => { + onVisualizationClick({ ...clicked, element: e.currentTarget }); })} > - <div - className={cx("cellData", { "cursor-pointer": isClickable })} - onClick={isClickable && ((e) => { - onVisualizationClick({ ...clicked, element: e.currentTarget }); - })} - > + <div className="cellData"> + {isSortable && isRightAligned && + <Icon className="Icon mr1" name={isAscending ? "chevronup" : "chevrondown"} size={8} /> + } {columnTitle} - {isSortable && - <Icon - className="Icon ml1" - name={sort && sort[0] && sort[0][1] === "ascending" ? "chevronup" : "chevrondown"} - size={8} - /> + {isSortable && !isRightAligned && + <Icon className="Icon ml1" name={isAscending ? "chevronup" : "chevrondown"} size={8} /> } </div> <Draggable diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx index c79e17910203353dc917ec5be1f54ee02fce93b3..08eaf523bf9b7af294807b3becf44b5a15a0170c 100644 --- a/frontend/src/metabase/visualizations/components/TableSimple.jsx +++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx @@ -11,7 +11,7 @@ import Icon from "metabase/components/Icon.jsx"; import { formatValue } from "metabase/lib/formatting"; import { getFriendlyName } from "metabase/visualizations/lib/utils"; -import { getTableCellClickedObject } from "metabase/visualizations/lib/table"; +import { getTableCellClickedObject, isColumnRightAligned } from "metabase/visualizations/lib/table"; import cx from "classnames"; import _ from "underscore"; @@ -97,7 +97,14 @@ export default class TableSimple extends Component<*, Props, State> { <thead ref="header"> <tr> {cols.map((col, colIndex) => - <th key={colIndex} className={cx("TableInteractive-headerCellData cellData text-brand-hover", { "TableInteractive-headerCellData--sorted": sortColumn === colIndex })} onClick={() => this.setSort(colIndex)}> + <th + key={colIndex} + className={cx("TableInteractive-headerCellData cellData text-brand-hover", { + "TableInteractive-headerCellData--sorted": sortColumn === colIndex, + "text-right": isColumnRightAligned(col) + })} + onClick={() => this.setSort(colIndex)} + > <div className="relative"> <Icon name={sortDescending ? "chevrondown" : "chevronup"} @@ -120,14 +127,16 @@ export default class TableSimple extends Component<*, Props, State> { <td key={columnIndex} style={{ whiteSpace: "nowrap" }} - className={cx("px1 border-bottom", { - "cursor-pointer text-brand-hover": isClickable - })} - onClick={isClickable && ((e) => { - onVisualizationClick({ ...clicked, element: e.currentTarget }); - })} + className={cx("px1 border-bottom", { "text-right": isColumnRightAligned(cols[columnIndex]) })} > - { cell == null ? "-" : formatValue(cell, { column: cols[columnIndex], jsx: true }) } + <span + className={cx({ "cursor-pointer text-brand-hover": isClickable })} + onClick={isClickable && ((e) => { + onVisualizationClick({ ...clicked, element: e.currentTarget }); + })} + > + { cell == null ? "-" : formatValue(cell, { column: cols[columnIndex], jsx: true }) } + </span> </td> ); })} diff --git a/frontend/src/metabase/visualizations/lib/table.js b/frontend/src/metabase/visualizations/lib/table.js index 3e05c3cb8278037c17d38b37e09456021b142138..8203808bc944ae10cd35f03ef6df5a49e6df4930 100644 --- a/frontend/src/metabase/visualizations/lib/table.js +++ b/frontend/src/metabase/visualizations/lib/table.js @@ -1,7 +1,8 @@ /* @flow */ -import type { DatasetData } from "metabase/meta/types/Dataset"; +import type { DatasetData, Column } from "metabase/meta/types/Dataset"; import type { ClickObject } from "metabase/meta/types/Visualization"; +import { isNumber, isCoordinate } from "metabase/lib/schema_metadata"; export function getTableCellClickedObject(data: DatasetData, rowIndex: number, columnIndex: number, isPivoted: boolean): ClickObject { const { rows, cols } = data; @@ -35,3 +36,11 @@ export function getTableCellClickedObject(data: DatasetData, rowIndex: number, c return { value, column }; } } + +/* + * Returns whether the column should be right-aligned in a table. + * Includes numbers and lat/lon coordinates, but not zip codes, IDs, etc. + */ +export function isColumnRightAligned(column: Column) { + return isNumber(column) || isCoordinate(column); +} diff --git a/frontend/src/metabase/visualizations/lib/table.spec.js b/frontend/src/metabase/visualizations/lib/table.spec.js index 9e89fa2f791c8f8758e4ca9074d1e67f264f3a6c..8b12d8c8480601951420d4273a8e93e889173607 100644 --- a/frontend/src/metabase/visualizations/lib/table.spec.js +++ b/frontend/src/metabase/visualizations/lib/table.spec.js @@ -1,4 +1,5 @@ -import { getTableCellClickedObject } from "./table"; +import { getTableCellClickedObject, isColumnRightAligned } from "./table"; +import { TYPE } from "metabase/lib/types"; const RAW_COLUMN = { source: "fields" @@ -40,4 +41,24 @@ describe("metabase/visualization/lib/table", () => { // TODO: }) }) + + describe("isColumnRightAligned", () => { + it("should return true for numeric columns without a special type", () => { + expect(isColumnRightAligned({ base_type: TYPE.Integer })).toBe(true); + }); + it("should return true for numeric columns with special type Number", () => { + expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.Number })).toBe(true); + }); + it("should return true for numeric columns with special type latitude or longitude ", () => { + expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.Latitude })).toBe(true); + expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.Longitude })).toBe(true); + }); + it("should return false for numeric columns with special type zip code", () => { + expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.ZipCode })).toBe(false) + }); + it("should return false for numeric columns with special type FK or PK", () => { + expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.FK })).toBe(false); + expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.FK })).toBe(false); + }); + }) })