Skip to content
Snippets Groups Projects
Unverified Commit dee6226e authored by Kamil Mielnik's avatar Kamil Mielnik Committed by GitHub
Browse files

Migrate dashboards to RTK (#41311)

* Add provideDashboardListTags and provideDashboardTags

* Keep the utils sorted alphabetically

* Include collection tags in dashboard tags

* Add getDashboard

* Migrate basic CRUD dashboard endpoints to RTK

* Migrate remaining dashboard endpoints to RTK

* Fix save dashboard action

* Remove unused favorite/unfavorite dashboard endpoints

* Fix copy dashboard action

* Move types to types file

* Type dashboard API requests

* Fix get

* Fix typo

* Pass body

* Add a comment

* Support noEvent option to fix a failing test
parent 152ca0e5
No related branches found
No related tags found
No related merge requests found
......@@ -39,7 +39,9 @@ export interface Dashboard {
model?: string;
dashcards: DashboardCard[];
tabs?: DashboardTab[];
show_in_getting_started?: boolean | null;
parameters?: Parameter[] | null;
point_of_interest?: string | null;
collection_authority_level?: CollectionAuthorityLevel;
can_write: boolean;
cache_ttl: number | null;
......@@ -201,3 +203,52 @@ export interface GetCompatibleCardsPayload {
query?: string;
exclude_ids: number[];
}
export type ListDashboardsRequest = {
f?: "all" | "mine" | "archived";
};
export type GetDashboardRequest = {
id: DashboardId;
ignore_error?: boolean;
};
export type CreateDashboardRequest = {
name: string;
description?: string | null;
parameters?: Parameter[] | null;
cache_ttl?: number;
collection_id?: CollectionId | null;
collection_position?: number | null;
};
export type UpdateDashboardRequest = {
id: DashboardId;
parameters?: Parameter[] | null;
point_of_interest?: string | null;
description?: string | null;
archived?: boolean | null;
dashcards?: DashboardCard[] | null;
collection_position?: number | null;
tabs?: DashboardTab[];
show_in_getting_started?: boolean | null;
enable_embedding?: boolean | null;
collection_id?: CollectionId | null;
name?: string | null;
width?: DashboardWidth | null;
caveats?: string | null;
embedding_params?: EmbeddingParameters | null;
cache_ttl?: number;
position?: number | null;
};
export type SaveDashboardRequest = Omit<UpdateDashboardRequest, "id">;
export type CopyDashboardRequest = {
id: DashboardId;
name?: string | null;
description?: string | null;
collection_id?: CollectionId | null;
collection_position?: number | null;
is_deep_copy?: boolean | null;
};
import type {
CopyDashboardRequest,
CreateDashboardRequest,
Dashboard,
DashboardId,
GetDashboardRequest,
ListDashboardsRequest,
SaveDashboardRequest,
UpdateDashboardRequest,
} from "metabase-types/api";
import { Api } from "./api";
import {
idTag,
invalidateTags,
listTag,
provideDashboardListTags,
provideDashboardTags,
} from "./tags";
export const dashboardApi = Api.injectEndpoints({
endpoints: builder => ({
listDashboards: builder.query<Dashboard[], ListDashboardsRequest | void>({
query: body => ({
method: "GET",
url: "/api/dashboard",
body,
}),
providesTags: dashboards =>
dashboards ? provideDashboardListTags(dashboards) : [],
}),
getDashboard: builder.query<Dashboard, GetDashboardRequest>({
query: ({ id, ignore_error }) => ({
method: "GET",
url: `/api/dashboard/${id}`,
noEvent: ignore_error,
}),
providesTags: dashboard =>
dashboard ? provideDashboardTags(dashboard) : [],
}),
createDashboard: builder.mutation<Dashboard, CreateDashboardRequest>({
query: body => ({
method: "POST",
url: "/api/dashboard",
body,
}),
invalidatesTags: (_, error) =>
invalidateTags(error, [listTag("dashboard")]),
}),
updateDashboard: builder.mutation<Dashboard, UpdateDashboardRequest>({
query: ({ id, ...body }) => ({
method: "PUT",
url: `/api/dashboard/${id}`,
body,
}),
invalidatesTags: (_, error, { id }) =>
invalidateTags(error, [listTag("dashboard"), idTag("dashboard", id)]),
}),
deleteDashboard: builder.mutation<void, DashboardId>({
query: id => ({
method: "DELETE",
url: `/api/dashboard/${id}`,
}),
invalidatesTags: (_, error, id) =>
invalidateTags(error, [listTag("dashboard"), idTag("dashboard", id)]),
}),
saveDashboard: builder.mutation<Dashboard, SaveDashboardRequest>({
query: body => ({
method: "POST",
url: `/api/dashboard/save`,
body,
}),
invalidatesTags: (_, error) =>
invalidateTags(error, [listTag("dashboard")]),
}),
copyDashboard: builder.mutation<Dashboard, CopyDashboardRequest>({
query: ({ id, ...body }) => ({
method: "POST",
url: `/api/dashboard/${id}/copy`,
body,
}),
invalidatesTags: (_, error) =>
invalidateTags(error, [listTag("dashboard")]),
}),
}),
});
export const {
useCopyDashboardMutation,
useCreateDashboardMutation,
useDeleteDashboardMutation,
useGetDashboardQuery,
useListDashboardsQuery,
useSaveDashboardMutation,
useUpdateDashboardMutation,
} = dashboardApi;
......@@ -6,6 +6,7 @@ export * from "./automagic-dashboards";
export * from "./card";
export * from "./collection";
export * from "./database";
export * from "./dashboard";
export * from "./bookmark";
export * from "./dataset";
export * from "./field";
......
import type { TagDescription } from "@reduxjs/toolkit/query";
import { isVirtualDashCard } from "metabase/dashboard/utils";
import type {
Alert,
ApiKey,
......@@ -8,6 +9,7 @@ import type {
Collection,
CollectionItem,
CollectionItemModel,
Dashboard,
Database,
DatabaseCandidate,
Field,
......@@ -56,6 +58,10 @@ export function invalidateTags(
return !error ? tags : [];
}
// ----------------------------------------------------------------------- //
// Keep the below list of entity-specific functions alphabetically sorted. //
// ----------------------------------------------------------------------- //
export function provideActivityItemListTags(
items: RecentItem[] | PopularItem[],
): TagDescription<TagType>[] {
......@@ -94,36 +100,6 @@ export function provideApiKeyTags(apiKey: ApiKey): TagDescription<TagType>[] {
return [idTag("api-key", apiKey.id)];
}
export function provideDatabaseCandidateListTags(
candidates: DatabaseCandidate[],
): TagDescription<TagType>[] {
return [
listTag("schema"),
...candidates.flatMap(provideDatabaseCandidateTags),
];
}
export function provideDatabaseCandidateTags(
candidate: DatabaseCandidate,
): TagDescription<TagType>[] {
return [idTag("schema", candidate.schema)];
}
export function provideDatabaseListTags(
databases: Database[],
): TagDescription<TagType>[] {
return [listTag("database"), ...databases.flatMap(provideDatabaseTags)];
}
export function provideDatabaseTags(
database: Database,
): TagDescription<TagType>[] {
return [
idTag("database", database.id),
...(database.tables ? provideTableListTags(database.tables) : []),
];
}
export function provideBookmarkListTags(
bookmarks: Bookmark[],
): TagDescription<TagType>[] {
......@@ -172,6 +148,58 @@ export function provideCollectionTags(
return [idTag("collection", collection.id)];
}
export function provideDatabaseCandidateListTags(
candidates: DatabaseCandidate[],
): TagDescription<TagType>[] {
return [
listTag("schema"),
...candidates.flatMap(provideDatabaseCandidateTags),
];
}
export function provideDatabaseCandidateTags(
candidate: DatabaseCandidate,
): TagDescription<TagType>[] {
return [idTag("schema", candidate.schema)];
}
export function provideDatabaseListTags(
databases: Database[],
): TagDescription<TagType>[] {
return [listTag("database"), ...databases.flatMap(provideDatabaseTags)];
}
export function provideDatabaseTags(
database: Database,
): TagDescription<TagType>[] {
return [
idTag("database", database.id),
...(database.tables ? provideTableListTags(database.tables) : []),
];
}
export function provideDashboardListTags(
dashboards: Dashboard[],
): TagDescription<TagType>[] {
return [listTag("dashboard"), ...dashboards.flatMap(provideDashboardTags)];
}
export function provideDashboardTags(
dashboard: Dashboard,
): TagDescription<TagType>[] {
const cards = dashboard.dashcards
.flatMap(dashcard => (isVirtualDashCard(dashcard) ? [] : [dashcard]))
.map(dashcard => dashcard.card);
return [
idTag("dashboard", dashboard.id),
...provideCardListTags(cards),
...(dashboard.collection
? provideCollectionTags(dashboard.collection)
: []),
];
}
export function provideFieldListTags(
fields: Field[],
): TagDescription<TagType>[] {
......
......@@ -31,7 +31,7 @@ const setup = () => {
renderWithProviders(<TestComponent />);
};
describe("useDatabaseQuery", () => {
describe("useDashboardQuery", () => {
it("should be initially loading", () => {
setup();
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
......
......@@ -9,11 +9,11 @@ import _ from "underscore";
import ArchiveModal from "metabase/components/ArchiveModal";
import Collection from "metabase/entities/collections";
import Dashboard from "metabase/entities/dashboards";
import Dashboards from "metabase/entities/dashboards";
import * as Urls from "metabase/lib/urls";
const mapDispatchToProps = {
setDashboardArchived: Dashboard.actions.setArchived,
setDashboardArchived: Dashboards.actions.setArchived,
push,
};
......@@ -56,7 +56,7 @@ class ArchiveDashboardModal extends Component {
export const ArchiveDashboardModalConnected = _.compose(
connect(null, mapDispatchToProps),
Dashboard.load({
Dashboards.load({
id: (state, props) => Urls.extractCollectionId(props.params.slug),
}),
Collection.load({
......
import { assocIn } from "icepick";
import { t } from "ttag";
import { dashboardApi } from "metabase/api";
import { canonicalCollectionId } from "metabase/collections/utils";
import {
getCollectionType,
normalizedCollection,
} from "metabase/entities/collections";
import { POST, DELETE } from "metabase/lib/api";
import { color } from "metabase/lib/colors";
import { createEntity, undo } from "metabase/lib/entities";
import {
createEntity,
entityCompatibleQuery,
undo,
} from "metabase/lib/entities";
import {
compose,
withAction,
......@@ -20,8 +23,6 @@ import { addUndo } from "metabase/redux/undo";
import forms from "./dashboards/forms";
const FAVORITE_ACTION = `metabase/entities/dashboards/FAVORITE`;
const UNFAVORITE_ACTION = `metabase/entities/dashboards/UNFAVORITE`;
const COPY_ACTION = `metabase/entities/dashboards/COPY`;
/**
......@@ -36,10 +37,48 @@ const Dashboards = createEntity({
displayNameMany: t`dashboards`,
api: {
favorite: POST("/api/dashboard/:id/favorite"),
unfavorite: DELETE("/api/dashboard/:id/favorite"),
save: POST("/api/dashboard/save"),
copy: POST("/api/dashboard/:id/copy"),
list: (entityQuery, dispatch) =>
entityCompatibleQuery(
entityQuery,
dispatch,
dashboardApi.endpoints.listDashboards,
),
get: (entityQuery, options, dispatch) =>
entityCompatibleQuery(
{ ...entityQuery, ignore_error: options?.noEvent },
dispatch,
dashboardApi.endpoints.getDashboard,
),
create: (entityQuery, dispatch) =>
entityCompatibleQuery(
entityQuery,
dispatch,
dashboardApi.endpoints.createDashboard,
),
update: (entityQuery, dispatch) =>
entityCompatibleQuery(
entityQuery,
dispatch,
dashboardApi.endpoints.updateDashboard,
),
delete: ({ id }, dispatch) =>
entityCompatibleQuery(
id,
dispatch,
dashboardApi.endpoints.deleteDashboard,
),
save: (entityQuery, dispatch) =>
entityCompatibleQuery(
entityQuery,
dispatch,
dashboardApi.endpoints.saveDashboard,
),
copy: (entityQuery, dispatch) =>
entityCompatibleQuery(
entityQuery,
dispatch,
dashboardApi.endpoints.copyDashboard,
),
},
objectActions: {
......@@ -67,16 +106,6 @@ const Dashboards = createEntity({
opts,
),
setFavorited: async ({ id }, favorite) => {
if (favorite) {
await Dashboards.api.favorite({ id });
return { type: FAVORITE_ACTION, payload: id };
} else {
await Dashboards.api.unfavorite({ id });
return { type: UNFAVORITE_ACTION, payload: id };
}
},
// TODO move into more common area as copy is implemented for more entities
copy: compose(
withAction(COPY_ACTION),
......@@ -92,11 +121,15 @@ const Dashboards = createEntity({
(entityObject, overrides, { notify } = {}) =>
async (dispatch, getState) => {
const result = Dashboards.normalize(
await Dashboards.api.copy({
id: entityObject.id,
...overrides,
is_deep_copy: !overrides.is_shallow_copy,
}),
await entityCompatibleQuery(
{
id: entityObject.id,
...overrides,
is_deep_copy: !overrides.is_shallow_copy,
},
dispatch,
dashboardApi.endpoints.copyDashboard,
),
);
if (notify) {
dispatch(addUndo(notify));
......@@ -109,7 +142,11 @@ const Dashboards = createEntity({
actions: {
save: dashboard => async dispatch => {
const savedDashboard = await Dashboards.api.save(dashboard);
const savedDashboard = await entityCompatibleQuery(
dashboard,
dispatch,
dashboardApi.endpoints.saveDashboard,
);
dispatch({ type: Dashboards.actionTypes.INVALIDATE_LISTS_ACTION });
return {
type: "metabase/entities/dashboards/SAVE_DASHBOARD",
......@@ -119,18 +156,13 @@ const Dashboards = createEntity({
},
reducer: (state = {}, { type, payload, error }) => {
if (type === FAVORITE_ACTION && !error) {
return assocIn(state, [payload, "favorite"], true);
} else if (type === UNFAVORITE_ACTION && !error) {
return assocIn(state, [payload, "favorite"], false);
} else if (type === COPY_ACTION && !error && state[""]) {
if (type === COPY_ACTION && !error && state[""]) {
return { ...state, "": state[""].concat([payload.result]) };
}
return state;
},
objectSelectors: {
getFavorited: dashboard => dashboard && dashboard.favorite,
getName: dashboard => dashboard && dashboard.name,
getUrl: dashboard => dashboard && Urls.dashboard(dashboard),
getCollection: dashboard =>
......
......@@ -182,8 +182,6 @@ export const DashboardApi = {
get: GET("/api/dashboard/:dashId"),
update: PUT("/api/dashboard/:id"),
delete: DELETE("/api/dashboard/:dashId"),
favorite: POST("/api/dashboard/:dashId/favorite"),
unfavorite: DELETE("/api/dashboard/:dashId/favorite"),
parameterValues: GET("/api/dashboard/:dashId/params/:paramId/values"),
parameterSearch: GET("/api/dashboard/:dashId/params/:paramId/search/:query"),
validFilterFields: GET("/api/dashboard/params/valid-filter-fields"),
......
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