Skip to content

Commit

Permalink
Basic top nav bar for dashboard (opensearch-project#4108)
Browse files Browse the repository at this point in the history
Basic top nav bar for dashboard

This PR will add basic structure to render top nav bar, including a basic implementation for dashboard app state. This is not functionality complete, but to help implement a basic working dashboard app as the first step.

Signed-off-by: abbyhu2000 <abigailhu2000@gmail.com>
  • Loading branch information
abbyhu2000 committed Jun 27, 2023
1 parent b091b74 commit 9c23f76
Show file tree
Hide file tree
Showing 8 changed files with 709 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,212 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { EventEmitter } from 'events';
import { EMPTY, Subscription, merge } from 'rxjs';
import { catchError, distinctUntilChanged, map, mapTo, startWith, switchMap } from 'rxjs/operators';
import deepEqual from 'fast-deep-equal';
import { DashboardTopNav } from '../components/dashboard_top_nav';
import { useChromeVisibility } from '../utils/use/use_chrome_visibility';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { useSavedDashboardInstance } from '../utils/use/use_saved_dashboard_instance';

import { DashboardServices, SavedDashboardPanel } from '../../types';
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerInput,
DashboardPanelState,
} from '../embeddable';
import {
ContainerOutput,
ErrorEmbeddable,
ViewMode,
isErrorEmbeddable,
} from '../../embeddable_plugin';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from '../dashboard_empty_screen';
import { convertSavedDashboardPanelToPanelState } from '../lib/embeddable_saved_object_converters';
import { useDashboardAppState } from '../utils/use/use_dashboard_app_state';

export const DashboardEditor = () => {
return <div>Dashboard Editor</div>;
const { id: dashboardIdFromUrl } = useParams<{ id: string }>();
const { services } = useOpenSearchDashboards<DashboardServices>();
const { embeddable, data, dashboardConfig, embeddableCapabilities, uiSettings, http } = services;
const { query: queryService } = data;
const { visualizeCapabilities, mapsCapabilities } = embeddableCapabilities;
const timefilter = queryService.timefilter.timefilter;
const isChromeVisible = useChromeVisibility(services.chrome);
const [eventEmitter] = useState(new EventEmitter());

const { savedDashboardInstance } = useSavedDashboardInstance(
services,
eventEmitter,
isChromeVisible,
dashboardIdFromUrl
);

const { appState } = useDashboardAppState(services, eventEmitter, savedDashboardInstance);

const appStateData = appState?.get();
if (!appStateData) {
return null;
}
let dashboardContainer: DashboardContainer | undefined;
let inputSubscription: Subscription | undefined;
let outputSubscription: Subscription | undefined;

const dashboardDom = document.getElementById('dashboardViewport');
const dashboardFactory = embeddable.getEmbeddableFactory<
DashboardContainerInput,
ContainerOutput,
DashboardContainer
>(DASHBOARD_CONTAINER_TYPE);

const getShouldShowEditHelp = () => {
return (
!appStateData.panels.length &&
appStateData.viewMode === ViewMode.EDIT &&
!dashboardConfig.getHideWriteControls()
);
};

const getShouldShowViewHelp = () => {
return (
!appStateData.panels.length &&
appStateData.viewMode === ViewMode.VIEW &&
!dashboardConfig.getHideWriteControls()
);
};

const shouldShowUnauthorizedEmptyState = () => {
const readonlyMode =
!appStateData.panels.length &&
!getShouldShowEditHelp() &&
!getShouldShowViewHelp() &&
dashboardConfig.getHideWriteControls();
const userHasNoPermissions =
!appStateData.panels.length && !visualizeCapabilities.save && !mapsCapabilities.save;
return readonlyMode || userHasNoPermissions;
};

const getEmptyScreenProps = (
shouldShowEditHelp: boolean,
isEmptyInReadOnlyMode: boolean
): DashboardEmptyScreenProps => {
const emptyScreenProps: DashboardEmptyScreenProps = {
onLinkClick: () => {}, // TODO
showLinkToVisualize: shouldShowEditHelp,
uiSettings,
http,
};
if (shouldShowEditHelp) {
emptyScreenProps.onVisualizeClick = () => {
alert('click'); // TODO
};
}
if (isEmptyInReadOnlyMode) {
emptyScreenProps.isReadonlyMode = true;
}
return emptyScreenProps;
};

const getDashboardInput = () => {
const embeddablesMap: {
[key: string]: DashboardPanelState;
} = {};
appStateData.panels.forEach((panel: SavedDashboardPanel) => {
embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel);
});

const lastReloadRequestTime = 0;
return {
id: savedDashboardInstance.id || '',
filters: appStateData.filters,
hidePanelTitles: appStateData?.options.hidePanelTitles,
query: appStateData.query,
timeRange: {
..._.cloneDeep(timefilter.getTime()),
},
refreshConfig: timefilter.getRefreshInterval(),
viewMode: appStateData.viewMode,
panels: embeddablesMap,
isFullScreenMode: appStateData?.fullScreenMode,
isEmbeddedExternally: false, // TODO
// isEmptyState: shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadonlyMode,
isEmptyState: false, // TODO
useMargins: appStateData.options.useMargins,
lastReloadRequestTime, // TODO
title: appStateData.title,
description: appStateData.description,
expandedPanelId: appStateData.expandedPanelId,
};
};

if (dashboardFactory) {
dashboardFactory
.create(getDashboardInput())
.then((container: DashboardContainer | ErrorEmbeddable | undefined) => {
if (container && !isErrorEmbeddable(container)) {
dashboardContainer = container;

dashboardContainer.renderEmpty = () => {
const shouldShowEditHelp = getShouldShowEditHelp();
const shouldShowViewHelp = getShouldShowViewHelp();
const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState();
const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode;
return isEmptyState ? (
<DashboardEmptyScreen
{...getEmptyScreenProps(shouldShowEditHelp, isEmptyInReadOnlyMode)}
/>
) : null;
};

outputSubscription = merge(
// output of dashboard container itself
dashboardContainer.getOutput$(),
// plus output of dashboard container children,
// children may change, so make sure we subscribe/unsubscribe with switchMap
dashboardContainer.getOutput$().pipe(
map(() => dashboardContainer!.getChildIds()),
distinctUntilChanged(deepEqual),
switchMap((newChildIds: string[]) =>
merge(
...newChildIds.map((childId) =>
dashboardContainer!
.getChild(childId)
.getOutput$()
.pipe(catchError(() => EMPTY))
)
)
)
)
)
.pipe(
mapTo(dashboardContainer),
startWith(dashboardContainer) // to trigger initial index pattern update
// updateIndexPatternsOperator //TODO
)
.subscribe();

inputSubscription = dashboardContainer.getInput$().subscribe(() => {});

if (dashboardDom && container) {
container.render(dashboardDom);
}
}
});
}

return (
<div>
{savedDashboardInstance && appState && (
<DashboardTopNav
isChromeVisible={isChromeVisible}
savedDashboardInstance={savedDashboardInstance}
appState={appState}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,115 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { memo, useState, useEffect } from 'react';
import { Filter } from 'src/plugins/data/public';
import { useCallback } from 'react';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { getTopNavConfig } from '../top_nav/get_top_nav_config';
import { DashboardAppState, DashboardServices, NavAction } from '../../types';

export const DashboardTopNav = () => {
return <div>Dashboard Top Nav</div>;
interface DashboardTopNavProps {
isChromeVisible: boolean;
savedDashboardInstance: any;
appState: DashboardAppState;
}

enum UrlParams {
SHOW_TOP_MENU = 'show-top-menu',
SHOW_QUERY_INPUT = 'show-query-input',
SHOW_TIME_FILTER = 'show-time-filter',
SHOW_FILTER_BAR = 'show-filter-bar',
HIDE_FILTER_BAR = 'hide-filter-bar',
}

const TopNav = ({ isChromeVisible, savedDashboardInstance, appState }: DashboardTopNavProps) => {
const [filters, setFilters] = useState<Filter[]>([]);
const [topNavMenu, setTopNavMenu] = useState<any>();
const [isFullScreenMode, setIsFullScreenMode] = useState<any>();

const { services } = useOpenSearchDashboards<DashboardServices>();
const { TopNavMenu } = services.navigation.ui;
const { data, dashboardConfig, setHeaderActionMenu } = services;
const { query: queryService } = data;

// TODO: this should base on URL
const isEmbeddedExternally = false;

// TODO: should use URL params
const shouldForceDisplay = (param: string): boolean => {
// const [searchParams] = useSearchParams();
return false;
};

const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
(forceShow || isChromeVisible) && !appState?.fullScreenMode;

useEffect(() => {
setFilters(queryService.filterManager.getFilters());
}, [services, queryService]);

useEffect(() => {
const navActions: {
[key: string]: NavAction;
} = {}; // TODO: need to implement nav actions
setTopNavMenu(
getTopNavConfig(appState?.viewMode, navActions, dashboardConfig.getHideWriteControls())
);
}, [appState, services, dashboardConfig]);

useEffect(() => {
setIsFullScreenMode(appState?.fullScreenMode);
}, [appState, services]);

const shouldShowFilterBar = (forceHide: boolean): boolean =>
!forceHide && (filters!.length > 0 || !appState?.fullScreenMode);

const forceShowTopNavMenu = shouldForceDisplay(UrlParams.SHOW_TOP_MENU);
const forceShowQueryInput = shouldForceDisplay(UrlParams.SHOW_QUERY_INPUT);
const forceShowDatePicker = shouldForceDisplay(UrlParams.SHOW_TIME_FILTER);
const forceHideFilterBar = shouldForceDisplay(UrlParams.HIDE_FILTER_BAR);
const showTopNavMenu = shouldShowNavBarComponent(forceShowTopNavMenu);
const showQueryInput = shouldShowNavBarComponent(forceShowQueryInput);
const showDatePicker = shouldShowNavBarComponent(forceShowDatePicker);
const showQueryBar = showQueryInput || showDatePicker;
const showFilterBar = shouldShowFilterBar(forceHideFilterBar);
const showSearchBar = showQueryBar || showFilterBar;

// TODO: implement handleRefresh
const handleRefresh = useCallback((_payload: any, isUpdate?: boolean) => {
/* if (isUpdate === false) {
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
lastReloadRequestTime = new Date().getTime();
const changes = getChangesFromAppStateForContainerState();
if (changes && dashboardContainer) {
dashboardContainer.updateInput(changes);
}*/
}, []);

return isChromeVisible ? (
<TopNavMenu
appName={'dashboard'}
savedQueryId={appState?.savedQuery}
config={showTopNavMenu ? topNavMenu : undefined}
className={isFullScreenMode ? 'osdTopNavMenu-isFullScreen' : undefined}
screenTitle={appState.title}
// showTopNavMenu={showTopNavMenu}
showSearchBar={showSearchBar}
showQueryBar={showQueryBar}
showQueryInput={showQueryInput}
showDatePicker={showDatePicker}
showFilterBar={showFilterBar}
useDefaultBehaviors={true}
indexPatterns={[]}
showSaveQuery={services.dashboardCapabilities.saveQuery as boolean}
savedQuery={undefined}
onSavedQueryIdChange={() => {}}
onQuerySubmit={handleRefresh}
setMenuMountPoint={isEmbeddedExternally ? undefined : setHeaderActionMenu}
/>
) : null;
};

export const DashboardTopNav = memo(TopNav);
Loading

0 comments on commit 9c23f76

Please sign in to comment.