Skip to content
Snippets Groups Projects
Unverified Commit 594cdb01 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Convert `Table` viz and `TableSimple` to TypeScript (#38099)


* Convert `TableSimple` to TypeScript

* Convert `Table` viz to TypeScript

* Update frontend/src/metabase/visualizations/components/TableSimple/TableCell.tsx

Co-authored-by: default avatarKamil Mielnik <kamil@kamilmielnik.com>

* Update frontend/src/metabase/visualizations/components/TableSimple/TableSimple.tsx

Co-authored-by: default avatarKamil Mielnik <kamil@kamilmielnik.com>

---------

Co-authored-by: default avatarKamil Mielnik <kamil@kamilmielnik.com>
parent 221ca042
No related branches found
No related tags found
No related merge requests found
......@@ -9,6 +9,7 @@ export interface OptionsType extends TimeOnlyOptions {
click_behavior?: any;
clicked?: any;
column?: any;
column_title?: string;
compact?: boolean;
date_abbreviate?: boolean;
date_format?: string;
......@@ -30,6 +31,7 @@ export interface OptionsType extends TimeOnlyOptions {
removeDay?: boolean;
removeYear?: boolean;
rich?: boolean;
show_mini_bar?: boolean;
suffix?: string;
type?: string;
view_as?: string | null;
......
/* eslint-disable react/prop-types */
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, isValidElement } from "react";
import cx from "classnames";
import ExternalLink from "metabase/core/components/ExternalLink";
import type { OptionsType } from "metabase/lib/formatting/types";
import { formatValue } from "metabase/lib/formatting";
import {
getTableCellClickedObject,
......@@ -11,11 +11,31 @@ import {
isColumnRightAligned,
} from "metabase/visualizations/lib/table";
import { getColumnExtent } from "metabase/visualizations/lib/utils";
import type {
DatasetColumn,
DatasetData,
RowValue,
RowValues,
Series,
VisualizationSettings,
} from "metabase-types/api";
import type { ClickObject } from "metabase-lib";
import { isID, isFK } from "metabase-lib/types/utils/isa";
import MiniBar from "../MiniBar";
import { CellRoot, CellContent } from "./TableCell.styled";
type GetCellDataOpts = {
value: RowValue;
clicked: ClickObject;
extraData: Record<string, unknown>;
cols: DatasetColumn[];
rows: RowValues[];
columnIndex: number;
columnSettings: OptionsType;
};
function getCellData({
value,
clicked,
......@@ -24,7 +44,7 @@ function getCellData({
rows,
columnIndex,
columnSettings,
}) {
}: GetCellDataOpts) {
if (value == null) {
return "-";
}
......@@ -46,7 +66,25 @@ function getCellData({
});
}
function TableCell({
interface TableCellProps {
value: RowValue;
data: DatasetData;
series: Series;
settings: VisualizationSettings;
rowIndex: number;
columnIndex: number;
isPivoted: boolean;
getCellBackgroundColor: (
value: RowValue,
rowIndex: number,
columnName: string,
) => string | undefined;
getExtraDataForClick: (clickObject: ClickObject) => Record<string, unknown>;
checkIsVisualizationClickable: (clickObject: ClickObject) => boolean;
onVisualizationClick?: (clickObject: ClickObject) => void;
}
export function TableCell({
value,
data,
series,
......@@ -58,7 +96,7 @@ function TableCell({
getExtraDataForClick,
checkIsVisualizationClickable,
onVisualizationClick,
}) {
}: TableCellProps) {
const { rows, cols } = data;
const column = cols[columnIndex];
const columnSettings = settings.column(column);
......@@ -66,6 +104,7 @@ function TableCell({
const clickedRowData = useMemo(
() =>
getTableClickedObjectRowData(
// @ts-expect-error -- visualizations/lib/table should be typed
series,
rowIndex,
columnIndex,
......@@ -107,13 +146,13 @@ function TableCell({
[value, clicked, extraData, cols, rows, columnIndex, columnSettings],
);
const isLink = cellData && cellData.type === ExternalLink;
const isLink = isValidElement(cellData) && cellData.type === ExternalLink;
const isClickable = !isLink;
const onClick = useCallback(
e => {
if (checkIsVisualizationClickable(clicked)) {
onVisualizationClick({
onVisualizationClick?.({
...clicked,
element: e.currentTarget,
extraData,
......@@ -155,5 +194,3 @@ function TableCell({
</CellRoot>
);
}
export default TableCell;
/* eslint-disable react/prop-types */
import { useCallback, useLayoutEffect, useMemo, useState, useRef } from "react";
import { getIn } from "icepick";
import _ from "underscore";
......@@ -8,9 +7,19 @@ import { Ellipsified } from "metabase/core/components/Ellipsified";
import { isPositiveInteger } from "metabase/lib/number";
import { isColumnRightAligned } from "metabase/visualizations/lib/table";
import type {
Card,
DatasetColumn,
DatasetData,
RowValue,
Series,
VisualizationSettings,
} from "metabase-types/api";
import type { ClickObject } from "metabase-lib";
import { isID } from "metabase-lib/types/utils/isa";
import TableCell from "./TableCell";
import { TableCell } from "./TableCell";
import TableFooter from "./TableFooter";
import {
Root,
......@@ -21,11 +30,13 @@ import {
SortIcon,
} from "./TableSimple.styled";
function getBoundingClientRectSafe(ref) {
function getBoundingClientRectSafe(ref: {
current?: HTMLElement | null;
}): Partial<DOMRect> {
return ref.current?.getBoundingClientRect?.() ?? {};
}
function formatCellValueForSorting(value, column) {
function formatCellValueForSorting(value: RowValue, column: DatasetColumn) {
if (typeof value === "string") {
if (isID(column) && isPositiveInteger(value)) {
return parseInt(value, 10);
......@@ -39,7 +50,23 @@ function formatCellValueForSorting(value, column) {
return value;
}
function TableSimple({
interface TableSimpleProps {
card: Card;
data: DatasetData;
series: Series;
settings: VisualizationSettings;
height: number;
isDashboard?: boolean;
isEditing?: boolean;
isPivoted: boolean;
className?: string;
getColumnTitle: (colIndex: number) => string;
getExtraDataForClick: (clickObject: ClickObject) => Record<string, unknown>;
onVisualizationClick?: (clickObject: ClickObject) => void;
visualizationIsClickable?: (clickObject: ClickObject) => boolean;
}
function TableSimpleInner({
card,
data,
series,
......@@ -51,7 +78,7 @@ function TableSimple({
visualizationIsClickable,
getColumnTitle,
getExtraDataForClick,
}) {
}: TableSimpleProps) {
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(1);
const [sortColumn, setSortColumn] = useState(null);
......@@ -62,7 +89,7 @@ function TableSimple({
const firstRowRef = useRef(null);
useLayoutEffect(() => {
const { height: headerHeight } = getBoundingClientRectSafe(headerRef);
const { height: headerHeight = 0 } = getBoundingClientRectSafe(headerRef);
const { height: footerHeight = 0 } = getBoundingClientRectSafe(footerRef);
const { height: rowHeight = 0 } = getBoundingClientRectSafe(firstRowRef);
const currentPageSize = Math.floor(
......@@ -87,10 +114,10 @@ function TableSimple({
const checkIsVisualizationClickable = useCallback(
clickedItem => {
return (
return Boolean(
onVisualizationClick &&
visualizationIsClickable &&
visualizationIsClickable(clickedItem)
visualizationIsClickable &&
visualizationIsClickable(clickedItem),
);
},
[onVisualizationClick, visualizationIsClickable],
......@@ -217,7 +244,7 @@ function TableSimple({
);
}
export default ExplicitSize({
export const TableSimple = ExplicitSize<TableSimpleProps>({
refreshMode: props =>
props.isDashboard && !props.isEditing ? "debounceLeading" : "throttle",
})(TableSimple);
})(TableSimpleInner);
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./TableSimple";
export * from "./TableSimple";
import type {
Card,
DatasetColumn,
DatasetData,
RawSeries,
Series,
......@@ -10,6 +11,8 @@ import type { ClickObject } from "metabase/visualizations/types";
import type { ColorGetter } from "metabase/static-viz/lib/colors";
import type { OptionsType } from "metabase/lib/formatting/types";
import type { IconName, IconProps } from "metabase/ui";
import type Metadata from "metabase-lib/metadata/Metadata";
import type Query from "metabase-lib/queries/Query";
import type { HoveredObject } from "./hover";
......@@ -46,6 +49,7 @@ export interface VisualizationProps {
series: Series;
card: Card;
data: DatasetData;
metadata: Metadata;
rawSeries: RawSeries;
settings: ComputedVisualizationSettings;
headerIcon: IconProps;
......@@ -88,6 +92,24 @@ export interface VisualizationProps {
onUpdateWarnings?: any;
}
export type ColumnSettingDefinition<TValue, TProps = unknown> = {
title?: string;
hint?: string;
widget?: string | React.ComponentType<any>;
default?: TValue;
props?: TProps;
inline?: boolean;
readDependencies?: string[];
getDefault?: (col: DatasetColumn) => TValue;
getHidden?: (col: DatasetColumn, settings: OptionsType) => boolean;
getProps?: (
col: DatasetColumn,
settings: OptionsType,
onChange: (value: TValue) => void,
extra: { series: Series },
) => TProps;
};
export type VisualizationSettingDefinition<TValue, TProps = void> = {
section?: string;
title?: string;
......
/* eslint-disable react/prop-types */
import { Component } from "react";
import { t } from "ttag";
import _ from "underscore";
import cx from "classnames";
import * as DataGrid from "metabase/lib/data_grid";
import { getOptionFromColumn } from "metabase/visualizations/lib/settings/utils";
import { formatColumn } from "metabase/lib/formatting";
import * as DataGrid from "metabase/lib/data_grid";
import ChartSettingLinkUrlInput from "metabase/visualizations/components/settings/ChartSettingLinkUrlInput";
import ChartSettingsTableFormatting, {
......@@ -20,12 +18,19 @@ import {
getTitleForColumn,
isPivoted as _isPivoted,
} from "metabase/visualizations/lib/settings/column";
import { getOptionFromColumn } from "metabase/visualizations/lib/settings/utils";
import { getDefaultPivotColumn } from "metabase/visualizations/lib/utils";
import {
getDefaultSize,
getMinSize,
} from "metabase/visualizations/shared/utils/sizes";
import { getDefaultPivotColumn } from "metabase/visualizations/lib/utils";
import type {
DatasetColumn,
DatasetData,
Series,
VisualizationSettings,
} from "metabase-types/api";
import * as Lib from "metabase-lib";
import Question from "metabase-lib/Question";
import {
......@@ -40,10 +45,20 @@ import {
import { findColumnIndexForColumnSetting } from "metabase-lib/queries/utils/dataset";
import * as Q_DEPRECATED from "metabase-lib/queries/utils";
import TableSimple from "../components/TableSimple";
import type { ColumnSettingDefinition, VisualizationProps } from "../types";
import { TableSimple } from "../components/TableSimple";
import TableInteractive from "../components/TableInteractive/TableInteractive.jsx";
export default class Table extends Component {
interface TableProps extends VisualizationProps {
isShowingDetailsOnlyColumns?: boolean;
}
interface TableState {
data: Pick<DatasetData, "cols" | "rows" | "results_timezone"> | null;
question: Question | null;
}
class Table extends Component<TableProps, TableState> {
static uiName = t`Table`;
static identifier = "table";
static iconName = "table";
......@@ -52,19 +67,15 @@ export default class Table extends Component {
static minSize = getMinSize("table");
static defaultSize = getDefaultSize("table");
static isSensible({ cols, rows }) {
static isSensible() {
return true;
}
static isLiveResizable(series) {
static isLiveResizable() {
return false;
}
static checkRenderable([
{
data: { cols, rows },
},
]) {
static checkRenderable() {
// scalar can always be rendered, nothing needed here
}
......@@ -77,8 +88,8 @@ export default class Table extends Component {
title: t`Pivot table`,
widget: "toggle",
inline: true,
getHidden: ([{ card, data }]) => data && data.cols.length !== 3,
getDefault: ([{ card, data }]) => {
getHidden: ([{ data }]: Series) => data && data.cols.length !== 3,
getDefault: ([{ card, data }]: Series) => {
if (
!data ||
data.cols.length !== 3 ||
......@@ -100,20 +111,18 @@ export default class Table extends Component {
{
data: { cols, rows },
},
]) => {
]: Series) => {
return getDefaultPivotColumn(cols, rows)?.name;
},
getProps: (
[
{
data: { cols },
},
],
settings,
) => ({
getProps: ([
{
data: { cols },
},
]: Series) => ({
options: cols.filter(isDimension).map(getOptionFromColumn),
}),
getHidden: (series, settings) => !settings["table.pivot"],
getHidden: (series: Series, settings: VisualizationSettings) =>
!settings["table.pivot"],
readDependencies: ["table.pivot"],
persistDefault: true,
},
......@@ -121,7 +130,10 @@ export default class Table extends Component {
section: t`Columns`,
title: t`Cell column`,
widget: "field",
getDefault: ([{ data }], { "table.pivot_column": pivotCol }) => {
getDefault: (
[{ data }]: Series,
{ "table.pivot_column": pivotCol }: VisualizationSettings,
) => {
// We try to show numeric values in pivot cells, but if none are
// available, we fall back to the last column in the unpivoted table
const nonPivotCols = data.cols.filter(c => c.name !== pivotCol);
......@@ -129,24 +141,15 @@ export default class Table extends Component {
const { name } = nonPivotCols.find(isMetric) || lastCol || {};
return name;
},
getProps: (
[
{
data: { cols },
},
],
settings,
) => ({
getProps: ([
{
data: { cols },
},
]: Series) => ({
options: cols.map(getOptionFromColumn),
}),
getHidden: (
[
{
data: { cols },
},
],
settings,
) => !settings["table.pivot"],
getHidden: (series: Series, settings: VisualizationSettings) =>
!settings["table.pivot"],
readDependencies: ["table.pivot", "table.pivot_column"],
persistDefault: true,
},
......@@ -156,19 +159,16 @@ export default class Table extends Component {
section: t`Conditional Formatting`,
widget: ChartSettingsTableFormatting,
default: [],
getProps: (series, settings) => ({
getProps: (series: Series, settings: VisualizationSettings) => ({
cols: series[0].data.cols.filter(isFormattable),
isPivoted: settings["table.pivot"],
}),
getHidden: (
[
{
data: { cols },
},
],
settings,
) => cols.filter(isFormattable).length === 0,
getHidden: ([
{
data: { cols },
},
]: Series) => cols.filter(isFormattable).length === 0,
readDependencies: ["table.pivot"],
},
"table._cell_background_getter": {
......@@ -177,8 +177,8 @@ export default class Table extends Component {
{
data: { rows, cols },
},
],
settings,
]: Series,
settings: VisualizationSettings,
) {
return makeCellBackgroundGetter(
rows,
......@@ -191,8 +191,11 @@ export default class Table extends Component {
},
};
static columnSettings = column => {
const settings = {
static columnSettings = (column: DatasetColumn) => {
const settings: Record<
string,
ColumnSettingDefinition<unknown, unknown>
> = {
column_title: {
title: t`Column title`,
widget: "input",
......@@ -200,6 +203,7 @@ export default class Table extends Component {
},
click_behavior: {},
};
if (isNumber(column)) {
settings["show_mini_bar"] = {
title: t`Show a mini bar chart`,
......@@ -250,7 +254,7 @@ export default class Table extends Component {
settings["view_as"] !== "link" && settings["view_as"] !== "email_link",
readDependencies: ["view_as"],
getProps: (
col,
column,
settings,
onChange,
{
......@@ -276,7 +280,7 @@ export default class Table extends Component {
getHidden: (_, settings) => settings["view_as"] !== "link",
readDependencies: ["view_as"],
getProps: (
col,
column,
settings,
onChange,
{
......@@ -297,19 +301,16 @@ export default class Table extends Component {
return settings;
};
constructor(props) {
super(props);
this.state = {
data: null,
};
}
state: TableState = {
data: null,
question: null,
};
UNSAFE_componentWillMount() {
this._updateData(this.props);
}
UNSAFE_componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps: VisualizationProps) {
if (
newProps.series !== this.props.series ||
!_.isEqual(newProps.settings, this.props.settings)
......@@ -318,7 +319,7 @@ export default class Table extends Component {
}
}
_updateData({ series, settings, metadata }) {
_updateData({ series, settings, metadata }: VisualizationProps) {
const [{ card, data }] = series;
if (Table.isPivoted(series, settings)) {
......@@ -335,18 +336,12 @@ export default class Table extends Component {
(col, index) => index !== pivotIndex && index !== cellIndex,
);
this.setState({
data: DataGrid.pivot(
data,
normalIndex,
pivotIndex,
cellIndex,
settings,
),
data: DataGrid.pivot(data, normalIndex, pivotIndex, cellIndex),
});
} else {
const { cols, rows, results_timezone } = data;
const columnSettings = settings["table.columns"];
const columnIndexes = columnSettings
const columnIndexes = (columnSettings || [])
.filter(
columnSetting =>
columnSetting.enabled || this.props.isShowingDetailsOnlyColumns,
......@@ -362,6 +357,7 @@ export default class Table extends Component {
rows: rows.map(row => columnIndexes.map(i => row[i])),
results_timezone,
},
// construct a Question that is in-sync with query results
// cache it here for performance reasons
question: new Question(card, metadata),
......@@ -371,7 +367,7 @@ export default class Table extends Component {
// shared helpers for table implementations
getColumnTitle = columnIndex => {
getColumnTitle = (columnIndex: number) => {
const cols = this.state.data && this.state.data.cols;
if (!cols) {
return null;
......@@ -380,7 +376,7 @@ export default class Table extends Component {
return getTitleForColumn(cols[columnIndex], series, settings);
};
getColumnSortDirection = columnIndex => {
getColumnSortDirection = (columnIndex: number) => {
const { question, data } = this.state;
if (!question || !data) {
return;
......@@ -410,7 +406,7 @@ export default class Table extends Component {
const { series, isDashboard, settings } = this.props;
const { data } = this.state;
const isPivoted = Table.isPivoted(series, settings);
const areAllColumnsHidden = data.cols.length === 0;
const areAllColumnsHidden = data?.cols.length === 0;
const TableComponent = isDashboard ? TableSimple : TableInteractive;
if (!data) {
......@@ -450,3 +446,6 @@ export default class Table extends Component {
);
}
}
// eslint-disable-next-line import/no-default-export
export default Table;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment