diff --git a/frontend/src/metabase-lib/metadata/Metric.ts b/frontend/src/metabase-lib/metadata/Metric.ts index 0212cf0af58c9ee4eb2433feb4a54a34bb97ae7a..77f5bfabdd4b40a1f326088b2d92dd9d27a08a80 100644 --- a/frontend/src/metabase-lib/metadata/Metric.ts +++ b/frontend/src/metabase-lib/metadata/Metric.ts @@ -1,38 +1,36 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck +import { Aggregation, NormalizedMetric } from "metabase-types/api"; import Filter from "metabase-lib/queries/structured/Filter"; -import Base from "./Base"; import type Metadata from "./Metadata"; import type Table from "./Table"; -/** - * @typedef { import("./Metadata").Aggregation } Aggregation - */ -/** - * Wrapper class for a metric. Belongs to a {@link Database} and possibly a {@link Table} - */ +interface Metric extends Omit<NormalizedMetric, "table"> { + table?: Table; + metadata?: Metadata; +} -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default class Metric extends Base { - name: string; - table_id: Table["id"]; - table: Table; - metadata: Metadata; +class Metric { + private readonly _plainObject: NormalizedMetric; + + constructor(metric: NormalizedMetric) { + this._plainObject = metric; + Object.assign(this, metric); + } + + getPlainObject() { + return this._plainObject; + } displayName() { return this.name; } - /** - * @returns {Aggregation} - */ - aggregationClause() { + aggregationClause(): Aggregation { return ["metric", this.id]; } /** Underlying query for this metric */ definitionQuery() { - return this.definition + return this.table && this.definition ? this.table.query().setQuery(this.definition) : null; } @@ -66,26 +64,7 @@ export default class Metric extends Base { isActive() { return !this.archived; } - - /** - * @private - * @param {string} name - * @param {string} description - * @param {Database} database - * @param {Table} table - * @param {number} id - * @param {StructuredQuery} definition - * @param {boolean} archived - */ - - /* istanbul ignore next */ - _constructor(name, description, database, table, id, definition, archived) { - this.name = name; - this.description = description; - this.database = database; - this.table = table; - this.id = id; - this.definition = definition; - this.archived = archived; - } } + +// eslint-disable-next-line import/no-default-export -- deprecated usage +export default Metric; diff --git a/frontend/src/metabase-lib/metadata/Schema.ts b/frontend/src/metabase-lib/metadata/Schema.ts index 80704ceaec65a6c1d5c444d7d853836559bd273a..2a18c70beddb31da6f045d8eec7711885fb45781 100644 --- a/frontend/src/metabase-lib/metadata/Schema.ts +++ b/frontend/src/metabase-lib/metadata/Schema.ts @@ -4,27 +4,22 @@ import type Metadata from "./Metadata"; import type Database from "./Database"; import type Table from "./Table"; -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default class Schema { - private readonly schema: NormalizedSchema; - metadata?: Metadata; +interface Schema extends Omit<NormalizedSchema, "database" | "tables"> { database?: Database; - tables: Table[] = []; - - constructor(schema: NormalizedSchema) { - this.schema = schema; - } + tables?: Table[]; + metadata?: Metadata; +} - get id() { - return this.schema.id; - } +class Schema { + private readonly _plainObject: NormalizedSchema; - get name() { - return this.schema.name; + constructor(schema: NormalizedSchema) { + this._plainObject = schema; + Object.assign(this, schema); } getPlainObject() { - return this.schema; + return this._plainObject; } displayName() { @@ -32,6 +27,9 @@ export default class Schema { } getTables() { - return this.tables; + return this.tables ?? []; } } + +// eslint-disable-next-line import/no-default-export -- deprecated usage +export default Schema; diff --git a/frontend/src/metabase-lib/metadata/Segment.ts b/frontend/src/metabase-lib/metadata/Segment.ts index 8f26365f9753928a79ffcc89948fc20b93ec8d3a..7ba1970b76f96088fcc9f7ceab89af9d21632f2d 100644 --- a/frontend/src/metabase-lib/metadata/Segment.ts +++ b/frontend/src/metabase-lib/metadata/Segment.ts @@ -1,56 +1,36 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import Base from "./Base"; +import { Filter, NormalizedSegment } from "metabase-types/api"; import type Metadata from "./Metadata"; import type Table from "./Table"; -/** - * @typedef { import("./Metadata").FilterClause } FilterClause - */ -/** - * Wrapper class for a segment. Belongs to a {@link Database} and possibly a {@link Table} - */ +interface Segment extends Omit<NormalizedSegment, "table"> { + table?: Table; + metadata?: Metadata; +} -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default class Segment extends Base { - id: number; - name: string; - table_id: Table["id"]; - table: Table; - metadata: Metadata; +class Segment { + private readonly _plainObject: NormalizedSegment; + + constructor(segment: NormalizedSegment) { + this._plainObject = segment; + Object.assign(this, segment); + } + + getPlainObject() { + return this._plainObject; + } displayName() { return this.name; } - /** - * @returns {FilterClause} - */ - filterClause() { + filterClause(): Filter { return ["segment", this.id]; } isActive() { return !this.archived; } - - /** - * @private - * @param {string} name - * @param {string} description - * @param {Database} database - * @param {Table} table - * @param {number} id - * @param {boolean} archived - */ - - /* istanbul ignore next */ - _constructor(name, description, database, table, id, archived) { - this.name = name; - this.description = description; - this.database = database; - this.table = table; - this.id = id; - this.archived = archived; - } } + +// eslint-disable-next-line import/no-default-export -- deprecated usage +export default Segment; diff --git a/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts b/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts index 0dd0163a0fd2f2b7beb8b2e693a011240cc14f24..56332969d54897f4937ffac04ec6b79b8a700af2 100644 --- a/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts +++ b/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts @@ -1,20 +1,12 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import Segment from "./Segment"; -import Base from "./Base"; + describe("Segment", () => { describe("instantiation", () => { it("should create an instance of Segment", () => { expect(new Segment()).toBeInstanceOf(Segment); }); - it("should add `object` props to the instance (because it extends Base)", () => { - expect(new Segment()).toBeInstanceOf(Base); - expect( - new Segment({ - foo: "bar", - }), - ).toHaveProperty("foo", "bar"); - }); }); describe("displayName", () => { it("should return the `name` property found on the instance", () => { diff --git a/frontend/src/metabase-types/api/metric.ts b/frontend/src/metabase-types/api/metric.ts index 6912b8b01f955c4765b5ae15e43f72e49c06e792..8bf9b99a8c245738f34005cdccabf787d2efda7e 100644 --- a/frontend/src/metabase-types/api/metric.ts +++ b/frontend/src/metabase-types/api/metric.ts @@ -1,7 +1,7 @@ import { StructuredQuery } from "./query"; import { TableId } from "./table"; -export type MetricId = number; +export type MetricId = number | string; export interface Metric { id: MetricId; diff --git a/frontend/src/metabase-types/api/mocks/segment.ts b/frontend/src/metabase-types/api/mocks/segment.ts index 4ddf87391e975dd80c547b40d2fb860b3685c45b..656b72c66996eaf0ff912450b474e9194e38839a 100644 --- a/frontend/src/metabase-types/api/mocks/segment.ts +++ b/frontend/src/metabase-types/api/mocks/segment.ts @@ -8,5 +8,6 @@ export const createMockSegment = (opts?: Partial<Segment>): Segment => ({ table_id: 1, archived: false, definition: createMockStructuredQuery(), + definition_description: "", ...opts, }); diff --git a/frontend/src/metabase-types/api/segment.ts b/frontend/src/metabase-types/api/segment.ts index e27252b15e6be5125da4ce910c5dd145e7be2f0d..809f8a0cb393dd22ce4b5a44b45efdd0ebe21274 100644 --- a/frontend/src/metabase-types/api/segment.ts +++ b/frontend/src/metabase-types/api/segment.ts @@ -10,5 +10,6 @@ export interface Segment { table_id: TableId; archived: boolean; definition: StructuredQuery; + definition_description: string; revision_message?: string; } diff --git a/frontend/src/metabase/admin/datamodel/components/MetricItem.jsx b/frontend/src/metabase/admin/datamodel/components/MetricItem.jsx index c228f1300af3e5341236c6e7862847deae46d880..b7141f404e09b28244f59aa2223ed7486dcd1af5 100644 --- a/frontend/src/metabase/admin/datamodel/components/MetricItem.jsx +++ b/frontend/src/metabase/admin/datamodel/components/MetricItem.jsx @@ -17,7 +17,7 @@ export default class MetricItem extends Component { <tr> <td className="px1 py1 text-wrap"> <span className="flex align-center"> - <Icon {...metric.getIcon()} size={12} className="mr1 text-medium" /> + <Icon name="sum" size={12} className="mr1 text-medium" /> <span className="text-dark text-bold">{metric.name}</span> </span> </td> diff --git a/frontend/src/metabase/admin/datamodel/components/SegmentItem.jsx b/frontend/src/metabase/admin/datamodel/components/SegmentItem.jsx index a92ddb35d18709467d304d561cac01953fd0907c..51000fe43bff1c85178b901cac64e8f096f09d64 100644 --- a/frontend/src/metabase/admin/datamodel/components/SegmentItem.jsx +++ b/frontend/src/metabase/admin/datamodel/components/SegmentItem.jsx @@ -17,11 +17,7 @@ export default class SegmentItem extends Component { <tr className="mt1 mb3"> <td className="px1 py1 text-wrap"> <span className="flex align-center"> - <Icon - {...segment.getIcon()} - size={12} - className="mr1 text-medium" - /> + <Icon name="segment" size={12} className="mr1 text-medium" /> <span className="text-dark text-bold">{segment.name}</span> </span> </td> diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricListApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricListApp.jsx index 180a598bcc0eb9b3f4ce422a4904142ea590bf14..e9f170d9fb2806634db07d7cc4a13cb29661e001 100644 --- a/frontend/src/metabase/admin/datamodel/containers/MetricListApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/MetricListApp.jsx @@ -1,5 +1,6 @@ /* eslint-disable react/prop-types */ import React from "react"; +import { connect } from "react-redux"; import { t } from "ttag"; import _ from "underscore"; @@ -12,7 +13,7 @@ import Link from "metabase/core/components/Link"; class MetricListAppInner extends React.Component { render() { - const { metrics, tableSelector } = this.props; + const { metrics, tableSelector, setArchived } = this.props; return ( <div className="px3 pb2"> @@ -34,7 +35,7 @@ class MetricListAppInner extends React.Component { {metrics.map(metric => ( <MetricItem key={metric.id} - onRetire={() => metric.setArchived(true)} + onRetire={() => setArchived(metric, true)} metric={metric} /> ))} @@ -51,8 +52,9 @@ class MetricListAppInner extends React.Component { } const MetricListApp = _.compose( - Metrics.loadList({ wrapped: true }), + Metrics.loadList(), FilteredToUrlTable("metrics"), + connect(null, { setArchived: Metrics.actions.setArchived }), )(MetricListAppInner); export default MetricListApp; diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentListApp.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentListApp.jsx index 70a5521f8152965bcd8a48fdb5911abd18837045..58a1ed522e401716b127eb63b6a9f5937c7700ec 100644 --- a/frontend/src/metabase/admin/datamodel/containers/SegmentListApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/SegmentListApp.jsx @@ -1,9 +1,10 @@ /* eslint-disable react/prop-types */ import React from "react"; +import { connect } from "react-redux"; import { t } from "ttag"; import _ from "underscore"; -import Segment from "metabase/entities/segments"; +import Segments from "metabase/entities/segments"; import SegmentItem from "metabase/admin/datamodel/components/SegmentItem"; import FilteredToUrlTable from "metabase/admin/datamodel/hoc/FilteredToUrlTable"; @@ -12,7 +13,7 @@ import Link from "metabase/core/components/Link"; class SegmentListAppInner extends React.Component { render() { - const { segments, tableSelector } = this.props; + const { segments, tableSelector, setArchived } = this.props; return ( <div className="px3 pb2"> @@ -34,7 +35,7 @@ class SegmentListAppInner extends React.Component { {segments.map(segment => ( <SegmentItem key={segment.id} - onRetire={() => segment.setArchived(true)} + onRetire={() => setArchived(segment, true)} segment={segment} /> ))} @@ -51,8 +52,9 @@ class SegmentListAppInner extends React.Component { } const SegmentListApp = _.compose( - Segment.loadList({ wrapped: true }), + Segments.loadList(), FilteredToUrlTable("segments"), + connect(null, { setArchived: Segments.actions.setArchived }), )(SegmentListAppInner); export default SegmentListApp; diff --git a/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts b/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts index d53f002d3ae7eec954c38000cd03c383ed73e4da..e246ea03d8c07c1fd4cfdf21143e624c1f254a9c 100644 --- a/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts +++ b/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts @@ -353,7 +353,7 @@ export function updateTablesPermission( downgradeNative?: boolean, ) { const schema = database.schema(schemaName); - const tableIds = schema?.tables.map((t: Table) => t.id); + const tableIds = schema?.getTables().map((t: Table) => t.id); permissions = updateSchemasPermission( permissions, diff --git a/frontend/src/metabase/common/hooks/index.ts b/frontend/src/metabase/common/hooks/index.ts index 975276fc031b56e5556eb763bc077607d923bca2..4875b686164a9e1471096fe653f434fc1d582dc5 100644 --- a/frontend/src/metabase/common/hooks/index.ts +++ b/frontend/src/metabase/common/hooks/index.ts @@ -1,6 +1,10 @@ export * from "./use-database-id-field-list-query"; export * from "./use-database-list-query"; export * from "./use-database-query"; +export * from "./use-metric-list-query"; +export * from "./use-metric-query"; export * from "./use-schema-list-query"; +export * from "./use-segment-list-query"; +export * from "./use-segment-query"; export * from "./use-table-list-query"; export * from "./use-table-query"; diff --git a/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.ts b/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.ts index 43db832bd06867072f5e429ba3566ba1fab8c433..0f6c35a27f77981637b83bfea8dd7dff79a5bb0f 100644 --- a/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.ts +++ b/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.ts @@ -11,10 +11,11 @@ export const useDatabaseIdFieldListQuery = ( props: UseEntityQueryProps<DatabaseId, DatabaseIdFieldListQuery>, ): UseEntityQueryResult<Field[]> => { return useEntityQuery(props, { - fetch: Databases.actions.fetchIdfields, - getObject: Databases.selectors.getIdfields, + fetch: Databases.actions.fetchIdFields, + getObject: state => + Databases.selectors.getIdFields(state, { databaseId: props.id }), getLoading: Databases.selectors.getLoading, getError: Databases.selectors.getError, - requestType: "idfields", + requestType: "idFields", }); }; diff --git a/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fba4e56191e0d8e7bfb3720180d704ba9f540346 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-database-id-field-list-query/use-database-id-field-list-query.unit.spec.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createSampleDatabase } from "metabase-types/api/mocks/presets"; +import { setupDatabasesEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useDatabaseIdFieldListQuery } from "./use-database-id-field-list-query"; + +const TEST_DB = createSampleDatabase(); + +const TestComponent = () => { + const { + data = [], + isLoading, + error, + } = useDatabaseIdFieldListQuery({ id: TEST_DB.id }); + + if (isLoading || error) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return ( + <div> + {data.map(field => ( + <div key={field.getId()}> + {field.displayName({ includeTable: true })} + </div> + ))} + </div> + ); +}; + +const setup = () => { + setupDatabasesEndpoints([TEST_DB]); + renderWithProviders(<TestComponent />); +}; + +describe("useDatabaseIdFieldListQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText("Orders → ID")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/common/hooks/use-entity-list-query/use-entity-list-query.ts b/frontend/src/metabase/common/hooks/use-entity-list-query/use-entity-list-query.ts index d0b41e9c0a5af982239a0f2f3fd48f0ebf7f4135..0db9c12bcc61cbe13cd9788e5cc3be7b5b542b98 100644 --- a/frontend/src/metabase/common/hooks/use-entity-list-query/use-entity-list-query.ts +++ b/frontend/src/metabase/common/hooks/use-entity-list-query/use-entity-list-query.ts @@ -7,11 +7,11 @@ export interface EntityFetchOptions { reload?: boolean; } -export interface EntityQueryOptions<TQuery> { +export interface EntityQueryOptions<TQuery = never> { entityQuery?: TQuery; } -export interface UseEntityListOwnProps<TItem, TQuery> { +export interface UseEntityListOwnProps<TItem, TQuery = never> { fetchList: (query?: TQuery, options?: EntityFetchOptions) => Action; getList: ( state: State, @@ -21,7 +21,7 @@ export interface UseEntityListOwnProps<TItem, TQuery> { getError: (state: State, options: EntityQueryOptions<TQuery>) => unknown; } -export interface UseEntityListQueryProps<TQuery> { +export interface UseEntityListQueryProps<TQuery = never> { query?: TQuery; reload?: boolean; enabled?: boolean; @@ -33,7 +33,7 @@ export interface UseEntityListQueryResult<TItem> { error: unknown; } -export const useEntityListQuery = <TItem, TQuery>( +export const useEntityListQuery = <TItem, TQuery = never>( { query: entityQuery, reload = false, diff --git a/frontend/src/metabase/common/hooks/use-entity-query/use-entity-query.ts b/frontend/src/metabase/common/hooks/use-entity-query/use-entity-query.ts index 11d8128a734d589ea3a09c6c02518c353f0de47a..d4e3f1914bb51b2b7c2f6d0c19e444cac52a6714 100644 --- a/frontend/src/metabase/common/hooks/use-entity-query/use-entity-query.ts +++ b/frontend/src/metabase/common/hooks/use-entity-query/use-entity-query.ts @@ -27,7 +27,7 @@ export interface UseEntityOwnProps<TId, TItem> { requestType?: string; } -export interface UseEntityQueryProps<TId, TQuery> { +export interface UseEntityQueryProps<TId, TQuery = never> { id?: TId; query?: TQuery; reload?: boolean; @@ -40,7 +40,7 @@ export interface UseEntityQueryResult<TItem> { error: unknown; } -export const useEntityQuery = <TId, TItem, TQuery>( +export const useEntityQuery = <TId, TItem, TQuery = never>( { id: entityId, query: entityQuery, diff --git a/frontend/src/metabase/common/hooks/use-metric-list-query/index.ts b/frontend/src/metabase/common/hooks/use-metric-list-query/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..191fc79e4fb73949efc3d118a6f42427f3817a98 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-metric-list-query/index.ts @@ -0,0 +1 @@ +export * from "./use-metric-list-query"; diff --git a/frontend/src/metabase/common/hooks/use-metric-list-query/use-metric-list-query.ts b/frontend/src/metabase/common/hooks/use-metric-list-query/use-metric-list-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..13db1b8ce3a37817d231587962fe633ab56f72b1 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-metric-list-query/use-metric-list-query.ts @@ -0,0 +1,18 @@ +import Metrics from "metabase/entities/metrics"; +import { + useEntityListQuery, + UseEntityListQueryProps, + UseEntityListQueryResult, +} from "metabase/common/hooks/use-entity-list-query"; +import Metric from "metabase-lib/metadata/Metric"; + +export const useMetricListQuery = ( + props: UseEntityListQueryProps = {}, +): UseEntityListQueryResult<Metric> => { + return useEntityListQuery(props, { + fetchList: Metrics.actions.fetchList, + getList: Metrics.selectors.getList, + getLoading: Metrics.selectors.getLoading, + getError: Metrics.selectors.getError, + }); +}; diff --git a/frontend/src/metabase/common/hooks/use-metric-list-query/use-metric-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-metric-list-query/use-metric-list-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e159a2d2371f6240c633c6391f86ce160f4177d6 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-metric-list-query/use-metric-list-query.unit.spec.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createMockMetric } from "metabase-types/api/mocks"; +import { setupMetricsEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useMetricListQuery } from "./use-metric-list-query"; + +const TEST_METRIC = createMockMetric(); + +const TestComponent = () => { + const { data = [], isLoading, error } = useMetricListQuery(); + + if (isLoading || error) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return ( + <div> + {data.map(metric => ( + <div key={metric.id}>{metric.name}</div> + ))} + </div> + ); +}; + +const setup = () => { + setupMetricsEndpoints([TEST_METRIC]); + renderWithProviders(<TestComponent />); +}; + +describe("useMetricListQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText(TEST_METRIC.name)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/common/hooks/use-metric-query/index.ts b/frontend/src/metabase/common/hooks/use-metric-query/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c139a22d25faab57ec0fb35972414f8802c3e4a --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-metric-query/index.ts @@ -0,0 +1 @@ +export * from "./use-metric-query"; diff --git a/frontend/src/metabase/common/hooks/use-metric-query/use-metric-query.ts b/frontend/src/metabase/common/hooks/use-metric-query/use-metric-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9ebb4ba95c10ce543193ecc6b4a3e7dc35029e7 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-metric-query/use-metric-query.ts @@ -0,0 +1,19 @@ +import Metrics from "metabase/entities/metrics"; +import { + useEntityQuery, + UseEntityQueryProps, + UseEntityQueryResult, +} from "metabase/common/hooks/use-entity-query"; +import { MetricId } from "metabase-types/api"; +import Metric from "metabase-lib/metadata/Metric"; + +export const useMetricQuery = ( + props: UseEntityQueryProps<MetricId>, +): UseEntityQueryResult<Metric> => { + return useEntityQuery(props, { + fetch: Metrics.actions.fetch, + getObject: Metrics.selectors.getObject, + getLoading: Metrics.selectors.getLoading, + getError: Metrics.selectors.getError, + }); +}; diff --git a/frontend/src/metabase/common/hooks/use-metric-query/use-metric-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-metric-query/use-metric-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..59a08cca4ec1828d52ad3406a7318fb0706770e2 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-metric-query/use-metric-query.unit.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createMockMetric } from "metabase-types/api/mocks"; +import { setupMetricsEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useMetricQuery } from "./use-metric-query"; + +const TEST_METRIC = createMockMetric(); + +const TestComponent = () => { + const { data, isLoading, error } = useMetricQuery({ + id: TEST_METRIC.id, + }); + + if (isLoading || error || !data) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return <div>{data.name}</div>; +}; + +const setup = () => { + setupMetricsEndpoints([TEST_METRIC]); + renderWithProviders(<TestComponent />); +}; + +describe("useMetricQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText(TEST_METRIC.name)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/common/hooks/use-segment-list-query/index.ts b/frontend/src/metabase/common/hooks/use-segment-list-query/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7d5bb5039da5a871d7fa59100614913ac7b170d --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-segment-list-query/index.ts @@ -0,0 +1 @@ +export * from "./use-segment-list-query"; diff --git a/frontend/src/metabase/common/hooks/use-segment-list-query/use-segment-list-query.ts b/frontend/src/metabase/common/hooks/use-segment-list-query/use-segment-list-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..75cec17f77d993102167c98f58e4380607ea01c3 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-segment-list-query/use-segment-list-query.ts @@ -0,0 +1,18 @@ +import Segments from "metabase/entities/segments"; +import { + useEntityListQuery, + UseEntityListQueryProps, + UseEntityListQueryResult, +} from "metabase/common/hooks/use-entity-list-query"; +import Segment from "metabase-lib/metadata/Segment"; + +export const useSegmentListQuery = ( + props: UseEntityListQueryProps = {}, +): UseEntityListQueryResult<Segment> => { + return useEntityListQuery(props, { + fetchList: Segments.actions.fetchList, + getList: Segments.selectors.getList, + getLoading: Segments.selectors.getLoading, + getError: Segments.selectors.getError, + }); +}; diff --git a/frontend/src/metabase/common/hooks/use-segment-list-query/use-segment-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-segment-list-query/use-segment-list-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..239b8d992707d68747acf2ec2f24b8b23e18014f --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-segment-list-query/use-segment-list-query.unit.spec.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createMockSegment } from "metabase-types/api/mocks"; +import { setupSegmentsEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useSegmentListQuery } from "./use-segment-list-query"; + +const TEST_SEGMENT = createMockSegment(); + +const TestComponent = () => { + const { data = [], isLoading, error } = useSegmentListQuery(); + + if (isLoading || error) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return ( + <div> + {data.map(segment => ( + <div key={segment.id}>{segment.name}</div> + ))} + </div> + ); +}; + +const setup = () => { + setupSegmentsEndpoints([TEST_SEGMENT]); + renderWithProviders(<TestComponent />); +}; + +describe("useSegmentListQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText(TEST_SEGMENT.name)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/common/hooks/use-segment-query/index.ts b/frontend/src/metabase/common/hooks/use-segment-query/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7617b6e8c81088f1a70dc98c45ef7fa6e9e05e9d --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-segment-query/index.ts @@ -0,0 +1 @@ +export * from "./use-segment-query"; diff --git a/frontend/src/metabase/common/hooks/use-segment-query/use-segment-query.ts b/frontend/src/metabase/common/hooks/use-segment-query/use-segment-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7d4b3e3339199b8c72c29367cdcaa60f4ee2a86 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-segment-query/use-segment-query.ts @@ -0,0 +1,19 @@ +import Segments from "metabase/entities/segments"; +import { + useEntityQuery, + UseEntityQueryProps, + UseEntityQueryResult, +} from "metabase/common/hooks/use-entity-query"; +import { SegmentId } from "metabase-types/api"; +import Segment from "metabase-lib/metadata/Segment"; + +export const useSegmentQuery = ( + props: UseEntityQueryProps<SegmentId>, +): UseEntityQueryResult<Segment> => { + return useEntityQuery(props, { + fetch: Segments.actions.fetch, + getObject: Segments.selectors.getObject, + getLoading: Segments.selectors.getLoading, + getError: Segments.selectors.getError, + }); +}; diff --git a/frontend/src/metabase/common/hooks/use-segment-query/use-segment-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/use-segment-query/use-segment-query.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..01190b36d53e89abad8ad10eac04e31b6a54b2e0 --- /dev/null +++ b/frontend/src/metabase/common/hooks/use-segment-query/use-segment-query.unit.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; +import { createMockSegment } from "metabase-types/api/mocks"; +import { setupSegmentsEndpoints } from "__support__/server-mocks"; +import { + renderWithProviders, + screen, + waitForElementToBeRemoved, +} from "__support__/ui"; +import { useSegmentQuery } from "./use-segment-query"; + +const TEST_SEGMENT = createMockSegment(); + +const TestComponent = () => { + const { data, isLoading, error } = useSegmentQuery({ + id: TEST_SEGMENT.id, + }); + + if (isLoading || error || !data) { + return <LoadingAndErrorWrapper loading={isLoading} error={error} />; + } + + return <div>{data.name}</div>; +}; + +const setup = () => { + setupSegmentsEndpoints([TEST_SEGMENT]); + renderWithProviders(<TestComponent />); +}; + +describe("useSegmentQuery", () => { + it("should be initially loading", () => { + setup(); + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("should show data from the response", async () => { + setup(); + await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); + expect(screen.getByText(TEST_SEGMENT.name)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/dataref/SchemaPane.tsx b/frontend/src/metabase/query_builder/components/dataref/SchemaPane.tsx index 6ea9533ca0287136fb88abc4a645c57cd50d47ca..802869b1415b8b04968d07d09e14ad1799a5f7f8 100644 --- a/frontend/src/metabase/query_builder/components/dataref/SchemaPane.tsx +++ b/frontend/src/metabase/query_builder/components/dataref/SchemaPane.tsx @@ -30,8 +30,8 @@ const SchemaPane = ({ schema, }: SchemaPaneProps) => { const tables = useMemo( - () => schema.tables.sort((a, b) => a.name.localeCompare(b.name)), - [schema.tables], + () => schema.getTables().sort((a, b) => a.name.localeCompare(b.name)), + [schema], ); return ( <SidebarContent diff --git a/frontend/test/__support__/server-mocks/database.ts b/frontend/test/__support__/server-mocks/database.ts index 6f3439320f35add5b006cf48ff319fc959e1bc41..169dde73e56d25421a6102bacfb8690dffac6083 100644 --- a/frontend/test/__support__/server-mocks/database.ts +++ b/frontend/test/__support__/server-mocks/database.ts @@ -45,9 +45,11 @@ export const setupSchemaEndpoints = (db: Database) => { }; export function setupDatabaseIdFieldsEndpoints({ id, tables = [] }: Database) { - const fields = tables - .flatMap(table => table.fields ?? []) - .filter(field => isTypeFK(field.semantic_type)); + const fields = tables.flatMap(table => + (table.fields ?? []) + .filter(field => isTypeFK(field.semantic_type)) + .map(field => ({ ...field, table })), + ); fetchMock.get(`path:/api/database/${id}/idfields`, fields); } diff --git a/frontend/test/__support__/server-mocks/index.ts b/frontend/test/__support__/server-mocks/index.ts index 051d40a738ee28614c72ca24b412892a1249a7a6..79c0fdc2ae95841ead71fc1c05cbadef0bfce96c 100644 --- a/frontend/test/__support__/server-mocks/index.ts +++ b/frontend/test/__support__/server-mocks/index.ts @@ -9,7 +9,10 @@ export * from "./dashboard"; export * from "./database"; export * from "./dataset"; export * from "./field"; +export * from "./metabot"; +export * from "./metric"; export * from "./search"; +export * from "./segment"; export * from "./session"; export * from "./settings"; export * from "./setup"; diff --git a/frontend/test/__support__/server-mocks/metric.ts b/frontend/test/__support__/server-mocks/metric.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c6ed2422b4ea97831a2183fdf4f83e631669228 --- /dev/null +++ b/frontend/test/__support__/server-mocks/metric.ts @@ -0,0 +1,11 @@ +import fetchMock from "fetch-mock"; +import { Metric } from "metabase-types/api"; + +export function setupMetricEndpoint(metric: Metric) { + fetchMock.get(`path:/api/metric/${metric.id}`, metric); +} + +export function setupMetricsEndpoints(metrics: Metric[]) { + fetchMock.get(`path:/api/metric`, metrics); + metrics.forEach(metric => setupMetricEndpoint(metric)); +} diff --git a/frontend/test/__support__/server-mocks/segment.ts b/frontend/test/__support__/server-mocks/segment.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e47264844aef545fb684ef3044593fb68cbd0a1 --- /dev/null +++ b/frontend/test/__support__/server-mocks/segment.ts @@ -0,0 +1,11 @@ +import fetchMock from "fetch-mock"; +import { Segment } from "metabase-types/api"; + +export function setupSegmentEndpoint(segment: Segment) { + fetchMock.get(`path:/api/segment/${segment.id}`, segment); +} + +export function setupSegmentsEndpoints(segments: Segment[]) { + fetchMock.get(`path:/api/segment`, segments); + segments.forEach(segment => setupSegmentEndpoint(segment)); +}