Skip to content
Snippets Groups Projects
Unverified Commit 850909a1 authored by metabase-bot[bot]'s avatar metabase-bot[bot] Committed by GitHub
Browse files

:robot: backported "Dashboard Header FC, using entity, select, and dispatch hooks" (#39426)


* Dashboard Header FC, using entity, select, and dispatch hooks (#39267)

* Dashboard Header FC, using entity, select, and dispatch hooks

* resolving types

* updating other use of getDashboardActions

* PR feedback

* shimming in bookmark list hook

---------

Co-authored-by: default avatarNick Fitzpatrick <nick@metabase.com>
Co-authored-by: default avatarNick Fitzpatrick <nickfitz.582@gmail.com>
parent 6ebf7b63
No related branches found
No related tags found
No related merge requests found
Showing
with 407 additions and 298 deletions
......@@ -2,7 +2,7 @@ export type BookmarkType = "card" | "collection" | "dashboard";
export interface Bookmark {
authority_level?: string;
card_id: string;
card_id?: string;
display?: string;
id: string;
item_id: number;
......
import type { Bookmark } from "metabase-types/api";
export const createMockBookmark = (opts?: Partial<Bookmark>): Bookmark => ({
id: "collection-1",
name: "My Collection",
item_id: 1,
type: "collection",
...opts,
});
......@@ -2,6 +2,7 @@ export * from "./actions";
export * from "./activity";
export * from "./alert";
export * from "./automagic-dashboards";
export * from "./bookmark";
export * from "./card";
export * from "./collection";
export * from "./dashboard";
......
......@@ -33,10 +33,10 @@ export function isPublicCollection(
}
export function isInstanceAnalyticsCollection(
collection: Partial<Collection>,
collection?: Partial<Collection>,
): boolean {
return (
collection &&
!!collection &&
PLUGIN_COLLECTIONS.getCollectionType(collection).type ===
"instance-analytics"
);
......
export * from "./use-action-list-query";
export * from "./use-action-query";
export * from "./use-bookmark-list-query";
export * from "./use-collection-query";
export * from "./use-collection-list-query";
export * from "./use-dashboard-query";
......
export * from "./use-bookmark-list-query";
import Bookmarks from "metabase/entities/bookmarks";
import type { Bookmark } from "metabase-types/api";
import type {
UseEntityListQueryProps,
UseEntityListQueryResult,
} from "../use-entity-list-query";
import { useEntityListQuery } from "../use-entity-list-query";
export const useBookmarkListQuery = (
props: UseEntityListQueryProps = {},
): UseEntityListQueryResult<Bookmark> => {
return useEntityListQuery(props, {
fetchList: Bookmarks.actions.fetchList,
getError: Bookmarks.selectors.getError,
getList: Bookmarks.selectors.getList,
getLoaded: Bookmarks.selectors.getLoaded,
getLoading: Bookmarks.selectors.getLoading,
getListMetadata: Bookmarks.selectors.getListMetadata,
});
};
import {
setupBookmarksEndpoints,
setupBookmarksEndpointsWithError,
} from "__support__/server-mocks";
import {
renderWithProviders,
screen,
waitForLoaderToBeRemoved,
within,
} from "__support__/ui";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import { createMockBookmark } from "metabase-types/api/mocks";
import { useBookmarkListQuery } from "./use-bookmark-list-query";
const TEST_BOOKMARK = createMockBookmark();
const TestComponent = () => {
const { data = [], metadata, isLoading, error } = useBookmarkListQuery();
if (isLoading || error) {
return <LoadingAndErrorWrapper loading={isLoading} error={error} />;
}
return (
<div>
{data.map(bookmark => (
<div key={bookmark.id}>{bookmark.name}</div>
))}
<div data-testid="metadata">
{(!metadata || Object.keys(metadata).length === 0) && "No metadata"}
</div>
</div>
);
};
const setup = ({ error }: { error?: string } = {}) => {
if (error) {
setupBookmarksEndpointsWithError({ error });
} else {
setupBookmarksEndpoints([TEST_BOOKMARK]);
}
renderWithProviders(<TestComponent />);
};
describe("useBookmarkListQuery", () => {
it("should be initially loading", () => {
setup();
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
});
it("should display error", async () => {
const ERROR = "Server error";
setup({ error: ERROR });
await waitForLoaderToBeRemoved();
expect(screen.getByText(ERROR)).toBeInTheDocument();
});
it("should show data from the response", async () => {
setup();
await waitForLoaderToBeRemoved();
expect(screen.getByText(TEST_BOOKMARK.name)).toBeInTheDocument();
});
it("should not have any metadata in the response", async () => {
setup();
await waitForLoaderToBeRemoved();
expect(
within(screen.getByTestId("metadata")).getByText("No metadata"),
).toBeInTheDocument();
});
});
......@@ -7,7 +7,7 @@ import { trackCardCreated } from "../analytics";
import { addDashCardToDashboard } from "./cards-typed";
export const addActionToDashboard =
async ({ dashId, tabId, action, displayType }) =>
({ dashId, tabId, action, displayType }) =>
dispatch => {
trackCardCreated("action", dashId);
......
......@@ -5,14 +5,18 @@ import type { Route } from "react-router";
import { usePrevious, useUnmount } from "react-use";
import _ from "underscore";
import type { NewDashCardOpts } from "metabase/dashboard/actions";
import { DashboardHeader } from "metabase/dashboard/components/DashboardHeader";
import { DashboardControls } from "metabase/dashboard/hoc/DashboardControls";
import type {
FetchDashboardResult,
SuccessfulFetchDashboardResult,
} from "metabase/dashboard/types";
import { isSmallScreen, getMainElement } from "metabase/lib/dom";
import { FilterApplyButton } from "metabase/parameters/components/FilterApplyButton";
import SyncedParametersList from "metabase/parameters/components/SyncedParametersList/SyncedParametersList";
import { getVisibleParameters } from "metabase/parameters/utils/ui";
import type { EmbeddingParameterVisibility } from "metabase/public/lib/types";
import type Database from "metabase-lib/metadata/Database";
import type Metadata from "metabase-lib/metadata/Metadata";
import type { UiParameter } from "metabase-lib/parameters/types";
import { getValuePopulatedParameters } from "metabase-lib/parameters/utils/parameter-values";
......@@ -21,6 +25,7 @@ import type {
DashboardId,
DashCardDataMap,
DashCardId,
Database,
DatabaseId,
Parameter,
ParameterId,
......@@ -61,13 +66,6 @@ import {
DashboardEmptyStateWithoutAddPrompt,
} from "./DashboardEmptyState/DashboardEmptyState";
type SuccessfulFetchDashboardResult = { payload: { dashboard: IDashboard } };
type FailedFetchDashboardResult = { error: unknown; payload: unknown };
type FetchDashboardResult =
| SuccessfulFetchDashboardResult
| FailedFetchDashboardResult;
interface DashboardProps {
dashboardId: DashboardId;
route: Route;
......@@ -108,6 +106,8 @@ interface DashboardProps {
location: Location;
isNightMode: boolean;
isFullscreen: boolean;
hasNightModeToggle: boolean;
refreshPeriod: number | null;
initialize: (opts?: { clearCache?: boolean }) => void;
fetchDashboard: (opts: {
......@@ -130,10 +130,13 @@ interface DashboardProps {
cardId: CardId;
tabId: DashboardTabId | null;
}) => void;
addHeadingDashCardToDashboard: (opts: NewDashCardOpts) => void;
addMarkdownDashCardToDashboard: (opts: NewDashCardOpts) => void;
addLinkDashCardToDashboard: (opts: NewDashCardOpts) => void;
archiveDashboard: (id: DashboardId) => Promise<void>;
onRefreshPeriodChange: (period: number | null) => void;
setEditingDashboard: (dashboard: IDashboard) => void;
setEditingDashboard: (dashboard: IDashboard | boolean) => void;
setDashboardAttributes: (opts: {
id: DashboardId;
attributes: Partial<IDashboard>;
......@@ -184,6 +187,15 @@ interface DashboardProps {
getEmbeddedParameterVisibility: (
slug: string,
) => EmbeddingParameterVisibility | null;
updateDashboardAndCards: () => void;
onFullscreenChange: (
isFullscreen: boolean,
browserFullscreen?: boolean,
) => void;
onNightModeChange: () => void;
setSidebar: (opts: { name: DashboardSidebarName }) => void;
hideAddParameterPopover: () => void;
}
function DashboardInner(props: DashboardProps) {
......@@ -285,7 +297,7 @@ function DashboardInner(props: DashboardProps) {
);
const handleSetEditing = useCallback(
(dashboard: IDashboard) => {
(dashboard: IDashboard | boolean) => {
onRefreshPeriodChange(null);
setEditingDashboard(dashboard);
},
......
......@@ -10,9 +10,8 @@ import {
RefreshWidgetButton,
} from "./DashboardActions.styled";
export const getDashboardActions = (
self,
{
export const getDashboardActions = props => {
const {
dashboard,
isAdmin,
canManageSubscriptions,
......@@ -29,8 +28,7 @@ export const getDashboardActions = (
onSharingClick,
onFullscreenChange,
hasNightModeToggle,
},
) => {
} = props;
const buttons = [];
const isLoaded = !!dashboard;
......
......@@ -14,7 +14,11 @@ import {
createMockDashboardCard,
createMockTokenFeatures,
} from "metabase-types/api/mocks";
import { createMockDashboardState } from "metabase-types/store/mocks";
import type { DashboardSidebarName } from "metabase-types/store";
import {
createMockDashboardState,
createMockLocation,
} from "metabase-types/store/mocks";
import { DashboardHeader } from "../DashboardHeader";
......@@ -98,11 +102,15 @@ export const setup = async ({
const dashboardHeaderProps = {
isAdmin,
dashboard,
dashboardId: dashboard.id,
canManageSubscriptions: true,
isEditing: false,
isFullscreen: false,
isNavBarOpen: false,
isNightMode: false,
isDirty: false,
isAddParameterPopoverOpen: false,
hasNightModeToggle: false,
isAdditionalInfoVisible: false,
refreshPeriod: 0,
addMarkdownDashCardToDashboard: jest.fn(),
......@@ -121,17 +129,18 @@ export const setup = async ({
onChangeLocation: jest.fn(),
toggleSidebar: jest.fn(),
sidebar: {
name: "",
name: "" as DashboardSidebarName,
props: {},
},
location: {
query: {},
},
location: createMockLocation(),
setSidebar: jest.fn(),
closeSidebar: jest.fn(),
addActionToDashboard: jest.fn(),
databases: {},
params: { tabSlug: undefined },
addParameter: jest.fn(),
showAddParameterPopover: jest.fn(),
hideAddParameterPopover: jest.fn(),
};
renderWithProviders(<DashboardHeader {...dashboardHeaderProps} />, {
......
......@@ -15,7 +15,6 @@ import { getEmbedOptions, getIsEmbedded } from "metabase/selectors/embed";
import { getMetadata } from "metabase/selectors/metadata";
import Question from "metabase-lib/Question";
import type {
Bookmark,
Card,
CardId,
DashboardId,
......@@ -231,20 +230,6 @@ export const getDocumentTitle = (state: State) =>
export const getIsNavigatingBackToDashboard = (state: State) =>
state.dashboard.isNavigatingBackToDashboard;
type IsBookmarkedSelectorProps = {
bookmarks: Bookmark[];
dashboardId: DashboardId;
};
export const getIsBookmarked = (
state: State,
{ bookmarks, dashboardId }: IsBookmarkedSelectorProps,
) =>
bookmarks.some(
bookmark =>
bookmark.type === "dashboard" && bookmark.item_id === dashboardId,
);
export const getIsDirty = createSelector(
[getDashboard, getDashcards],
(dashboard, dashcards) => {
......
import type { Dashboard } from "metabase-types/api";
export type SuccessfulFetchDashboardResult = {
payload: { dashboard: Dashboard };
};
type FailedFetchDashboardResult = { error: unknown; payload: unknown };
export type FetchDashboardResult =
| SuccessfulFetchDashboardResult
| FailedFetchDashboardResult;
......@@ -156,7 +156,7 @@ class PublicDashboard extends Component {
} = this.props;
const buttons = !isWithinIframe()
? getDashboardActions(this, { ...this.props, isPublic: true })
? getDashboardActions({ ...this.props, isPublic: true })
: [];
const visibleDashcards = (dashboard?.dashcards ?? []).filter(
......
......@@ -5,3 +5,16 @@ import type { Bookmark } from "metabase-types/api";
export function setupBookmarksEndpoints(bookmarks: Bookmark[]) {
fetchMock.get("path:/api/bookmark", bookmarks);
}
export function setupBookmarksEndpointsWithError({
error,
status = 500,
}: {
error: string;
status?: number;
}) {
fetchMock.get("path:/api/bookmark", {
body: error,
status,
});
}
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