Compare commits

...

11 Commits

Author SHA1 Message Date
Romeo Van Snick
8c72149945 Rename docker-compose.yml to compose.yml for better compatibility with python-runner repo 2025-09-29 21:08:08 +10:00
Romeo Van Snick
350e69e4e9 Add RUN_MODE to python-runner 2025-09-29 21:08:08 +10:00
Romeo Van Snick
ad9e3af2cd Disable limits in local dev 2025-09-29 21:08:08 +10:00
Romeo Van Snick
9bc2f603e2 Remove python runner dir in CI 2025-09-29 21:08:08 +10:00
Romeo Van Snick
b9e1f5774d Set up the python runner bucket 2025-09-29 21:08:07 +10:00
Romeo Van Snick
0e93fb8dbc Add missing setting 2025-09-29 21:08:07 +10:00
Romeo Van Snick
92e9305d20 Import python runner in Cypress docker compose file 2025-09-29 21:08:07 +10:00
Romeo Van Snick
df4a0869b5 Add docker-compose file for python-runner 2025-09-29 21:08:07 +10:00
Kamil Mielnik
581c1e7a38 WRK-871 Details view: JSON formatting missing in details view (#63892)
* Format JSON in new detail view

* Use mantine props

* Use Code

* Use CodeEditor
2025-09-29 10:31:59 +00:00
Romeo Van Snick
17a61a81c8 Add missing import (#64171) 2025-09-29 11:17:26 +02:00
Romeo Van Snick
1d699a5fae Use the entities display name if possible (#64076) 2025-09-29 13:32:11 +10:00
11 changed files with 271 additions and 4 deletions

View File

@@ -19,6 +19,7 @@ while [[ $# -gt 0 ]]; do
done
echo "Cloning python-runner repository..."
rm -r python-runner
git clone --depth 1 https://${METABASE_AUTOMATION_USER_TOKEN}@github.com/metabase/python-runner-container.git python-runner
if [ "$SKIP_START" = false ]; then

View File

@@ -2,6 +2,7 @@ name: metabase-e2e
include:
- ../../../snowplow/docker-compose.yml
- ../../../python-runner/compose.yaml
services:
postgres-sample:

View File

@@ -1,6 +1,7 @@
(ns metabase-enterprise.transforms.util
(:require
[clojure.core.async :as a]
[clojure.string :as str]
[java-time.api :as t]
[metabase-enterprise.transforms.canceling :as canceling]
[metabase-enterprise.transforms.models.transform-run :as transform-run]

View File

@@ -47,7 +47,7 @@ import {
import styles from "./SmartLinkNode.module.css";
type SmartLinkEntity =
export type SmartLinkEntity =
| Card
| Dashboard
| Collection
@@ -398,7 +398,7 @@ export const SmartLinkComponent = memo(
>
<span className={styles.smartLinkInner}>
<Icon name={iconData.name} className={styles.icon} />
{entity.name}
{getName(entity)}
</span>
</a>
</NodeViewWrapper>
@@ -427,3 +427,13 @@ function entityToObjectWithModel(
is_personal: (entity as Collection).is_personal,
};
}
function getName(entity: SmartLinkEntity) {
if ("display_name" in entity && entity.display_name !== "") {
return entity.display_name;
}
if ("name" in entity && entity.name !== "") {
return entity.name;
}
return "";
}

View File

@@ -0,0 +1,146 @@
import type { NodeViewProps } from "@tiptap/react";
import {
setupCardEndpoints,
setupCollectionByIdEndpoint,
setupDashboardEndpoints,
setupDatabaseEndpoints,
setupDocumentEndpoints,
setupTableEndpoints,
} from "__support__/server-mocks";
import { renderWithProviders, screen, waitFor } from "__support__/ui";
import type { SuggestionModel } from "metabase-enterprise/documents/components/Editor/types";
import {
createMockCard,
createMockCollection,
createMockDashboard,
createMockDatabase,
createMockDocument,
createMockTable,
} from "metabase-types/api/mocks";
import { SmartLinkComponent, type SmartLinkEntity } from "./SmartLinkNode";
function createProps(model: SuggestionModel, entity: SmartLinkEntity) {
const node = { attrs: { entityId: entity.id, model } };
return { node } as unknown as NodeViewProps;
}
function setup({
entity,
model,
}: {
model: SuggestionModel;
entity: SmartLinkEntity;
}) {
const props = createProps(model, entity);
renderWithProviders(<SmartLinkComponent {...props} />);
}
describe("SmartLink", () => {
describe("for Card", () => {
it("should render the name of a card", async () => {
const card = createMockCard({
name: "My Card",
});
setupCardEndpoints(card);
setup({ model: "card", entity: card });
await waitFor(() => {
expect(screen.getByText("My Card")).toBeInTheDocument();
});
});
});
describe("for Dashboard", () => {
it("should render the name of a dashboard", async () => {
const dashboard = createMockDashboard({
name: "My Dashboard",
});
setupDashboardEndpoints(dashboard);
setup({ model: "dashboard", entity: dashboard });
await waitFor(() => {
expect(screen.getByText("My Dashboard")).toBeInTheDocument();
});
});
});
describe("for Collection", () => {
it("should render the name of a collection", async () => {
const collection = createMockCollection({
name: "My Collection",
});
setupCollectionByIdEndpoint({ collections: [collection] });
setup({ model: "collection", entity: collection });
await waitFor(() => {
expect(screen.getByText("My Collection")).toBeInTheDocument();
});
});
});
describe("for Table", () => {
it("should render the display name of a table when possible", async () => {
const table = createMockTable({
name: "TABLE_A",
display_name: "Table A",
});
setupTableEndpoints(table);
setup({ model: "table", entity: table });
await waitFor(() => {
expect(screen.getByText("Table A")).toBeInTheDocument();
});
expect(screen.queryByText("TABLE_A")).not.toBeInTheDocument();
});
it("should fall back to the name of a table when not possible", async () => {
const table = createMockTable({
name: "TABLE_A",
display_name: "",
});
setupTableEndpoints(table);
setup({ model: "table", entity: table });
await waitFor(() => {
expect(screen.getByText("TABLE_A")).toBeInTheDocument();
});
});
});
describe("for Database", () => {
it("should render the name of a database", async () => {
const database = createMockDatabase({
name: "DATABASE_A",
});
setupDatabaseEndpoints(database);
setup({ model: "database", entity: database });
await waitFor(() => {
expect(screen.getByText("DATABASE_A")).toBeInTheDocument();
});
});
});
describe("for Document", () => {
it("should render the name of a document", async () => {
const document = createMockDocument({
name: "My Document",
});
setupDocumentEndpoints(document);
setup({ model: "document", entity: document });
await waitFor(() => {
expect(screen.getByText("My Document")).toBeInTheDocument();
});
});
});
});

View File

@@ -683,6 +683,7 @@ export interface EnterpriseSettings extends Settings {
"python-storage-s-3-endpoint"?: string | null;
"python-storage-s-3-region"?: string | null;
"python-storage-s-3-bucket"?: string | null;
"python-storage-s-3-prefix"?: string | null;
"python-storage-s-3-access-key"?: string | null;
"python-storage-s-3-secret-key"?: string | null;
"python-storage-s-3-container-endpoint"?: string | null;

View File

@@ -32,3 +32,11 @@
max-height: 560px;
overflow: auto;
}
.json {
max-height: 200px;
width: 100%;
background-color: var(--mb-color-bg-light);
border: 1px solid var(--mb-color-border);
border-radius: 4px;
}

View File

@@ -1,10 +1,12 @@
import cx from "classnames";
import type { ReactNode } from "react";
import { type ReactNode, useMemo } from "react";
import { Link } from "react-router";
import { t } from "ttag";
import { CodeEditor } from "metabase/common/components/CodeEditor";
import { Box, Image, Stack, Text, rem } from "metabase/ui";
import { isFK } from "metabase-lib/v1/types/utils/isa";
import { TYPE } from "metabase-lib/v1/types/constants";
import { isFK, isa } from "metabase-lib/v1/types/utils/isa";
import type { DatasetColumn, Field, RowValue } from "metabase-types/api";
import S from "./Value.module.css";
@@ -27,11 +29,23 @@ export const Value = ({ children, column, field, value }: Props) => {
column.settings.link_text &&
column.settings.link_url,
);
const json = useMemo(() => getJson(column, value), [column, value]);
if (isEmptyValue) {
return <Text c="text-light">{t`empty`}</Text>;
}
if (json) {
return (
<CodeEditor
className={S.json}
language="json"
lineNumbers={false}
value={json}
/>
);
}
if (isFK(column) && newTableId != null && !isValidLink) {
return (
<Text
@@ -83,3 +97,34 @@ export const Value = ({ children, column, field, value }: Props) => {
</Text>
);
};
function getJson(
column: DatasetColumn,
value: RowValue | undefined,
): string | undefined {
if (!value || typeof value === "number" || typeof value === "boolean") {
return undefined;
}
if (
column.semantic_type &&
isa(column.semantic_type, TYPE.SerializedJSON) &&
typeof value == "string"
) {
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch (error) {
return undefined;
}
}
if (typeof value === "object") {
try {
return JSON.stringify(value, null, 2);
} catch (error) {
return undefined;
}
}
return undefined;
}

View File

@@ -0,0 +1,13 @@
import fetchMock from "fetch-mock";
import type { Document } from "metabase-types/api";
export function setupDocumentEndpoints(doc: Document) {
fetchMock.get(`path:/api/ee/document/${doc.id}`, doc);
fetchMock.post(`path:/api/ee/document/${doc.id}`, {});
fetchMock.put(`path:/api/ee/document/${doc.id}`, async (call) => {
const body = await call?.request?.json();
return { ...doc, ...body };
});
fetchMock.delete(`path:/api/ee/document/${doc.id}`, {});
}

View File

@@ -14,6 +14,7 @@ export * from "./constants";
export * from "./dashboard";
export * from "./database";
export * from "./dataset";
export * from "./document";
export * from "./email";
export * from "./embed";
export * from "./field";

View File

@@ -0,0 +1,40 @@
---
name: python-runner
services:
localstack:
image: localstack/localstack:3.5.0
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
environment:
- DEBUG=0
volumes:
- "/tmp/python-exec-localstack:/var/lib/localstack"
setup-bucket:
# Run a script to ensure the bucket needed for running the tests exists in
# the localstack s3 instance.
image: localstack/localstack:3.5.0
depends_on:
- localstack
entrypoint: >
sh -c "aws s3 mb s3://metabase-python-runner"
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_ENDPOINT_URL=http://localstack:4566
python-runner:
image: metabase/python-runner:beta
depends_on:
- localstack
- setup-bucket
ports:
- "127.0.0.1:5001:5000"
volumes:
- "/tmp/python-exec-work:/tmp/python-exec-work"
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=us-east-1
- AWS_ENDPOINT_URL=http://localstack:4566
- AUTH_TOKEN=dev-token-12345
- DISABLE_LIMITS=1
- RUN_MODE=dev