Skip to content

Commit

Permalink
Improve the flow for adding a question to a dashboard (metabase#30347)
Browse files Browse the repository at this point in the history
  • Loading branch information
calherries authored May 17, 2023
1 parent 871a03f commit a2c8b6d
Show file tree
Hide file tree
Showing 20 changed files with 552 additions and 107 deletions.
1 change: 0 additions & 1 deletion docs/developers-guide/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,4 +538,3 @@ describe("Component", () => {
Key points:
- Create `scope` in `setup`
- Call helpers from `__support__/server-mocks` to setup endpoints for your data
- Call `nock.clearAll` to remove all mocks
2 changes: 2 additions & 0 deletions frontend/src/metabase/common/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export * from "./use-segment-query";
export * from "./use-table-list-query";
export * from "./use-table-metadata-query";
export * from "./use-table-query";
export * from "./use-collection-query";
export * from "./use-most-recently-viewed-dashboard";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-collection-query";
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Collections from "metabase/entities/collections";
import { Collection, CollectionId } from "metabase-types/api";
import {
useEntityQuery,
UseEntityQueryProps,
UseEntityQueryResult,
} from "../use-entity-query";

export const useCollectionQuery = (
props: UseEntityQueryProps<CollectionId, unknown>,
): UseEntityQueryResult<Collection> => {
return useEntityQuery(props, {
fetch: Collections.actions.fetch,
getObject: Collections.selectors.getObject,
getLoading: Collections.selectors.getLoading,
getError: Collections.selectors.getError,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import { createMockCollection } from "metabase-types/api/mocks";
import {
setupCollectionsEndpoints,
setupCollectionByIdEndpoint,
} from "__support__/server-mocks";
import {
renderWithProviders,
screen,
waitForElementToBeRemoved,
} from "__support__/ui";
import { useCollectionQuery } from "./use-collection-query";

const TEST_COLLECTION = createMockCollection();

const TestComponent = () => {
const { data, isLoading, error } = useCollectionQuery({
id: TEST_COLLECTION.id,
});

if (isLoading || error) {
return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
}

return <div>{data?.name}</div>;
};

const setup = ({ error }: { error?: string } = {}) => {
setupCollectionsEndpoints([TEST_COLLECTION]);
setupCollectionByIdEndpoint({ collections: [TEST_COLLECTION], error });
renderWithProviders(<TestComponent />);
};

describe("useCollectionQuery", () => {
it("should be initially loading", () => {
setup();
expect(screen.getByText("Loading...")).toBeInTheDocument();
});

it("should display error", async () => {
const ERROR = "Server error";
setup({
error: ERROR,
});

await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));

expect(screen.getByText(ERROR)).toBeInTheDocument();
});

it("should show data from the response", async () => {
setup();
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
expect(screen.getByText(TEST_COLLECTION.name)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-most-recently-viewed-dashboard";
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useAsync } from "react-use";
import { Dashboard } from "metabase-types/api";
import { ActivityApi } from "metabase/services";

export const useMostRecentlyViewedDashboard = () => {
const {
loading: isLoading,
error,
value: data,
} = useAsync(async () => {
const dashboard: Dashboard | undefined =
await ActivityApi.most_recently_viewed_dashboard();

return dashboard;
});

return { data, isLoading, error };
};
125 changes: 72 additions & 53 deletions frontend/src/metabase/containers/AddToDashSelectDashModal.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import React, { Component, ComponentPropsWithoutRef } from "react";
import React, { ComponentPropsWithoutRef, useState } from "react";
import { connect } from "react-redux";
import { t } from "ttag";

import Icon from "metabase/components/Icon";
import Link from "metabase/core/components/Link";
import ModalContent from "metabase/components/ModalContent";
import DashboardPicker from "metabase/containers/DashboardPicker";
import type { State } from "metabase-types/store";
import { CreateDashboardFormOwnProps } from "metabase/dashboard/containers/CreateDashboardForm";
import * as Urls from "metabase/lib/urls";
import CreateDashboardModal from "metabase/dashboard/containers/CreateDashboardModal";
import { Card, Dashboard } from "metabase-types/api";
import {
useCollectionQuery,
useMostRecentlyViewedDashboard,
} from "metabase/common/hooks";
import { coerceCollectionId } from "metabase/collections/utils";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import type { State } from "metabase-types/store";
import type { Card, Dashboard } from "metabase-types/api";
import type { CreateDashboardFormOwnProps } from "metabase/dashboard/containers/CreateDashboardForm";
import { LinkContent } from "./AddToDashSelectDashModal.styled";

function mapStateToProps(state: State) {
Expand All @@ -19,10 +25,6 @@ function mapStateToProps(state: State) {
};
}

interface AddToDashSelectDashModalState {
shouldCreateDashboard: boolean;
}

interface AddToDashSelectDashModalProps {
card: Card;
onChangeLocation: (location: string) => void;
Expand All @@ -32,18 +34,28 @@ interface AddToDashSelectDashModalProps {

type DashboardPickerProps = ComponentPropsWithoutRef<typeof DashboardPicker>;

class AddToDashSelectDashModal extends Component<
AddToDashSelectDashModalProps,
AddToDashSelectDashModalState
> {
state = {
shouldCreateDashboard: false,
};
export const AddToDashSelectDashModal = ({
card,
dashboards,
onClose,
onChangeLocation,
}: AddToDashSelectDashModalProps) => {
const [shouldCreateDashboard, setShouldCreateDashboard] = useState(false);

navigateToDashboard: Required<CreateDashboardFormOwnProps>["onCreate"] =
dashboard => {
const { card, onChangeLocation } = this.props;
const mostRecentDashboardQuery = useMostRecentlyViewedDashboard();

const collectionId = coerceCollectionId(
mostRecentDashboardQuery.data?.collection_id,
);
// when collectionId is null and loading is completed, show root collection
// as user didnt' visit any dashboard last 24hrs
const collectionQuery = useCollectionQuery({
id: collectionId,
enabled: collectionId !== undefined,
});

const navigateToDashboard: Required<CreateDashboardFormOwnProps>["onCreate"] =
dashboard => {
onChangeLocation(
Urls.dashboard(dashboard, {
editMode: true,
Expand All @@ -52,48 +64,55 @@ class AddToDashSelectDashModal extends Component<
);
};

onDashboardSelected: DashboardPickerProps["onChange"] = dashboardId => {
const onDashboardSelected: DashboardPickerProps["onChange"] = dashboardId => {
if (dashboardId) {
const dashboard = this.props.dashboards[dashboardId];
this.navigateToDashboard(dashboard);
const dashboard = dashboards[dashboardId];
navigateToDashboard(dashboard);
}
};

render() {
if (this.state.shouldCreateDashboard) {
return (
<CreateDashboardModal
collectionId={this.props.card.collection_id}
onCreate={this.navigateToDashboard}
onClose={() => this.setState({ shouldCreateDashboard: false })}
/>
);
}

if (shouldCreateDashboard) {
return (
<ModalContent
id="AddToDashSelectDashModal"
title={
this.props.card.dataset
? t`Add this model to a dashboard`
: t`Add this question to a dashboard`
}
onClose={this.props.onClose}
>
<DashboardPicker onChange={this.onDashboardSelected} />
<Link
onClick={() => this.setState({ shouldCreateDashboard: true })}
to={""}
>
<LinkContent>
<Icon name="add" mx={1} />
<h4>{t`Create a new dashboard`}</h4>
</LinkContent>
</Link>
</ModalContent>
<CreateDashboardModal
collectionId={card.collection_id}
onCreate={navigateToDashboard}
onClose={() => setShouldCreateDashboard(false)}
/>
);
}
}

const isLoading =
mostRecentDashboardQuery.isLoading || collectionQuery.isLoading;
const error = mostRecentDashboardQuery.error ?? collectionQuery.error;

if (isLoading || error) {
return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
}

return (
<ModalContent
id="AddToDashSelectDashModal"
title={
card.dataset
? t`Add this model to a dashboard`
: t`Add this question to a dashboard`
}
onClose={onClose}
>
<DashboardPicker
onChange={onDashboardSelected}
collectionId={collectionId}
value={mostRecentDashboardQuery.data?.id}
/>
<Link onClick={() => setShouldCreateDashboard(true)} to="">
<LinkContent>
<Icon name="add" mx={1} />
<h4>{t`Create a new dashboard`}</h4>
</LinkContent>
</Link>
</ModalContent>
);
};

// eslint-disable-next-line import/no-default-export -- deprecated usage
export default connect(mapStateToProps)(AddToDashSelectDashModal);
Loading

0 comments on commit a2c8b6d

Please sign in to comment.