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 { createMockCollection } from "./collection";
import { createMockDashboard } from "./dashboard";
export const createMockDataApp = ({ export const createMockDataApp = ({
collection: collectionProps, collection: collectionProps,
...@@ -18,3 +19,7 @@ export const createMockDataApp = ({ ...@@ -18,3 +19,7 @@ export const createMockDataApp = ({
collection, 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"; ...@@ -9,7 +9,7 @@ import { Collection, DataApp } from "metabase-types/api";
import { DEFAULT_COLLECTION_COLOR_ALIAS } from "../collections/constants"; import { DEFAULT_COLLECTION_COLOR_ALIAS } from "../collections/constants";
import { createNewAppForm, createAppSettingsForm } from "./forms"; import { createNewAppForm, createAppSettingsForm } from "./forms";
import { getDataAppIcon, isDataAppCollection } from "./utils"; import { getDataAppIcon } from "./utils";
type EditableDataAppParams = Pick< type EditableDataAppParams = Pick<
DataApp, DataApp,
...@@ -74,6 +74,5 @@ const DataApps = createEntity({ ...@@ -74,6 +74,5 @@ const DataApps = createEntity({
}, },
}); });
export { getDataAppIcon, isDataAppCollection }; export * from "./utils";
export default DataApps; 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) { export function getDataAppIcon(app?: DataApp) {
return { name: "star" }; return { name: "star" };
...@@ -7,3 +8,8 @@ export function getDataAppIcon(app?: DataApp) { ...@@ -7,3 +8,8 @@ export function getDataAppIcon(app?: DataApp) {
export function isDataAppCollection(collection: Collection) { export function isDataAppCollection(collection: Collection) {
return typeof collection.app_id === "number"; 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"; ...@@ -6,7 +6,7 @@ import Modal from "metabase/components/Modal";
import * as Urls 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 Dashboards from "metabase/entities/dashboards"; import Dashboards from "metabase/entities/dashboards";
import Search from "metabase/entities/search"; import Search from "metabase/entities/search";
...@@ -20,15 +20,24 @@ import DataAppNavbarView from "./DataAppNavbarView"; ...@@ -20,15 +20,24 @@ import DataAppNavbarView from "./DataAppNavbarView";
const FETCHING_SEARCH_MODELS = ["dashboard", "dataset", "card"]; const FETCHING_SEARCH_MODELS = ["dashboard", "dataset", "card"];
const LIMIT = 100; 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; type NavbarModal = "MODAL_APP_SETTINGS" | "MODAL_NEW_PAGE" | null;
interface Props extends MainNavbarProps { interface DataAppNavbarContainerProps extends MainNavbarProps {
dataApp: DataApp; dataApp: DataApp;
loading: boolean; items: any[];
selectedItems: SelectedItem[]; selectedItems: SelectedItem[];
onChangeLocation: (location: LocationDescriptor) => void; onChangeLocation: (location: LocationDescriptor) => void;
} }
type DataAppNavbarContainerLoaderProps = DataAppNavbarContainerProps & {
dataApp?: DataApp;
};
type SearchRenderProps = { type SearchRenderProps = {
list: any[]; list: any[];
loading: boolean; loading: boolean;
...@@ -36,22 +45,33 @@ type SearchRenderProps = { ...@@ -36,22 +45,33 @@ type SearchRenderProps = {
function DataAppNavbarContainer({ function DataAppNavbarContainer({
dataApp, dataApp,
loading: loadingDataApp, items,
selectedItems,
onChangeLocation, onChangeLocation,
...props ...props
}: Props) { }: DataAppNavbarContainerProps) {
const [modal, setModal] = useState<NavbarModal>(null); const [modal, setModal] = useState<NavbarModal>(null);
const collectionContentQuery = useMemo(() => { const finalSelectedItems: SelectedItem[] = useMemo(() => {
if (!dataApp) { const isHomepage = isAtDataAppHomePage(selectedItems);
return {};
// 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, return selectedItems;
models: FETCHING_SEARCH_MODELS, }, [items, selectedItems]);
limit: LIMIT,
};
}, [dataApp]);
const onEditAppSettings = useCallback(() => { const onEditAppSettings = useCallback(() => {
setModal("MODAL_APP_SETTINGS"); setModal("MODAL_APP_SETTINGS");
...@@ -95,38 +115,54 @@ function DataAppNavbarContainer({ ...@@ -95,38 +115,54 @@ function DataAppNavbarContainer({
return null; return null;
}, [dataApp, modal, closeModal, onChangeLocation]); }, [dataApp, modal, closeModal, onChangeLocation]);
if (loadingDataApp) {
return <NavbarLoadingView />;
}
return ( return (
<> <>
<Search.ListLoader <DataAppNavbarView
query={collectionContentQuery} {...props}
loadingAndErrorWrapper={false} dataApp={dataApp}
> items={items}
{({ list = [], loading: loadingAppContent }: SearchRenderProps) => { selectedItems={finalSelectedItems}
if (loadingAppContent) { onNewPage={onNewPage}
return <NavbarLoadingView />; onEditAppSettings={onEditAppSettings}
} />
return (
<DataAppNavbarView
{...props}
dataApp={dataApp}
items={list}
onEditAppSettings={onEditAppSettings}
onNewPage={onNewPage}
/>
);
}}
</Search.ListLoader>
{modal && <Modal onClose={closeModal}>{renderModalContent()}</Modal>} {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) { function getDataAppId(state: State, props: MainNavbarOwnProps) {
return Urls.extractEntityId(props.params.slug); 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 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 CollectionContent from "metabase/collections/containers/CollectionContent";
import DashboardApp from "metabase/dashboard/containers/DashboardApp";
import { DataApp } from "metabase-types/api"; import { DataApp } from "metabase-types/api";
import { State } from "metabase-types/store"; import { State } from "metabase-types/store";
interface DataAppLandingOwnProps { interface DataAppLandingOwnProps {
location: Location;
params: { params: {
slug: string; slug: string;
}; };
...@@ -20,10 +24,39 @@ interface DataAppLandingProps extends DataAppLandingOwnProps { ...@@ -20,10 +24,39 @@ interface DataAppLandingProps extends DataAppLandingOwnProps {
dataApp: DataApp; 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 ( 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} {children}
</> </>
); );
...@@ -31,5 +64,5 @@ const DataAppLanding = ({ dataApp, children }: DataAppLandingProps) => { ...@@ -31,5 +64,5 @@ const DataAppLanding = ({ dataApp, children }: DataAppLandingProps) => {
export default DataApps.load({ export default DataApps.load({
id: (state: State, { params }: DataAppLandingOwnProps) => id: (state: State, { params }: DataAppLandingOwnProps) =>
extractCollectionId(params.slug), Urls.extractCollectionId(params.slug),
})(DataAppLanding); })(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