Skip to content

Commit

Permalink
Add TableInfoPopover and add it to Table cards on the /browse rou…
Browse files Browse the repository at this point in the history
…te (metabase#19322)

* add TableInfoPopover

* Finish TableInfo implementation

* Add TableInfoPopover to TableBrowser

* TableInfo & TableInfoPopover styling tweaks

* add a cypress test for the /browse popover

* TableInfo unit tests update

* fix smoketest test

* shorter delay

* Add loading state for table metadata

* Fix table fk link

* make everything typescript

* add more unit tests

* fix types

* fix e2e test

* styles cleanup

* review changes

Co-authored-by: Maz Ameli <[email protected]>
  • Loading branch information
daltojohnso and mazameli authored Dec 21, 2021
1 parent 98da3e5 commit 67ed5c3
Show file tree
Hide file tree
Showing 27 changed files with 625 additions and 145 deletions.
12 changes: 12 additions & 0 deletions frontend/src/metabase-lib/lib/metadata/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { memoize, createLookupByProperty } from "metabase-lib/lib/utils";
/** This is the primary way people interact with tables */

export default class Table extends Base {
id: number;
description?: string;

hasSchema() {
return (this.schema_name && this.db && this.db.schemas.length > 1) || false;
}
Expand Down Expand Up @@ -116,6 +119,15 @@ export default class Table extends Base {
return this.fieldsLookup();
}

numFields(): number {
return this.fields?.length || 0;
}

connectedTables(): Table[] {
const fks = this.fks || [];
return fks.map(fk => new Table(fk.origin.table));
}

/**
* @private
* @param {string} description
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/metabase-lib/lib/metadata/Table.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { PRODUCTS } from "__support__/sample_dataset_fixture";
import Table from "./Table";

describe("Table", () => {
const productsTable = new Table(PRODUCTS);

describe("numFields", () => {
it("should return the number of fields", () => {
expect(productsTable.numFields()).toBe(8);
});

it("should handle scenario where fields prop is missing", () => {
const table = new Table({});
expect(table.numFields()).toBe(0);
});
});

describe("connectedTables", () => {
it("should return a list of table instances connected to it via fk", () => {
const table = new Table({
fks: [
{
origin: { table: PRODUCTS },
},
],
});

expect(table.connectedTables()).toEqual([productsTable]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Database from "metabase/entities/databases";
import EntityItem from "metabase/components/EntityItem";
import Icon from "metabase/components/Icon";
import { Grid, GridItem } from "metabase/components/Grid";
import TableInfoPopover from "metabase/components/MetadataInfo/TableInfoPopover";

import { ANALYTICS_CONTEXT, ITEM_WIDTHS } from "../../constants";
import BrowseHeader from "../BrowseHeader";
import { TableActionLink, TableCard, TableLink } from "./TableBrowser.styled";
Expand Down Expand Up @@ -44,18 +46,22 @@ const TableBrowser = ({
<Grid>
{tables.map(table => (
<GridItem key={table.id} width={ITEM_WIDTHS}>
<TableCard hoverable={isSyncCompleted(table)}>
<TableLink
to={isSyncCompleted(table) ? getTableUrl(table, metadata) : ""}
data-metabase-event={`${ANALYTICS_CONTEXT};Table Click`}
>
<TableBrowserItem
table={table}
dbId={dbId}
xraysEnabled={xraysEnabled}
/>
</TableLink>
</TableCard>
<TableInfoPopover tableId={table.id} placement="bottom-start">
<TableCard hoverable={isSyncCompleted(table)}>
<TableLink
to={
isSyncCompleted(table) ? getTableUrl(table, metadata) : ""
}
data-metabase-event={`${ANALYTICS_CONTEXT};Table Click`}
>
<TableBrowserItem
table={table}
dbId={dbId}
xraysEnabled={xraysEnabled}
/>
</TableLink>
</TableCard>
</TableInfoPopover>
</GridItem>
))}
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import styled from "styled-components";
import { space } from "metabase/styled-components/theme";
import Card from "metabase/components/Card";
import Link from "metabase/components/Link";
import { forwardRefToInnerRef } from "metabase/styled-components/utils";

export const TableLink = styled(Link)`
display: block;
Expand All @@ -14,7 +15,7 @@ export const TableActionLink = styled(Link)`
margin-left: ${space(1)};
`;

export const TableCard = styled(Card)`
export const TableCard = forwardRefToInnerRef(styled(Card)`
padding-left: ${space(1)};
padding-right: ${space(1)};
Expand All @@ -25,4 +26,4 @@ export const TableCard = styled(Card)`
&:hover ${TableActionLink} {
visibility: visible;
}
`;
`);
7 changes: 0 additions & 7 deletions frontend/src/metabase/components/EntityItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,6 @@ const EntityItem = ({

<Flex ml="auto" pr={1} align="center" onClick={e => e.preventDefault()}>
{buttons}
{!loading && item.description && (
<Icon
tooltip={item.description}
name="info"
className="ml1 text-medium"
/>
)}
{loading && <EntityItemSpinner size={24} borderWidth={3} />}
<EntityItemMenu
item={item}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import TippyPopver, {
ITippyPopoverProps,
} from "metabase/components/Popover/TippyPopover";
import DimensionInfo from "metabase/components/MetadataInfo/DimensionInfo";
import { isCypressActive } from "metabase/env";

export const POPOVER_DELAY: [number, number] = [1000, 300];

Expand All @@ -23,7 +24,7 @@ type Props = { dimension: Dimension } & Pick<
function DimensionInfoPopover({ dimension, children, placement }: Props) {
return dimension ? (
<TippyPopver
delay={POPOVER_DELAY}
delay={isCypressActive ? 0 : POPOVER_DELAY}
interactive
placement={placement || "left-start"}
content={<DimensionInfo dimension={dimension} />}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import styled from "styled-components";

import { space } from "metabase/styled-components/theme";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import { isReducedMotionPreferred } from "metabase/lib/dom";
import _LoadingSpinner from "metabase/components/LoadingSpinner";

const TRANSITION_DURATION = () => (isReducedMotionPreferred() ? "0" : "0.25s");

export const Container = styled.div`
position: relative;
display: flex;
flex-direction: column;
gap: ${space(1)};
overflow: auto;
`;

export const AbsoluteContainer = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
`;

export const InfoContainer = styled(Container)`
padding: ${space(2)};
`;

export const Description = styled.div`
font-size: 14px;
white-space: pre-line;
max-height: 200px;
overflow: auto;
`;

export const EmptyDescription = styled(Description)`
color: ${color("text-light")};
font-weight: 700;
`;

export const LabelContainer = styled.div`
display: inline-flex;
align-items: center;
column-gap: ${space(0)};
font-size: 12px;
color: ${({ color: _color = "brand" }) => color(_color)};
`;

export const Label = styled.span`
font-weight: bold;
font-size: 1em;
`;

export const RelativeSizeIcon = styled(Icon)`
height: 1em;
width: 1em;
`;

export const InvertedColorRelativeSizeIcon = styled(RelativeSizeIcon)`
padding: ${space(0)};
background-color: ${color("brand")};
color: ${color("white")};
border-radius: ${space(0)};
padding: ${space(0)};
`;

type FadeProps = {
visible?: boolean;
};

export const Fade = styled.div<FadeProps>`
width: 100%;
transition: opacity ${TRANSITION_DURATION} linear;
opacity: ${({ visible }) => (visible ? "1" : "0")};
`;

export const FadeAndSlide = styled.div<FadeProps>`
width: 100%;
transition: opacity ${TRANSITION_DURATION} linear,
transform ${TRANSITION_DURATION} linear;
opacity: ${({ visible }) => (visible ? "1" : "0")};
`;

export const LoadingSpinner = styled(_LoadingSpinner)`
display: flex;
flex-grow: 1;
align-self: center;
justify-content: center;
color: ${color("brand")};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import PropTypes from "prop-types";
import { msgid, ngettext } from "ttag";

import Table from "metabase-lib/lib/metadata/Table";

import { Label, LabelContainer } from "../MetadataInfo.styled";

ColumnCount.propTypes = {
table: PropTypes.instanceOf(Table).isRequired,
};

function ColumnCount({ table }: { table: Table }) {
const fieldCount = table.numFields();
return (
<LabelContainer color="text-dark">
<Label>
{ngettext(
msgid`${fieldCount} column`,
`${fieldCount} columns`,
fieldCount,
)}
</Label>
</LabelContainer>
);
}

export default ColumnCount;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import { render, screen } from "@testing-library/react";

import Table from "metabase-lib/lib/metadata/Table";

import ColumnCount from "./ColumnCount";

function setup(table: Table) {
return render(<ColumnCount table={table} />);
}

describe("ColumnCount", () => {
it("should show a non-plural label for a table with a single field", () => {
const table = new Table({ fields: [{}] });
setup(table);

expect(screen.getByText("1 column")).toBeInTheDocument();
});

it("should show a plural label for a table with multiple fields", () => {
const table = new Table({ fields: [{}, {}] });
setup(table);

expect(screen.getByText("2 columns")).toBeInTheDocument();
});

it("should handle a scenario where a table has no fields property", () => {
const table = new Table({ id: 123, display_name: "Foo" });
setup(table);

expect(screen.getByText("0 columns")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from "styled-components";

import { color } from "metabase/lib/colors";

import TableLabel from "../TableLabel/TableLabel";

export const InteractiveTableLabel = styled(TableLabel)`
cursor: pointer;
color: ${color("text-light")};
&:hover {
color: ${color("brand")};
}
`;
Loading

0 comments on commit 67ed5c3

Please sign in to comment.