diff --git a/frontend/src/metabase-lib/lib/metadata/Database.ts b/frontend/src/metabase-lib/lib/metadata/Database.ts index f00f26ed59db888b683fa81d6bb8c279f3dbcfbb..6bbeacfba425647a76139ff34be3e74d47476b89 100644 --- a/frontend/src/metabase-lib/lib/metadata/Database.ts +++ b/frontend/src/metabase-lib/lib/metadata/Database.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { generateSchemaId } from "metabase/lib/schema"; +import { generateSchemaId } from "metabase-lib/lib/metadata/utils/schema"; import { createLookupByProperty, memoizeClass } from "metabase-lib/lib/utils"; import Question from "../Question"; import Base from "./Base"; diff --git a/frontend/src/metabase-lib/lib/metadata/Field.ts b/frontend/src/metabase-lib/lib/metadata/Field.ts index 4b6ced77392887c7447278f46f34d43ac5bef954..7d2164b4581cfec1c50ecfc9ad6bffca4dcebb22 100644 --- a/frontend/src/metabase-lib/lib/metadata/Field.ts +++ b/frontend/src/metabase-lib/lib/metadata/Field.ts @@ -4,7 +4,6 @@ import _ from "underscore"; import moment from "moment-timezone"; import { formatField, stripId } from "metabase/lib/formatting"; -import { getIconForField } from "metabase/lib/schema_metadata"; import type { FieldFingerprint } from "metabase-types/api/field"; import type { Field as FieldRef } from "metabase-types/types/Query"; import { @@ -41,7 +40,7 @@ import { FieldDimension } from "../Dimension"; import Base from "./Base"; import type Table from "./Table"; import type Metadata from "./Metadata"; -import { getUniqueFieldId } from "./utils/fields"; +import { getIconForField, getUniqueFieldId } from "./utils/fields"; export const LONG_TEXT_MIN = 80; diff --git a/frontend/src/metabase-lib/lib/metadata/utils/fields.ts b/frontend/src/metabase-lib/lib/metadata/utils/fields.ts index 5337568626db00052c30230f55d5cc40571bf698..a3ca1f3b8c14a0c5cc95a21866809c9953f726a5 100644 --- a/frontend/src/metabase-lib/lib/metadata/utils/fields.ts +++ b/frontend/src/metabase-lib/lib/metadata/utils/fields.ts @@ -1,6 +1,35 @@ import { isVirtualCardId } from "metabase-lib/lib/metadata/utils/saved-questions"; +import { + BOOLEAN, + COORDINATE, + FOREIGN_KEY, + LOCATION, + NUMBER, + PRIMARY_KEY, + STRING, + STRING_LIKE, + TEMPORAL, +} from "metabase-lib/lib/types/constants"; +import { getFieldType } from "metabase-lib/lib/types/utils/isa"; import type Field from "../Field"; +const ICON_MAPPING: Record<string, string> = { + [TEMPORAL]: "calendar", + [LOCATION]: "location", + [COORDINATE]: "location", + [STRING]: "string", + [STRING_LIKE]: "string", + [NUMBER]: "int", + [BOOLEAN]: "io", + [FOREIGN_KEY]: "connections", + [PRIMARY_KEY]: "label", +}; + +export function getIconForField(fieldOrColumn: any) { + const type = getFieldType(fieldOrColumn); + return type && ICON_MAPPING[type] ? ICON_MAPPING[type] : "unknown"; +} + export function getUniqueFieldId(field: Field): number | string { const { table_id } = field; const fieldIdentifier = getFieldIdentifier(field); diff --git a/frontend/src/metabase-lib/lib/metadata/utils/saved-questions.js b/frontend/src/metabase-lib/lib/metadata/utils/saved-questions.js index ff73cfc70fe427ccc0f6cc4284d3a48a90551927..39c0d66576882e433e9c40e9b092d283ddac04d9 100644 --- a/frontend/src/metabase-lib/lib/metadata/utils/saved-questions.js +++ b/frontend/src/metabase-lib/lib/metadata/utils/saved-questions.js @@ -1,4 +1,4 @@ -import { generateSchemaId } from "metabase/lib/schema"; +import { generateSchemaId } from "metabase-lib/lib/metadata/utils/schema"; export const SAVED_QUESTIONS_VIRTUAL_DB_ID = -1337; export const ROOT_COLLECTION_VIRTUAL_SCHEMA_NAME = "Everything else"; diff --git a/frontend/src/metabase-lib/lib/metadata/utils/schema.js b/frontend/src/metabase-lib/lib/metadata/utils/schema.js new file mode 100644 index 0000000000000000000000000000000000000000..dbe898a8b7d6fb53de8e87decab966dd8637e196 --- /dev/null +++ b/frontend/src/metabase-lib/lib/metadata/utils/schema.js @@ -0,0 +1,27 @@ +export const getSchemaName = id => { + return parseSchemaId(id)[1]; +}; + +export const parseSchemaId = id => { + const schemaId = String(id || ""); + const [databaseId, schemaName, encodedPayload] = schemaId.split(":"); + const result = [databaseId, decodeURIComponent(schemaName)]; + if (encodedPayload) { + result.push(JSON.parse(decodeURIComponent(encodedPayload))); + } + return result; +}; + +export const generateSchemaId = (dbId, schemaName, payload) => { + // Schema ID components are separated with colons + // Schema name should be encoded to escape colon characters + // so parseSchemaId can work correctly + const name = schemaName ? encodeURIComponent(schemaName) : ""; + let id = `${dbId}:${name}`; + if (payload) { + const json = JSON.stringify(payload); + const encodedPayload = encodeURIComponent(json); + id += `:${encodedPayload}`; + } + return id; +}; diff --git a/frontend/src/metabase-lib/lib/metadata/utils/schema.unit.spec.js b/frontend/src/metabase-lib/lib/metadata/utils/schema.unit.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..93e407c9c3b2910897fb1be9e46aed13d6d49108 --- /dev/null +++ b/frontend/src/metabase-lib/lib/metadata/utils/schema.unit.spec.js @@ -0,0 +1,93 @@ +import { generateSchemaId, getSchemaName, parseSchemaId } from "./schema"; + +const SCHEMA_TEST_CASES = [ + { dbId: 1, schemaName: 2, schema: "1:2" }, + { dbId: "1", schemaName: "2", schema: "1:2" }, + { dbId: "1", schema: "1:" }, + { + dbId: -1337, + schemaName: "Collection", + schema: "-1337:Collection", + }, +]; + +describe("generateSchemaId", () => { + SCHEMA_TEST_CASES.forEach(testCase => { + const { dbId, schemaName, schema } = testCase; + it(`returns "${schema}" for "${dbId}" DB and ${schemaName} schema`, () => { + expect(generateSchemaId(dbId, schemaName)).toBe(schema); + }); + }); + + it("encodes extra payload", () => { + const payload = { isDataset: true }; + const expectedPayload = getEncodedPayload(payload); + + const schema = generateSchemaId(1, 2, payload); + + expect(schema).toBe(`1:2:${expectedPayload}`); + }); +}); + +describe("parseSchemaId", () => { + SCHEMA_TEST_CASES.forEach(testCase => { + const { schema, dbId, schemaName } = testCase; + + const expectedDatabaseId = String(dbId); + const expectedSchemaName = schemaName ? String(schemaName) : ""; + + it(`parses "${schema}" correctly`, () => { + const [parsedDatabaseId, parsedSchemaName] = parseSchemaId(schema); + expect({ + dbId: parsedDatabaseId, + schemaName: parsedSchemaName, + }).toEqual({ + dbId: expectedDatabaseId, + schemaName: expectedSchemaName, + }); + }); + }); + + it("decodes extra payload", () => { + const payload = { isDataset: true }; + const [dbId, schemaName, decodedPayload] = parseSchemaId( + `1:2:${getEncodedPayload(payload)}`, + ); + expect({ dbId, schemaName, payload: decodedPayload }).toEqual({ + dbId: "1", + schemaName: "2", + payload, + }); + }); + + it("handles colons inside schema name", () => { + const databaseId = "-1337"; + const collectionName = "test:collection"; + const payload = { foo: "bar" }; + + const schemaId = generateSchemaId(databaseId, collectionName, payload); + const [decodedDatabaseId, decodedCollectionName, decodedPayload] = + parseSchemaId(schemaId); + + expect({ + databaseId: decodedDatabaseId, + collectionName: decodedCollectionName, + payload: decodedPayload, + }).toEqual({ databaseId, collectionName, payload }); + }); +}); + +describe("getSchemaName", () => { + SCHEMA_TEST_CASES.forEach(testCase => { + const { schema, schemaName } = testCase; + const expectedSchemaName = schemaName ? String(schemaName) : ""; + it(`returns "${expectedSchemaName}" for "${schema}"`, () => { + expect(getSchemaName(schema)).toBe(expectedSchemaName); + }); + }); +}); + +function getEncodedPayload(object) { + const json = JSON.stringify(object); + return encodeURIComponent(json); +} diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.tsx index 1038faa182eac1e9482c24f383dbd30a174ea55e..497b8b9d52b529a59621a729416821d2343c28f5 100644 --- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.tsx +++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/Column.tsx @@ -2,8 +2,6 @@ import React from "react"; import { t, jt, ngettext, msgid } from "ttag"; import { color } from "metabase/lib/colors"; -import { getIconForField } from "metabase/lib/schema_metadata"; - import Dashboards from "metabase/entities/dashboards"; import Questions from "metabase/entities/questions"; @@ -13,6 +11,7 @@ import type { EntityCustomDestinationClickBehavior, } from "metabase-types/api"; import type { Column as IColumn } from "metabase-types/types/Dataset"; +import { getIconForField } from "metabase-lib/lib/metadata/utils/fields"; import { SidebarItem } from "../SidebarItem"; diff --git a/frontend/src/metabase/entities/schemas.js b/frontend/src/metabase/entities/schemas.js index da98f9c74ced0e8467237d2b469f867292211a71..d9f4e19eb167263a2c6701342294b451c6193c8b 100644 --- a/frontend/src/metabase/entities/schemas.js +++ b/frontend/src/metabase/entities/schemas.js @@ -2,10 +2,13 @@ import { updateIn } from "icepick"; import { createEntity } from "metabase/lib/entities"; import { GET } from "metabase/lib/api"; -import { generateSchemaId, parseSchemaId } from "metabase/lib/schema"; import { SchemaSchema } from "metabase/schema"; import Questions from "metabase/entities/questions"; +import { + generateSchemaId, + parseSchemaId, +} from "metabase-lib/lib/metadata/utils/schema"; import { getCollectionVirtualSchemaId, getQuestionVirtualTableId, diff --git a/frontend/src/metabase/lib/schema/schema.js b/frontend/src/metabase/lib/schema/schema.js index 7ff272430cf14e67be31d94a0ad9e018b26678d0..fddc5dee636e81cb345063605bc979e2728d4e23 100644 --- a/frontend/src/metabase/lib/schema/schema.js +++ b/frontend/src/metabase/lib/schema/schema.js @@ -14,29 +14,3 @@ export const entityTypeForModel = model => { export const entityTypeForObject = object => object && entityTypeForModel(object.model); - -export const getSchemaName = id => parseSchemaId(id)[1]; - -export const parseSchemaId = id => { - const schemaId = String(id || ""); - const [databaseId, schemaName, encodedPayload] = schemaId.split(":"); - const result = [databaseId, decodeURIComponent(schemaName)]; - if (encodedPayload) { - result.push(JSON.parse(decodeURIComponent(encodedPayload))); - } - return result; -}; - -export const generateSchemaId = (dbId, schemaName, payload) => { - // Schema ID components are separated with colons - // Schema name should be encoded to escape colon characters - // so parseSchemaId can work correctly - const name = schemaName ? encodeURIComponent(schemaName) : ""; - let id = `${dbId}:${name}`; - if (payload) { - const json = JSON.stringify(payload); - const encodedPayload = encodeURIComponent(json); - id += `:${encodedPayload}`; - } - return id; -}; diff --git a/frontend/src/metabase/lib/schema/schema.unit.spec.js b/frontend/src/metabase/lib/schema/schema.unit.spec.js index 0688a24b8eb1a79540b8c26d161895bfb8072e50..16b3dcb0a67ec7354cccf308a5e66a1e2c974f26 100644 --- a/frontend/src/metabase/lib/schema/schema.unit.spec.js +++ b/frontend/src/metabase/lib/schema/schema.unit.spec.js @@ -1,10 +1,4 @@ -import { - entityTypeForModel, - entityTypeForObject, - getSchemaName, - parseSchemaId, - generateSchemaId, -} from "./schema"; +import { entityTypeForModel, entityTypeForObject } from "./schema"; describe("schemas", () => { const MODEL_ENTITY_TYPE = [ @@ -19,22 +13,6 @@ describe("schemas", () => { { model: "snippetCollection", entityType: "snippetCollections" }, ]; - const SCHEMA_TEST_CASES = [ - { dbId: 1, schemaName: 2, schema: "1:2" }, - { dbId: "1", schemaName: "2", schema: "1:2" }, - { dbId: "1", schema: "1:" }, - { - dbId: -1337, - schemaName: "Collection", - schema: "-1337:Collection", - }, - ]; - - function getEncodedPayload(object) { - const json = JSON.stringify(object); - return encodeURIComponent(json); - } - describe("entityTypeForModel", () => { MODEL_ENTITY_TYPE.forEach(testCase => { const { model, entityType } = testCase; @@ -56,80 +34,4 @@ describe("schemas", () => { expect(entityTypeForObject()).toBe(undefined); }); }); - - describe("generateSchemaId", () => { - SCHEMA_TEST_CASES.forEach(testCase => { - const { dbId, schemaName, schema } = testCase; - it(`returns "${schema}" for "${dbId}" DB and ${schemaName} schema`, () => { - expect(generateSchemaId(dbId, schemaName)).toBe(schema); - }); - }); - - it("encodes extra payload", () => { - const payload = { isDataset: true }; - const expectedPayload = getEncodedPayload(payload); - - const schema = generateSchemaId(1, 2, payload); - - expect(schema).toBe(`1:2:${expectedPayload}`); - }); - }); - - describe("parseSchemaId", () => { - SCHEMA_TEST_CASES.forEach(testCase => { - const { schema, dbId, schemaName } = testCase; - - const expectedDatabaseId = String(dbId); - const expectedSchemaName = schemaName ? String(schemaName) : ""; - - it(`parses "${schema}" correctly`, () => { - const [parsedDatabaseId, parsedSchemaName] = parseSchemaId(schema); - expect({ - dbId: parsedDatabaseId, - schemaName: parsedSchemaName, - }).toEqual({ - dbId: expectedDatabaseId, - schemaName: expectedSchemaName, - }); - }); - }); - - it("decodes extra payload", () => { - const payload = { isDataset: true }; - const [dbId, schemaName, decodedPayload] = parseSchemaId( - `1:2:${getEncodedPayload(payload)}`, - ); - expect({ dbId, schemaName, payload: decodedPayload }).toEqual({ - dbId: "1", - schemaName: "2", - payload, - }); - }); - - it("handles colons inside schema name", () => { - const databaseId = "-1337"; - const collectionName = "test:collection"; - const payload = { foo: "bar" }; - - const schemaId = generateSchemaId(databaseId, collectionName, payload); - const [decodedDatabaseId, decodedCollectionName, decodedPayload] = - parseSchemaId(schemaId); - - expect({ - databaseId: decodedDatabaseId, - collectionName: decodedCollectionName, - payload: decodedPayload, - }).toEqual({ databaseId, collectionName, payload }); - }); - }); - - describe("getSchemaName", () => { - SCHEMA_TEST_CASES.forEach(testCase => { - const { schema, schemaName } = testCase; - const expectedSchemaName = schemaName ? String(schemaName) : ""; - it(`returns "${expectedSchemaName}" for "${schema}"`, () => { - expect(getSchemaName(schema)).toBe(expectedSchemaName); - }); - }); - }); }); diff --git a/frontend/src/metabase/lib/schema_metadata.js b/frontend/src/metabase/lib/schema_metadata.js index 90432cbbea0a325d54f9920fe486edba3cafa178..ed6cb9544c40d46fdb1ae15ca4df0e430fc6ce5d 100644 --- a/frontend/src/metabase/lib/schema_metadata.js +++ b/frontend/src/metabase/lib/schema_metadata.js @@ -1,16 +1,4 @@ import { field_semantic_types_map } from "metabase/lib/core"; -import { getFieldType } from "metabase-lib/lib/types/utils/isa"; -import { - TEMPORAL, - LOCATION, - COORDINATE, - FOREIGN_KEY, - PRIMARY_KEY, - STRING, - STRING_LIKE, - NUMBER, - BOOLEAN, -} from "metabase-lib/lib/types/constants"; export function foreignKeyCountsByOriginTable(fks) { if (fks === null || !Array.isArray(fks)) { @@ -32,22 +20,6 @@ export function foreignKeyCountsByOriginTable(fks) { }, {}); } -export const ICON_MAPPING = { - [TEMPORAL]: "calendar", - [LOCATION]: "location", - [COORDINATE]: "location", - [STRING]: "string", - [STRING_LIKE]: "string", - [NUMBER]: "int", - [BOOLEAN]: "io", - [FOREIGN_KEY]: "connections", - [PRIMARY_KEY]: "label", -}; - -export function getIconForField(field) { - return ICON_MAPPING[getFieldType(field)] || "unknown"; -} - export function getSemanticTypeIcon(semanticType, fallback) { const semanticTypeMetadata = field_semantic_types_map[semanticType]; return semanticTypeMetadata?.icon ?? fallback; diff --git a/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx index b31becd7d275ad13fdb789294a3b4795cb2945cc..2bdbd7c89ad69481667459574852f87e4dd490ea 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx +++ b/frontend/src/metabase/query_builder/components/DataSelector/DataSelector.jsx @@ -12,7 +12,6 @@ import PopoverWithTrigger from "metabase/components/PopoverWithTrigger"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import MetabaseSettings from "metabase/lib/settings"; -import { getSchemaName } from "metabase/lib/schema"; import Databases from "metabase/entities/databases"; import Schemas from "metabase/entities/schemas"; @@ -23,6 +22,7 @@ import { PLUGIN_MODERATION } from "metabase/plugins"; import { getMetadata } from "metabase/selectors/metadata"; import { getHasDataAccess } from "metabase/new_query/selectors"; +import { getSchemaName } from "metabase-lib/lib/metadata/utils/schema"; import { isVirtualCardId, getQuestionVirtualTableId, diff --git a/frontend/src/metabase/reference/databases/FieldList.jsx b/frontend/src/metabase/reference/databases/FieldList.jsx index 06a227199e07eb3efc0a5e7e858deb6663242de4..f259bd55ad8a6f4d475d98e0be7431d2332853c1 100644 --- a/frontend/src/metabase/reference/databases/FieldList.jsx +++ b/frontend/src/metabase/reference/databases/FieldList.jsx @@ -17,10 +17,9 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import EditHeader from "metabase/reference/components/EditHeader"; import EditableReferenceHeader from "metabase/reference/components/EditableReferenceHeader"; -import { getIconForField } from "metabase/lib/schema_metadata"; - import * as metadataActions from "metabase/redux/metadata"; import * as actions from "metabase/reference/reference"; +import { getIconForField } from "metabase-lib/lib/metadata/utils/fields"; import { getError, getFieldsByTable, diff --git a/frontend/src/metabase/reference/segments/SegmentFieldList.jsx b/frontend/src/metabase/reference/segments/SegmentFieldList.jsx index 261fb437a530f671d65cd3245219192e9ec95642..cd1954249c1268e77f8cda4edf54a407ec497130 100644 --- a/frontend/src/metabase/reference/segments/SegmentFieldList.jsx +++ b/frontend/src/metabase/reference/segments/SegmentFieldList.jsx @@ -17,10 +17,9 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import EditHeader from "metabase/reference/components/EditHeader"; import EditableReferenceHeader from "metabase/reference/components/EditableReferenceHeader"; -import { getIconForField } from "metabase/lib/schema_metadata"; - import * as metadataActions from "metabase/redux/metadata"; import * as actions from "metabase/reference/reference"; +import { getIconForField } from "metabase-lib/lib/metadata/utils/fields"; import { getError, getFieldsBySegment, diff --git a/frontend/src/metabase/schema.js b/frontend/src/metabase/schema.js index 263edf3ceba966e75f463b9bb35d16f6ec3cb5de..8ba20e5feaa160170baa3782d899f3cd0482859c 100644 --- a/frontend/src/metabase/schema.js +++ b/frontend/src/metabase/schema.js @@ -1,7 +1,8 @@ // normalizr schema for use in actions/reducers import { schema } from "normalizr"; -import { generateSchemaId, entityTypeForObject } from "metabase/lib/schema"; +import { entityTypeForObject } from "metabase/lib/schema"; +import { generateSchemaId } from "metabase-lib/lib/metadata/utils/schema"; import { SAVED_QUESTIONS_VIRTUAL_DB_ID } from "metabase-lib/lib/metadata/utils/saved-questions"; import { getUniqueFieldId } from "metabase-lib/lib/metadata/utils/fields";