Skip to content
Snippets Groups Projects
Unverified Commit fd3b146f authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Allow image URL preview in object detail (#31539)

* allow image preview in object detail
parent dafeb690
No related branches found
No related tags found
No related merge requests found
......@@ -83,6 +83,13 @@ export const GridCell = styled.div<GridItemProps>`
${props => props.colSpan || 1};
`;
export const FitImage = styled.img`
max-width: 100%;
max-height: 18rem;
object-fit: contain;
margin: 1rem auto;
`;
export const RootModal = styled(Modal)`
${ObjectDetailContainer} {
overflow: hidden;
......
import { render, screen } from "@testing-library/react";
import testDataset from "__support__/testDataset";
import { testDataset } from "__support__/testDataset";
import { setupCardDataset } from "__support__/server-mocks";
import { createMockCard } from "metabase-types/api/mocks";
import Question from "metabase-lib/Question";
......
......@@ -9,7 +9,12 @@ import EmptyState from "metabase/components/EmptyState";
import { formatValue, formatColumn } from "metabase/lib/formatting";
import Ellipsified from "metabase/core/components/Ellipsified";
import { isa, isID } from "metabase-lib/types/utils/isa";
import {
isa,
isID,
isImageURL,
isAvatarURL,
} from "metabase-lib/types/utils/isa";
import { TYPE } from "metabase-lib/types/constants";
import { findColumnIndexForColumnSetting } from "metabase-lib/queries/utils/dataset";
......@@ -18,6 +23,7 @@ import {
ObjectDetailsTable,
GridContainer,
GridCell,
FitImage,
} from "./ObjectDetail.styled";
export interface DetailsTableCellProps {
......@@ -83,6 +89,12 @@ export function DetailsTableCell({
const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
const isImage =
!isColumnName &&
(isImageURL(column) || isAvatarURL(column)) &&
typeof value === "string" &&
value.startsWith("http");
return (
<div>
<span
......@@ -103,6 +115,11 @@ export function DetailsTableCell({
>
{cellValue}
</span>
{isImage && (
<div>
<FitImage src={value} alt={value} />
</div>
)}
</div>
);
}
......@@ -158,32 +175,36 @@ export function DetailsTable({
return (
<ObjectDetailsTable>
<GridContainer cols={3}>
{cols.map((column, columnIndex) => (
<Fragment key={columnIndex}>
<GridCell>
<DetailsTableCell
column={column}
value={row[columnIndex] ?? t`Empty`}
isColumnName
settings={settings}
className="text-bold text-medium"
onVisualizationClick={onVisualizationClick}
visualizationIsClickable={visualizationIsClickable}
/>
</GridCell>
<GridCell colSpan={2}>
<DetailsTableCell
column={column}
value={row[columnIndex]}
isColumnName={false}
settings={settings}
className="text-bold text-dark text-spaced text-wrap"
onVisualizationClick={onVisualizationClick}
visualizationIsClickable={visualizationIsClickable}
/>
</GridCell>
</Fragment>
))}
{cols.map((column, columnIndex) => {
const columnValue = row[columnIndex];
return (
<Fragment key={columnIndex}>
<GridCell>
<DetailsTableCell
column={column}
value={row[columnIndex] ?? t`Empty`}
isColumnName
settings={settings}
className="text-bold text-medium"
onVisualizationClick={onVisualizationClick}
visualizationIsClickable={visualizationIsClickable}
/>
</GridCell>
<GridCell colSpan={2}>
<DetailsTableCell
column={column}
value={columnValue}
isColumnName={false}
settings={settings}
className="text-bold text-dark text-spaced text-wrap"
onVisualizationClick={onVisualizationClick}
visualizationIsClickable={visualizationIsClickable}
/>
</GridCell>
</Fragment>
);
})}
</GridContainer>
</ObjectDetailsTable>
);
......
import { render, screen } from "@testing-library/react";
import { DetailsTable } from "metabase/visualizations/components/ObjectDetail/ObjectDetailsTable";
import testDataset from "__support__/testDataset";
import { testDataset } from "__support__/testDataset";
import {
createMockColumn,
createMockDatasetData,
......@@ -42,6 +42,55 @@ const invalidObjectDetailCard = {
rows: [["i am not json"]],
}),
};
const objectDetailImageCard = {
card: {
display: "object",
},
data: createMockDatasetData({
cols: [
createMockColumn({
name: "id",
display_name: "ID",
base_type: TYPE.Integer,
semantic_type: TYPE.PK,
effective_type: TYPE.Integer,
}),
createMockColumn({
name: "image",
display_name: "Image",
base_type: TYPE.String,
semantic_type: TYPE.ImageURL,
effective_type: TYPE.String,
}),
createMockColumn({
name: "avatar_image",
display_name: "Avatar Image",
base_type: TYPE.String,
semantic_type: TYPE.AvatarURL,
effective_type: TYPE.String,
}),
],
rows: [
[
"1",
"https://www.metabase.com/images/logo.svg",
"https://www.metabase.com/images/home/cloud.svg",
],
[
"2",
"https://www.metabase.com/images/logo.svg",
"https://www.metabase.com/images/home/cloud.svg",
],
[
"3",
"https://www.metabase.com/images/logo.svg",
"https://www.metabase.com/images/home/cloud.svg",
],
],
}),
};
describe("ObjectDetailsTable", () => {
it("renders an object details table", () => {
render(
......@@ -60,6 +109,44 @@ describe("ObjectDetailsTable", () => {
expect(screen.getByText("Doohickey")).toBeInTheDocument();
});
describe("image rendering", () => {
it("should render an image if the column is an image url", () => {
render(
<DetailsTable
data={objectDetailImageCard.data}
zoomedRow={objectDetailImageCard.data.rows[1]}
onVisualizationClick={() => null}
visualizationIsClickable={() => false}
settings={{
column: () => null,
}}
/>,
);
expect(
screen.getByAltText(String(objectDetailImageCard.data.rows[1][1])),
).toBeInTheDocument();
});
it("should render an image if the column is an avatar image url", () => {
render(
<DetailsTable
data={objectDetailImageCard.data}
zoomedRow={objectDetailImageCard.data.rows[1]}
onVisualizationClick={() => null}
visualizationIsClickable={() => false}
settings={{
column: () => null,
}}
/>,
);
expect(
screen.getByAltText(String(objectDetailImageCard.data.rows[1][2])),
).toBeInTheDocument();
});
});
describe("json field rendering", () => {
it("should properly display JSON semantic type data as JSON", () => {
render(
......
export default {
export const testDataset = {
rows: [
[
"1",
......
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