Skip to content
Snippets Groups Projects
Unverified Commit 5234c0df authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Automatically pick data app homepage (#25081)

* Add `createMockDataAppPage` test utility

* Add `getDataAppHomePageId` utility

* Render the homepage when launching a data app

* Break down `DataAppNavbarContainer`

* Highlight the homepage in app nav sidebar
parent 3766feea
No related branches found
No related tags found
No related merge requests found
import { DataApp } from "metabase-types/api";
import { DataApp, Dashboard } from "metabase-types/api";
import { createMockCollection } from "./collection";
import { createMockDashboard } from "./dashboard";
export const createMockDataApp = ({
collection: collectionProps,
......@@ -18,3 +19,7 @@ export const createMockDataApp = ({
collection,
};
};
export const createMockDataAppPage = (
params: Partial<Omit<Dashboard, "is_app_page">>,
): Dashboard => createMockDashboard({ ...params, is_app_page: true });
......@@ -9,7 +9,7 @@ import { Collection, DataApp } from "metabase-types/api";
import { DEFAULT_COLLECTION_COLOR_ALIAS } from "../collections/constants";
import { createNewAppForm, createAppSettingsForm } from "./forms";
import { getDataAppIcon, isDataAppCollection } from "./utils";
import { getDataAppIcon } from "./utils";
type EditableDataAppParams = Pick<
DataApp,
......@@ -74,6 +74,5 @@ const DataApps = createEntity({
},
});
export { getDataAppIcon, isDataAppCollection };
export * from "./utils";
export default DataApps;
import type { Collection, DataApp } from "metabase-types/api";
import _ from "underscore";
import type { Collection, DataApp, Dashboard } from "metabase-types/api";
export function getDataAppIcon(app?: DataApp) {
return { name: "star" };
......@@ -7,3 +8,8 @@ export function getDataAppIcon(app?: DataApp) {
export function isDataAppCollection(collection: Collection) {
return typeof collection.app_id === "number";
}
export function getDataAppHomePageId(pages: Dashboard[]) {
const [firstPage] = _.sortBy(pages, "name");
return firstPage?.id;
}
import { createMockDataAppPage } from "metabase-types/api/mocks";
import { getDataAppHomePageId } from "./utils";
describe("data app utils", () => {
describe("getDataAppHomePageId", () => {
it("returns fist page in alphabetical order", () => {
const page1 = createMockDataAppPage({ id: 1, name: "A" });
const page2 = createMockDataAppPage({ id: 2, name: "B" });
const page3 = createMockDataAppPage({ id: 3, name: "C" });
expect(getDataAppHomePageId([page2, page1, page3])).toEqual(page1.id);
});
it("returns undefined when there're no pages", () => {
expect(getDataAppHomePageId([])).toBeUndefined();
});
});
});
......@@ -6,7 +6,7 @@ import Modal from "metabase/components/Modal";
import * as Urls from "metabase/lib/urls";
import DataApps from "metabase/entities/data-apps";
import DataApps, { getDataAppHomePageId } from "metabase/entities/data-apps";
import Dashboards from "metabase/entities/dashboards";
import Search from "metabase/entities/search";
......@@ -20,15 +20,24 @@ import DataAppNavbarView from "./DataAppNavbarView";
const FETCHING_SEARCH_MODELS = ["dashboard", "dataset", "card"];
const LIMIT = 100;
function isAtDataAppHomePage(selectedItems: SelectedItem[]) {
const [selectedItem] = selectedItems;
return selectedItems.length === 1 && selectedItem.type === "data-app";
}
type NavbarModal = "MODAL_APP_SETTINGS" | "MODAL_NEW_PAGE" | null;
interface Props extends MainNavbarProps {
interface DataAppNavbarContainerProps extends MainNavbarProps {
dataApp: DataApp;
loading: boolean;
items: any[];
selectedItems: SelectedItem[];
onChangeLocation: (location: LocationDescriptor) => void;
}
type DataAppNavbarContainerLoaderProps = DataAppNavbarContainerProps & {
dataApp?: DataApp;
};
type SearchRenderProps = {
list: any[];
loading: boolean;
......@@ -36,22 +45,33 @@ type SearchRenderProps = {
function DataAppNavbarContainer({
dataApp,
loading: loadingDataApp,
items,
selectedItems,
onChangeLocation,
...props
}: Props) {
}: DataAppNavbarContainerProps) {
const [modal, setModal] = useState<NavbarModal>(null);
const collectionContentQuery = useMemo(() => {
if (!dataApp) {
return {};
const finalSelectedItems: SelectedItem[] = useMemo(() => {
const isHomepage = isAtDataAppHomePage(selectedItems);
// Once a data app is launched, the first view is going to be the app homepage
// Homepage is an app page specified by a user or picked automatically (just the first one)
// The homepage doesn't have a regular page path like /a/1/page/1, but an app one like /a/1
// So we need to overwrite the selectedItems list here and specify the homepage
if (isHomepage) {
return [
{
type: "data-app-page",
id: getDataAppHomePageId(
items.filter(item => item.model === "dashboard"),
),
},
];
}
return {
collection: dataApp.collection_id,
models: FETCHING_SEARCH_MODELS,
limit: LIMIT,
};
}, [dataApp]);
return selectedItems;
}, [items, selectedItems]);
const onEditAppSettings = useCallback(() => {
setModal("MODAL_APP_SETTINGS");
......@@ -95,38 +115,54 @@ function DataAppNavbarContainer({
return null;
}, [dataApp, modal, closeModal, onChangeLocation]);
if (loadingDataApp) {
return <NavbarLoadingView />;
}
return (
<>
<Search.ListLoader
query={collectionContentQuery}
loadingAndErrorWrapper={false}
>
{({ list = [], loading: loadingAppContent }: SearchRenderProps) => {
if (loadingAppContent) {
return <NavbarLoadingView />;
}
return (
<DataAppNavbarView
{...props}
dataApp={dataApp}
items={list}
onEditAppSettings={onEditAppSettings}
onNewPage={onNewPage}
/>
);
}}
</Search.ListLoader>
<DataAppNavbarView
{...props}
dataApp={dataApp}
items={items}
selectedItems={finalSelectedItems}
onNewPage={onNewPage}
onEditAppSettings={onEditAppSettings}
/>
{modal && <Modal onClose={closeModal}>{renderModalContent()}</Modal>}
</>
);
}
function DataAppNavbarContainerLoader({
dataApp,
...props
}: DataAppNavbarContainerLoaderProps) {
if (!dataApp) {
return <NavbarLoadingView />;
}
return (
<Search.ListLoader
query={{
collection: dataApp.collection_id,
models: FETCHING_SEARCH_MODELS,
limit: LIMIT,
}}
loadingAndErrorWrapper={false}
>
{({ list = [], loading: loadingAppContent }: SearchRenderProps) => {
if (loadingAppContent) {
return <NavbarLoadingView />;
}
return (
<DataAppNavbarContainer {...props} dataApp={dataApp} items={list} />
);
}}
</Search.ListLoader>
);
}
function getDataAppId(state: State, props: MainNavbarOwnProps) {
return Urls.extractEntityId(props.params.slug);
}
export default DataApps.load({ id: getDataAppId })(DataAppNavbarContainer);
export default DataApps.load({ id: getDataAppId })(
DataAppNavbarContainerLoader,
);
import React, { ReactNode } from "react";
import { Location } from "history";
import { extractCollectionId } from "metabase/lib/urls";
import * as Urls from "metabase/lib/urls";
import DataApps from "metabase/entities/data-apps";
import DataApps, { getDataAppHomePageId } from "metabase/entities/data-apps";
import Search from "metabase/entities/search";
import CollectionContent from "metabase/collections/containers/CollectionContent";
import DashboardApp from "metabase/dashboard/containers/DashboardApp";
import { DataApp } from "metabase-types/api";
import { State } from "metabase-types/store";
interface DataAppLandingOwnProps {
location: Location;
params: {
slug: string;
};
......@@ -20,10 +24,39 @@ interface DataAppLandingProps extends DataAppLandingOwnProps {
dataApp: DataApp;
}
const DataAppLanding = ({ dataApp, children }: DataAppLandingProps) => {
const DataAppLanding = ({
dataApp,
location,
params,
children,
}: DataAppLandingProps) => {
if (Urls.isDataAppPreviewPath(location.pathname)) {
return (
<CollectionContent collectionId={dataApp.collection_id} isRoot={false} />
);
}
return (
<>
<CollectionContent collectionId={dataApp.collection_id} isRoot={false} />
<Search.ListLoader
query={{
collection: dataApp.collection_id,
models: ["dashboard"],
limit: 100,
}}
loadingAndErrorWrapper={false}
>
{({ list: pages = [] }: { list: any[] }) => {
const homepageId = getDataAppHomePageId(pages);
return homepageId ? (
<DashboardApp
dashboardId={homepageId}
location={location}
params={params}
/>
) : null;
}}
</Search.ListLoader>
{children}
</>
);
......@@ -31,5 +64,5 @@ const DataAppLanding = ({ dataApp, children }: DataAppLandingProps) => {
export default DataApps.load({
id: (state: State, { params }: DataAppLandingOwnProps) =>
extractCollectionId(params.slug),
Urls.extractCollectionId(params.slug),
})(DataAppLanding);
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