Skip to content
Snippets Groups Projects
Unverified Commit 5b19999d authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Use RTK Query instead of entity framework for DatabaseCandidates (#40403)

* use RTK Query instead of entity framework for DatabaseCandidates

* use skipToken to tighten up API request types
parent 69353015
No related branches found
No related tags found
No related merge requests found
Showing
with 62 additions and 171 deletions
......@@ -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";
......
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";
......
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;
......@@ -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";
......
......@@ -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";
......
export * from "./use-database-candidate-list-query";
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,
});
};
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();
});
});
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} />;
};
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();
......
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./DatabaseSyncModal";
export * from "./DatabaseSyncModal";
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);
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./DatabaseSyncModal";
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;
......@@ -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";
......
......@@ -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;
......
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