Skip to content

Commit

Permalink
Merge branch 'workspace' into feat-object-acl
Browse files Browse the repository at this point in the history
  • Loading branch information
raintygao authored Aug 18, 2023
2 parents ddee6a9 + d732833 commit d9275a0
Show file tree
Hide file tree
Showing 20 changed files with 360 additions and 126 deletions.
7 changes: 5 additions & 2 deletions src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { FormattedMessage } from '@osd/i18n/react';
import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs';
import { flatMap, map, takeUntil } from 'rxjs/operators';
import { EuiLink } from '@elastic/eui';
import { mountReactNode } from '../utils/mount';
import { mountReactNode } from '../utils';
import { InternalApplicationStart } from '../application';
import { DocLinksStart } from '../doc_links';
import { HttpStart } from '../http';
Expand All @@ -48,7 +48,7 @@ import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links';
import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed';
import { Header } from './ui';
import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu';
import { Branding } from '../';
import { Branding, WorkspaceStart } from '../';
export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle };

const IS_LOCKED_KEY = 'core.chrome.isLocked';
Expand Down Expand Up @@ -99,6 +99,7 @@ interface StartDeps {
injectedMetadata: InjectedMetadataStart;
notifications: NotificationsStart;
uiSettings: IUiSettingsClient;
workspaces: WorkspaceStart;
}

/** @internal */
Expand Down Expand Up @@ -152,6 +153,7 @@ export class ChromeService {
injectedMetadata,
notifications,
uiSettings,
workspaces,
}: StartDeps): Promise<InternalChromeStart> {
this.initVisibility(application);

Expand Down Expand Up @@ -262,6 +264,7 @@ export class ChromeService {
isLocked$={getIsNavDrawerLocked$}
branding={injectedMetadata.getBranding()}
survey={injectedMetadata.getSurvey()}
workspaces={workspaces}
/>
),

Expand Down
4 changes: 4 additions & 0 deletions src/core/public/chrome/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@
export const OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK = 'https://forum.opensearch.org/';
export const GITHUB_CREATE_ISSUE_LINK =
'https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose';

export const WORKSPACE_CREATE_APP_ID = 'workspace_create';
export const WORKSPACE_LIST_APP_ID = 'workspace_list';
export const WORKSPACE_OVERVIEW_APP_ID = 'workspace_overview';
3 changes: 3 additions & 0 deletions src/core/public/chrome/ui/header/collapsible_nav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..';
import { httpServiceMock } from '../../../http/http_service.mock';
import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed';
import { CollapsibleNav } from './collapsible_nav';
import { workspacesServiceMock } from '../../../workspace/workspaces_service.mock';

jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => 'mockId',
Expand Down Expand Up @@ -79,6 +80,8 @@ function mockProps() {
closeNav: () => {},
navigateToApp: () => Promise.resolve(),
navigateToUrl: () => Promise.resolve(),
getUrlForApp: jest.fn(),
workspaces: workspacesServiceMock.createStartContract(),
customNavLink$: new BehaviorSubject(undefined),
branding: {
darkMode: false,
Expand Down
12 changes: 12 additions & 0 deletions src/core/public/chrome/ui/header/collapsible_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ import { groupBy, sortBy } from 'lodash';
import React, { useRef } from 'react';
import useObservable from 'react-use/lib/useObservable';
import * as Rx from 'rxjs';
import { WorkspaceStart } from 'opensearch-dashboards/public';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..';
import { AppCategory } from '../../../../types';
import { InternalApplicationStart } from '../../../application';
import { HttpStart } from '../../../http';
import { OnIsLockedUpdate } from './';
import { createEuiListItem, isModifiedOrPrevented, createRecentNavLink } from './nav_link';
import { ChromeBranding } from '../../chrome_service';
import { CollapsibleNavHeader } from './collapsible_nav_header';

function getAllCategories(allCategorizedLinks: Record<string, ChromeNavLink[]>) {
const allCategories = {} as Record<string, AppCategory | undefined>;
Expand Down Expand Up @@ -121,10 +123,12 @@ interface Props {
storage?: Storage;
onIsLockedUpdate: OnIsLockedUpdate;
closeNav: () => void;
getUrlForApp: InternalApplicationStart['getUrlForApp'];
navigateToApp: InternalApplicationStart['navigateToApp'];
navigateToUrl: InternalApplicationStart['navigateToUrl'];
customNavLink$: Rx.Observable<ChromeNavLink | undefined>;
branding: ChromeBranding;
workspaces: WorkspaceStart;
}

export function CollapsibleNav({
Expand All @@ -136,9 +140,11 @@ export function CollapsibleNav({
storage = window.localStorage,
onIsLockedUpdate,
closeNav,
getUrlForApp,
navigateToApp,
navigateToUrl,
branding,
workspaces,
...observables
}: Props) {
const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden);
Expand Down Expand Up @@ -215,6 +221,12 @@ export function CollapsibleNav({
outsideClickCloses={false}
>
<EuiFlexItem className="eui-yScroll">
<CollapsibleNavHeader
getUrlForApp={getUrlForApp}
workspaces={workspaces}
basePath={basePath}
/>

{/* Recently viewed */}
<EuiCollapsibleNavGroup
key="recentlyViewed"
Expand Down
237 changes: 237 additions & 0 deletions src/core/public/chrome/ui/header/collapsible_nav_header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { i18n } from '@osd/i18n';
import React, { useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import {
EuiContextMenu,
EuiPopover,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiCollapsibleNavGroup,
} from '@elastic/eui';
import {
HttpStart,
WorkspaceStart,
WorkspaceAttribute,
MANAGEMENT_WORKSPACE,
} from '../../../../public';
import { InternalApplicationStart } from '../../../application';
import { formatUrlWithWorkspaceId } from '../../../utils';
import {
WORKSPACE_CREATE_APP_ID,
WORKSPACE_LIST_APP_ID,
WORKSPACE_OVERVIEW_APP_ID,
} from '../../constants';

interface Props {
workspaces: WorkspaceStart;
basePath: HttpStart['basePath'];
getUrlForApp: InternalApplicationStart['getUrlForApp'];
}

function getFilteredWorkspaceList(
workspaceList: WorkspaceAttribute[],
currentWorkspace: WorkspaceAttribute | null
): WorkspaceAttribute[] {
// list top5 workspaces except management workspace, place current workspace at the top
return [
...(currentWorkspace ? [currentWorkspace] : []),
...workspaceList.filter(
(workspace) => workspace.id !== MANAGEMENT_WORKSPACE && workspace.id !== currentWorkspace?.id
),
].slice(0, 5);
}

export function CollapsibleNavHeader({ workspaces, getUrlForApp, basePath }: Props) {
const workspaceEnabled = useObservable(workspaces.workspaceEnabled$, false);
const workspaceList = useObservable(workspaces.workspaceList$, []);
const currentWorkspace = useObservable(workspaces.currentWorkspace$, null);
const filteredWorkspaceList = getFilteredWorkspaceList(workspaceList, currentWorkspace);
const defaultHeaderName = i18n.translate(
'core.ui.primaryNav.workspacePickerMenu.defaultHeaderName',
{
defaultMessage: 'OpenSearch Analytics',
}
);
const managementWorkspaceName =
workspaceList.find((workspace) => workspace.id === MANAGEMENT_WORKSPACE)?.name ??
i18n.translate('core.ui.primaryNav.workspacePickerMenu.managementWorkspaceName', {
defaultMessage: 'Management',
});
const currentWorkspaceName = currentWorkspace?.name ?? defaultHeaderName;
const [isPopoverOpen, setPopover] = useState(false);

if (!workspaceEnabled) {
return (
<EuiCollapsibleNavGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiIcon type="logoOpenSearch" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<strong> {defaultHeaderName} </strong>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCollapsibleNavGroup>
);
}
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};

const closePopover = () => {
setPopover(false);
};

const workspaceToItem = (workspace: WorkspaceAttribute, index: number) => {
const href = formatUrlWithWorkspaceId(
getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, {
absolute: false,
}),
workspace.id,
basePath
);
const name =
currentWorkspace !== null && index === 0 ? (
<EuiText>
<strong> {workspace.name} </strong>
</EuiText>
) : (
workspace.name
);
return {
href,
name,
key: index.toString(),
icon: <EuiIcon type="stopFilled" color={workspace.color ?? 'primary'} />,
};
};

const getWorkspaceListItems = () => {
const workspaceListItems = filteredWorkspaceList.map((workspace, index) =>
workspaceToItem(workspace, index)
);
const length = workspaceListItems.length;
workspaceListItems.push({
icon: <EuiIcon type="plus" />,
name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.createWorkspace', {
defaultMessage: 'Create workspace',
}),
key: length.toString(),
href: formatUrlWithWorkspaceId(
getUrlForApp(WORKSPACE_CREATE_APP_ID, {
absolute: false,
}),
currentWorkspace?.id ?? '',
basePath
),
});
workspaceListItems.push({
icon: <EuiIcon type="folderClosed" />,
name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.allWorkspace', {
defaultMessage: 'All workspaces',
}),
key: (length + 1).toString(),
href: formatUrlWithWorkspaceId(
getUrlForApp(WORKSPACE_LIST_APP_ID, {
absolute: false,
}),
currentWorkspace?.id ?? '',
basePath
),
});
return workspaceListItems;
};

const currentWorkspaceButton = (
<EuiCollapsibleNavGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiIcon type="logoOpenSearch" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<strong> {currentWorkspaceName} </strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiIcon type="arrowDown" onClick={onButtonClick} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiCollapsibleNavGroup>
);

const currentWorkspaceTitle = (
<EuiFlexGroup>
<EuiFlexItem>
<EuiIcon type="logoOpenSearch" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<strong> {currentWorkspaceName} </strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiIcon type="cross" onClick={closePopover} />
</EuiFlexItem>
</EuiFlexGroup>
);

const panels = [
{
id: 0,
title: currentWorkspaceTitle,
items: [
{
name: (
<EuiText>
<strong>
{i18n.translate('core.ui.primaryNav.workspacePickerMenu.workspaceList', {
defaultMessage: 'Workspaces',
})}
</strong>
</EuiText>
),
icon: 'folderClosed',
panel: 1,
},
{
name: managementWorkspaceName,
icon: 'managementApp',
href: formatUrlWithWorkspaceId(
getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, {
absolute: false,
}),
MANAGEMENT_WORKSPACE,
basePath
),
},
],
},
{
id: 1,
title: 'Workspaces',
items: getWorkspaceListItems(),
},
];

return (
<EuiPopover
id="contextMenuExample"
button={currentWorkspaceButton}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
3 changes: 2 additions & 1 deletion src/core/public/chrome/ui/header/header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { act } from 'react-dom/test-utils';
import { BehaviorSubject } from 'rxjs';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { httpServiceMock } from '../../../http/http_service.mock';
import { applicationServiceMock } from '../../../mocks';
import { applicationServiceMock, workspacesServiceMock } from '../../../mocks';
import { Header } from './header';
import { StubBrowserStorage } from 'test_utils/stub_browser_storage';

Expand Down Expand Up @@ -76,6 +76,7 @@ function mockProps() {
applicationTitle: 'OpenSearch Dashboards',
},
survey: '/',
workspaces: workspacesServiceMock.createStartContract(),
};
}

Expand Down
Loading

0 comments on commit d9275a0

Please sign in to comment.