diff --git a/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx b/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx index c7cb2e6f6265fae8b1fc08c1819c3b99c2f790c5..a028ab9955bd5f342409e7ab86fc007bd7402ee1 100644 --- a/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx +++ b/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx @@ -11,7 +11,7 @@ import FormMessage from "metabase/components/form/FormMessage"; import AdminS from "metabase/css/admin.module.css"; import ButtonsS from "metabase/css/components/buttons.module.css"; import CS from "metabase/css/core/index.css"; -import DatabaseSyncModal from "metabase/databases/containers/DatabaseSyncModal"; +import { DatabaseSyncModal } from "metabase/databases/components/DatabaseSyncModal"; import { isSyncCompleted } from "metabase/lib/syncing"; import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins"; diff --git a/frontend/src/metabase/api/api.ts b/frontend/src/metabase/api/api.ts index f16782be4f309c456181c07f711810d4f53fab62..a888f8004e358216c7e146e258d2253070ba735f 100644 --- a/frontend/src/metabase/api/api.ts +++ b/frontend/src/metabase/api/api.ts @@ -1,4 +1,5 @@ -import { createApi } from "@reduxjs/toolkit/query/react"; +import { createApi, skipToken } from "@reduxjs/toolkit/query/react"; +export { skipToken }; import { apiQuery } from "./query"; import { TAG_TYPES } from "./tags"; diff --git a/frontend/src/metabase/api/database-candidates.ts b/frontend/src/metabase/api/database-candidates.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7d93392b39306e143dbf7d1e2900ace75221533 --- /dev/null +++ b/frontend/src/metabase/api/database-candidates.ts @@ -0,0 +1,13 @@ +import { Api } from "metabase/api"; +import type { DatabaseCandidate, DatabaseId } from "metabase-types/api"; + +// a "database candidate" is a set of sample x-rays we suggest for new users +export const databaseCandidatesApi = Api.injectEndpoints({ + endpoints: builder => ({ + listDatabaseCandidates: builder.query<DatabaseCandidate[], DatabaseId>({ + query: id => `/api/automagic-dashboards/database/${id}/candidates`, + }), + }), +}); + +export const { useListDatabaseCandidatesQuery } = databaseCandidatesApi; diff --git a/frontend/src/metabase/api/index.ts b/frontend/src/metabase/api/index.ts index 868d5d6335b5784f06b5721a0fdc9f5315fa5ec8..50e0ae088f9992884155ed3d370259e70bd33f89 100644 --- a/frontend/src/metabase/api/index.ts +++ b/frontend/src/metabase/api/index.ts @@ -2,6 +2,7 @@ export * from "./api"; export * from "./api-key"; export * from "./card"; export * from "./database"; +export * from "./database-candidates"; export * from "./dataset"; export * from "./field"; export * from "./login-history"; diff --git a/frontend/src/metabase/common/hooks/entity-framework/index.ts b/frontend/src/metabase/common/hooks/entity-framework/index.ts index 2e1837c7037dbc536150d77f487c62ce724b8179..cf956d375d2cd6a10b3ef94e8ef21bd078f94afc 100644 --- a/frontend/src/metabase/common/hooks/entity-framework/index.ts +++ b/frontend/src/metabase/common/hooks/entity-framework/index.ts @@ -9,7 +9,6 @@ export * from "./use-bookmark-list-query"; export * from "./use-collection-list-query"; export * from "./use-collection-query"; export * from "./use-dashboard-query"; -export * from "./use-database-candidate-list-query"; export * from "./use-database-list-query"; export * from "./use-database-query"; export * from "./use-group-list-query"; diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/index.ts b/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/index.ts deleted file mode 100644 index 70cff8c35b062af9b8cc92513d54e08d4d3d879a..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./use-database-candidate-list-query"; diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/use-database-candidate-list-query.ts b/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/use-database-candidate-list-query.ts deleted file mode 100644 index 15b4157797e58e8c19aa01e90543b5ec6dc9450d..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/use-database-candidate-list-query.ts +++ /dev/null @@ -1,27 +0,0 @@ -import DatabaseCandidates from "metabase/entities/database-candidates"; -import type { - DatabaseCandidateListQuery, - DatabaseCandidate, -} from "metabase-types/api"; - -import type { - UseEntityListQueryProps, - UseEntityListQueryResult, -} from "../use-entity-list-query"; -import { useEntityListQuery } from "../use-entity-list-query"; - -/** - * @deprecated use "metabase/api" instead - */ -export const useDatabaseCandidateListQuery = ( - props: UseEntityListQueryProps<DatabaseCandidateListQuery> = {}, -): UseEntityListQueryResult<DatabaseCandidate> => { - return useEntityListQuery(props, { - fetchList: DatabaseCandidates.actions.fetchList, - getList: DatabaseCandidates.selectors.getList, - getLoading: DatabaseCandidates.selectors.getLoading, - getLoaded: DatabaseCandidates.selectors.getLoaded, - getError: DatabaseCandidates.selectors.getError, - getListMetadata: DatabaseCandidates.selectors.getListMetadata, - }); -}; diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/use-database-candidate-list-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/use-database-candidate-list-query.unit.spec.tsx deleted file mode 100644 index 2c04233dcdedd28387fe2fc910c32b8ad47cdd6b..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/common/hooks/entity-framework/use-database-candidate-list-query/use-database-candidate-list-query.unit.spec.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { setupDatabaseCandidatesEndpoint } from "__support__/server-mocks"; -import { - renderWithProviders, - screen, - waitForLoaderToBeRemoved, -} from "__support__/ui"; -import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; -import { createMockDatabaseCandidate } from "metabase-types/api/mocks"; - -import { useDatabaseCandidateListQuery } from "./use-database-candidate-list-query"; - -const TEST_DB_ID = 1; -const TEST_DB_CANDIDATE = createMockDatabaseCandidate(); - -const TestComponent = () => { - const { - data = [], - isLoading, - error, - } = useDatabaseCandidateListQuery({ - query: { id: TEST_DB_ID }, - }); - - if (isLoading || error) { - return <LoadingAndErrorWrapper loading={isLoading} error={error} />; - } - - return ( - <div> - {data.map((item, index) => ( - <div key={index}>{item.schema}</div> - ))} - </div> - ); -}; - -const setup = () => { - setupDatabaseCandidatesEndpoint(TEST_DB_ID, [TEST_DB_CANDIDATE]); - renderWithProviders(<TestComponent />); -}; - -describe("useDatabaseCandidateListQuery", () => { - it("should be initially loading", () => { - setup(); - expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); - }); - - it("should show data from the response", async () => { - setup(); - await waitForLoaderToBeRemoved(); - expect(screen.getByText(TEST_DB_CANDIDATE.schema)).toBeInTheDocument(); - }); -}); diff --git a/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.tsx b/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.tsx index 7eb826ab8a21ef9435a21571084410fbca33dc5a..76dbca8f2c8de827fe173f72ead56e95e8421eed 100644 --- a/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.tsx +++ b/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.tsx @@ -1,9 +1,13 @@ import cx from "classnames"; import { jt, t } from "ttag"; +import { useListDatabaseCandidatesQuery, skipToken } from "metabase/api"; +import { useDatabaseListQuery } from "metabase/common/hooks"; +import { useSetting } from "metabase/common/hooks/use-setting"; import Button from "metabase/core/components/Button"; import Link from "metabase/core/components/Link"; import ButtonsS from "metabase/css/components/buttons.module.css"; +import type { DatabaseCandidate, TableCandidate } from "metabase-types/api"; import { ModalMessage, @@ -14,12 +18,38 @@ import { ModalCloseIcon, } from "./DatabaseSyncModal.styled"; +const getSampleUrl = (candidates: DatabaseCandidate[]) => { + const tables = candidates.flatMap(d => d.tables); + const table = + tables.find((t: TableCandidate) => t.title.includes("Orders")) ?? tables[0]; + return table?.url; +}; + +const useSampleDatabaseLink = () => { + const { data: databases } = useDatabaseListQuery(); + const xraysEnabled = useSetting("enable-xrays"); + const sampleDatabase = databases?.find(d => d.is_sample); + + const { data: databaseCandidates } = useListDatabaseCandidatesQuery( + sampleDatabase?.id && xraysEnabled ? sampleDatabase.id : skipToken, + ); + + const sampleUrl = databaseCandidates + ? getSampleUrl(databaseCandidates) + : undefined; + + return sampleUrl; +}; + export interface DatabaseSyncModalProps { sampleUrl?: string; onClose?: () => void; } -const DatabaseSyncModal = ({ sampleUrl, onClose }: DatabaseSyncModalProps) => { +export const DatabaseSyncModalView = ({ + sampleUrl, + onClose, +}: DatabaseSyncModalProps) => { return ( <ModalRoot> <ModalBody> @@ -56,5 +86,8 @@ const DatabaseSyncModal = ({ sampleUrl, onClose }: DatabaseSyncModalProps) => { ); }; -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default DatabaseSyncModal; +export const DatabaseSyncModal = ({ onClose }: { onClose: () => void }) => { + const sampleUrl = useSampleDatabaseLink(); + + return <DatabaseSyncModalView sampleUrl={sampleUrl} onClose={onClose} />; +}; diff --git a/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.unit.spec.tsx b/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.unit.spec.tsx index 30a77cac7627b79fb0c89d3603bbaeb4f0dcc97e..216c0194735dae3befde9b711fb2f1d1130abff7 100644 --- a/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.unit.spec.tsx +++ b/frontend/src/metabase/databases/components/DatabaseSyncModal/DatabaseSyncModal.unit.spec.tsx @@ -1,11 +1,11 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import DatabaseSyncModal from "./DatabaseSyncModal"; +import { DatabaseSyncModalView } from "./DatabaseSyncModal"; describe("DatabaseSyncModal", () => { it("should render with a table from the sample database", () => { - render(<DatabaseSyncModal sampleUrl={"/auto/table/1"} />); + render(<DatabaseSyncModalView sampleUrl={"/auto/table/1"} />); expect(screen.getByText("Explore sample data")).toBeInTheDocument(); }); @@ -13,7 +13,7 @@ describe("DatabaseSyncModal", () => { it("should render with no sample database", async () => { const onClose = jest.fn(); - render(<DatabaseSyncModal onClose={onClose} />); + render(<DatabaseSyncModalView onClose={onClose} />); await userEvent.click(screen.getByText("Got it")); expect(onClose).toHaveBeenCalled(); diff --git a/frontend/src/metabase/databases/components/DatabaseSyncModal/index.ts b/frontend/src/metabase/databases/components/DatabaseSyncModal/index.ts index 937585c38cf2dc349557346cf97bc90feecc6f1c..c17a9ef0d871c9221c410157e3281da70bb64205 100644 --- a/frontend/src/metabase/databases/components/DatabaseSyncModal/index.ts +++ b/frontend/src/metabase/databases/components/DatabaseSyncModal/index.ts @@ -1,2 +1 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./DatabaseSyncModal"; +export * from "./DatabaseSyncModal"; diff --git a/frontend/src/metabase/databases/containers/DatabaseSyncModal/DatabaseSyncModal.tsx b/frontend/src/metabase/databases/containers/DatabaseSyncModal/DatabaseSyncModal.tsx deleted file mode 100644 index b6ee6fb324648e9984db8012ac658c2312aa7ac1..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/databases/containers/DatabaseSyncModal/DatabaseSyncModal.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { createSelector } from "@reduxjs/toolkit"; -import { connect } from "react-redux"; -import _ from "underscore"; - -import DatabaseCandidates from "metabase/entities/database-candidates"; -import Databases from "metabase/entities/databases"; -import { getSetting } from "metabase/selectors/settings"; -import type Database from "metabase-lib/v1/metadata/Database"; -import type { DatabaseCandidate } from "metabase-types/api"; -import type { State } from "metabase-types/store"; - -import DatabaseSyncModal from "../../components/DatabaseSyncModal"; - -interface DatabaseProps { - databases: Database[]; -} - -interface CandidatesProps { - databaseCandidates: DatabaseCandidate[]; -} - -const getSampleQuery = createSelector( - (state: State, props: DatabaseProps) => props.databases, - (state: State) => getSetting(state, "enable-xrays"), - (databases, enableXrays) => { - const sampleDatabase = databases.find(d => d.is_sample); - - if (sampleDatabase && enableXrays) { - return { id: sampleDatabase.id }; - } - }, -); - -const getSampleUrl = createSelector( - (state: unknown, props: CandidatesProps) => props.databaseCandidates, - candidates => { - const tables = candidates.flatMap(d => d.tables); - const table = tables.find(t => t.title.includes("Orders")) ?? tables[0]; - return table?.url; - }, -); - -const mapStateToProps = (state: unknown, props: CandidatesProps) => ({ - sampleUrl: getSampleUrl(state, props), -}); - -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default _.compose( - Databases.loadList(), - DatabaseCandidates.loadList({ query: getSampleQuery }), - connect(mapStateToProps), -)(DatabaseSyncModal); diff --git a/frontend/src/metabase/databases/containers/DatabaseSyncModal/index.ts b/frontend/src/metabase/databases/containers/DatabaseSyncModal/index.ts deleted file mode 100644 index 937585c38cf2dc349557346cf97bc90feecc6f1c..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/databases/containers/DatabaseSyncModal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/no-default-export -- deprecated usage -export { default } from "./DatabaseSyncModal"; diff --git a/frontend/src/metabase/entities/database-candidates.js b/frontend/src/metabase/entities/database-candidates.js deleted file mode 100644 index 91263ef756bd1142e21a7537c7e2cb8fb153f82c..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/entities/database-candidates.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createEntity } from "metabase/lib/entities"; -import { AutoApi } from "metabase/services"; - -/** - * @deprecated use "metabase/api" instead - */ -const DatabaseCandidates = createEntity({ - name: "databaseCandidates", - api: { - list: async (query = {}) => { - return query.id ? AutoApi.db_candidates(query) : []; - }, - }, -}); - -export default DatabaseCandidates; diff --git a/frontend/src/metabase/entities/index.js b/frontend/src/metabase/entities/index.js index 30e02cb313961263e4d4c1dcb311b66d013ff2ca..0d6e1dd3fef6f615ed052f8e8746fc0304d94402 100644 --- a/frontend/src/metabase/entities/index.js +++ b/frontend/src/metabase/entities/index.js @@ -3,7 +3,6 @@ export { default as alerts } from "./alerts"; export { default as collections } from "./collections"; export { default as snippetCollections } from "./snippet-collections"; export { default as dashboards } from "./dashboards"; -export { default as databaseCandidates } from "./database-candidates"; export { default as pulses } from "./pulses"; export { default as questions } from "./questions"; export { ModelIndexes as modelIndexes } from "./model-indexes"; diff --git a/frontend/src/metabase/home/components/HomeXraySection/HomeXraySection.tsx b/frontend/src/metabase/home/components/HomeXraySection/HomeXraySection.tsx index 3dc7c70bfbdfa81c49c2d502807efcb1729f9b95..a3b9d2497cf5e84db886ae0dcde2b87d1da286a9 100644 --- a/frontend/src/metabase/home/components/HomeXraySection/HomeXraySection.tsx +++ b/frontend/src/metabase/home/components/HomeXraySection/HomeXraySection.tsx @@ -3,10 +3,8 @@ import { useCallback, useMemo, useState } from "react"; import { t } from "ttag"; import _ from "underscore"; -import { - useDatabaseCandidateListQuery, - useDatabaseListQuery, -} from "metabase/common/hooks"; +import { skipToken, useListDatabaseCandidatesQuery } from "metabase/api"; +import { useDatabaseListQuery } from "metabase/common/hooks"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import Select from "metabase/core/components/Select"; import { useSelector } from "metabase/lib/redux"; @@ -33,10 +31,9 @@ import { export const HomeXraySection = () => { const databaseListState = useDatabaseListQuery(); const database = getXrayDatabase(databaseListState.data); - const candidateListState = useDatabaseCandidateListQuery({ - query: database ? { id: database.id } : undefined, - enabled: database != null, - }); + const candidateListState = useListDatabaseCandidatesQuery( + database?.id ?? skipToken, + ); const isLoading = databaseListState.isLoading || candidateListState.isLoading; const error = databaseListState.error ?? candidateListState.error;