Skip to content
Snippets Groups Projects
Unverified Commit 22c19cb6 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Improve sample state fixture (#27399)

* Remove not used `StaticEntitiesProvider`

* Fix typos

* Convert sample database fixture to TypeScript

* Fix `EnhancedState` typing

* Remove global eslint comment

* Fix invalid import

* Fix incorrect type
parent 73b6ee7d
No related branches found
No related tags found
No related merge requests found
Showing
with 123 additions and 58 deletions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck // @ts-nocheck
import { Database as IDatabase, NativePermissions } from "metabase-types/api"; import {
Database as IDatabase,
NativePermissions,
StructuredQuery,
} from "metabase-types/api";
import { generateSchemaId } from "metabase-lib/metadata/utils/schema"; import { generateSchemaId } from "metabase-lib/metadata/utils/schema";
import { createLookupByProperty, memoizeClass } from "metabase-lib/utils"; import { createLookupByProperty, memoizeClass } from "metabase-lib/utils";
import Question from "../Question"; import Question from "../Question";
...@@ -127,7 +131,7 @@ class DatabaseInner extends Base { ...@@ -127,7 +131,7 @@ class DatabaseInner extends Base {
} }
question( question(
query = { query: StructuredQuery = {
"source-table": null, "source-table": null,
}, },
) { ) {
......
...@@ -4,7 +4,11 @@ import _ from "underscore"; ...@@ -4,7 +4,11 @@ import _ from "underscore";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { formatField, stripId } from "metabase/lib/formatting"; import { formatField, stripId } from "metabase/lib/formatting";
import type { FieldFingerprint } from "metabase-types/api/field"; import type {
DatasetColumn,
Field as IField,
FieldFingerprint,
} from "metabase-types/api";
import type { Field as FieldRef } from "metabase-types/types/Query"; import type { Field as FieldRef } from "metabase-types/types/Query";
import { import {
isDate, isDate,
...@@ -71,6 +75,10 @@ class FieldInner extends Base { ...@@ -71,6 +75,10 @@ class FieldInner extends Base {
// added when creating "virtual fields" that are associated with a given query // added when creating "virtual fields" that are associated with a given query
query?: StructuredQuery | NativeQuery; query?: StructuredQuery | NativeQuery;
getPlainObject(): IField {
return this._plainObject;
}
getId() { getId() {
if (Array.isArray(this.id)) { if (Array.isArray(this.id)) {
return this.id[1]; return this.id[1];
...@@ -436,7 +444,7 @@ class FieldInner extends Base { ...@@ -436,7 +444,7 @@ class FieldInner extends Base {
return this.isString(); return this.isString();
} }
column(extra = {}) { column(extra = {}): DatasetColumn {
return this.dimension().column({ return this.dimension().column({
source: "fields", source: "fields",
...extra, ...extra,
......
...@@ -72,7 +72,6 @@ describe("parameters/utils/targets", () => { ...@@ -72,7 +72,6 @@ describe("parameters/utils/targets", () => {
describe("getParameterTargetField", () => { describe("getParameterTargetField", () => {
it("should return null when the target is not a dimension", () => { it("should return null when the target is not a dimension", () => {
// @ts-expect-error - SAMPLE_DATABASE is defined
const question = SAMPLE_DATABASE.nativeQuestion({ const question = SAMPLE_DATABASE.nativeQuestion({
query: "select * from PRODUCTS where CATEGORY = {{foo}}", query: "select * from PRODUCTS where CATEGORY = {{foo}}",
"template-tags": { "template-tags": {
...@@ -96,7 +95,6 @@ describe("parameters/utils/targets", () => { ...@@ -96,7 +95,6 @@ describe("parameters/utils/targets", () => {
"dimension", "dimension",
["template-tag", "foo"], ["template-tag", "foo"],
]; ];
// @ts-expect-error - SAMPLE_DATABASE is defined
const question = SAMPLE_DATABASE.nativeQuestion({ const question = SAMPLE_DATABASE.nativeQuestion({
query: "select * from PRODUCTS where {{foo}}", query: "select * from PRODUCTS where {{foo}}",
"template-tags": { "template-tags": {
...@@ -119,7 +117,6 @@ describe("parameters/utils/targets", () => { ...@@ -119,7 +117,6 @@ describe("parameters/utils/targets", () => {
"dimension", "dimension",
["field", PRODUCTS.CATEGORY.id, null], ["field", PRODUCTS.CATEGORY.id, null],
]; ];
// @ts-expect-error - SAMPLE_DATABASE is defined
const question = SAMPLE_DATABASE.question({ const question = SAMPLE_DATABASE.question({
"source-table": PRODUCTS.id, "source-table": PRODUCTS.id,
}); });
......
...@@ -137,7 +137,9 @@ describe("metabase-lib/queries/utils/structured-query-table", () => { ...@@ -137,7 +137,9 @@ describe("metabase-lib/queries/utils/structured-query-table", () => {
metadata.tables[ORDERS_DATASET_TABLE.id] = ORDERS_DATASET_TABLE; metadata.tables[ORDERS_DATASET_TABLE.id] = ORDERS_DATASET_TABLE;
const table = getStructuredQueryTable(ORDERS_DATASET.query()); const table = getStructuredQueryTable(
ORDERS_DATASET.query() as StructuredQuery,
);
it("should return a nested card table using the given query's question", () => { it("should return a nested card table using the given query's question", () => {
expect(table?.getPlainObject()).toEqual( expect(table?.getPlainObject()).toEqual(
expect.objectContaining({ expect.objectContaining({
......
import { metadata, PRODUCTS } from "__support__/sample_database_fixture"; import { metadata, PRODUCTS } from "__support__/sample_database_fixture";
import StructuredQuery from "metabase-lib/queries/StructuredQuery";
import Field from "metabase-lib/metadata/Field"; import Field from "metabase-lib/metadata/Field";
import Table from "metabase-lib/metadata/Table"; import Table from "metabase-lib/metadata/Table";
import { createVirtualField, createVirtualTable } from "./virtual-table"; import { createVirtualField, createVirtualTable } from "./virtual-table";
describe("metabase-lib/queries/utils/virtual-table", () => { describe("metabase-lib/queries/utils/virtual-table", () => {
const query = PRODUCTS.newQuestion().query(); const query = PRODUCTS.newQuestion().query() as StructuredQuery;
const field = createVirtualField({ const field = createVirtualField({
id: 123, id: 123,
metadata, metadata,
...@@ -28,7 +31,7 @@ describe("metabase-lib/queries/utils/virtual-table", () => { ...@@ -28,7 +31,7 @@ describe("metabase-lib/queries/utils/virtual-table", () => {
}); });
describe("createVirtualTable", () => { describe("createVirtualTable", () => {
const query = PRODUCTS.newQuestion().query(); const query = PRODUCTS.newQuestion().query() as StructuredQuery;
const field1 = createVirtualField({ const field1 = createVirtualField({
id: 1, id: 1,
metadata, metadata,
......
...@@ -50,8 +50,8 @@ export type FieldDimension = { ...@@ -50,8 +50,8 @@ export type FieldDimension = {
name: string; name: string;
}; };
export interface Field { export interface ConcreteField {
id?: FieldId; id: FieldId;
table_id: TableId; table_id: TableId;
name: string; name: string;
...@@ -85,3 +85,7 @@ export interface Field { ...@@ -85,3 +85,7 @@ export interface Field {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export type Field = Omit<ConcreteField, "id"> & {
id?: FieldId;
};
...@@ -2,6 +2,8 @@ import { ...@@ -2,6 +2,8 @@ import {
Collection, Collection,
CollectionId, CollectionId,
Database, Database,
Field,
FieldId,
NativeQuerySnippet, NativeQuerySnippet,
NativeQuerySnippetId, NativeQuerySnippetId,
Table, Table,
...@@ -12,6 +14,7 @@ import { ...@@ -12,6 +14,7 @@ import {
export interface EntitiesState { export interface EntitiesState {
collections?: Record<CollectionId, Collection>; collections?: Record<CollectionId, Collection>;
databases?: Record<number, Database>; databases?: Record<number, Database>;
fields?: Record<FieldId, Field>;
tables?: Record<number | string, Table>; tables?: Record<number | string, Table>;
snippets?: Record<NativeQuerySnippetId, NativeQuerySnippet>; snippets?: Record<NativeQuerySnippetId, NativeQuerySnippet>;
users?: Record<UserId, User>; users?: Record<UserId, User>;
......
...@@ -107,7 +107,9 @@ describe("TableInfo", () => { ...@@ -107,7 +107,9 @@ describe("TableInfo", () => {
}); });
it("should display the given table's description", () => { it("should display the given table's description", () => {
expect(screen.getByText(PRODUCTS.description)).toBeInTheDocument(); expect(
screen.getByText(PRODUCTS.description as string),
).toBeInTheDocument();
}); });
it("should show a count of columns on the table", () => { it("should show a count of columns on the table", () => {
......
...@@ -9,7 +9,7 @@ import Databases from "metabase/entities/databases"; ...@@ -9,7 +9,7 @@ import Databases from "metabase/entities/databases";
import Snippets from "metabase/entities/snippets"; import Snippets from "metabase/entities/snippets";
import { setErrorPage } from "metabase/redux/app"; import { setErrorPage } from "metabase/redux/app";
import { User } from "metabase-types/api"; import { DatabaseId, TableId, User } from "metabase-types/api";
import { createMockUser } from "metabase-types/api/mocks"; import { createMockUser } from "metabase-types/api/mocks";
import { Card, NativeDatasetQuery } from "metabase-types/types/Card"; import { Card, NativeDatasetQuery } from "metabase-types/types/Card";
import { TemplateTag } from "metabase-types/types/Query"; import { TemplateTag } from "metabase-types/types/Query";
...@@ -648,8 +648,8 @@ describe("QB Actions > initializeQB", () => { ...@@ -648,8 +648,8 @@ describe("QB Actions > initializeQB", () => {
describe("blank question", () => { describe("blank question", () => {
type BlankSetupOpts = Omit<BaseSetupOpts, "location" | "params"> & { type BlankSetupOpts = Omit<BaseSetupOpts, "location" | "params"> & {
db?: number; db?: DatabaseId;
table?: number; table?: TableId;
segment?: number; segment?: number;
metric?: number; metric?: number;
}; };
......
...@@ -19,7 +19,6 @@ import Question from "metabase-lib/Question"; ...@@ -19,7 +19,6 @@ import Question from "metabase-lib/Question";
import NativeQuery from "metabase-lib/queries/NativeQuery"; import NativeQuery from "metabase-lib/queries/NativeQuery";
import StructuredQuery from "metabase-lib/queries/StructuredQuery"; import StructuredQuery from "metabase-lib/queries/StructuredQuery";
import Join from "metabase-lib/queries/structured/Join"; import Join from "metabase-lib/queries/structured/Join";
import Field from "metabase-lib/metadata/Field";
import { import {
getAdHocQuestion, getAdHocQuestion,
getSavedStructuredQuestion, getSavedStructuredQuestion,
...@@ -76,7 +75,7 @@ async function setup({ ...@@ -76,7 +75,7 @@ async function setup({
const queryResult = createMockDataset({ const queryResult = createMockDataset({
data: { data: {
cols: ORDERS.fields.map((field: Field) => field.column()), cols: ORDERS.fields.map(field => field.column()),
}, },
}); });
......
...@@ -3,8 +3,8 @@ import { t } from "ttag"; ...@@ -3,8 +3,8 @@ import { t } from "ttag";
import AccordionList from "metabase/core/components/AccordionList"; import AccordionList from "metabase/core/components/AccordionList";
import Icon from "metabase/components/Icon"; import Icon from "metabase/components/Icon";
import type { Field } from "metabase-types/api/field";
import type { Table } from "metabase-types/api/table"; import type { Table } from "metabase-types/api/table";
import type Field from "metabase-lib/metadata/Field";
import DataSelectorLoading from "../DataSelectorLoading"; import DataSelectorLoading from "../DataSelectorLoading";
import { import {
...@@ -31,10 +31,7 @@ type HeaderProps = { ...@@ -31,10 +31,7 @@ type HeaderProps = {
type FieldWithName = { type FieldWithName = {
name: string; name: string;
field: { field: Field;
id: number;
dimension: () => any;
};
}; };
const DataSelectorFieldPicker = ({ const DataSelectorFieldPicker = ({
...@@ -57,7 +54,7 @@ const DataSelectorFieldPicker = ({ ...@@ -57,7 +54,7 @@ const DataSelectorFieldPicker = ({
{ {
name: header, name: header,
items: fields.map(field => ({ items: fields.map(field => ({
name: field.display_name, name: field.displayName(),
field: field, field: field,
})), })),
}, },
......
...@@ -57,13 +57,11 @@ describe("DataSelectorFieldPicker", () => { ...@@ -57,13 +57,11 @@ describe("DataSelectorFieldPicker", () => {
display_name: tableDisplayName, display_name: tableDisplayName,
}; };
const fields = [ORDERS.PRODUCT_ID];
render( render(
<DataSelectorFieldPicker <DataSelectorFieldPicker
{...props} {...props}
selectedTable={selectedTable as Table} selectedTable={selectedTable as Table}
fields={fields} fields={[ORDERS.PRODUCT_ID]}
/>, />,
); );
......
...@@ -35,7 +35,7 @@ async function setup({ ...@@ -35,7 +35,7 @@ async function setup({
const modelCacheInfo = getMockModelCacheInfo({ const modelCacheInfo = getMockModelCacheInfo({
...cacheInfo, ...cacheInfo,
card_id: model.id(), card_id: model.id(),
card_name: model.displayName(), card_name: model.displayName() as string,
}); });
const onRefreshMock = jest const onRefreshMock = jest
......
/* eslint-disable react/prop-types */
import React from "react";
import { Provider } from "react-redux";
import { normalize } from "normalizr"; import { normalize } from "normalizr";
import { chain } from "icepick"; import { chain } from "icepick";
import { getStore } from "metabase/store";
import { getMetadata } from "metabase/selectors/metadata"; import { getMetadata } from "metabase/selectors/metadata";
import { FieldSchema } from "metabase/schema"; import { FieldSchema } from "metabase/schema";
import state from "./sample_database_fixture.json"; import type { Field as IField, FieldId } from "metabase-types/api";
export { default as state } from "./sample_database_fixture.json"; import type { State } from "metabase-types/store";
import type Database from "metabase-lib/metadata/Database";
import type Field from "metabase-lib/metadata/Field";
import type Metadata from "metabase-lib/metadata/Metadata";
import type Table from "metabase-lib/metadata/Table";
import stateFixture from "./sample_database_fixture.json";
export const state = stateFixture as unknown as State;
export default state;
export const SAMPLE_DATABASE_ID = 1; export const SAMPLE_DATABASE_ID = 1;
export const ANOTHER_DATABASE_ID = 2; export const ANOTHER_DATABASE_ID = 2;
...@@ -19,31 +26,45 @@ export const OTHER_MULTI_SCHEMA_DATABASE_ID = 5; ...@@ -19,31 +26,45 @@ export const OTHER_MULTI_SCHEMA_DATABASE_ID = 5;
export const MAIN_METRIC_ID = 1; export const MAIN_METRIC_ID = 1;
function aliasTablesAndFields(metadata) { function aliasTablesAndFields(metadata: Metadata) {
// alias DATABASE.TABLE.FIELD for convienence in tests // alias DATABASE.TABLE.FIELD for convenience in tests
// NOTE: this assume names don't conflict with other properties in Database/Table which I think is safe for Sample Database // NOTE: this assume names don't conflict with other properties in Database/Table which I think is safe for Sample Database
/* eslint-disable @typescript-eslint/ban-ts-comment */
for (const database of Object.values(metadata.databases)) { for (const database of Object.values(metadata.databases)) {
for (const table of database.tables) { for (const table of database.tables) {
if (!(table.name in database)) { if (!(table.name in database)) {
// @ts-ignore
database[table.name] = table; database[table.name] = table;
} }
for (const field of table.fields) { for (const field of table.fields) {
if (!(field.name in table)) { if (!(field.name in table)) {
// @ts-ignore
table[field.name] = field; table[field.name] = field;
} }
} }
} }
} }
/* eslint-enable @typescript-eslint/ban-ts-comment */
} }
function normalizeFields(fields) { function normalizeFields(fields: Record<string, IField>) {
return normalize(fields, [FieldSchema]).entities.fields || {}; return normalize(fields, [FieldSchema]).entities.fields || {};
} }
export function createMetadata(updateState = state => state) { // Icepick doesn't expose it's IcepickWrapper type,
// so this trick pulls it out of the return type of chain()
// `icepickChainWrapper` is needed because typeof chain<State> doesn't work
// See: https://stackoverflow.com/questions/50321419/typescript-returntype-of-generic-function
const icepickChainWrapper = (state: State) => chain(state);
type EnhancedState = ReturnType<typeof icepickChainWrapper>;
export function createMetadata(updateState = (state: EnhancedState) => state) {
// This allows to use icepick helpers inside custom `updateState` functions
// Example: const metadata = createMetadata(state => state.assocIn(...))
const stateModified = updateState(chain(state)).thaw().value(); const stateModified = updateState(chain(state)).thaw().value();
stateModified.entities.fields = normalizeFields( stateModified.entities.fields = normalizeFields(
stateModified.entities.fields, stateModified.entities.fields || {},
); );
const metadata = getMetadata(stateModified); const metadata = getMetadata(stateModified);
...@@ -53,22 +74,55 @@ export function createMetadata(updateState = state => state) { ...@@ -53,22 +74,55 @@ export function createMetadata(updateState = state => state) {
export const metadata = createMetadata(); export const metadata = createMetadata();
export const SAMPLE_DATABASE = metadata.database(SAMPLE_DATABASE_ID); /* eslint-disable @typescript-eslint/no-non-null-assertion */
export const ANOTHER_DATABASE = metadata.database(ANOTHER_DATABASE_ID);
export const MONGO_DATABASE = metadata.database(MONGO_DATABASE_ID); /**
* In the wild, fields might not have a concrete ID
* (e.g. when coming from a native query)
* But for our sample data we can be sure that they're always concrete.
*/
type SimpleField = Omit<Field, "id"> & {
id: FieldId;
};
type AliasedTable = Table & {
[fieldName: string]: SimpleField;
};
/**
* Databases below are extended with table aliases.
* So it's possible to do SAMPLE_DATABASE.ORDERS or SAMPLE_DATABASE.ORDERS.TOTAL
* to retrieve tables and field instances.
*/
type AliasedSampleDatabase = Database & {
ORDERS: AliasedTable;
PRODUCTS: AliasedTable;
PEOPLE: AliasedTable;
REVIEWS: AliasedTable;
};
export const SAMPLE_DATABASE = metadata.database(
SAMPLE_DATABASE_ID,
) as AliasedSampleDatabase;
export const ANOTHER_DATABASE = metadata.database(ANOTHER_DATABASE_ID)!;
export const MONGO_DATABASE = metadata.database(MONGO_DATABASE_ID)!;
export const MULTI_SCHEMA_DATABASE = metadata.database( export const MULTI_SCHEMA_DATABASE = metadata.database(
MULTI_SCHEMA_DATABASE_ID, MULTI_SCHEMA_DATABASE_ID,
); )!;
export const OTHER_MULTI_SCHEMA_DATABASE = metadata.database( export const OTHER_MULTI_SCHEMA_DATABASE = metadata.database(
OTHER_MULTI_SCHEMA_DATABASE_ID, OTHER_MULTI_SCHEMA_DATABASE_ID,
); )!;
/* eslint-enable @typescript-eslint/no-non-null-assertion */
export const ORDERS = SAMPLE_DATABASE.ORDERS; export const ORDERS = SAMPLE_DATABASE.ORDERS;
export const PRODUCTS = SAMPLE_DATABASE.PRODUCTS; export const PRODUCTS = SAMPLE_DATABASE.PRODUCTS;
export const PEOPLE = SAMPLE_DATABASE.PEOPLE; export const PEOPLE = SAMPLE_DATABASE.PEOPLE;
export const REVIEWS = SAMPLE_DATABASE.REVIEWS; export const REVIEWS = SAMPLE_DATABASE.REVIEWS;
export function makeMetadata(metadata) { export function makeMetadata(
metadata: Record<string, Record<string, any>>,
): Metadata {
metadata = { metadata = {
databases: { databases: {
1: { name: "database", tables: [] }, 1: { name: "database", tables: [] },
...@@ -90,7 +144,8 @@ export function makeMetadata(metadata) { ...@@ -90,7 +144,8 @@ export function makeMetadata(metadata) {
questions: {}, questions: {},
...metadata, ...metadata,
}; };
// convienence for filling in missing bits
// convenience for filling in missing bits
for (const objects of Object.values(metadata)) { for (const objects of Object.values(metadata)) {
for (const [id, object] of Object.entries(objects)) { for (const [id, object] of Object.entries(objects)) {
object.id = /^\d+$/.test(id) ? parseInt(id) : id; object.id = /^\d+$/.test(id) ? parseInt(id) : id;
...@@ -99,6 +154,7 @@ export function makeMetadata(metadata) { ...@@ -99,6 +154,7 @@ export function makeMetadata(metadata) {
} }
} }
} }
// linking to default db // linking to default db
for (const table of Object.values(metadata.tables)) { for (const table of Object.values(metadata.tables)) {
if (table.db == null) { if (table.db == null) {
...@@ -107,6 +163,7 @@ export function makeMetadata(metadata) { ...@@ -107,6 +163,7 @@ export function makeMetadata(metadata) {
(db0.tables = db0.tables || []).push(table.id); (db0.tables = db0.tables || []).push(table.id);
} }
} }
// linking to default table // linking to default table
for (const childType of ["fields", "segments", "metrics"]) { for (const childType of ["fields", "segments", "metrics"]) {
for (const child of Object.values(metadata[childType])) { for (const child of Object.values(metadata[childType])) {
...@@ -122,12 +179,3 @@ export function makeMetadata(metadata) { ...@@ -122,12 +179,3 @@ export function makeMetadata(metadata) {
return getMetadata({ entities: metadata }); return getMetadata({ entities: metadata });
} }
const nopEntitiesReducer = (s = state.entities, a) => s;
// simple provider which only supports static metadata defined above, no actions will take effect
export const StaticEntitiesProvider = ({ children }) => (
<Provider store={getStore({ entities: nopEntitiesReducer }, null, state)}>
{children}
</Provider>
);
import React from "react"; import React from "react";
import { render, screen, fireEvent } from "@testing-library/react"; import { render, screen, fireEvent } from "@testing-library/react";
import { ORDERS } from "__support__/sample_database_fixture";
import ChartSettingOrderedColumns from "metabase/visualizations/components/settings/ChartSettingOrderedColumns"; import ChartSettingOrderedColumns from "metabase/visualizations/components/settings/ChartSettingOrderedColumns";
import { ORDERS } from "__support__/sample_database_fixture.js";
function renderChartSettingOrderedColumns(props) { function renderChartSettingOrderedColumns(props) {
render( render(
......
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
"allowJs": true, "allowJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}, },
"include": [ "include": [
"frontend/src/**/*.ts", "frontend/src/**/*.ts",
......
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