Skip to content
Snippets Groups Projects
Unverified Commit 253801b5 authored by Oisin Coveney's avatar Oisin Coveney Committed by GitHub
Browse files

Fix: Inconsistent icon position in pinned dashboard item loading state (#31364)

parent dfcfe5e0
No related branches found
No related tags found
No related merge requests found
Showing
with 233 additions and 334 deletions
......@@ -67,14 +67,12 @@ const PinnedQuestionCard = ({
<PinnedQuestionLoader id={item.id}>
{({ question, rawSeries, loading, error, errorIcon }) =>
loading ? (
<>
{positionedActionMenu}
<CardPreviewSkeleton
name={question?.displayName()}
display={question?.display()}
description={question?.description()}
/>
</>
<CardPreviewSkeleton
name={question?.displayName()}
display={question?.display()}
description={question?.description()}
actionMenu={actionMenu}
/>
) : (
<Visualization
actionButtons={actionMenu}
......
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonImage, SkeletonRoot } from "./AreaSkeleton.styled";
import { SkeletonImage } from "./AreaSkeleton.styled";
export interface AreaSkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const AreaSkeleton = ({
name,
description,
...props
}: AreaSkeletonProps): JSX.Element => {
const AreaSkeleton = (): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 371 113"
preserveAspectRatio="none"
>
<path
d="M15.254 97.568 0 107.524V113h371V59.736L345.455 0l-48.453 59.736-15.317-15.432-15.191 15.432-24.227-30.864-43.517 40.82-20.19-15.93-30.847 15.93-30.169-15.93-46.658 46.793-15.254-9.458L33.2 107.524l-17.946-9.956Z"
fill="currentColor"
/>
</SkeletonImage>
</SkeletonRoot>
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 371 113"
preserveAspectRatio="none"
>
<path
d="M15.254 97.568 0 107.524V113h371V59.736L345.455 0l-48.453 59.736-15.317-15.432-15.191 15.432-24.227-30.864-43.517 40.82-20.19-15.93-30.847 15.93-30.169-15.93-46.658 46.793-15.254-9.458L33.2 107.524l-17.946-9.956Z"
fill="currentColor"
/>
</SkeletonImage>
);
};
......
import styled from "@emotion/styled";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonImage, SkeletonRoot } from "./BarSkeleton.styled";
import { SkeletonImage } from "./BarSkeleton.styled";
export interface BarSkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const BarSkeleton = ({
name,
description,
...props
}: BarSkeletonProps): JSX.Element => {
const BarSkeleton = (): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 372 117"
preserveAspectRatio="none"
>
<path
fill="currentColor"
d="M0 38.71h23.878V117H0zM28.906 20.503h23.878V117H28.906zM57.81 38.71h23.878V117H57.81zM86.715 0h23.878v117H86.715zM115.62 20.503h23.878V117H115.62zM144.527 25.965h23.878v91.034h-23.878zM173.431 9.579h25.135V117h-25.135zM202.337 20.503h25.135V117h-25.135zM231.244 20.503h25.135V117h-25.135zM261.406 9.579h23.878V117h-23.878zM290.311 20.503h23.878V117h-23.878zM319.216 20.503h23.878V117h-23.878zM348.121 9.579h23.878V117h-23.878z"
/>
</SkeletonImage>
</SkeletonRoot>
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 372 117"
preserveAspectRatio="none"
>
<path
fill="currentColor"
d="M0 38.71h23.878V117H0zM28.906 20.503h23.878V117H28.906zM57.81 38.71h23.878V117H57.81zM86.715 0h23.878v117H86.715zM115.62 20.503h23.878V117H115.62zM144.527 25.965h23.878v91.034h-23.878zM173.431 9.579h25.135V117h-25.135zM202.337 20.503h25.135V117h-25.135zM231.244 20.503h25.135V117h-25.135zM261.406 9.579h23.878V117h-23.878zM290.311 20.503h23.878V117h-23.878zM319.216 20.503h23.878V117h-23.878zM348.121 9.579h23.878V117h-23.878z"
/>
</SkeletonImage>
);
};
......
......@@ -14,13 +14,6 @@ export const fadingKeyframes = keyframes`
opacity: 0.5;
}
`;
export const containerStyles = css`
display: flex;
flex-direction: column;
height: 100%;
`;
export const animationStyles = css`
color: ${color("bg-medium")};
animation: ${fadingKeyframes} 1.5s infinite;
......
import { HTMLAttributes } from "react";
import AreaSkeleton from "../AreaSkeleton";
import BarSkeleton from "../BarSkeleton";
import EmptySkeleton from "../EmptySkeleton";
import FunnelSkeleton from "../FunnelSkeleton";
import GaugeSkeleton from "../GaugeSkeleton";
import LineSkeleton from "../LineSkeleton";
import MapSkeleton from "../MapSkeleton";
import PieSkeleton from "../PieSkeleton";
import ProgressSkeleton from "../ProgressSkeleton";
import RowSkeleton from "../RowSkeleton";
import ScalarSkeleton from "../ScalarSkeleton";
import ScatterSkeleton from "../ScatterSkeleton";
import SkeletonCaption from "../SkeletonCaption";
import SmartScalarSkeleton from "../SmartScalarSkeleton";
import TableSkeleton from "../TableSkeleton";
import WaterfallSkeleton from "../WaterfallSkeleton";
import AreaSkeleton from "metabase/visualizations/components/skeletons/AreaSkeleton";
import FunnelSkeleton from "metabase/visualizations/components/skeletons/FunnelSkeleton";
import LineSkeleton from "metabase/visualizations/components/skeletons/LineSkeleton";
import GaugeSkeleton from "metabase/visualizations/components/skeletons/GaugeSkeleton";
import MapSkeleton from "metabase/visualizations/components/skeletons/MapSkeleton";
import BarSkeleton from "metabase/visualizations/components/skeletons/BarSkeleton";
import TableSkeleton from "metabase/visualizations/components/skeletons/TableSkeleton";
import PieSkeleton from "metabase/visualizations/components/skeletons/PieSkeleton";
import ProgressSkeleton from "metabase/visualizations/components/skeletons/ProgressSkeleton";
import RowSkeleton from "metabase/visualizations/components/skeletons/RowSkeleton";
import ScatterSkeleton from "metabase/visualizations/components/skeletons/ScatterSkeleton";
import WaterfallSkeleton from "metabase/visualizations/components/skeletons/WaterfallSkeleton";
import SkeletonCaption from "metabase/visualizations/components/skeletons/SkeletonCaption";
import { VisualizationSkeleton } from "metabase/visualizations/components/skeletons/VisualizationSkeleton/VisualizationSkeleton";
import ScalarSkeleton from "metabase/visualizations/components/skeletons/ScalarSkeleton/ScalarSkeleton";
import { CardDisplayType } from "metabase-types/api";
export interface ChartSkeletonProps extends HTMLAttributes<HTMLDivElement> {
export type ChartSkeletonProps = HTMLAttributes<HTMLDivElement> & {
display?: CardDisplayType;
name?: string | null;
display?: string | null;
description?: string | null;
}
actionMenu?: JSX.Element | null;
};
const ChartSkeleton = ({
display,
...props
}: ChartSkeletonProps): JSX.Element => {
const skeletonComponent: (display?: CardDisplayType) => JSX.Element | null = (
display?: CardDisplayType,
) => {
if (!display) {
return <EmptySkeleton {...props} />;
return null;
}
switch (display) {
case "area":
return <AreaSkeleton {...props} />;
return <AreaSkeleton />;
case "bar":
return <BarSkeleton {...props} />;
return <BarSkeleton />;
case "funnel":
return <FunnelSkeleton {...props} />;
return <FunnelSkeleton />;
case "gauge":
return <GaugeSkeleton {...props} />;
return <GaugeSkeleton />;
case "line":
return <LineSkeleton {...props} />;
return <LineSkeleton />;
case "map":
return <MapSkeleton {...props} />;
return <MapSkeleton />;
case "object":
case "pivot":
case "table":
return <TableSkeleton {...props} />;
return <TableSkeleton />;
case "pie":
return <PieSkeleton {...props} />;
return <PieSkeleton />;
case "progress":
return <ProgressSkeleton {...props} />;
return <ProgressSkeleton />;
case "row":
return <RowSkeleton {...props} />;
case "scalar":
return <ScalarSkeleton {...props} />;
return <RowSkeleton />;
case "scatter":
return <ScatterSkeleton {...props} />;
case "smartscalar":
return <SmartScalarSkeleton {...props} />;
return <ScatterSkeleton />;
case "waterfall":
return <WaterfallSkeleton {...props} />;
return <WaterfallSkeleton />;
default:
return <TableSkeleton {...props} />;
return <TableSkeleton />;
}
};
const ChartSkeleton = ({
actionMenu,
description,
display,
name,
className,
}: ChartSkeletonProps) => {
if (display === "scalar" || display === "smartscalar") {
return (
<ScalarSkeleton
className={className}
scalarType={display}
name={name}
description={description}
actionMenu={actionMenu}
/>
);
}
return (
<VisualizationSkeleton
className={className}
name={name}
description={description}
actionMenu={actionMenu}
>
{skeletonComponent(display)}
</VisualizationSkeleton>
);
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default Object.assign(ChartSkeleton, {
Title: SkeletonCaption.Title,
......
import { render, screen } from "@testing-library/react";
import ChartSkeleton from "./ChartSkeleton";
import userEvent from "@testing-library/user-event";
import { CardDisplayType } from "metabase-types/api";
import ChartSkeleton, { ChartSkeletonProps } from "./ChartSkeleton";
describe("ChartSkeleton", () => {
it("should render area", () => {
render(<ChartSkeleton display="area" name="Area" />);
expect(screen.getByText("Area")).toBeInTheDocument();
});
it("should render bar", () => {
render(<ChartSkeleton display="bar" name="Bar" />);
expect(screen.getByText("Bar")).toBeInTheDocument();
});
it("should render funnel", () => {
render(<ChartSkeleton display="funnel" name="Funnel" />);
expect(screen.getByText("Funnel")).toBeInTheDocument();
});
it("should render gauge", () => {
render(<ChartSkeleton display="gauge" name="Gauge" />);
expect(screen.getByText("Gauge")).toBeInTheDocument();
});
it("should render line", () => {
render(<ChartSkeleton display="line" name="Line" />);
expect(screen.getByText("Line")).toBeInTheDocument();
});
const MockActionMenu = <div>Action Menu</div>;
it("should render map", () => {
render(<ChartSkeleton display="map" name="Map" />);
expect(screen.getByText("Map")).toBeInTheDocument();
});
it("should render table", () => {
render(<ChartSkeleton display="table" name="Table" />);
expect(screen.getByText("Table")).toBeInTheDocument();
});
it("should render pie", () => {
render(<ChartSkeleton display="pie" name="Pie" />);
expect(screen.getByText("Pie")).toBeInTheDocument();
});
const chartSkeletonDisplayTypes: CardDisplayType[] = [
"area",
"bar",
"funnel",
"gauge",
"line",
"map",
"object",
"pivot",
"table",
"pie",
"progress",
"row",
"scalar",
"scatter",
"smartscalar",
"waterfall",
];
it("should render progress", () => {
render(<ChartSkeleton display="progress" name="Progress" />);
expect(screen.getByText("Progress")).toBeInTheDocument();
});
const displayTestData = [
{
name: "Empty",
display: undefined,
},
{
name: "Random display type",
display: "a display type",
},
...chartSkeletonDisplayTypes.map((display: CardDisplayType) => ({
name: display,
display,
})),
];
it("should render row", () => {
render(<ChartSkeleton display="row" name="Row" />);
expect(screen.getByText("Row")).toBeInTheDocument();
const setup = ({
display,
name,
description,
actionMenu,
}: ChartSkeletonProps) => {
render(
<ChartSkeleton
display={display}
name={name}
description={description}
actionMenu={actionMenu}
/>,
);
};
describe("ChartSkeleton", () => {
beforeAll(() => {
jest.unmock("metabase/components/Popover");
});
it("should render scalar", () => {
render(<ChartSkeleton display="scalar" name="Scalar" />);
expect(screen.getByText("Scalar")).toBeInTheDocument();
});
displayTestData.forEach(({ name, display }) => {
const displayDescription = `${name} description`;
it("should render scatter", () => {
render(<ChartSkeleton display="scatter" name="Scatter" />);
expect(screen.getByText("Scatter")).toBeInTheDocument();
});
it(`should render ${name} visualization`, () => {
setup({ name, display });
expect(screen.getByText(name)).toBeInTheDocument();
expect(screen.queryByLabelText("info icon")).not.toBeInTheDocument();
});
it("should render smartscalar", () => {
render(<ChartSkeleton display="smartscalar" name="Trend" />);
expect(screen.getByText("Trend")).toBeInTheDocument();
});
it(`should render ${name} visualization with description`, () => {
setup({ name, description: displayDescription, display });
userEvent.hover(screen.getByLabelText("info icon"));
expect(screen.getByText(name)).toBeInTheDocument();
expect(screen.getByText(displayDescription)).toBeInTheDocument();
});
it("should render waterfall", () => {
render(<ChartSkeleton display="waterfall" name="Waterfall" />);
expect(screen.getByText("Waterfall")).toBeInTheDocument();
it(`should render ${name} visualization with description and action menu`, () => {
setup({
name,
description: displayDescription,
actionMenu: MockActionMenu,
display,
});
userEvent.hover(screen.getByLabelText("info icon"));
expect(screen.getByText(name)).toBeInTheDocument();
expect(screen.getByText(displayDescription)).toBeInTheDocument();
expect(screen.getByText("Action Menu")).toBeInTheDocument();
});
});
});
import styled from "@emotion/styled";
import { containerStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonRoot } from "./EmptySkeleton.styled";
export interface EmptySkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const EmptySkeleton = ({
name,
description,
...props
}: EmptySkeletonProps): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
</SkeletonRoot>
);
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default EmptySkeleton;
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./EmptySkeleton";
import styled from "@emotion/styled";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonImage, SkeletonRoot } from "./FunnelSkeleton.styled";
import { SkeletonImage } from "./FunnelSkeleton.styled";
export interface FunnelSkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const FunnelSkeleton = ({
name,
description,
...props
}: FunnelSkeletonProps): JSX.Element => {
const FunnelSkeleton = (): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 370 104"
preserveAspectRatio="xMidYMid"
>
<path
d="m0 0 123 24v56L0 104V0ZM124 24l122 16v32l-122 8V24ZM247 40l123 8v15l-123 9V40Z"
fill="currentColor"
/>
</SkeletonImage>
</SkeletonRoot>
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 370 104"
preserveAspectRatio="xMidYMid"
>
<path
d="m0 0 123 24v56L0 104V0ZM124 24l122 16v32l-122 8V24ZM247 40l123 8v15l-123 9V40Z"
fill="currentColor"
/>
</SkeletonImage>
);
};
......
import styled from "@emotion/styled";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonImage, SkeletonRoot } from "./GaugeSkeleton.styled";
import { SkeletonImage } from "./GaugeSkeleton.styled";
export interface GaugeSkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const GaugeSkeleton = ({
name,
description,
...props
}: GaugeSkeletonProps): JSX.Element => {
const GaugeSkeleton = (): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 169 143"
preserveAspectRatio="xMidYMid"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M32.355 51.786c24.355-39.382 78.268-39.256 103.369.126 9.597 15.06 11.924 28.144 10.204 39.932-1.764 12.098-7.969 24.037-17.566 36.209l17.276 13.621c10.903-13.828 19.532-29.321 22.06-46.655 2.573-17.643-1.35-35.99-13.422-54.931-33.535-52.618-107.472-53.492-140.632.126-27.127 43.866-6.504 83.779 9.63 101.952l16.451-14.605c-12.866-14.493-27.242-43.64-7.37-75.775Zm38.916 25.991a5.994 5.994 0 0 0 0 11.988H97.87a5.994 5.994 0 0 0 0-11.988h-26.6Z"
fill="currentColor"
/>
</SkeletonImage>
</SkeletonRoot>
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 169 143"
preserveAspectRatio="xMidYMid"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M32.355 51.786c24.355-39.382 78.268-39.256 103.369.126 9.597 15.06 11.924 28.144 10.204 39.932-1.764 12.098-7.969 24.037-17.566 36.209l17.276 13.621c10.903-13.828 19.532-29.321 22.06-46.655 2.573-17.643-1.35-35.99-13.422-54.931-33.535-52.618-107.472-53.492-140.632.126-27.127 43.866-6.504 83.779 9.63 101.952l16.451-14.605c-12.866-14.493-27.242-43.64-7.37-75.775Zm38.916 25.991a5.994 5.994 0 0 0 0 11.988H97.87a5.994 5.994 0 0 0 0-11.988h-26.6Z"
fill="currentColor"
/>
</SkeletonImage>
);
};
......
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonRoot, SkeletonImage } from "./LineSkeleton.styled";
import { SkeletonImage } from "./LineSkeleton.styled";
export interface LineSkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const LineSkeleton = ({
name,
description,
...props
}: LineSkeletonProps): JSX.Element => {
const LineSkeleton = (): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 371 113"
fill="none"
preserveAspectRatio="none"
>
<path
d="m1 111 15.336-10 18.043 10 22.553-16.5 15.336 9.5 46.91-47 30.331 16 31.013-16 20.299 16 43.752-41 24.358 31 15.273-15.5L299.603 63l48.714-60L374 63"
stroke="currentColor"
strokeWidth="2"
/>
</SkeletonImage>
</SkeletonRoot>
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 371 113"
fill="none"
preserveAspectRatio="none"
>
<path
d="m1 111 15.336-10 18.043 10 22.553-16.5 15.336 9.5 46.91-47 30.331 16 31.013-16 20.299 16 43.752-41 24.358 31 15.273-15.5L299.603 63l48.714-60L374 63"
stroke="currentColor"
strokeWidth="2"
/>
</SkeletonImage>
);
};
......
import styled from "@emotion/styled";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
import { HTMLAttributes } from "react";
import SkeletonCaption from "../SkeletonCaption";
import { SkeletonImage, SkeletonRoot } from "./MapSkeleton.styled";
import { SkeletonImage } from "./MapSkeleton.styled";
export interface MapSkeletonProps extends HTMLAttributes<HTMLDivElement> {
name?: string | null;
description?: string | null;
}
const MapSkeleton = ({
name,
description,
...props
}: MapSkeletonProps): JSX.Element => {
const MapSkeleton = (): JSX.Element => {
return (
<SkeletonRoot {...props}>
<SkeletonCaption name={name} description={description} />
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 242 157"
preserveAspectRatio="xMidYMid"
>
<image href="/app/assets/img/map.svg" />
</SkeletonImage>
</SkeletonRoot>
<SkeletonImage
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 242 157"
preserveAspectRatio="xMidYMid"
>
<image href="/app/assets/img/map.svg" />
</SkeletonImage>
);
};
......
import styled from "@emotion/styled";
import { containerStyles, animationStyles } from "../Skeleton";
export const SkeletonRoot = styled.div`
${containerStyles};
`;
import { animationStyles } from "metabase/visualizations/components/skeletons/ChartSkeleton/ChartSkeleton.styled";
export const SkeletonImage = styled.svg`
${animationStyles};
......
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