From 7f9bdd739ea4a90e161e1efd512b053872d8b6c2 Mon Sep 17 00:00:00 2001
From: yuboluo
Date: Sat, 20 Jul 2024 13:34:43 +0800
Subject: [PATCH 01/10] [Workspace] Refactor the UI of workspace picker (#7045)
* Refactor the UI of workspace picker
Signed-off-by: yubonluo
* Changeset file for PR #7045 created/updated
* Optimize the code
Signed-off-by: yubonluo
* Optimize the code
Signed-off-by: yubonluo
* Optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* support suggested workspace
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* delete search and use case menu
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* delete useless code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* add timestamp
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimzie the code
Signed-off-by: yubonluo
* optimzie the code
Signed-off-by: yubonluo
* fix unit test error
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
* Replace menuContext with listGroup
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
---------
Signed-off-by: yubonluo
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7045.yml | 2 +
src/core/public/chrome/index.ts | 6 +-
.../public/chrome/recently_accessed/index.ts | 2 +
src/core/public/index.ts | 2 +
src/plugins/workspace/common/constants.ts | 2 +
.../workspace_menu/workspace_menu.test.tsx | 159 +++++---
.../workspace_menu/workspace_menu.tsx | 373 +++++++++++-------
src/plugins/workspace/public/plugin.test.ts | 17 +-
src/plugins/workspace/public/plugin.ts | 27 +-
.../public/recent_workspace_manager.test.ts | 33 ++
.../public/recent_workspace_manager.ts | 51 +++
11 files changed, 471 insertions(+), 203 deletions(-)
create mode 100644 changelogs/fragments/7045.yml
create mode 100644 src/plugins/workspace/public/recent_workspace_manager.test.ts
create mode 100644 src/plugins/workspace/public/recent_workspace_manager.ts
diff --git a/changelogs/fragments/7045.yml b/changelogs/fragments/7045.yml
new file mode 100644
index 000000000000..5e1d9e73addb
--- /dev/null
+++ b/changelogs/fragments/7045.yml
@@ -0,0 +1,2 @@
+feat:
+- [Workspace] Refactor the UI of workspace picker ([#7045](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7045))
\ No newline at end of file
diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts
index f49e0e7efe80..02100ce4f63a 100644
--- a/src/core/public/chrome/index.ts
+++ b/src/core/public/chrome/index.ts
@@ -46,7 +46,11 @@ export {
} from './ui/header/header_help_menu';
export { NavType, RightNavigationButton, RightNavigationButtonProps } from './ui';
export { ChromeNavLink, ChromeNavLinks, ChromeNavLinkUpdateableFields } from './nav_links';
-export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem } from './recently_accessed';
+export {
+ ChromeRecentlyAccessed,
+ ChromeRecentlyAccessedHistoryItem,
+ PersistedLog,
+} from './recently_accessed';
export { ChromeNavControl, ChromeNavControls } from './nav_controls';
export { ChromeDocTitle } from './doc_title';
export { RightNavigationOrder } from './constants';
diff --git a/src/core/public/chrome/recently_accessed/index.ts b/src/core/public/chrome/recently_accessed/index.ts
index 8bb2e306b221..9fb34b520470 100644
--- a/src/core/public/chrome/recently_accessed/index.ts
+++ b/src/core/public/chrome/recently_accessed/index.ts
@@ -33,3 +33,5 @@ export {
ChromeRecentlyAccessedHistoryItem,
RecentlyAccessedService,
} from './recently_accessed_service';
+
+export { PersistedLog } from './persisted_log';
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 59da446d1fe9..8c387361a9ca 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -73,6 +73,7 @@ import {
RightNavigationButtonProps,
ChromeRegistrationNavLink,
ChromeNavGroupUpdater,
+ PersistedLog,
NavGroupItemInMap,
fulfillRegistrationLinksToChromeNavLinks,
} from './chrome';
@@ -375,6 +376,7 @@ export {
RightNavigationButtonProps,
ChromeRegistrationNavLink,
ChromeNavGroupUpdater,
+ PersistedLog,
NavGroupItemInMap,
fulfillRegistrationLinksToChromeNavLinks,
};
diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts
index bdf9aa8ddfc5..9fef78152d8d 100644
--- a/src/plugins/workspace/common/constants.ts
+++ b/src/plugins/workspace/common/constants.ts
@@ -182,4 +182,6 @@ export const WORKSPACE_USE_CASES = Object.freeze({
},
});
+export const MAX_WORKSPACE_PICKER_NUM = 3;
+export const RECENT_WORKSPACES_KEY = 'recentWorkspaces';
export const CURRENT_USER_PLACEHOLDER = '%me%';
diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
index 85ee89482724..d3578498c858 100644
--- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
+++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
@@ -9,18 +9,51 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { WorkspaceMenu } from './workspace_menu';
import { coreMock } from '../../../../../core/public/mocks';
import { CoreStart } from '../../../../../core/public';
+import { BehaviorSubject, of } from 'rxjs';
+import { IntlProvider } from 'react-intl';
+import { recentWorkspaceManager } from '../../recent_workspace_manager';
+import { WORKSPACE_USE_CASES } from '../../../common/constants';
+import * as workspaceUtils from '../utils/workspace';
describe('', () => {
let coreStartMock: CoreStart;
+ const navigateToApp = jest.fn();
+ const registeredUseCases$ = new BehaviorSubject([
+ WORKSPACE_USE_CASES.observability,
+ WORKSPACE_USE_CASES['security-analytics'],
+ WORKSPACE_USE_CASES.analytics,
+ WORKSPACE_USE_CASES.search,
+ ]);
beforeEach(() => {
coreStartMock = coreMock.createStart();
+ coreStartMock.application.capabilities = {
+ navLinks: {},
+ management: {},
+ catalogue: {},
+ savedObjectsManagement: {},
+ workspaces: { permissionEnabled: true },
+ dashboards: { isDashboardAdmin: true },
+ };
+ coreStartMock.application = {
+ ...coreStartMock.application,
+ navigateToApp,
+ };
+
coreStartMock.workspaces.initialized$.next(true);
jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => {
return `https://test.com/app/${appId}`;
});
});
+ const WorkspaceMenuCreatorComponent = () => {
+ return (
+
+
+
+ );
+ };
+
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
@@ -28,38 +61,69 @@ describe('', () => {
it('should display a list of workspaces in the dropdown', () => {
coreStartMock.workspaces.workspaceList$.next([
- { id: 'workspace-1', name: 'workspace 1' },
- { id: 'workspace-2', name: 'workspace 2' },
+ { id: 'workspace-1', name: 'workspace 1', features: [] },
+ { id: 'workspace-2', name: 'workspace 2', features: [] },
+ ]);
+
+ render();
+ const selectButton = screen.getByTestId('workspace-select-button');
+ fireEvent.click(selectButton);
+
+ expect(screen.getByText(/all workspaces/i)).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-all-workspace-1')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-all-workspace-2')).toBeInTheDocument();
+ });
+
+ it('should display a list of recent workspaces in the dropdown', () => {
+ jest.spyOn(recentWorkspaceManager, 'getRecentWorkspaces').mockReturnValue([
+ { id: 'workspace-1', timestamp: 1234567890 },
+ { id: 'workspace-2', timestamp: 1234567899 },
+ ]);
+
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: [] },
+ { id: 'workspace-2', name: 'workspace 2', features: [] },
]);
- render();
- fireEvent.click(screen.getByText(/select a workspace/i));
+ render();
+
+ const selectButton = screen.getByTestId('workspace-select-button');
+ fireEvent.click(selectButton);
- expect(screen.getByText(/workspace 1/i)).toBeInTheDocument();
- expect(screen.getByText(/workspace 2/i)).toBeInTheDocument();
+ expect(screen.getByText(/recent workspaces/i)).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-recent-workspace-1')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-recent-workspace-2')).toBeInTheDocument();
});
- it('should display current workspace name', () => {
- coreStartMock.workspaces.currentWorkspace$.next({ id: 'workspace-1', name: 'workspace 1' });
- render();
- expect(screen.getByText(/workspace 1/i)).toBeInTheDocument();
+ it('should display current workspace name and use case name', () => {
+ coreStartMock.workspaces.currentWorkspace$.next({
+ id: 'workspace-1',
+ name: 'workspace 1',
+ features: ['use-case-observability'],
+ });
+ render();
+
+ fireEvent.click(screen.getByTestId('current-workspace-button'));
+ expect(screen.getByTestId('workspace-menu-current-workspace-name')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-current-use-case')).toBeInTheDocument();
+ expect(screen.getByText('Observability')).toBeInTheDocument();
});
it('should close the workspace dropdown list', async () => {
- render();
- fireEvent.click(screen.getByText(/select a workspace/i));
+ render();
+
+ fireEvent.click(screen.getByTestId('workspace-select-button'));
- expect(screen.getByLabelText(/close workspace dropdown/i)).toBeInTheDocument();
- fireEvent.click(screen.getByLabelText(/close workspace dropdown/i));
+ expect(screen.getByText(/all workspaces/i)).toBeInTheDocument();
+ fireEvent.click(screen.getByTestId('workspace-select-button'));
await waitFor(() => {
- expect(screen.queryByLabelText(/close workspace dropdown/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/all workspaces/i)).not.toBeInTheDocument();
});
});
it('should navigate to the workspace', () => {
coreStartMock.workspaces.workspaceList$.next([
- { id: 'workspace-1', name: 'workspace 1' },
- { id: 'workspace-2', name: 'workspace 2' },
+ { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
]);
const originalLocation = window.location;
@@ -69,12 +133,12 @@ describe('', () => {
},
});
- render();
- fireEvent.click(screen.getByText(/select a workspace/i));
+ render();
+ fireEvent.click(screen.getByTestId('workspace-select-button'));
fireEvent.click(screen.getByText(/workspace 1/i));
expect(window.location.assign).toHaveBeenCalledWith(
- 'https://test.com/w/workspace-1/app/workspace_detail'
+ 'https://test.com/w/workspace-1/app/discover'
);
Object.defineProperty(window, 'location', {
@@ -82,39 +146,40 @@ describe('', () => {
});
});
- it('should navigate to create workspace page', () => {
- const originalLocation = window.location;
- Object.defineProperty(window, 'location', {
- value: {
- assign: jest.fn(),
- },
+ it('should navigate to workspace management page', () => {
+ coreStartMock.workspaces.currentWorkspace$.next({
+ id: 'workspace-1',
+ name: 'workspace 1',
+ features: ['use-case-observability'],
});
+ const navigateToWorkspaceDetail = jest.spyOn(workspaceUtils, 'navigateToWorkspaceDetail');
+ render();
- render();
- fireEvent.click(screen.getByText(/select a workspace/i));
- fireEvent.click(screen.getByText(/create workspace/i));
- expect(window.location.assign).toHaveBeenCalledWith('https://test.com/app/workspace_create');
+ fireEvent.click(screen.getByTestId('current-workspace-button'));
+ const button = screen.getByText(/Manage workspace/i);
+ fireEvent.click(button);
+ expect(navigateToWorkspaceDetail).toBeCalled();
+ });
- Object.defineProperty(window, 'location', {
- value: originalLocation,
- });
+ it('should navigate to workspaces management page', () => {
+ render();
+ fireEvent.click(screen.getByTestId('workspace-select-button'));
+ fireEvent.click(screen.getByText(/manage workspaces/i));
+ expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_list');
});
- it('should navigate to workspace list page', () => {
- const originalLocation = window.location;
- Object.defineProperty(window, 'location', {
- value: {
- assign: jest.fn(),
- },
- });
+ it('should navigate to create workspace page', () => {
+ render();
+ fireEvent.click(screen.getByTestId('workspace-select-button'));
+ fireEvent.click(screen.getByText(/create workspace/i));
+ expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_create');
+ });
- render();
- fireEvent.click(screen.getByText(/select a workspace/i));
- fireEvent.click(screen.getByText(/all workspace/i));
- expect(window.location.assign).toHaveBeenCalledWith('https://test.com/app/workspace_list');
+ it('should navigate to workspace list page', () => {
+ render();
- Object.defineProperty(window, 'location', {
- value: originalLocation,
- });
+ fireEvent.click(screen.getByTestId('workspace-select-button'));
+ fireEvent.click(screen.getByText(/View all/i));
+ expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_list');
});
});
diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
index cd6b147b90fe..bda11fb3d113 100644
--- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
+++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
@@ -4,61 +4,96 @@
*/
import { i18n } from '@osd/i18n';
-import React, { useState } from 'react';
+import React, { useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import {
- EuiSmallButtonIcon,
- EuiContextMenu,
- EuiFlexGroup,
+ EuiText,
+ EuiPanel,
+ EuiTitle,
+ EuiAvatar,
+ EuiButton,
+ EuiPopover,
+ EuiToolTip,
EuiFlexItem,
- EuiIcon,
+ EuiFlexGroup,
EuiListGroup,
+ EuiButtonIcon,
+ EuiButtonEmpty,
EuiListGroupItem,
- EuiPopover,
- EuiText,
} from '@elastic/eui';
-import type { EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
-
+import { BehaviorSubject } from 'rxjs';
import {
WORKSPACE_CREATE_APP_ID,
WORKSPACE_LIST_APP_ID,
+ MAX_WORKSPACE_PICKER_NUM,
WORKSPACE_DETAIL_APP_ID,
} from '../../../common/constants';
-import { cleanWorkspaceId, formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
+import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
import { CoreStart, WorkspaceObject } from '../../../../../core/public';
+import { getFirstUseCaseOfFeatureConfigs } from '../../utils';
+import { recentWorkspaceManager } from '../../recent_workspace_manager';
+import { WorkspaceUseCase } from '../../types';
+import { navigateToWorkspaceDetail } from '../utils/workspace';
+
+const defaultHeaderName = i18n.translate('workspace.menu.defaultHeaderName', {
+ defaultMessage: 'Workspaces',
+});
+
+const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces', {
+ defaultMessage: 'All workspaces',
+});
+
+const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', {
+ defaultMessage: 'recent workspaces',
+});
+
+const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorkspace', {
+ defaultMessage: 'Create workspace',
+});
+
+const viewAllButton = i18n.translate('workspace.menu.button.viewAll', {
+ defaultMessage: 'View all',
+});
+
+const manageWorkspaceButton = i18n.translate('workspace.menu.button.manageWorkspace', {
+ defaultMessage: 'Manage workspace',
+});
+
+const manageWorkspacesButton = i18n.translate('workspace.menu.button.manageWorkspaces', {
+ defaultMessage: 'Manage workspaces',
+});
interface Props {
coreStart: CoreStart;
+ registeredUseCases$: BehaviorSubject;
}
-/**
- * Return maximum five workspaces, the current selected workspace
- * will be on the top of the list.
- */
-function getFilteredWorkspaceList(
- workspaceList: WorkspaceObject[],
- currentWorkspace: WorkspaceObject | null
-): WorkspaceObject[] {
- return [
- ...(currentWorkspace ? [currentWorkspace] : []),
- ...workspaceList.filter((workspace) => workspace.id !== currentWorkspace?.id),
- ].slice(0, 5);
-}
-
-export const WorkspaceMenu = ({ coreStart }: Props) => {
+export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
const [isPopoverOpen, setPopover] = useState(false);
const currentWorkspace = useObservable(coreStart.workspaces.currentWorkspace$, null);
const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []);
+ const isDashboardAdmin = !!coreStart.application.capabilities.dashboards;
+ const availableUseCases = useObservable(registeredUseCases$, []);
+
+ const filteredWorkspaceList = useMemo(() => {
+ return workspaceList.slice(0, MAX_WORKSPACE_PICKER_NUM);
+ }, [workspaceList]);
+
+ const filteredRecentWorkspaces = useMemo(() => {
+ return recentWorkspaceManager
+ .getRecentWorkspaces()
+ .map((workspace) => workspaceList.find((ws) => ws.id === workspace.id))
+ .filter((workspace): workspace is WorkspaceObject => workspace !== undefined)
+ .slice(0, MAX_WORKSPACE_PICKER_NUM);
+ }, [workspaceList]);
- const defaultHeaderName = i18n.translate(
- 'core.ui.primaryNav.workspacePickerMenu.defaultHeaderName',
- {
- defaultMessage: 'Select a workspace',
- }
- );
- const filteredWorkspaceList = getFilteredWorkspaceList(workspaceList, currentWorkspace);
const currentWorkspaceName = currentWorkspace?.name ?? defaultHeaderName;
+ const getUseCase = (workspace: WorkspaceObject) => {
+ const useCaseId = getFirstUseCaseOfFeatureConfigs(workspace?.features!);
+ return availableUseCases.find((useCase) => useCase.id === useCaseId);
+ };
+
const openPopover = () => {
setPopover(!isPopoverOpen);
};
@@ -67,113 +102,78 @@ export const WorkspaceMenu = ({ coreStart }: Props) => {
setPopover(false);
};
- const workspaceToItem = (workspace: WorkspaceObject) => {
- const workspaceURL = formatUrlWithWorkspaceId(
- coreStart.application.getUrlForApp(WORKSPACE_DETAIL_APP_ID, {
- absolute: false,
- }),
- workspace.id,
- coreStart.http.basePath
- );
- const name =
- currentWorkspace?.name === workspace.name ? (
-
- {workspace.name}
-
- ) : (
- workspace.name
- );
- return {
- name,
- key: workspace.id,
- icon: ,
- onClick: () => {
- window.location.assign(workspaceURL);
- },
- };
- };
-
- const getWorkspaceListItems = () => {
- const workspaceListItems: EuiContextMenuPanelItemDescriptor[] = filteredWorkspaceList.map(
- workspaceToItem
- );
- workspaceListItems.push({
- icon: ,
- name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.createWorkspace', {
- defaultMessage: 'Create workspace',
- }),
- key: WORKSPACE_CREATE_APP_ID,
- onClick: () => {
- window.location.assign(
- cleanWorkspaceId(
- coreStart.application.getUrlForApp(WORKSPACE_CREATE_APP_ID, {
- absolute: false,
- })
- )
- );
- },
- });
- workspaceListItems.push({
- icon: ,
- name: i18n.translate('core.ui.primaryNav.workspaceContextMenu.allWorkspace', {
- defaultMessage: 'All workspaces',
- }),
- key: WORKSPACE_LIST_APP_ID,
- onClick: () => {
- window.location.assign(
- cleanWorkspaceId(
- coreStart.application.getUrlForApp(WORKSPACE_LIST_APP_ID, {
- absolute: false,
- })
- )
- );
- },
- });
- return workspaceListItems;
- };
+ const currentWorkspaceButton = currentWorkspace ? (
+
+
+
+ ) : (
+
+ );
- const currentWorkspaceButton = (
- <>
-
+ const getWorkspaceListGroup = (filterWorkspaceList: WorkspaceObject[], itemType: string) => {
+ const listItems = filterWorkspaceList.map((workspace: WorkspaceObject) => {
+ const appId = getUseCase(workspace)?.features[0] ?? WORKSPACE_DETAIL_APP_ID;
+ const useCaseURL = formatUrlWithWorkspaceId(
+ coreStart.application.getUrlForApp(appId, {
+ absolute: false,
+ }),
+ workspace.id,
+ coreStart.http.basePath
+ );
+ return (
+ }
+ label={
+
+
+ {workspace.name}
+
+
+ }
+ onClick={() => {
+ window.location.assign(useCaseURL);
}}
/>
-
- >
- );
-
- const currentWorkspaceTitle = (
-
-
- {currentWorkspaceName}
-
-
-
-
-
- );
-
- const panels = [
- {
- id: 0,
- title: currentWorkspaceTitle,
- items: getWorkspaceListItems(),
- },
- ];
+ );
+ });
+ return (
+ <>
+
+ {itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}
+
+
+ {listItems}
+
+ >
+ );
+ };
return (
{
panelPaddingSize="none"
anchorPosition="downCenter"
>
-
+
+
+ {currentWorkspace ? (
+ <>
+
+
+
+
+
+
+ {currentWorkspaceName}
+
+
+
+
+ {getUseCase(currentWorkspace)?.title ?? ''}
+
+
+ {
+ navigateToWorkspaceDetail(coreStart, currentWorkspace.id);
+ }}
+ >
+ {manageWorkspaceButton}
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+ {currentWorkspaceName}
+
+
+ {
+ coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
+ }}
+ >
+ {manageWorkspacesButton}
+
+
+ >
+ )}
+
+
+
+ {getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')}
+ {getWorkspaceListGroup(filteredWorkspaceList, 'all')}
+
+
+
+
+ {
+ coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
+ }}
+ >
+ {viewAllButton}
+
+
+ {isDashboardAdmin && (
+
+ {
+ coreStart.application.navigateToApp(WORKSPACE_CREATE_APP_ID);
+ }}
+ >
+ {createWorkspaceButton}
+
+
+ )}
+
+
);
};
diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts
index d3fff9a3f577..b2ed55c08de6 100644
--- a/src/plugins/workspace/public/plugin.test.ts
+++ b/src/plugins/workspace/public/plugin.test.ts
@@ -149,15 +149,6 @@ describe('Workspace plugin', () => {
windowSpy.mockRestore();
});
- it('#setup register workspace dropdown menu when setup', async () => {
- const setupMock = coreMock.createSetup();
- const workspacePlugin = new WorkspacePlugin();
- await workspacePlugin.setup(setupMock, {
- management: managementPluginMock.createSetupContract(),
- });
- expect(setupMock.chrome.registerCollapsibleNavHeader).toBeCalledTimes(1);
- });
-
it('#setup should register workspace list with a visible application and register to settingsAndSetup nav group', async () => {
const setupMock = coreMock.createSetup();
setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
@@ -241,6 +232,14 @@ describe('Workspace plugin', () => {
});
});
+ it('#start register workspace dropdown menu at left navigation bottom when start', async () => {
+ const coreStart = coreMock.createStart();
+ coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
+ const workspacePlugin = new WorkspacePlugin();
+ workspacePlugin.start(coreStart);
+
+ expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1);
+ });
it('#start should not update systematic use case features after currentWorkspace set', async () => {
const registeredUseCases$ = new BehaviorSubject([
{
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index a2c84554205a..104db7d9b91f 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -43,6 +43,8 @@ import {
isAppAccessibleInWorkspace,
isNavGroupInFeatureConfigs,
} from './utils';
+import { recentWorkspaceManager } from './recent_workspace_manager';
+import { toMountPoint } from '../../opensearch_dashboards_react/public';
import { UseCaseService } from './services/use_case_service';
type WorkspaceAppType = (
@@ -262,6 +264,8 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
}
currentAppIdSubscription.unsubscribe();
});
+ // Add workspace id to recent workspaces.
+ recentWorkspaceManager.addRecentWorkspace(workspaceId);
})();
}
}
@@ -319,16 +323,6 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
},
});
- /**
- * Register workspace dropdown selector on the top of left navigation menu
- */
- core.chrome.registerCollapsibleNavHeader(() => {
- if (!this.coreStart) {
- return null;
- }
- return React.createElement(WorkspaceMenu, { coreStart: this.coreStart });
- });
-
// workspace list
core.application.register({
id: WORKSPACE_LIST_APP_ID,
@@ -398,6 +392,19 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
if (!core.chrome.navGroup.getNavGroupEnabled()) {
this.addWorkspaceToBreadcrumbs(core);
+ } else {
+ /**
+ * Register workspace dropdown selector on the left navigation bottom
+ */
+ core.chrome.navControls.registerLeftBottom({
+ order: 2,
+ mount: toMountPoint(
+ React.createElement(WorkspaceMenu, {
+ coreStart: core,
+ registeredUseCases$: this.registeredUseCases$,
+ })
+ ),
+ });
}
return {};
diff --git a/src/plugins/workspace/public/recent_workspace_manager.test.ts b/src/plugins/workspace/public/recent_workspace_manager.test.ts
new file mode 100644
index 000000000000..16ea6291faef
--- /dev/null
+++ b/src/plugins/workspace/public/recent_workspace_manager.test.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { RecentWorkspaceManager } from './recent_workspace_manager';
+
+describe('RecentWorkspaceManager', () => {
+ let recentWorkspaceManager: RecentWorkspaceManager;
+
+ beforeEach(() => {
+ recentWorkspaceManager = RecentWorkspaceManager.getInstance();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should be a singleton', () => {
+ const anotherInstance = RecentWorkspaceManager.getInstance();
+ expect(recentWorkspaceManager).toBe(anotherInstance);
+ });
+
+ it('should add and get recent workspaces', () => {
+ recentWorkspaceManager.addRecentWorkspace('workspace1');
+ recentWorkspaceManager.addRecentWorkspace('workspace2');
+
+ const recentWorkspaces = recentWorkspaceManager.getRecentWorkspaces();
+ expect(recentWorkspaces.length).toEqual(2);
+ expect(recentWorkspaces[0].id).toEqual('workspace2');
+ expect(recentWorkspaces[1].id).toEqual('workspace1');
+ });
+});
diff --git a/src/plugins/workspace/public/recent_workspace_manager.ts b/src/plugins/workspace/public/recent_workspace_manager.ts
new file mode 100644
index 000000000000..2bcadc72e1bd
--- /dev/null
+++ b/src/plugins/workspace/public/recent_workspace_manager.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { PersistedLog } from '../../../core/public';
+import { RECENT_WORKSPACES_KEY } from '../common/constants';
+
+export interface WorkspaceEntry {
+ id: string;
+ timestamp: number;
+}
+
+export class RecentWorkspaceManager {
+ private static instance: RecentWorkspaceManager;
+ private recentWorkspaceLog: PersistedLog;
+
+ private constructor() {
+ const customIsEqual = (oldItem: WorkspaceEntry, newItem: WorkspaceEntry) => {
+ return oldItem.id === newItem.id;
+ };
+ this.recentWorkspaceLog = new PersistedLog(RECENT_WORKSPACES_KEY, {
+ maxLength: 10,
+ isEqual: customIsEqual,
+ });
+ }
+
+ // Singleton pattern to ensure only one instance is used
+ public static getInstance(): RecentWorkspaceManager {
+ if (!RecentWorkspaceManager.instance) {
+ RecentWorkspaceManager.instance = new RecentWorkspaceManager();
+ }
+ return RecentWorkspaceManager.instance;
+ }
+
+ public getRecentWorkspaces(): WorkspaceEntry[] {
+ return this.recentWorkspaceLog.get();
+ }
+
+ public addRecentWorkspace(newWorkspace: string): WorkspaceEntry[] {
+ const newEntry: WorkspaceEntry = {
+ id: newWorkspace,
+ timestamp: Date.now(),
+ };
+ this.recentWorkspaceLog.add(newEntry);
+ return this.getRecentWorkspaces();
+ }
+}
+
+// Export the singleton instance
+export const recentWorkspaceManager = RecentWorkspaceManager.getInstance();
From 4c619067f2d1f85afd9ad3f67af15d8f42c3e4da Mon Sep 17 00:00:00 2001
From: Shenoy Pratik
Date: Sat, 20 Jul 2024 08:06:15 -0700
Subject: [PATCH 02/10] [Navigation] Update dev tools tab css for new left
navigation (#7328)
* update dev tools tab css
Signed-off-by: Shenoy Pratik
* Changeset file for PR #7328 created/updated
---------
Signed-off-by: Shenoy Pratik
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7328.yml | 2 ++
src/plugins/dev_tools/public/index.scss | 6 ------
2 files changed, 2 insertions(+), 6 deletions(-)
create mode 100644 changelogs/fragments/7328.yml
diff --git a/changelogs/fragments/7328.yml b/changelogs/fragments/7328.yml
new file mode 100644
index 000000000000..2e2f63275d8b
--- /dev/null
+++ b/changelogs/fragments/7328.yml
@@ -0,0 +1,2 @@
+fix:
+- [Navigation] Update dev tools tab css for new left navigation ([#7328](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7328))
\ No newline at end of file
diff --git a/src/plugins/dev_tools/public/index.scss b/src/plugins/dev_tools/public/index.scss
index 56d7bc9c11c1..554452a8923e 100644
--- a/src/plugins/dev_tools/public/index.scss
+++ b/src/plugins/dev_tools/public/index.scss
@@ -26,9 +26,3 @@
margin: 7px 8px 0 0;
min-width: 400px;
}
-
-.devAppTabs {
- display: flex;
- flex-flow: row wrap;
- justify-content: space-between;
-}
From 54cbaaa3e086cac4675ab733a91bc2d1abad8f5a Mon Sep 17 00:00:00 2001
From: "opensearch-trigger-bot[bot]"
<98922864+opensearch-trigger-bot[bot]@users.noreply.github.com>
Date: Sun, 21 Jul 2024 12:01:06 +0800
Subject: [PATCH 03/10] [navigation-next] feat: update category (#7339) (#7340)
* feat: update category
* Changeset file for PR #7339 created/updated
---------
(cherry picked from commit d4d1f7700b29a1b5af211585cf9338b8a2ba2748)
Signed-off-by: SuZhou-Joe
Signed-off-by: github-actions[bot]
Co-authored-by: github-actions[bot]
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7339.yml | 2 ++
src/core/utils/default_app_categories.ts | 21 +++++++++++++++------
2 files changed, 17 insertions(+), 6 deletions(-)
create mode 100644 changelogs/fragments/7339.yml
diff --git a/changelogs/fragments/7339.yml b/changelogs/fragments/7339.yml
new file mode 100644
index 000000000000..23f897df941d
--- /dev/null
+++ b/changelogs/fragments/7339.yml
@@ -0,0 +1,2 @@
+feat:
+- [navigation-next] update category ([#7339](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7339))
\ No newline at end of file
diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts
index d22dbaf1b7ac..f274cecf2cda 100644
--- a/src/core/utils/default_app_categories.ts
+++ b/src/core/utils/default_app_categories.ts
@@ -80,19 +80,20 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze
}),
order: 1000,
},
+ // TODO remove this default category
dashboardAndReport: {
id: 'visualizeAndReport',
label: i18n.translate('core.ui.visualizeAndReport.label', {
defaultMessage: 'Visualize and report',
}),
- order: 2000,
+ order: 3000,
},
visualizeAndReport: {
id: 'visualizeAndReport',
label: i18n.translate('core.ui.visualizeAndReport.label', {
defaultMessage: 'Visualize and report',
}),
- order: 2000,
+ order: 3000,
},
analyzeSearch: {
id: 'analyzeSearch',
@@ -101,12 +102,20 @@ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze
}),
order: 4000,
},
+ // TODO remove this default category
detect: {
- id: 'detect',
- label: i18n.translate('core.ui.detect.label', {
- defaultMessage: 'Detect',
+ id: 'configure',
+ label: i18n.translate('core.ui.configure.label', {
+ defaultMessage: 'Configure',
}),
- order: 3000,
+ order: 2000,
+ },
+ configure: {
+ id: 'configure',
+ label: i18n.translate('core.ui.configure.label', {
+ defaultMessage: 'Configure',
+ }),
+ order: 2000,
},
manage: {
id: 'manage',
From 08c2a006fa47b614215cf9364f7161fc55b2b429 Mon Sep 17 00:00:00 2001
From: yuboluo
Date: Sun, 21 Jul 2024 14:05:46 +0800
Subject: [PATCH 04/10] [Workspace] Register four get started cards in home
page (#7333)
* support get start card in home page
Signed-off-by: yubonluo
* Changeset file for PR #7333 created/updated
* fix unit test errors
Signed-off-by: yubonluo
* optimize the code
Signed-off-by: yubonluo
---------
Signed-off-by: yubonluo
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7333.yml | 2 +
.../card_container/card_container.tsx | 7 +-
.../card_container/card_embeddable.test.tsx | 9 +-
.../card_container/card_embeddable.tsx | 10 +-
.../public/components/page_render.tsx | 25 +-
.../public/components/section_input.ts | 2 +
.../public/components/section_render.tsx | 27 ++-
.../services/content_management/types.ts | 2 +
src/plugins/home/public/index.ts | 2 +
.../workspace/opensearch_dashboards.json | 4 +-
.../components/home_get_start_card/index.ts | 6 +
.../use_case_footer.test.tsx | 156 +++++++++++++
.../home_get_start_card/use_case_footer.tsx | 219 ++++++++++++++++++
.../workspace_menu/workspace_menu.test.tsx | 12 -
.../workspace_menu/workspace_menu.tsx | 12 +-
src/plugins/workspace/public/plugin.test.ts | 22 +-
src/plugins/workspace/public/plugin.ts | 52 ++++-
17 files changed, 522 insertions(+), 47 deletions(-)
create mode 100644 changelogs/fragments/7333.yml
create mode 100644 src/plugins/workspace/public/components/home_get_start_card/index.ts
create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx
create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx
diff --git a/changelogs/fragments/7333.yml b/changelogs/fragments/7333.yml
new file mode 100644
index 000000000000..09d225d51ca2
--- /dev/null
+++ b/changelogs/fragments/7333.yml
@@ -0,0 +1,2 @@
+feat:
+- [Workspace] Register four get started cards in home page ([#7333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7333))
\ No newline at end of file
diff --git a/src/plugins/content_management/public/components/card_container/card_container.tsx b/src/plugins/content_management/public/components/card_container/card_container.tsx
index 518734563607..f3784f1f5fc4 100644
--- a/src/plugins/content_management/public/components/card_container/card_container.tsx
+++ b/src/plugins/content_management/public/components/card_container/card_container.tsx
@@ -10,7 +10,12 @@ import { CardList } from './card_list';
export const CARD_CONTAINER = 'CARD_CONTAINER';
-export type CardContainerInput = ContainerInput<{ description: string; onClick?: () => void }>;
+export type CardContainerInput = ContainerInput<{
+ description: string;
+ onClick?: () => void;
+ getIcon?: () => React.ReactElement;
+ getFooter?: () => React.ReactElement;
+}>;
export class CardContainer extends Container<{}, CardContainerInput> {
public readonly type = CARD_CONTAINER;
diff --git a/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx b/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx
index b335bac6d996..a87cd43554ea 100644
--- a/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx
+++ b/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx
@@ -3,10 +3,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import React from 'react';
import { CardEmbeddable } from './card_embeddable';
test('CardEmbeddable should render a card with the title', () => {
- const embeddable = new CardEmbeddable({ id: 'card-id', title: 'card title', description: '' });
+ const embeddable = new CardEmbeddable({
+ id: 'card-id',
+ title: 'card title',
+ description: '',
+ getIcon: () => <>icon>,
+ getFooter: () => <>footer>,
+ });
const node = document.createElement('div');
embeddable.render(node);
diff --git a/src/plugins/content_management/public/components/card_container/card_embeddable.tsx b/src/plugins/content_management/public/components/card_container/card_embeddable.tsx
index 844cf13a777c..0e7b6b2c82e5 100644
--- a/src/plugins/content_management/public/components/card_container/card_embeddable.tsx
+++ b/src/plugins/content_management/public/components/card_container/card_embeddable.tsx
@@ -10,7 +10,12 @@ import { EuiCard } from '@elastic/eui';
import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public';
export const CARD_EMBEDDABLE = 'card_embeddable';
-export type CardEmbeddableInput = EmbeddableInput & { description: string; onClick?: () => void };
+export type CardEmbeddableInput = EmbeddableInput & {
+ description: string;
+ onClick?: () => void;
+ getIcon: () => React.ReactElement;
+ getFooter: () => React.ReactElement;
+};
export class CardEmbeddable extends Embeddable {
public readonly type = CARD_EMBEDDABLE;
@@ -27,10 +32,13 @@ export class CardEmbeddable extends Embeddable {
this.node = node;
ReactDOM.render(
,
node
);
diff --git a/src/plugins/content_management/public/components/page_render.tsx b/src/plugins/content_management/public/components/page_render.tsx
index 9a5211ca3a46..90d6033576bb 100644
--- a/src/plugins/content_management/public/components/page_render.tsx
+++ b/src/plugins/content_management/public/components/page_render.tsx
@@ -7,6 +7,7 @@ import React from 'react';
import { useObservable } from 'react-use';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Page } from '../services';
import { SectionRender } from './section_render';
import { EmbeddableStart } from '../../../embeddable/public';
@@ -21,16 +22,22 @@ export const PageRender = ({ page, embeddable, savedObjectsClient }: Props) => {
const sections = useObservable(page.getSections$()) || [];
return (
-
+
{sections.map((section) => (
-
+
+
+
))}
-
+
);
};
diff --git a/src/plugins/content_management/public/components/section_input.ts b/src/plugins/content_management/public/components/section_input.ts
index 1d37feef8ecc..00bb5b0683c7 100644
--- a/src/plugins/content_management/public/components/section_input.ts
+++ b/src/plugins/content_management/public/components/section_input.ts
@@ -42,6 +42,8 @@ export const createCardInput = (
title: content.title,
description: content.description,
onClick: content.onClick,
+ getIcon: content?.getIcon,
+ getFooter: content?.getFooter,
},
};
}
diff --git a/src/plugins/content_management/public/components/section_render.tsx b/src/plugins/content_management/public/components/section_render.tsx
index 19f14bdb1d67..d28fbad7296a 100644
--- a/src/plugins/content_management/public/components/section_render.tsx
+++ b/src/plugins/content_management/public/components/section_render.tsx
@@ -6,9 +6,8 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs';
-import { EuiTitle } from '@elastic/eui';
+import { EuiButtonIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
-
import { Content, Section } from '../services';
import { EmbeddableInput, EmbeddableRenderer, EmbeddableStart } from '../../../embeddable/public';
import { DashboardContainerInput } from '../../../dashboard/public';
@@ -49,6 +48,10 @@ const DashboardSection = ({ section, embeddable, contents$, savedObjectsClient }
};
const CardSection = ({ section, embeddable, contents$ }: Props) => {
+ const [isCardVisible, setIsCardVisible] = useState(true);
+ const toggleCardVisibility = () => {
+ setIsCardVisible(!isCardVisible);
+ };
const contents = useObservable(contents$);
const input = useMemo(() => {
return createCardInput(section, contents ?? []);
@@ -58,12 +61,24 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => {
if (section.kind === 'card' && factory && input) {
return (
-
+
- {section.title}
+
+
+ {section.title}
+
-
-
+ {isCardVisible && (
+ <>
+
+ >
+ )}
+
);
}
diff --git a/src/plugins/content_management/public/services/content_management/types.ts b/src/plugins/content_management/public/services/content_management/types.ts
index 0a0020ed6254..55da19f26b87 100644
--- a/src/plugins/content_management/public/services/content_management/types.ts
+++ b/src/plugins/content_management/public/services/content_management/types.ts
@@ -59,6 +59,8 @@ export type Content =
title: string;
description: string;
onClick?: () => void;
+ getIcon?: () => React.ReactElement;
+ getFooter?: () => React.ReactElement;
};
export type SavedObjectInput =
diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts
index 58ad10cdf04b..d252a31a0977 100644
--- a/src/plugins/home/public/index.ts
+++ b/src/plugins/home/public/index.ts
@@ -53,3 +53,5 @@ import { HomePublicPlugin } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) =>
new HomePublicPlugin(initializerContext);
+
+export { HOME_PAGE_ID, HOME_CONTENT_AREAS } from '../common/constants';
diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json
index 2e9377b3bda9..99a66fb1743a 100644
--- a/src/plugins/workspace/opensearch_dashboards.json
+++ b/src/plugins/workspace/opensearch_dashboards.json
@@ -7,6 +7,6 @@
"savedObjects",
"opensearchDashboardsReact"
],
- "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement"],
- "requiredBundles": ["opensearchDashboardsReact"]
+ "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"],
+ "requiredBundles": ["opensearchDashboardsReact", "home"]
}
diff --git a/src/plugins/workspace/public/components/home_get_start_card/index.ts b/src/plugins/workspace/public/components/home_get_start_card/index.ts
new file mode 100644
index 000000000000..f78300a492d3
--- /dev/null
+++ b/src/plugins/workspace/public/components/home_get_start_card/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { UseCaseFooter } from './use_case_footer';
diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx
new file mode 100644
index 000000000000..8296a5ba8359
--- /dev/null
+++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx
@@ -0,0 +1,156 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { UseCaseFooter as UseCaseFooterComponent, UseCaseFooterProps } from './use_case_footer';
+import { coreMock, httpServiceMock } from '../../../../../core/public/mocks';
+import { IntlProvider } from 'react-intl';
+import { WorkspaceUseCase } from '../../types';
+import { CoreStart } from 'opensearch-dashboards/public';
+import { BehaviorSubject } from 'rxjs';
+import { WORKSPACE_USE_CASES } from '../../../common/constants';
+
+describe('UseCaseFooter', () => {
+ // let coreStartMock: CoreStart;
+ const navigateToApp = jest.fn();
+ const registeredUseCases$ = new BehaviorSubject([
+ WORKSPACE_USE_CASES.observability,
+ WORKSPACE_USE_CASES['security-analytics'],
+ WORKSPACE_USE_CASES.analytics,
+ WORKSPACE_USE_CASES.search,
+ ]);
+
+ const getMockCore = (isDashboardAdmin: boolean = true) => {
+ const coreStartMock = coreMock.createStart();
+ coreStartMock.application.capabilities = {
+ ...coreStartMock.application.capabilities,
+ dashboards: { isDashboardAdmin },
+ };
+ coreStartMock.application = {
+ ...coreStartMock.application,
+ navigateToApp,
+ };
+ jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => {
+ return `https://test.com/app/${appId}`;
+ });
+ return coreStartMock;
+ };
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+
+ const UseCaseFooter = (props: UseCaseFooterProps) => {
+ return (
+
+
+
+ );
+ };
+ it('renders create workspace button for admin when no workspaces within use case exist', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const button = getByTestId('useCase.footer.createWorkspace.button');
+ expect(button).toBeInTheDocument();
+ fireEvent.click(button);
+ const createWorkspaceButtonInModal = getByTestId('useCase.footer.modal.create.button');
+ expect(createWorkspaceButtonInModal).toHaveAttribute(
+ 'href',
+ 'https://test.com/app/workspace_create'
+ );
+ });
+
+ it('renders create workspace button for non-admin when no workspaces within use case exist', () => {
+ const { getByTestId } = render(
+
+ );
+
+ const button = getByTestId('useCase.footer.createWorkspace.button');
+ expect(button).toBeInTheDocument();
+ fireEvent.click(button);
+ expect(screen.getByText('Unable to create workspace')).toBeInTheDocument();
+ expect(screen.queryByTestId('useCase.footer.modal.create.button')).not.toBeInTheDocument();
+ fireEvent.click(getByTestId('useCase.footer.modal.close.button'));
+ });
+
+ it('renders open workspace button when one workspace exists', () => {
+ const core = getMockCore();
+ core.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
+ ]);
+ const { getByTestId } = render(
+
+ );
+
+ const button = getByTestId('useCase.footer.openWorkspace.button');
+ expect(button).toBeInTheDocument();
+ expect(button).not.toBeDisabled();
+ expect(button).toHaveAttribute('href', 'https://test.com/w/workspace-1/app/discover');
+ });
+
+ it('renders select workspace popover when multiple workspaces exist', () => {
+ const core = getMockCore();
+ core.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
+ { id: 'workspace-2', name: 'workspace 2', features: ['use-case-observability'] },
+ ]);
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: {
+ assign: jest.fn(),
+ },
+ });
+
+ render(
+
+ );
+
+ const button = screen.getByText('Select workspace');
+ expect(button).toBeInTheDocument();
+
+ fireEvent.click(button);
+ expect(screen.getByText('workspace 1')).toBeInTheDocument();
+ expect(screen.getByText('workspace 2')).toBeInTheDocument();
+ expect(screen.getByText('Observability Workspaces')).toBeInTheDocument();
+
+ const inputElement = screen.getByPlaceholderText('Search');
+ expect(inputElement).toBeInTheDocument();
+ fireEvent.change(inputElement, { target: { value: 'workspace 1' } });
+ expect(screen.queryByText('workspace 2')).toBeNull();
+
+ fireEvent.click(screen.getByText('workspace 1'));
+ expect(window.location.assign).toHaveBeenCalledWith(
+ 'https://test.com/w/workspace-1/app/discover'
+ );
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ });
+ });
+});
diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx
new file mode 100644
index 000000000000..fddd542f64d7
--- /dev/null
+++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx
@@ -0,0 +1,219 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ EuiText,
+ EuiModal,
+ EuiTitle,
+ EuiPanel,
+ EuiAvatar,
+ EuiSpacer,
+ EuiButton,
+ EuiPopover,
+ EuiFlexItem,
+ EuiModalBody,
+ EuiFlexGroup,
+ EuiFieldSearch,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiContextMenu,
+ EuiModalHeaderTitle,
+} from '@elastic/eui';
+import React, { useMemo, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { i18n } from '@osd/i18n';
+import { BehaviorSubject } from 'rxjs';
+import { WORKSPACE_DETAIL_APP_ID } from '../../../common/constants';
+import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
+import { CoreStart, WorkspaceObject } from '../../../../../core/public';
+import { WorkspaceUseCase } from '../../types';
+import { getUseCaseFromFeatureConfig } from '../../utils';
+
+export interface UseCaseFooterProps {
+ useCaseId: string;
+ useCaseTitle: string;
+ core: CoreStart;
+ registeredUseCases$: BehaviorSubject;
+}
+
+export const UseCaseFooter = ({
+ useCaseId,
+ useCaseTitle,
+ core,
+ registeredUseCases$,
+}: UseCaseFooterProps) => {
+ const workspaceList = core.workspaces.workspaceList$.getValue();
+ const availableUseCases = registeredUseCases$.getValue();
+ const basePath = core.http.basePath;
+ const isDashboardAdmin = core.application.capabilities?.dashboards?.isDashboardAdmin !== false;
+ const [isPopoverOpen, setPopover] = useState(false);
+ const [searchValue, setSearchValue] = useState('');
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const closeModal = () => setIsModalVisible(false);
+ const showModal = () => setIsModalVisible(!isModalVisible);
+ const onButtonClick = () => setPopover(!isPopoverOpen);
+ const closePopover = () => setPopover(false);
+
+ const appId =
+ availableUseCases?.find((useCase) => useCase.id === useCaseId)?.features[0] ??
+ WORKSPACE_DETAIL_APP_ID;
+
+ const filterWorkspaces = useMemo(
+ () =>
+ workspaceList.filter(
+ (workspace) =>
+ workspace.features?.map(getUseCaseFromFeatureConfig).filter(Boolean)[0] === useCaseId
+ ),
+ [useCaseId, workspaceList]
+ );
+
+ const searchWorkspaces = useMemo(
+ () =>
+ filterWorkspaces
+ .filter((workspace) => workspace.name.toLowerCase().includes(searchValue.toLowerCase()))
+ .slice(0, 5),
+ [filterWorkspaces, searchValue]
+ );
+
+ if (filterWorkspaces.length === 0) {
+ const modalHeaderTitle = i18n.translate('useCase.footer.modal.headerTitle', {
+ defaultMessage: isDashboardAdmin ? 'No workspaces found' : 'Unable to create workspace',
+ });
+ const modalBodyContent = i18n.translate('useCase.footer.modal.bodyContent', {
+ defaultMessage: isDashboardAdmin
+ ? 'There are no available workspaces found. You can create a workspace in the workspace creation page.'
+ : 'To create a workspace, contact your administrator.',
+ });
+
+ return (
+ <>
+
+
+
+ {isModalVisible && (
+
+
+ {modalHeaderTitle}
+
+
+
+ {modalBodyContent}
+
+
+
+
+
+
+ {isDashboardAdmin && (
+
+
+
+ )}
+
+
+ )}
+ >
+ );
+ }
+
+ if (filterWorkspaces.length === 1) {
+ const useCaseURL = formatUrlWithWorkspaceId(
+ core.application.getUrlForApp(appId, { absolute: false }),
+ filterWorkspaces[0].id,
+ basePath
+ );
+ return (
+
+
+
+ );
+ }
+
+ const workspaceToItem = (workspace: WorkspaceObject) => {
+ const useCaseURL = formatUrlWithWorkspaceId(
+ core.application.getUrlForApp(appId, { absolute: false }),
+ workspace.id,
+ basePath
+ );
+ const workspaceName = workspace.name;
+
+ return {
+ toolTipContent: {workspaceName}
,
+ name: (
+
+ {workspaceName}
+
+ ),
+ key: workspace.id,
+ icon: (
+
+ ),
+ onClick: () => {
+ window.location.assign(useCaseURL);
+ },
+ };
+ };
+
+ const button = (
+
+
+
+ );
+ const panels = [
+ {
+ id: 0,
+ items: searchWorkspaces.map(workspaceToItem),
+ },
+ ];
+
+ return (
+
+ );
+};
diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
index d3578498c858..68ed1c67359f 100644
--- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
+++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx
@@ -109,18 +109,6 @@ describe('', () => {
expect(screen.getByText('Observability')).toBeInTheDocument();
});
- it('should close the workspace dropdown list', async () => {
- render();
-
- fireEvent.click(screen.getByTestId('workspace-select-button'));
-
- expect(screen.getByText(/all workspaces/i)).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('workspace-select-button'));
- await waitFor(() => {
- expect(screen.queryByText(/all workspaces/i)).not.toBeInTheDocument();
- });
- });
-
it('should navigate to the workspace', () => {
coreStartMock.workspaces.workspaceList$.next([
{ id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
index bda11fb3d113..77d1cd6e602e 100644
--- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
+++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
@@ -44,7 +44,7 @@ const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces',
});
const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', {
- defaultMessage: 'recent workspaces',
+ defaultMessage: 'Recent workspaces',
});
const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorkspace', {
@@ -158,6 +158,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
}
onClick={() => {
+ closePopover();
window.location.assign(useCaseURL);
}}
/>
@@ -221,6 +222,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
{
+ closePopover();
navigateToWorkspaceDetail(coreStart, currentWorkspace.id);
}}
>
@@ -240,6 +242,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
{
+ closePopover();
coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
}}
>
@@ -251,8 +254,9 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
- {getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')}
- {getWorkspaceListGroup(filteredWorkspaceList, 'all')}
+ {filteredRecentWorkspaces.length > 0 &&
+ getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')}
+ {filteredWorkspaceList.length > 0 && getWorkspaceListGroup(filteredWorkspaceList, 'all')}
@@ -263,6 +267,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
key={WORKSPACE_LIST_APP_ID}
data-test-subj="workspace-menu-view-all-button"
onClick={() => {
+ closePopover();
coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
}}
>
@@ -278,6 +283,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
key={WORKSPACE_CREATE_APP_ID}
data-test-subj="workspace-menu-create-workspace-button"
onClick={() => {
+ closePopover();
coreStart.application.navigateToApp(WORKSPACE_CREATE_APP_ID);
}}
>
diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts
index b2ed55c08de6..6ffae95d40c3 100644
--- a/src/plugins/workspace/public/plugin.test.ts
+++ b/src/plugins/workspace/public/plugin.test.ts
@@ -19,9 +19,13 @@ import { savedObjectsManagementPluginMock } from '../../saved_objects_management
import { managementPluginMock } from '../../management/public/mocks';
import { UseCaseService } from './services/use_case_service';
import { workspaceClientMock, WorkspaceClientMock } from './workspace_client.mock';
-import { WorkspacePlugin } from './plugin';
+import { WorkspacePlugin, WorkspacePluginStartDeps } from './plugin';
+import { contentManagementPluginMocks } from '../../content_management/public';
describe('Workspace plugin', () => {
+ const mockDependencies: WorkspacePluginStartDeps = {
+ contentManagement: contentManagementPluginMocks.createStartContract(),
+ };
const getSetupMock = () => ({
...coreMock.createSetup(),
chrome: chromeServiceMock.createSetupContract(),
@@ -48,7 +52,7 @@ describe('Workspace plugin', () => {
const setupMock = getSetupMock();
const coreStart = coreMock.createStart();
await workspacePlugin.setup(setupMock, {});
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, mockDependencies);
coreStart.workspaces.currentWorkspaceId$.next('foo');
expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo');
expect(setupMock.application.register).toBeCalledTimes(4);
@@ -182,7 +186,7 @@ describe('Workspace plugin', () => {
const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]);
startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs);
const workspacePlugin = new WorkspacePlugin();
- workspacePlugin.start(startMock);
+ workspacePlugin.start(startMock, mockDependencies);
expect(startMock.chrome.setBreadcrumbs).toBeCalledWith(
expect.arrayContaining([
expect.objectContaining({
@@ -208,7 +212,7 @@ describe('Workspace plugin', () => {
]);
startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs);
const workspacePlugin = new WorkspacePlugin();
- workspacePlugin.start(startMock);
+ workspacePlugin.start(startMock, mockDependencies);
expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled();
});
@@ -225,7 +229,7 @@ describe('Workspace plugin', () => {
jest.spyOn(navGroupUpdater$, 'next');
expect(navGroupUpdater$.next).not.toHaveBeenCalled();
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, mockDependencies);
waitFor(() => {
expect(navGroupUpdater$.next).toHaveBeenCalled();
@@ -236,7 +240,7 @@ describe('Workspace plugin', () => {
const coreStart = coreMock.createStart();
coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
const workspacePlugin = new WorkspacePlugin();
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, mockDependencies);
expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1);
});
@@ -265,7 +269,7 @@ describe('Workspace plugin', () => {
const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0];
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, mockDependencies);
const appUpdater = await appUpdater$.pipe(first()).toPromise();
@@ -286,7 +290,7 @@ describe('Workspace plugin', () => {
const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0];
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, mockDependencies);
const navGroupUpdater = await navGroupUpdater$.pipe(first()).toPromise();
@@ -337,7 +341,7 @@ describe('Workspace plugin', () => {
const appUpdaterChangeMock = jest.fn();
appUpdater$.subscribe(appUpdaterChangeMock);
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, mockDependencies);
// Wait for filterNav been executed
await new Promise(setImmediate);
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index 104db7d9b91f..40f475d4a818 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -7,6 +7,7 @@ import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import React from 'react';
import { i18n } from '@osd/i18n';
import { map } from 'rxjs/operators';
+import { EuiIcon } from '@elastic/eui';
import {
Plugin,
CoreStart,
@@ -28,6 +29,7 @@ import {
WORKSPACE_DETAIL_APP_ID,
WORKSPACE_CREATE_APP_ID,
WORKSPACE_LIST_APP_ID,
+ WORKSPACE_USE_CASES,
} from '../common/constants';
import { getWorkspaceIdFromUrl } from '../../../core/public/utils';
import { Services, WorkspaceUseCase } from './types';
@@ -46,6 +48,9 @@ import {
import { recentWorkspaceManager } from './recent_workspace_manager';
import { toMountPoint } from '../../opensearch_dashboards_react/public';
import { UseCaseService } from './services/use_case_service';
+import { ContentManagementPluginStart } from '../../../plugins/content_management/public';
+import { UseCaseFooter } from './components/home_get_start_card';
+import { HOME_CONTENT_AREAS } from '../../home/public';
type WorkspaceAppType = (
params: AppMountParameters,
@@ -59,7 +64,12 @@ interface WorkspacePluginSetupDeps {
dataSourceManagement?: DataSourceManagementPluginSetup;
}
-export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> {
+export interface WorkspacePluginStartDeps {
+ contentManagement: ContentManagementPluginStart;
+}
+
+export class WorkspacePlugin
+ implements Plugin<{}, {}, WorkspacePluginSetupDeps, WorkspacePluginStartDeps> {
private coreStart?: CoreStart;
private currentWorkspaceSubscription?: Subscription;
private breadcrumbsSubscription?: Subscription;
@@ -372,7 +382,41 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
return {};
}
- public start(core: CoreStart) {
+ private registerGetStartedCardToNewHome(
+ core: CoreStart,
+ contentManagement: ContentManagementPluginStart
+ ) {
+ const useCases = [
+ WORKSPACE_USE_CASES.observability,
+ WORKSPACE_USE_CASES['security-analytics'],
+ WORKSPACE_USE_CASES.search,
+ WORKSPACE_USE_CASES.analytics,
+ ];
+
+ useCases.forEach((useCase, index) => {
+ contentManagement.registerContentProvider({
+ id: `home_get_start_${useCase.id}`,
+ getTargetArea: () => HOME_CONTENT_AREAS.GET_STARTED,
+ getContent: () => ({
+ id: useCase.id,
+ kind: 'card',
+ order: (index + 1) * 1000,
+ description: useCase.description,
+ title: useCase.title,
+ getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }),
+ getFooter: () =>
+ React.createElement(UseCaseFooter, {
+ useCaseId: useCase.id,
+ useCaseTitle: useCase.title,
+ core,
+ registeredUseCases$: this.registeredUseCases$,
+ }),
+ }),
+ });
+ });
+ }
+
+ public start(core: CoreStart, { contentManagement }: WorkspacePluginStartDeps) {
this.coreStart = core;
this.currentWorkspaceIdSubscription = this._changeSavedObjectCurrentWorkspace();
@@ -405,8 +449,10 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
})
),
});
- }
+ // register get started card in new home page
+ this.registerGetStartedCardToNewHome(core, contentManagement);
+ }
return {};
}
From 376ead0ce1ef5224476604e48aa487354fbb0387 Mon Sep 17 00:00:00 2001
From: Viraj Sanghvi
Date: Sun, 21 Jul 2024 01:18:20 -0700
Subject: [PATCH 05/10] fix: Fix wrapping of labels in filter by type popover
(#7327)
* fix: Fix wrapping of labels in filter by type popover
Signed-off-by: Viraj Sanghvi
* Changeset file for PR #7327 created/updated
---------
Signed-off-by: Viraj Sanghvi
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7327.yml | 2 ++
.../data_explorer/public/components/sidebar/index.scss | 4 ++++
.../application/components/sidebar/discover_field_search.tsx | 2 +-
3 files changed, 7 insertions(+), 1 deletion(-)
create mode 100644 changelogs/fragments/7327.yml
diff --git a/changelogs/fragments/7327.yml b/changelogs/fragments/7327.yml
new file mode 100644
index 000000000000..da4ffa480766
--- /dev/null
+++ b/changelogs/fragments/7327.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix wrapping of labels in filter by type popover ([#7327](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7327))
\ No newline at end of file
diff --git a/src/plugins/data_explorer/public/components/sidebar/index.scss b/src/plugins/data_explorer/public/components/sidebar/index.scss
index 6d8ad2324bc1..1828568bc361 100644
--- a/src/plugins/data_explorer/public/components/sidebar/index.scss
+++ b/src/plugins/data_explorer/public/components/sidebar/index.scss
@@ -7,3 +7,7 @@
border-bottom: $euiBorderThin !important;
}
}
+
+.dataPanelTypeFilterPopover {
+ min-width: 300px;
+}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx
index acef83191643..b009864411e0 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx
@@ -258,7 +258,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
Date: Mon, 22 Jul 2024 09:57:02 +0800
Subject: [PATCH 06/10] [Navigation-next]Register workspace list card into home
page (#7247)
* workspace list card on home
Signed-off-by: Hailong Cui
* fix merge conflicts
Signed-off-by: Hailong Cui
* add home as requiredBundles
Signed-off-by: Hailong Cui
* Changeset file for PR #7247 created/updated
* fix failed UT
Signed-off-by: Hailong Cui
* address review comments
Signed-off-by: Hailong Cui
* update to funtional component
Signed-off-by: Hailong Cui
* udpate content provider id
Signed-off-by: Hailong Cui
---------
Signed-off-by: Hailong Cui
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7247.yml | 2 +
src/core/types/workspace.ts | 1 +
.../workspace_list_card.test.tsx.snap | 121 +++++++++++
.../public/components/service_card/index.ts | 6 +
.../service_card/workspace_list_card.test.tsx | 66 ++++++
.../service_card/workspace_list_card.tsx | 192 ++++++++++++++++++
src/plugins/workspace/public/plugin.test.ts | 13 ++
src/plugins/workspace/public/plugin.ts | 24 ++-
src/plugins/workspace/public/utils.ts | 2 +-
.../workspace/server/workspace_client.ts | 1 +
10 files changed, 426 insertions(+), 2 deletions(-)
create mode 100644 changelogs/fragments/7247.yml
create mode 100644 src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap
create mode 100644 src/plugins/workspace/public/components/service_card/index.ts
create mode 100644 src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx
create mode 100644 src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
diff --git a/changelogs/fragments/7247.yml b/changelogs/fragments/7247.yml
new file mode 100644
index 000000000000..535f4c9843b0
--- /dev/null
+++ b/changelogs/fragments/7247.yml
@@ -0,0 +1,2 @@
+feat:
+- Register workspace list card into home page ([#7247](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7247))
\ No newline at end of file
diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts
index d0a0d47b2216..c00d3576d567 100644
--- a/src/core/types/workspace.ts
+++ b/src/core/types/workspace.ts
@@ -14,6 +14,7 @@ export interface WorkspaceAttribute {
icon?: string;
reserved?: boolean;
uiSettings?: Record;
+ lastUpdatedTime?: string;
}
export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute {
diff --git a/src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap b/src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap
new file mode 100644
index 000000000000..35970676eb7e
--- /dev/null
+++ b/src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap
@@ -0,0 +1,121 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workspace list card render normally should show workspace list card correctly 1`] = `
+
+
+
+
+
+
+
+
+
+ No Workspaces found
+
+
+
+
+ Workspaces you have recently viewed will appear here.
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/workspace/public/components/service_card/index.ts b/src/plugins/workspace/public/components/service_card/index.ts
new file mode 100644
index 000000000000..9bfc561f2561
--- /dev/null
+++ b/src/plugins/workspace/public/components/service_card/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { WorkspaceListCard } from './workspace_list_card';
diff --git a/src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx b/src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx
new file mode 100644
index 000000000000..24d45d42e725
--- /dev/null
+++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { coreMock } from '../../../../../core/public/mocks';
+import { fireEvent, render, waitFor } from '@testing-library/react';
+import { WorkspaceListCard } from './workspace_list_card';
+import { recentWorkspaceManager } from '../../recent_workspace_manager';
+
+describe('workspace list card render normally', () => {
+ const coreStart = coreMock.createStart();
+
+ beforeAll(() => {
+ const workspaceList = [
+ {
+ id: 'ws-1',
+ name: 'foo',
+ lastUpdatedTime: new Date().toISOString(),
+ },
+ {
+ id: 'ws-2',
+ name: 'bar',
+ lastUpdatedTime: new Date().toISOString(),
+ },
+ ];
+ coreStart.workspaces.workspaceList$.next(workspaceList);
+ });
+
+ it('should show workspace list card correctly', () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should show empty state if no recently viewed workspace', () => {
+ const { getByTestId, getByText } = render();
+ expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently viewed');
+
+ // empty statue for recently viewed
+ expect(getByText('Workspaces you have recently viewed will appear here.')).toBeInTheDocument();
+ });
+
+ it('should show default filter as recently viewed', () => {
+ recentWorkspaceManager.addRecentWorkspace('foo');
+ const { getByTestId, getByText } = render();
+ expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently viewed');
+
+ waitFor(() => {
+ expect(getByText('foo')).toBeInTheDocument();
+ });
+ });
+
+ it('should show updated filter correctly', () => {
+ const { getByTestId, getByText } = render();
+ expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently viewed');
+
+ const filterSelector = getByTestId('workspace_filter');
+ fireEvent.change(filterSelector, { target: { value: 'updated' } });
+ expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently updated');
+
+ // workspace list
+ expect(getByText('foo')).toBeInTheDocument();
+ expect(getByText('bar')).toBeInTheDocument();
+ });
+});
diff --git a/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
new file mode 100644
index 000000000000..12b14325ce11
--- /dev/null
+++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
@@ -0,0 +1,192 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ EuiPanel,
+ EuiLink,
+ EuiDescriptionList,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSelect,
+ EuiButtonIcon,
+ EuiSpacer,
+ EuiListGroup,
+ EuiText,
+ EuiTitle,
+ EuiToolTip,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import moment from 'moment';
+import { orderBy } from 'lodash';
+import { CoreStart, WorkspaceObject } from '../../../../../core/public';
+import { navigateToWorkspaceDetail } from '../utils/workspace';
+
+import { WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID } from '../../../common/constants';
+import { recentWorkspaceManager } from '../../recent_workspace_manager';
+
+const WORKSPACE_LIST_CARD_DESCRIPTION = i18n.translate('workspace.list.card.description', {
+ defaultMessage:
+ 'Workspaces are dedicated environments for organizing and collaborating on your data, dashboards, and analytics workflows. Each Workspace acts as a self-contained space with its own set of saved objects and access controls.',
+});
+
+const MAX_ITEM_IN_LIST = 5;
+
+export interface WorkspaceListCardProps {
+ core: CoreStart;
+}
+
+export const WorkspaceListCard = (props: WorkspaceListCardProps) => {
+ const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
+ const [filter, setFilter] = useState('viewed');
+
+ useEffect(() => {
+ const workspaceSub = props.core.workspaces.workspaceList$.subscribe((list) => {
+ setAvailableWorkspaces(list || []);
+ });
+ return () => {
+ workspaceSub.unsubscribe();
+ };
+ }, [props.core]);
+
+ const workspaceList = useMemo(() => {
+ const recentWorkspaces = recentWorkspaceManager.getRecentWorkspaces() || [];
+ if (filter === 'viewed') {
+ return orderBy(recentWorkspaces, ['timestamp'], ['desc'])
+ .filter((ws) => availableWorkspaces.some((a) => a.id === ws.id))
+ .slice(0, MAX_ITEM_IN_LIST)
+ .map((item) => ({
+ id: item.id,
+ name: availableWorkspaces.find((ws) => ws.id === item.id)?.name!,
+ time: item.timestamp,
+ }));
+ } else if (filter === 'updated') {
+ return orderBy(availableWorkspaces, ['lastUpdatedTime'], ['desc'])
+ .slice(0, MAX_ITEM_IN_LIST)
+ .map((ws) => ({
+ id: ws.id,
+ name: ws.name,
+ time: ws.lastUpdatedTime,
+ }));
+ }
+ return [];
+ }, [filter, availableWorkspaces]);
+
+ const handleSwitchWorkspace = (id: string) => {
+ const { application, http } = props.core;
+ if (application && http) {
+ navigateToWorkspaceDetail({ application, http }, id);
+ }
+ };
+
+ const { application } = props.core;
+
+ const isDashboardAdmin = application.capabilities.dashboards?.isDashboardAdmin;
+
+ return (
+
+
+
+
+ Workspaces
+
+
+
+
+
+
+
+
+ {
+ setFilter(e.target.value);
+ }}
+ options={[
+ {
+ value: 'viewed',
+ text: i18n.translate('workspace.list.card.filter.viewed', {
+ defaultMessage: 'Recently viewed',
+ }),
+ },
+ {
+ value: 'updated',
+ text: i18n.translate('workspace.list.card.filter.updated', {
+ defaultMessage: 'Recently updated',
+ }),
+ },
+ ]}
+ />
+
+ {isDashboardAdmin && (
+
+
+ {
+ application.navigateToApp(WORKSPACE_CREATE_APP_ID);
+ }}
+ />
+
+
+ )}
+
+
+
+
+ {workspaceList && workspaceList.length === 0 ? (
+ No Workspaces found
}
+ body={i18n.translate('workspace.list.card.empty', {
+ values: {
+ filter,
+ },
+ defaultMessage: 'Workspaces you have recently {filter} will appear here.',
+ })}
+ />
+ ) : (
+ ({
+ title: (
+ {
+ handleSwitchWorkspace(workspace.id);
+ }}
+ >
+ {workspace.name}
+
+ ),
+ description: (
+
+ {moment(workspace.time).fromNow()}
+
+ ),
+ }))}
+ />
+ )}
+
+
+ {
+ application.navigateToApp(WORKSPACE_LIST_APP_ID);
+ }}
+ >
+ View all
+
+
+ );
+};
diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts
index 6ffae95d40c3..0b683fd9862f 100644
--- a/src/plugins/workspace/public/plugin.test.ts
+++ b/src/plugins/workspace/public/plugin.test.ts
@@ -30,6 +30,7 @@ describe('Workspace plugin', () => {
...coreMock.createSetup(),
chrome: chromeServiceMock.createSetupContract(),
});
+
beforeEach(() => {
WorkspaceClientMock.mockClear();
Object.values(workspaceClientMock).forEach((item) => item.mockClear());
@@ -216,6 +217,18 @@ describe('Workspace plugin', () => {
expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled();
});
+ it('#start should register workspace list card into new home page', async () => {
+ const startMock = coreMock.createStart();
+ startMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
+ const workspacePlugin = new WorkspacePlugin();
+ workspacePlugin.start(startMock, mockDependencies);
+ expect(mockDependencies.contentManagement.registerContentProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'workspace_list_card_home',
+ })
+ );
+ });
+
it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => {
const workspacePlugin = new WorkspacePlugin();
const setupMock = getSetupMock();
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index 40f475d4a818..1be221b546ad 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -36,6 +36,7 @@ import { Services, WorkspaceUseCase } from './types';
import { WorkspaceClient } from './workspace_client';
import { SavedObjectsManagementPluginSetup } from '../../../plugins/saved_objects_management/public';
import { ManagementSetup } from '../../../plugins/management/public';
+import { ContentManagementPluginStart } from '../../../plugins/content_management/public';
import { WorkspaceMenu } from './components/workspace_menu/workspace_menu';
import { getWorkspaceColumn } from './components/workspace_column';
import { DataSourceManagementPluginSetup } from '../../../plugins/data_source_management/public';
@@ -48,7 +49,7 @@ import {
import { recentWorkspaceManager } from './recent_workspace_manager';
import { toMountPoint } from '../../opensearch_dashboards_react/public';
import { UseCaseService } from './services/use_case_service';
-import { ContentManagementPluginStart } from '../../../plugins/content_management/public';
+import { WorkspaceListCard } from './components/service_card';
import { UseCaseFooter } from './components/home_get_start_card';
import { HOME_CONTENT_AREAS } from '../../home/public';
@@ -450,12 +451,33 @@ export class WorkspacePlugin
),
});
+ // register workspace list in home page
+ this.registerWorkspaceListToHome(core, contentManagement);
+
// register get started card in new home page
this.registerGetStartedCardToNewHome(core, contentManagement);
}
return {};
}
+ private registerWorkspaceListToHome(
+ core: CoreStart,
+ contentManagement: ContentManagementPluginStart
+ ) {
+ if (contentManagement) {
+ contentManagement.registerContentProvider({
+ id: 'workspace_list_card_home',
+ getContent: () => ({
+ id: 'workspace_list',
+ kind: 'custom',
+ order: 0,
+ render: () => React.createElement(WorkspaceListCard, { core }),
+ }),
+ getTargetArea: () => HOME_CONTENT_AREAS.SERVICE_CARDS,
+ });
+ }
+ }
+
public stop() {
this.currentWorkspaceSubscription?.unsubscribe();
this.currentWorkspaceIdSubscription?.unsubscribe();
diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts
index 589f1d8159d2..d4bc61638744 100644
--- a/src/plugins/workspace/public/utils.ts
+++ b/src/plugins/workspace/public/utils.ts
@@ -18,8 +18,8 @@ import {
WorkspaceObject,
WorkspaceAvailability,
} from '../../../core/public';
-import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants';
import { WorkspaceUseCase } from './types';
+import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants';
const USE_CASE_PREFIX = 'use-case-';
diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts
index fe88436f38e5..0b7d7c8a57c1 100644
--- a/src/plugins/workspace/server/workspace_client.ts
+++ b/src/plugins/workspace/server/workspace_client.ts
@@ -77,6 +77,7 @@ export class WorkspaceClient implements IWorkspaceClientImpl {
): WorkspaceAttributeWithPermission {
return {
...savedObject.attributes,
+ lastUpdatedTime: savedObject.updated_at,
id: savedObject.id,
permissions: savedObject.permissions,
};
From d30677d72b58ad0c52fc553630ef1553de7313f9 Mon Sep 17 00:00:00 2001
From: "opensearch-trigger-bot[bot]"
<98922864+opensearch-trigger-bot[bot]@users.noreply.github.com>
Date: Mon, 22 Jul 2024 12:59:17 +0800
Subject: [PATCH 07/10] [navigation-next] fix: redirect to standard index
pattern applications while nav group is enabled (#7346)
* [navigation-next] fix: redirect to standard index pattern applications while nav group is enabled (#7305)
* feat: fix the incorrect jumping logic for Index pattern management
Signed-off-by: SuZhou-Joe
* Changeset file for PR #7305 created/updated
* feat: update
Signed-off-by: SuZhou-Joe
* feat: update with comment
Signed-off-by: SuZhou-Joe
* feat: update order and remove reset logic
Signed-off-by: SuZhou-Joe
* feat: update
Signed-off-by: SuZhou-Joe
* feat: update
Signed-off-by: SuZhou-Joe
* feat: update snapshot
Signed-off-by: SuZhou-Joe
* feat: some category change
Signed-off-by: SuZhou-Joe
* feat: update category
Signed-off-by: SuZhou-Joe
---------
Signed-off-by: SuZhou-Joe
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
(cherry picked from commit 2c708e320222f7eee43e96247a71529e3f0207b4)
Signed-off-by: github-actions[bot]
* feat: change the order
Signed-off-by: SuZhou-Joe
* feat: hide left navigation when workspace enabled
Signed-off-by: SuZhou-Joe
---------
Signed-off-by: SuZhou-Joe
Signed-off-by: github-actions[bot]
Co-authored-by: github-actions[bot]
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
Co-authored-by: SuZhou-Joe
---
changelogs/fragments/7305.yml | 2 +
.../nav_group/nav_group_service.test.ts | 47 -----------
.../chrome/nav_group/nav_group_service.ts | 12 ---
...ollapsible_nav_group_enabled.test.tsx.snap | 7 +-
.../header/collapsible_nav_group_enabled.scss | 17 +++-
.../collapsible_nav_group_enabled.test.tsx | 26 ++++++
.../header/collapsible_nav_group_enabled.tsx | 80 ++++++++++++-------
src/core/public/chrome/ui/header/header.tsx | 1 +
src/core/utils/default_app_categories.ts | 9 ++-
src/plugins/dashboard/public/plugin.tsx | 4 +-
.../data_source_management/public/plugin.ts | 15 ++--
.../public/plugin.test.ts | 36 +++++++++
.../index_pattern_management/public/plugin.ts | 20 +++--
.../management_app/management_app.tsx | 5 +-
src/plugins/management/public/plugin.ts | 2 +
.../saved_objects_management/public/plugin.ts | 8 ++
16 files changed, 182 insertions(+), 109 deletions(-)
create mode 100644 changelogs/fragments/7305.yml
diff --git a/changelogs/fragments/7305.yml b/changelogs/fragments/7305.yml
new file mode 100644
index 000000000000..ebb9060e2121
--- /dev/null
+++ b/changelogs/fragments/7305.yml
@@ -0,0 +1,2 @@
+feat:
+- [navigation-next] fix: redirect to standard index pattern applications while nav group is enabled ([#7305](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7305))
\ No newline at end of file
diff --git a/src/core/public/chrome/nav_group/nav_group_service.test.ts b/src/core/public/chrome/nav_group/nav_group_service.test.ts
index bc18483178fd..90911309ff9a 100644
--- a/src/core/public/chrome/nav_group/nav_group_service.test.ts
+++ b/src/core/public/chrome/nav_group/nav_group_service.test.ts
@@ -311,53 +311,6 @@ describe('ChromeNavGroupService#start()', () => {
expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toBeFalsy();
expect(currentNavGroup).toBeUndefined();
});
-
- it('should reset current nav group if app not belongs to any nav group', async () => {
- const uiSettings = uiSettingsServiceMock.createSetupContract();
- const navGroupEnabled$ = new Rx.BehaviorSubject(true);
- uiSettings.get$.mockImplementation(() => navGroupEnabled$);
-
- const chromeNavGroupService = new ChromeNavGroupService();
- const chromeNavGroupServiceSetup = chromeNavGroupService.setup({ uiSettings });
-
- chromeNavGroupServiceSetup.addNavLinksToGroup(
- {
- id: 'foo',
- title: 'foo title',
- description: 'foo description',
- },
- [{ id: 'foo-app1' }]
- );
-
- const chromeNavGroupServiceStart = await chromeNavGroupService.start({
- navLinks: mockedNavLinkService,
- application: mockedApplicationService,
- });
-
- // set an existing nav group id
- chromeNavGroupServiceStart.setCurrentNavGroup('foo');
-
- expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toEqual('foo');
-
- let currentNavGroup = await chromeNavGroupServiceStart
- .getCurrentNavGroup$()
- .pipe(first())
- .toPromise();
-
- expect(currentNavGroup?.id).toEqual('foo');
-
- // navigate to app don't belongs to any nav group
- mockedApplicationService.navigateToApp('bar-app');
-
- currentNavGroup = await chromeNavGroupServiceStart
- .getCurrentNavGroup$()
- .pipe(first())
- .toPromise();
-
- // verify current nav group been reset
- expect(currentNavGroup).toBeFalsy();
- expect(sessionStorageMock.getItem(CURRENT_NAV_GROUP_ID)).toBeFalsy();
- });
});
describe('nav group updater', () => {
diff --git a/src/core/public/chrome/nav_group/nav_group_service.ts b/src/core/public/chrome/nav_group/nav_group_service.ts
index bde7d0d9111a..bdf69b151da9 100644
--- a/src/core/public/chrome/nav_group/nav_group_service.ts
+++ b/src/core/public/chrome/nav_group/nav_group_service.ts
@@ -212,18 +212,6 @@ export class ChromeNavGroupService {
}
};
- // erase current nav group when switch app don't belongs to any nav group
- application.currentAppId$.subscribe((appId) => {
- const navGroupMap = this.navGroupsMap$.getValue();
- const appIdsWithNavGroup = Object.values(navGroupMap).flatMap(({ navLinks: links }) =>
- links.map(({ id }) => id)
- );
-
- if (appId && !appIdsWithNavGroup.includes(appId)) {
- setCurrentNavGroup(undefined);
- }
- });
-
const currentNavGroupSorted$ = combineLatest([
this.getSortedNavGroupsMap$(),
this.currentNavGroup$,
diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap
index 61fb739ad6c2..56600b067583 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[` should hide left navigation when in home page when workspace is enabled 1`] = ``;
+
exports[` should render correctly 1`] = `
should render correctly 1`] = `
/>
@@ -266,8 +267,7 @@ exports[` should render correctly 2`] = `
class="euiHorizontalRule euiHorizontalRule--full"
/>
@@ -341,7 +341,6 @@ exports[` should show all use case by default and
/>
diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss
index 50e822bae295..77626cab7eb7 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss
+++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss
@@ -2,8 +2,8 @@
border: none !important;
.nav-link-item {
- padding: $ouiSize / 4 $ouiSize;
- border-radius: $ouiSize;
+ padding: calc($euiSize / 4) $euiSize;
+ border-radius: $euiSize;
box-shadow: none;
margin-bottom: 0;
margin-top: 0;
@@ -39,11 +39,20 @@
}
.bottom-container {
- padding: 0 $ouiSize;
+ padding: 0 $euiSize;
display: flex;
+
+ &.bottom-container-collapsed {
+ flex-direction: column;
+ align-items: center;
+
+ > * {
+ margin: $euiSizeS 0;
+ }
+ }
}
.nav-controls-padding {
- padding: $ouiSize;
+ padding: $euiSize;
}
}
diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx
index b08029553b50..fa4abffac36c 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.test.tsx
@@ -18,6 +18,7 @@ import { httpServiceMock } from '../../../mocks';
import { getLogos } from '../../../../common';
import { ALL_USE_CASE_ID, DEFAULT_NAV_GROUPS } from '../../../../public';
import { CollapsibleNavTopProps } from './collapsible_nav_group_enabled_top';
+import { capabilitiesServiceMock } from '../../../application/capabilities/capabilities_service.mock';
jest.mock('./collapsible_nav_group_enabled_top', () => ({
CollapsibleNavTop: (props: CollapsibleNavTopProps) => (
@@ -166,6 +167,7 @@ describe('', () => {
currentNavGroup$.next(undefined);
}
},
+ capabilities: { ...capabilitiesServiceMock.createStartContract().capabilities },
...props,
};
}
@@ -223,4 +225,28 @@ describe('', () => {
fireEvent.click(getByTestId('back'));
expect(getAllByTestId('collapsibleNavAppLink-link-in-analytics').length).toEqual(2);
});
+
+ it('should hide left navigation when in home page when workspace is enabled', async () => {
+ const props = mockProps({
+ navGroupsMap: {
+ [DEFAULT_NAV_GROUPS.analytics.id]: {
+ ...DEFAULT_NAV_GROUPS.analytics,
+ navLinks: [
+ {
+ id: 'link-in-analytics',
+ title: 'link-in-analytics',
+ showInAllNavGroup: true,
+ },
+ ],
+ },
+ },
+ });
+ props.appId$ = new BehaviorSubject('home');
+ if (props.capabilities.workspaces) {
+ (props.capabilities.workspaces as Record) = {};
+ (props.capabilities.workspaces as Record).enabled = true;
+ }
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
});
diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx
index 0575dc997fc7..68b031232370 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx
@@ -19,7 +19,7 @@ import useObservable from 'react-use/lib/useObservable';
import * as Rx from 'rxjs';
import classNames from 'classnames';
import { ChromeNavControl, ChromeNavLink } from '../..';
-import { NavGroupStatus } from '../../../../types';
+import { AppCategory, NavGroupStatus } from '../../../../types';
import { InternalApplicationStart } from '../../../application/types';
import { HttpStart } from '../../../http';
import { OnIsLockedUpdate } from './';
@@ -36,7 +36,7 @@ import {
LinkItem,
LinkItemType,
} from '../../utils';
-import { ALL_USE_CASE_ID } from '../../../../../core/utils';
+import { ALL_USE_CASE_ID, DEFAULT_APP_CATEGORIES } from '../../../../../core/utils';
import { CollapsibleNavTop } from './collapsible_nav_group_enabled_top';
import { HeaderNavControls } from './header_nav_controls';
@@ -58,6 +58,7 @@ export interface CollapsibleNavGroupEnabledProps {
navControlsLeftBottom$: Rx.Observable;
currentNavGroup$: Rx.Observable;
setCurrentNavGroup: ChromeNavGroupServiceStartContract['setCurrentNavGroup'];
+ capabilities: InternalApplicationStart['capabilities'];
}
interface NavGroupsProps {
@@ -164,6 +165,14 @@ export function NavGroups({
);
}
+// Custom category is used for those features not belong to any of use cases in all use case.
+// and the custom category should always sit before manage category
+const customCategory: AppCategory = {
+ id: 'custom',
+ label: i18n.translate('core.ui.customNavList.label', { defaultMessage: 'Custom' }),
+ order: (DEFAULT_APP_CATEGORIES.manage.order || 0) - 500,
+};
+
export function CollapsibleNavGroupEnabled({
basePath,
id,
@@ -176,6 +185,7 @@ export function CollapsibleNavGroupEnabled({
navigateToUrl,
logos,
setCurrentNavGroup,
+ capabilities,
...observables
}: CollapsibleNavGroupEnabledProps) {
const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden);
@@ -183,29 +193,6 @@ export function CollapsibleNavGroupEnabled({
const navGroupsMap = useObservable(observables.navGroupsMap$, {});
const currentNavGroup = useObservable(observables.currentNavGroup$, undefined);
- const onGroupClick = (
- e: React.MouseEvent,
- group: NavGroupItemInMap
- ) => {
- const fulfilledLinks = fulfillRegistrationLinksToChromeNavLinks(
- navGroupsMap[group.id]?.navLinks,
- navLinks
- );
- setCurrentNavGroup(group.id);
-
- // the `navGroupsMap[group.id]?.navLinks` has already been sorted
- const firstLink = fulfilledLinks[0];
- if (firstLink) {
- const propsForEui = createEuiListItem({
- link: firstLink,
- appId,
- dataTestSubj: 'collapsibleNavAppLink',
- navigateToApp,
- });
- propsForEui.onClick(e);
- }
- };
-
const navLinksForRender: ChromeNavLink[] = useMemo(() => {
if (currentNavGroup) {
return fulfillRegistrationLinksToChromeNavLinks(
@@ -234,7 +221,10 @@ export function CollapsibleNavGroupEnabled({
navLinks
.filter((link) => !linkIdsWithUseGroupInfo.includes(link.id))
.forEach((navLink) => {
- navLinksForAll.push(navLink);
+ navLinksForAll.push({
+ ...navLink,
+ category: customCategory,
+ });
});
// Append all the links registered to all use case
@@ -281,6 +271,37 @@ export function CollapsibleNavGroupEnabled({
return 270;
}, [isNavOpen]);
+ // For now, only home page need to hide left navigation
+ // when workspace is enabled.
+ // If there are more pages need to hide left navigation in the future
+ // need to come up with a mechanism to register.
+ if (capabilities.workspaces.enabled && appId === 'home') {
+ return null;
+ }
+
+ const onGroupClick = (
+ e: React.MouseEvent,
+ group: NavGroupItemInMap
+ ) => {
+ const fulfilledLinks = fulfillRegistrationLinksToChromeNavLinks(
+ navGroupsMap[group.id]?.navLinks,
+ navLinks
+ );
+ setCurrentNavGroup(group.id);
+
+ // the `navGroupsMap[group.id]?.navLinks` has already been sorted
+ const firstLink = fulfilledLinks[0];
+ if (firstLink) {
+ const propsForEui = createEuiListItem({
+ link: firstLink,
+ appId,
+ dataTestSubj: 'collapsibleNavAppLink',
+ navigateToApp,
+ });
+ propsForEui.onClick(e);
+ }
+ };
+
return (
-
+
) : (
= Object.freeze
label: i18n.translate('core.ui.manageNav.label', {
defaultMessage: 'Manage',
}),
- order: 7000,
+ order: 8000,
+ },
+ manageData: {
+ id: 'manageData',
+ label: i18n.translate('core.ui.manageDataNav.label', {
+ defaultMessage: 'Manage data',
+ }),
+ order: 1000,
},
});
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index afa3b6daf281..bbe000f12b79 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -456,14 +456,14 @@ export class DashboardPlugin
core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.observability, [
{
id: app.id,
- order: 300,
+ order: 400,
category: undefined,
},
]);
core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS['security-analytics'], [
{
id: app.id,
- order: 300,
+ order: 400,
category: undefined,
},
]);
diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts
index 59f9cc91d613..df4edf40ddfd 100644
--- a/src/plugins/data_source_management/public/plugin.ts
+++ b/src/plugins/data_source_management/public/plugin.ts
@@ -146,11 +146,8 @@ export class DataSourceManagementPlugin
core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.dataAdministration, [
{
id: DSM_APP_ID_FOR_STANDARD_APPLICATION,
- category: {
- id: DSM_APP_ID_FOR_STANDARD_APPLICATION,
- label: PLUGIN_NAME,
- order: 200,
- },
+ category: DEFAULT_APP_CATEGORIES.manageData,
+ order: 100,
},
]);
@@ -186,6 +183,14 @@ export class DataSourceManagementPlugin
},
]);
+ core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [
+ {
+ id: DSM_APP_ID_FOR_STANDARD_APPLICATION,
+ category: DEFAULT_APP_CATEGORIES.manage,
+ order: 100,
+ },
+ ]);
+
const registerAuthenticationMethod = (authMethod: AuthenticationMethod) => {
if (this.started) {
throw new Error(
diff --git a/src/plugins/index_pattern_management/public/plugin.test.ts b/src/plugins/index_pattern_management/public/plugin.test.ts
index e207af770af3..a0525acae2dc 100644
--- a/src/plugins/index_pattern_management/public/plugin.test.ts
+++ b/src/plugins/index_pattern_management/public/plugin.test.ts
@@ -7,6 +7,11 @@ import { coreMock } from '../../../core/public/mocks';
import { IndexPatternManagementPlugin } from './plugin';
import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks';
import { managementPluginMock } from '../../management/public/mocks';
+import {
+ ManagementApp,
+ ManagementAppMountParams,
+ RegisterManagementAppArgs,
+} from 'src/plugins/management/public';
describe('DiscoverPlugin', () => {
it('setup successfully', () => {
@@ -22,4 +27,35 @@ describe('DiscoverPlugin', () => {
expect(setupMock.application.register).toBeCalledTimes(1);
expect(setupMock.chrome.navGroup.addNavLinksToGroup).toBeCalledTimes(5);
});
+
+ it('when new navigation is enabled, should navigate to standard IPM app', async () => {
+ const setupMock = coreMock.createSetup();
+ const startMock = coreMock.createStart();
+ setupMock.getStartServices.mockResolvedValue([startMock, {}, {}]);
+ const initializerContext = coreMock.createPluginInitializerContext();
+ const pluginInstance = new IndexPatternManagementPlugin(initializerContext);
+ const managementMock = managementPluginMock.createSetupContract();
+ let applicationRegistration = {} as Omit;
+ managementMock.sections.section.opensearchDashboards.registerApp = (
+ app: Omit
+ ) => {
+ applicationRegistration = app;
+ return {} as ManagementApp;
+ };
+
+ setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
+ startMock.application.getUrlForApp.mockReturnValue('/app/indexPatterns');
+
+ pluginInstance.setup(setupMock, {
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ management: managementMock,
+ });
+
+ await applicationRegistration.mount({} as ManagementAppMountParams);
+
+ expect(startMock.application.getUrlForApp).toBeCalledWith('indexPatterns');
+ expect(startMock.application.navigateToUrl).toBeCalledWith(
+ 'http://localhost/app/indexPatterns'
+ );
+ });
});
diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts
index 7ee82dbcc3b0..ef462374129e 100644
--- a/src/plugins/index_pattern_management/public/plugin.ts
+++ b/src/plugins/index_pattern_management/public/plugin.ts
@@ -111,6 +111,17 @@ export class IndexPatternManagementPlugin
title: sectionsHeader,
order: 0,
mount: async (params) => {
+ if (core.chrome.navGroup.getNavGroupEnabled()) {
+ const [coreStart] = await core.getStartServices();
+ const urlForStandardIPMApp = new URL(
+ coreStart.application.getUrlForApp(IPM_APP_ID),
+ window.location.href
+ );
+ const targetUrl = new URL(window.location.href);
+ targetUrl.pathname = urlForStandardIPMApp.pathname;
+ coreStart.application.navigateToUrl(targetUrl.toString());
+ return () => {};
+ }
const { mountManagementSection } = await import('./management_app');
return mountManagementSection(
@@ -178,14 +189,11 @@ export class IndexPatternManagementPlugin
},
]);
- core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.dataAdministration, [
+ core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [
{
id: IPM_APP_ID,
- category: {
- id: IPM_APP_ID,
- label: sectionsHeader,
- order: 100,
- },
+ category: DEFAULT_APP_CATEGORIES.manage,
+ order: 200,
},
]);
diff --git a/src/plugins/management/public/components/management_app/management_app.tsx b/src/plugins/management/public/components/management_app/management_app.tsx
index b2109ceb08ca..c30243563b01 100644
--- a/src/plugins/management/public/components/management_app/management_app.tsx
+++ b/src/plugins/management/public/components/management_app/management_app.tsx
@@ -51,6 +51,7 @@ export interface ManagementAppDependencies {
sections: SectionsServiceStart;
opensearchDashboardsVersion: string;
setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void;
+ hideInAppNavigation?: boolean;
}
export const ManagementApp = ({ dependencies, history }: ManagementAppProps) => {
@@ -89,7 +90,9 @@ export const ManagementApp = ({ dependencies, history }: ManagementAppProps) =>
return (
-
+ {dependencies.hideInAppNavigation ? null : (
+
+ )}
Date: Mon, 22 Jul 2024 14:10:27 +0800
Subject: [PATCH 08/10] fix: data source selector in dev tools tab moved to
left (#7347)
* fix: style issue that data source selector in dev tools tab moved to left
Signed-off-by: Yulong Ruan
* Changeset file for PR #7347 created/updated
---------
Signed-off-by: Yulong Ruan
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7347.yml | 2 ++
src/plugins/dev_tools/public/application.tsx | 1 +
src/plugins/dev_tools/public/index.scss | 8 +++++++-
3 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 changelogs/fragments/7347.yml
diff --git a/changelogs/fragments/7347.yml b/changelogs/fragments/7347.yml
new file mode 100644
index 000000000000..075f9d718446
--- /dev/null
+++ b/changelogs/fragments/7347.yml
@@ -0,0 +1,2 @@
+fix:
+- Data source selector in dev tools tab moved to left ([#7347](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7347))
\ No newline at end of file
diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx
index b1ccd290e59a..6c4cae6e18cf 100644
--- a/src/plugins/dev_tools/public/application.tsx
+++ b/src/plugins/dev_tools/public/application.tsx
@@ -124,6 +124,7 @@ function DevToolsWrapper({
onSelectedDataSource={onChange}
disabled={!dataSourceEnabled}
fullWidth={false}
+ compressed
/>
);
diff --git a/src/plugins/dev_tools/public/index.scss b/src/plugins/dev_tools/public/index.scss
index 554452a8923e..69754c302ff9 100644
--- a/src/plugins/dev_tools/public/index.scss
+++ b/src/plugins/dev_tools/public/index.scss
@@ -23,6 +23,12 @@
}
.devAppDataSourceSelector {
- margin: 7px 8px 0 0;
+ margin: 4px 8px 0 0;
min-width: 400px;
+ margin-left: auto;
+}
+
+.devAppTabs {
+ display: flex;
+ flex-flow: row wrap;
}
From e64de155546d584b92c868c38c6084d1d04b281e Mon Sep 17 00:00:00 2001
From: Tianyu Gao
Date: Mon, 22 Jul 2024 14:29:22 +0800
Subject: [PATCH 09/10] [HomePage] Add home page static list card (#7351)
* feat: add home static list card
Signed-off-by: tygao
* Changeset file for PR #7351 created/updated
* update link property
Signed-off-by: tygao
* add i18n and description
Signed-off-by: tygao
---------
Signed-off-by: tygao
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/7351.yml | 2 +
.../home_list_card.test.tsx.snap | 51 +++++++++
.../components/home_list_card.test.tsx | 27 +++++
.../application/components/home_list_card.tsx | 102 ++++++++++++++++++
.../home/public/application/home_render.tsx | 36 ++++++-
5 files changed, 216 insertions(+), 2 deletions(-)
create mode 100644 changelogs/fragments/7351.yml
create mode 100644 src/plugins/home/public/application/components/__snapshots__/home_list_card.test.tsx.snap
create mode 100644 src/plugins/home/public/application/components/home_list_card.test.tsx
create mode 100644 src/plugins/home/public/application/components/home_list_card.tsx
diff --git a/changelogs/fragments/7351.yml b/changelogs/fragments/7351.yml
new file mode 100644
index 000000000000..e8cbdc921c78
--- /dev/null
+++ b/changelogs/fragments/7351.yml
@@ -0,0 +1,2 @@
+feat:
+- Add home page static list card ([#7351](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7351))
\ No newline at end of file
diff --git a/src/plugins/home/public/application/components/__snapshots__/home_list_card.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home_list_card.test.tsx.snap
new file mode 100644
index 000000000000..892d6a8dc225
--- /dev/null
+++ b/src/plugins/home/public/application/components/__snapshots__/home_list_card.test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` should render static content normally 1`] = `
+
+
+
+`;
diff --git a/src/plugins/home/public/application/components/home_list_card.test.tsx b/src/plugins/home/public/application/components/home_list_card.test.tsx
new file mode 100644
index 000000000000..3e0a646e7279
--- /dev/null
+++ b/src/plugins/home/public/application/components/home_list_card.test.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+
+import { HomeListCard } from './home_list_card';
+
+describe('', () => {
+ it('should render static content normally', async () => {
+ const mockConfig = {
+ title: `What's New`,
+ list: [
+ {
+ label: 'Quickstart guide',
+ href: 'https://opensearch.org/docs/latest/dashboards/quickstart/',
+ target: '_blank',
+ description: 'Get started in minutes with OpenSearch Dashboards',
+ },
+ ],
+ };
+ const { baseElement } = render();
+ expect(baseElement).toMatchSnapshot();
+ });
+});
diff --git a/src/plugins/home/public/application/components/home_list_card.tsx b/src/plugins/home/public/application/components/home_list_card.tsx
new file mode 100644
index 000000000000..c905ca3272cc
--- /dev/null
+++ b/src/plugins/home/public/application/components/home_list_card.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import {
+ EuiDescriptionList,
+ EuiText,
+ EuiLink,
+ EuiTitle,
+ EuiPanel,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+ EuiSpacer,
+} from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export const LEARN_OPENSEARCH_CONFIG = {
+ title: i18n.translate('homepage.card.learnOpenSearch.title', {
+ defaultMessage: 'Learn Opensearch',
+ }),
+ list: [
+ {
+ label: 'Quickstart guide',
+ href: 'https://opensearch.org/docs/latest/dashboards/quickstart/',
+ description: 'Get started in minutes with OpenSearch Dashboards',
+ },
+ {
+ label: 'Building data visualizations',
+ href: 'https://opensearch.org/docs/latest/dashboards/visualize/viz-index/',
+ description: 'Design interactive charts and graphs to unlock insights form your data.',
+ },
+ {
+ label: 'Creating dashboards',
+ href: 'https://opensearch.org/docs/latest/dashboards/dashboard/index/',
+ description: 'Build interactive dashboards to explore and analyze your data',
+ },
+ ],
+ allLink: 'https://opensearch.org/docs/latest/',
+};
+
+export const WHATS_NEW_CONFIG = {
+ title: i18n.translate('homepage.card.whatsNew.title', {
+ defaultMessage: `What's New`,
+ }),
+ list: [
+ {
+ label: 'Quickstart guide',
+ href: 'https://opensearch.org/docs/latest/dashboards/quickstart/',
+ description: 'Get started in minutes with OpenSearch Dashboards',
+ },
+ ],
+};
+
+interface Config {
+ title: string;
+ list: Array<{
+ label: string;
+ href: string;
+ description: string;
+ }>;
+ allLink?: string;
+}
+
+export const HomeListCard = ({ config }: { config: Config }) => {
+ return (
+ <>
+
+
+ {config.title}
+
+
+ {config.list.length > 0 && (
+
+ {config.list.map((item) => (
+ <>
+
+
+ {item.label}
+
+
+ {item.description}
+ >
+ ))}
+
+ )}
+
+ {config.allLink ? (
+ <>
+
+
+
+ View all
+
+
+ >
+ ) : null}
+
+ >
+ );
+};
diff --git a/src/plugins/home/public/application/home_render.tsx b/src/plugins/home/public/application/home_render.tsx
index 49d5bd1c9043..757475dd7f45 100644
--- a/src/plugins/home/public/application/home_render.tsx
+++ b/src/plugins/home/public/application/home_render.tsx
@@ -9,7 +9,12 @@ import {
ContentManagementPluginSetup,
ContentManagementPluginStart,
} from '../../../../plugins/content_management/public';
-import { HOME_PAGE_ID, SECTIONS } from '../../common/constants';
+import { HOME_PAGE_ID, SECTIONS, HOME_CONTENT_AREAS } from '../../common/constants';
+import {
+ WHATS_NEW_CONFIG,
+ LEARN_OPENSEARCH_CONFIG,
+ HomeListCard,
+} from './components/home_list_card';
export const setupHome = (contentManagement: ContentManagementPluginSetup) => {
contentManagement.registerPage({
@@ -38,4 +43,31 @@ export const setupHome = (contentManagement: ContentManagementPluginSetup) => {
});
};
-export const initHome = (contentManagement: ContentManagementPluginStart, core: CoreStart) => {};
+export const initHome = (contentManagement: ContentManagementPluginStart, core: CoreStart) => {
+ contentManagement.registerContentProvider({
+ id: 'whats_new_cards',
+ getContent: () => ({
+ id: 'whats_new',
+ kind: 'custom',
+ order: 3,
+ render: () =>
+ React.createElement(HomeListCard, {
+ config: WHATS_NEW_CONFIG,
+ }),
+ }),
+ getTargetArea: () => HOME_CONTENT_AREAS.SERVICE_CARDS,
+ });
+ contentManagement.registerContentProvider({
+ id: 'learn_opensearch_new_cards',
+ getContent: () => ({
+ id: 'learn_opensearch',
+ kind: 'custom',
+ order: 4,
+ render: () =>
+ React.createElement(HomeListCard, {
+ config: LEARN_OPENSEARCH_CONFIG,
+ }),
+ }),
+ getTargetArea: () => HOME_CONTENT_AREAS.SERVICE_CARDS,
+ });
+};
From fcfddfa1d3c61dfebf321fb82d8c9f9b55dd323d Mon Sep 17 00:00:00 2001
From: Kawika Avilla
Date: Mon, 22 Jul 2024 13:12:29 +0000
Subject: [PATCH 10/10] ppl def working
Signed-off-by: Kawika Avilla
---
changelogs/fragments/7289.yml | 2 +
package.json | 2 +-
.../search/search_source/search_source.ts | 7 +-
.../create_dataset_navigator.tsx | 16 +
.../dataset_navigator/dataset_navigator.tsx | 385 ++++++++++++++++++
.../ui/dataset_navigator/fetch_datasources.ts | 18 +
.../dataset_navigator/fetch_index_patterns.ts | 22 +
.../ui/dataset_navigator/fetch_indices.ts | 46 +++
.../public/ui/dataset_navigator/index.tsx | 23 ++
.../ui/query_editor/_dataset_navigator.scss | 12 +
.../data/public/ui/query_editor/_index.scss | 1 +
.../public/ui/query_editor/_query_editor.scss | 6 +
.../public/ui/query_editor/query_editor.tsx | 74 +---
.../ui/query_editor/query_editor_top_row.tsx | 6 +-
.../ui/search_bar/create_search_bar.tsx | 19 +-
.../data/public/ui/search_bar/search_bar.tsx | 6 +-
.../data/public/ui/settings/settings.ts | 9 +
src/plugins/data/public/ui/types.ts | 9 +-
src/plugins/data/public/ui/ui_service.ts | 23 +-
.../public/components/sidebar/index.tsx | 43 +-
.../utils/state_management/metadata_slice.ts | 21 +-
.../public/utils/state_management/store.ts | 2 +-
.../opensearch_dashboards.json | 2 +-
.../components/connections_bar.tsx | 93 -----
.../components/index.ts | 6 -
.../public/data_source_connection/index.ts | 7 -
.../data_source_connection/services/index.ts | 6 -
.../utils/create_extension.tsx | 34 --
.../data_source_connection/utils/index.ts | 6 -
.../query_enhancements/public/plugin.tsx | 17 +-
.../public/search/ppl_search_interceptor.ts | 4 +-
.../query_enhancements/public/services.ts | 11 -
.../services/connections_service.ts | 4 +-
.../public/services/index.ts | 13 +
.../routes/data_source_connection/routes.ts | 5 +-
.../query_enhancements/server/types.ts | 2 +-
36 files changed, 662 insertions(+), 300 deletions(-)
create mode 100644 changelogs/fragments/7289.yml
create mode 100644 src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx
create mode 100644 src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx
create mode 100644 src/plugins/data/public/ui/dataset_navigator/fetch_datasources.ts
create mode 100644 src/plugins/data/public/ui/dataset_navigator/fetch_index_patterns.ts
create mode 100644 src/plugins/data/public/ui/dataset_navigator/fetch_indices.ts
create mode 100644 src/plugins/data/public/ui/dataset_navigator/index.tsx
create mode 100644 src/plugins/data/public/ui/query_editor/_dataset_navigator.scss
delete mode 100644 src/plugins/query_enhancements/public/data_source_connection/components/connections_bar.tsx
delete mode 100644 src/plugins/query_enhancements/public/data_source_connection/components/index.ts
delete mode 100644 src/plugins/query_enhancements/public/data_source_connection/index.ts
delete mode 100644 src/plugins/query_enhancements/public/data_source_connection/services/index.ts
delete mode 100644 src/plugins/query_enhancements/public/data_source_connection/utils/create_extension.tsx
delete mode 100644 src/plugins/query_enhancements/public/data_source_connection/utils/index.ts
delete mode 100644 src/plugins/query_enhancements/public/services.ts
rename src/plugins/query_enhancements/public/{data_source_connection => }/services/connections_service.ts (95%)
create mode 100644 src/plugins/query_enhancements/public/services/index.ts
diff --git a/changelogs/fragments/7289.yml b/changelogs/fragments/7289.yml
new file mode 100644
index 000000000000..b181433dbeca
--- /dev/null
+++ b/changelogs/fragments/7289.yml
@@ -0,0 +1,2 @@
+feat:
+- Add DataSet dropdown with index patterns and indices ([#7289](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7289))
\ No newline at end of file
diff --git a/package.json b/package.json
index f0600caadc20..13c78afffe0c 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
"start": "scripts/use_node scripts/opensearch_dashboards --dev",
"start:docker": "scripts/use_node scripts/opensearch_dashboards --dev --opensearch.hosts=$OPENSEARCH_HOSTS --opensearch.ignoreVersionMismatch=true --server.host=$SERVER_HOST",
"start:security": "scripts/use_node scripts/opensearch_dashboards --dev --security",
- "start:enhancements": "scripts/use_node scripts/opensearch_dashboards --dev --uiSettings.overrides['query:enhancements:enabled']=true",
+ "start:enhancements": "scripts/use_node scripts/opensearch_dashboards --dev --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true",
"debug": "scripts/use_node --nolazy --inspect scripts/opensearch_dashboards --dev",
"debug-break": "scripts/use_node --nolazy --inspect-brk scripts/opensearch_dashboards --dev",
"lint": "yarn run lint:es && yarn run lint:style",
diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts
index d9518e6a6cab..2c553fdbed97 100644
--- a/src/plugins/data/common/search/search_source/search_source.ts
+++ b/src/plugins/data/common/search/search_source/search_source.ts
@@ -324,7 +324,12 @@ export class SearchSource {
const dataFrame = createDataFrame({
name: searchRequest.index.title || searchRequest.index,
fields: [],
- ...(rawQueryString && { meta: { queryConfig: parseRawQueryString(rawQueryString) } }),
+ ...(rawQueryString && {
+ meta: {
+ queryConfig: parseRawQueryString(rawQueryString),
+ ...(searchRequest.dataSourceId && { dataSource: searchRequest.dataSourceId }),
+ },
+ }),
});
await this.setDataFrame(dataFrame);
return this.getDataFrame();
diff --git a/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx b/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx
new file mode 100644
index 000000000000..e20c3bf3f540
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx
@@ -0,0 +1,16 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { SavedObjectsClientContract } from 'src/core/public';
+import { DataSetNavigator, DataSetNavigatorProps } from './';
+
+// Updated function signature to include additional dependencies
+export function createDataSetNavigator(savedObjectsClient: SavedObjectsClientContract) {
+ // Return a function that takes props, omitting the dependencies from the props type
+ return (props: Omit) => (
+
+ );
+}
diff --git a/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx b/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx
new file mode 100644
index 000000000000..4e7b2601bdf9
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx
@@ -0,0 +1,385 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiContextMenu,
+ EuiContextMenuPanelItemDescriptor,
+ EuiForm,
+ EuiFormRow,
+ EuiLoadingSpinner,
+ EuiPopover,
+ EuiSelect,
+} from '@elastic/eui';
+import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
+import _ from 'lodash';
+import { i18n } from '@osd/i18n';
+import { fetchDataSources } from './fetch_datasources';
+import { fetchIndices } from './fetch_indices';
+import { getIndexPatterns, getQueryService, getSearchService, getUiService } from '../../services';
+import { fetchIndexPatterns } from './fetch_index_patterns';
+import { IIndexPattern } from '../..';
+
+export interface DataSetNavigatorProps {
+ dataSetId: string | undefined;
+ savedObjectsClient?: SavedObjectsClientContract;
+ onSelectDataSet: (dataSet: SimpleDataSet) => void;
+}
+
+export interface SimpleObject {
+ id: string;
+ title: string;
+ dataSourceRef?: SimpleDataSource;
+}
+
+export interface SimpleDataSource {
+ id: string;
+ name: string;
+ indices?: SimpleObject[];
+ type: 'data-source' | 'external-source';
+}
+
+export interface SimpleDataSet extends SimpleObject {
+ fields?: any[];
+ timeFieldName?: string;
+ timeFields?: any[];
+ type: 'index-pattern' | 'temporary';
+}
+
+interface DataSetNavigatorState {
+ isLoading: boolean;
+ isOpen: boolean;
+ indexPatterns: IIndexPattern[];
+ dataSources: SimpleDataSource[];
+ externalDataSources: SimpleDataSource[];
+ searchValue?: string;
+ selectedDataSource?: SimpleDataSource;
+ selectedObjects: SimpleObject[];
+ selectedObject?: SimpleDataSet;
+ selectedDataSet?: SimpleDataSet;
+}
+
+// eslint-disable-next-line import/no-default-export
+export default class DataSetNavigator extends Component {
+ private isMounted: boolean = false;
+ state: DataSetNavigatorState;
+ private searchService = getSearchService();
+ private queryService = getQueryService();
+ private uiService = getUiService();
+ private indexPatternsService = getIndexPatterns();
+
+ constructor(props: DataSetNavigatorProps) {
+ super(props);
+ this.state = {
+ isLoading: true,
+ isOpen: false,
+ indexPatterns: [],
+ dataSources: [],
+ externalDataSources: [],
+ searchValue: undefined,
+ selectedDataSource: undefined,
+ selectedObjects: [],
+ selectedObject: undefined,
+ selectedDataSet: undefined,
+ };
+ }
+
+ async componentDidMount() {
+ this.isMounted = true;
+ const indexPatterns = await fetchIndexPatterns(this.props.savedObjectsClient!, ''); // TODO: add search
+ const dataSources = await fetchDataSources(this.props.savedObjectsClient!);
+ const selectedDataSet =
+ !this.state.selectedDataSet && this.props.dataSetId
+ ? indexPatterns.find((pattern) => pattern.id === this.props.dataSetId)
+ : undefined;
+ // TODO: do i need to get datasource ref too?
+ this.setState({
+ indexPatterns,
+ dataSources,
+ selectedDataSet,
+ });
+ }
+
+ componentWillUnmount() {
+ this.isMounted = false;
+ }
+
+ getQueryStringInitialValue = (dataSet: SimpleDataSet) => {
+ const language = this.uiService.Settings.getUserQueryLanguage();
+ const input = this.uiService.Settings.getQueryEnhancements(language)?.searchBar
+ ?.queryStringInput?.initialValue;
+
+ if (!dataSet || !input)
+ return {
+ query: '',
+ language,
+ };
+
+ return {
+ query: input.replace('', dataSet.title),
+ language,
+ };
+ };
+
+ setSelectedDataSource = async (dataSource: SimpleDataSource) => {
+ if (!this.isMounted || !dataSource) return;
+ this.setState({
+ isLoading: false,
+ selectedDataSource: dataSource,
+ });
+ };
+
+ setSelectedObjects = async (dataSource: SimpleDataSource) => {
+ if (!this.isMounted || !dataSource) return;
+ const indices: any[] = await fetchIndices(this.searchService, dataSource.id);
+ const indicesWithDataSource = indices.map((indexName) => ({
+ id: indexName,
+ title: indexName,
+ dataSourceRef: {
+ id: dataSource.id,
+ name: dataSource.name,
+ type: 'data-source',
+ },
+ }));
+ this.setState({
+ selectedObjects: indicesWithDataSource,
+ });
+ };
+
+ setSelectedObject = async (object: SimpleObject) => {
+ if (!this.isMounted || !object) return;
+ this.setState({ isLoading: true });
+ const fields = await this.indexPatternsService.getFieldsForWildcard({
+ pattern: object.title,
+ dataSourceId: object.dataSourceRef?.id,
+ });
+ const timeFields = fields.filter((field: any) => field.type === 'date');
+ this.setState({
+ selectedObject: {
+ ...object,
+ fields,
+ timeFields,
+ ...(timeFields[0]?.name ? { timeFieldName: timeFields[0].name } : {}),
+ },
+ isLoading: false,
+ });
+ };
+
+ setSelectedObjectTimeField = async (object: SimpleObject, timeFieldName: string | undefined) => {
+ if (!this.isMounted) return;
+ const newObject = {
+ ...object,
+ timeFieldName: timeFieldName ?? undefined,
+ };
+ this.setState({
+ selectedObject: newObject,
+ });
+ };
+
+ setSelectedDataSet = async (dataSet: SimpleDataSet) => {
+ if (dataSet.type === 'temporary') {
+ const fieldsMap = dataSet.fields?.reduce((acc: any, field: any) => {
+ acc[field.name] = field;
+ return acc;
+ });
+ const temporaryIndexPattern = await this.indexPatternsService.create(
+ {
+ id: dataSet.id,
+ title: dataSet.title,
+ dataSourceRef: {
+ id: dataSet.dataSourceRef?.id!,
+ name: dataSet.dataSourceRef?.name!,
+ type: dataSet.dataSourceRef?.type!,
+ },
+ timeFieldName: dataSet.timeFieldName,
+ },
+ true
+ );
+ this.indexPatternsService.saveToCache(temporaryIndexPattern.title, temporaryIndexPattern);
+ }
+ this.searchService.df.clear();
+ this.props.onSelectDataSet(dataSet);
+ this.queryService.queryString.setQuery(this.getQueryStringInitialValue(dataSet));
+ this.setState({ selectedDataSet: dataSet });
+ this.closePopover();
+ };
+
+ closePopover = () => {
+ this.setState({ isOpen: false });
+ };
+
+ render() {
+ const indexPatternsLabel = i18n.translate('data.query.dataSetNavigator.indexPatternsName', {
+ defaultMessage: 'Index patterns',
+ });
+ const indicesLabel = i18n.translate('data.query.dataSetNavigator.indicesName', {
+ defaultMessage: 'Indexes',
+ });
+
+ const loadingSpinner = ;
+
+ return (
+ this.setState({ isOpen: !this.state.isOpen })}
+ >
+ {`${
+ this.state.selectedDataSet?.dataSourceRef
+ ? `${this.state.selectedDataSet.dataSourceRef.name}::`
+ : ''
+ }${
+ this.state.selectedDataSet?.title ??
+ i18n.translate('data.query.dataSetNavigator.selectDataSet', {
+ defaultMessage: 'Select data set',
+ })
+ }`}
+
+ }
+ isOpen={this.state.isOpen}
+ closePopover={this.closePopover}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+ ({
+ name: dataSource.name,
+ panel: 2,
+ onClick: async () => {
+ await this.setSelectedDataSource(dataSource);
+ },
+ })),
+ ],
+ },
+ {
+ id: 1,
+ title: indexPatternsLabel,
+ items: this.state.indexPatterns.flatMap((indexPattern, indexNum, arr) => [
+ {
+ name: indexPattern.title,
+ onClick: async () => {
+ await this.setSelectedDataSet({
+ id: indexPattern.id ?? indexPattern.title,
+ title: indexPattern.title,
+ fields: indexPattern.fields,
+ timeFieldName: indexPattern.timeFieldName,
+ type: 'index-pattern',
+ });
+ },
+ },
+ ...(indexNum < arr.length - 1 ? [{ isSeparator: true }] : []),
+ ]) as EuiContextMenuPanelItemDescriptor[],
+ },
+ {
+ id: 2,
+ title: this.state.selectedDataSource?.name,
+ items: [
+ {
+ name: indicesLabel,
+ panel: 3,
+ onClick: async () => {
+ await this.setSelectedObjects(this.state.selectedDataSource!);
+ },
+ },
+ {
+ name: 'Connected data sources',
+ disabled: true,
+ },
+ ] as EuiContextMenuPanelItemDescriptor[],
+ },
+ {
+ id: 3,
+ title: indicesLabel,
+ items: this.state.selectedObjects.flatMap(
+ (object: SimpleObject, indexNum: number, arr: any[]) => [
+ {
+ name: object.title,
+ panel: 4,
+ onClick: async () => {
+ await this.setSelectedObject(object);
+ },
+ },
+ ...(indexNum < arr.length - 1 ? [{ isSeparator: true }] : []),
+ ]
+ ) as EuiContextMenuPanelItemDescriptor[],
+ },
+ {
+ id: 4,
+ title: this.state.selectedObject?.title,
+ content:
+ this.state.isLoading && !this.state.selectedObject ? (
+ loadingSpinner
+ ) : (
+
+
+ 0
+ ? [
+ ...this.state.selectedObject!.timeFields.map((field: any) => ({
+ value: field.name,
+ text: field.name,
+ })),
+ ]
+ : []),
+ { value: 'no-time-filter', text: "I don't want to use a time filter" },
+ ]}
+ onChange={(event) => {
+ this.setSelectedObjectTimeField(
+ this.state.selectedObject!,
+ event.target.value !== 'no-time-filter' ? event.target.value : undefined
+ );
+ }}
+ aria-label="Select a date field"
+ />
+
+ {
+ await this.setSelectedDataSet({
+ ...this.state.selectedObject!,
+ type: 'temporary',
+ });
+ }}
+ >
+ Select
+
+
+ ),
+ },
+ ]}
+ />
+
+ );
+ }
+}
diff --git a/src/plugins/data/public/ui/dataset_navigator/fetch_datasources.ts b/src/plugins/data/public/ui/dataset_navigator/fetch_datasources.ts
new file mode 100644
index 000000000000..3b44733d8666
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_navigator/fetch_datasources.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
+
+export const fetchDataSources = async (client: SavedObjectsClientContract) => {
+ const resp = await client.find({
+ type: 'data-source',
+ perPage: 10000,
+ });
+ return resp.savedObjects.map((savedObject) => ({
+ id: savedObject.id,
+ name: savedObject.attributes.title,
+ type: 'data-source',
+ }));
+};
diff --git a/src/plugins/data/public/ui/dataset_navigator/fetch_index_patterns.ts b/src/plugins/data/public/ui/dataset_navigator/fetch_index_patterns.ts
new file mode 100644
index 000000000000..589b5f2b7e48
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_navigator/fetch_index_patterns.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
+import { IIndexPattern } from '../..';
+
+export const fetchIndexPatterns = async (client: SavedObjectsClientContract, search: string) => {
+ const resp = await client.find({
+ type: 'index-pattern',
+ fields: ['title'],
+ search: `${search}*`,
+ searchFields: ['title'],
+ perPage: 100,
+ });
+ return resp.savedObjects.map((savedObject) => ({
+ id: savedObject.id,
+ title: savedObject.attributes.title,
+ dataSourceId: savedObject.references[0]?.id,
+ }));
+};
diff --git a/src/plugins/data/public/ui/dataset_navigator/fetch_indices.ts b/src/plugins/data/public/ui/dataset_navigator/fetch_indices.ts
new file mode 100644
index 000000000000..35b1eb4a83f2
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_navigator/fetch_indices.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { map } from 'rxjs/operators';
+import { ISearchStart } from '../../search';
+
+export const fetchIndices = async (search: ISearchStart, dataSourceId?: string) => {
+ const buildSearchRequest = () => {
+ const request = {
+ params: {
+ ignoreUnavailable: true,
+ expand_wildcards: 'all',
+ index: '*',
+ body: {
+ size: 0, // no hits
+ aggs: {
+ indices: {
+ terms: {
+ field: '_index',
+ size: 100,
+ },
+ },
+ },
+ },
+ },
+ dataSourceId,
+ };
+
+ return request;
+ };
+
+ const searchResponseToArray = (response: any) => {
+ const { rawResponse } = response;
+ return rawResponse.aggregations
+ ? rawResponse.aggregations.indices.buckets.map((bucket: { key: any }) => bucket.key)
+ : [];
+ };
+
+ return search
+ .getDefaultSearchInterceptor()
+ .search(buildSearchRequest())
+ .pipe(map(searchResponseToArray))
+ .toPromise();
+};
diff --git a/src/plugins/data/public/ui/dataset_navigator/index.tsx b/src/plugins/data/public/ui/dataset_navigator/index.tsx
new file mode 100644
index 000000000000..e8ce9554d5a3
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_navigator/index.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import type { DataSetNavigatorProps } from './dataset_navigator';
+
+const Fallback = () => ;
+
+const LazyDataSetNavigator = React.lazy(() => import('./dataset_navigator'));
+export const DataSetNavigator = (props: DataSetNavigatorProps) => (
+ }>
+
+
+);
+
+export * from './create_dataset_navigator';
+export type { DataSetNavigatorProps } from './dataset_navigator';
+
+export { fetchDataSources } from './fetch_datasources';
+export { fetchIndexPatterns } from './fetch_index_patterns';
+export { fetchIndices } from './fetch_indices';
diff --git a/src/plugins/data/public/ui/query_editor/_dataset_navigator.scss b/src/plugins/data/public/ui/query_editor/_dataset_navigator.scss
new file mode 100644
index 000000000000..ab85456df1b0
--- /dev/null
+++ b/src/plugins/data/public/ui/query_editor/_dataset_navigator.scss
@@ -0,0 +1,12 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+.dataSetNavigator {
+ width: 300px;
+ border-bottom: $euiBorderThin !important;
+}
+
+.dataSetNavigatorFormWrapper {
+ padding: $euiSizeS;
+}
diff --git a/src/plugins/data/public/ui/query_editor/_index.scss b/src/plugins/data/public/ui/query_editor/_index.scss
index 64fb0056cb71..f51d45b8245c 100644
--- a/src/plugins/data/public/ui/query_editor/_index.scss
+++ b/src/plugins/data/public/ui/query_editor/_index.scss
@@ -1,2 +1,3 @@
@import "./language_selector";
+@import "./dataset_navigator";
@import "./query_editor";
diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss
index 8fc81308b533..ac411b38ab88 100644
--- a/src/plugins/data/public/ui/query_editor/_query_editor.scss
+++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss
@@ -86,6 +86,12 @@
}
}
+.osdQueryEditor__dataSetNavigatorWrapper {
+ :first-child {
+ border-bottom: $euiBorderThin !important;
+ }
+}
+
@include euiBreakpoint("xs", "s") {
.osdQueryEditor--withDatePicker {
> :first-child {
diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx
index 69d332c65c04..903287382d2d 100644
--- a/src/plugins/data/public/ui/query_editor/query_editor.tsx
+++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx
@@ -41,9 +41,7 @@ export interface QueryEditorProps {
dataSource?: DataSource;
query: Query;
container?: HTMLDivElement;
- dataSourceContainerRef?: React.RefCallback;
- containerRef?: React.RefCallback;
- languageSelectorContainerRef?: React.RefCallback;
+ dataSetContainerRef?: React.RefCallback;
settings: Settings;
disableAutoFocus?: boolean;
screenTitle?: string;
@@ -56,7 +54,7 @@ export interface QueryEditorProps {
onChange?: (query: Query, dateRange?: TimeRange) => void;
onChangeQueryEditorFocus?: (isFocused: boolean) => void;
onSubmit?: (query: Query, dateRange?: TimeRange) => void;
- getQueryStringInitialValue?: (language: string) => string;
+ getQueryStringInitialValue?: (language: string, dataSetName?: string) => string;
dataTestSubj?: string;
size?: SuggestionsListSize;
className?: string;
@@ -78,6 +76,7 @@ interface State {
isSuggestionsVisible: boolean;
index: number | null;
suggestions: QuerySuggestion[];
+ selectedDataSet: any;
indexPatterns: IIndexPattern[];
isCollapsed: boolean;
timeStamp: IFieldType | null;
@@ -107,6 +106,7 @@ export default class QueryEditorUI extends Component {
index: null,
suggestions: [],
indexPatterns: [],
+ selectedDataSet: null,
isCollapsed: false, // default to expand mode
timeStamp: null,
lineCount: undefined,
@@ -117,7 +117,6 @@ export default class QueryEditorUI extends Component {
private persistedLog: PersistedLog | undefined;
private abortController?: AbortController;
private services = this.props.opensearchDashboards.services;
- private componentIsUnmounting = false;
private headerRef: RefObject = createRef();
private bannerRef: RefObject = createRef();
private extensionMap = this.props.settings?.getQueryEditorExtensionMap();
@@ -246,10 +245,6 @@ export default class QueryEditorUI extends Component {
: undefined;
this.onChange(newQuery, dateRange);
this.onSubmit(newQuery, dateRange);
- this.setState({
- isDataSourcesVisible: enhancement?.searchBar?.showDataSourcesSelector ?? true,
- isDataSetsVisible: enhancement?.searchBar?.showDataSetsSelector ?? true,
- });
};
private initPersistedLog = () => {
@@ -259,20 +254,6 @@ export default class QueryEditorUI extends Component {
: getQueryLog(uiSettings, storage, appName, this.props.query.language);
};
- private initDataSourcesVisibility = () => {
- if (this.componentIsUnmounting) return;
-
- return this.props.settings.getQueryEnhancements(this.props.query.language)?.searchBar
- ?.showDataSourcesSelector;
- };
-
- private initDataSetsVisibility = () => {
- if (this.componentIsUnmounting) return;
-
- return this.props.settings.getQueryEnhancements(this.props.query.language)?.searchBar
- ?.showDataSetsSelector;
- };
-
public onMouseEnterSuggestion = (index: number) => {
this.setState({ index });
};
@@ -287,10 +268,6 @@ export default class QueryEditorUI extends Component {
this.initPersistedLog();
// this.fetchIndexPatterns().then(this.updateSuggestions);
- this.setState({
- isDataSourcesVisible: this.initDataSourcesVisibility() || true,
- isDataSetsVisible: this.initDataSetsVisibility() || true,
- });
}
public componentDidUpdate(prevProps: Props) {
@@ -304,7 +281,6 @@ export default class QueryEditorUI extends Component {
public componentWillUnmount() {
if (this.abortController) this.abortController.abort();
- this.componentIsUnmounting = true;
}
handleOnFocus = () => {
@@ -373,6 +349,15 @@ export default class QueryEditorUI extends Component {
const useQueryEditor =
this.props.query.language !== 'kuery' && this.props.query.language !== 'lucene';
+ const languageSelector = (
+
+ );
+
return (
@@ -385,17 +370,9 @@ export default class QueryEditorUI extends Component
{
isCollapsed={!this.state.isCollapsed}
/>
- {this.state.isDataSourcesVisible && (
-
-
-
- )}
-
- {this.state.isDataSetsVisible && (
-
-
-
- )}
+
+
+
{(this.state.isCollapsed || !useQueryEditor) && (
@@ -434,14 +411,7 @@ export default class QueryEditorUI extends Component {
)}
{!useQueryEditor && (
-
-
-
+ {languageSelector}
)}
@@ -491,15 +461,7 @@ export default class QueryEditorUI extends Component {
}
>
-
-
-
+ {languageSelector}
{this.state.lineCount} {this.state.lineCount === 1 ? 'line' : 'lines'}
diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
index 8304fdc252ee..7c2cc98d9299 100644
--- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
+++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
@@ -38,8 +38,7 @@ const QueryEditor = withOpenSearchDashboards(QueryEditorUI);
// @internal
export interface QueryEditorTopRowProps {
query?: Query;
- dataSourceContainerRef?: React.RefCallback;
- containerRef?: React.RefCallback;
+ dataSetContainerRef?: React.RefCallback;
settings?: Settings;
onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void;
onChange: (payload: { dateRange: TimeRange; query?: Query }) => void;
@@ -223,8 +222,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
dataSource={props.dataSource}
prepend={props.prepend}
query={parsedQuery}
- dataSourceContainerRef={props.dataSourceContainerRef}
- containerRef={props.containerRef}
+ dataSetContainerRef={props.dataSetContainerRef}
settings={props.settings!}
screenTitle={props.screenTitle}
onChange={onQueryChange}
diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx
index 244f4296216c..7fe38f548048 100644
--- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx
+++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx
@@ -48,8 +48,7 @@ interface StatefulSearchBarDeps {
data: Omit;
storage: IStorageWrapper;
settings: Settings;
- setDataSourceContainerRef: (ref: HTMLDivElement | null) => void;
- setContainerRef: (ref: HTMLDivElement | null) => void;
+ setDataSetContainerRef: (ref: HTMLDivElement | null) => void;
}
export type StatefulSearchBarProps = SearchBarOwnProps & {
@@ -139,8 +138,7 @@ export function createSearchBar({
storage,
data,
settings,
- setDataSourceContainerRef,
- setContainerRef,
+ setDataSetContainerRef,
}: StatefulSearchBarDeps) {
// App name should come from the core application service.
// Until it's available, we'll ask the user to provide it for the pre-wired component.
@@ -176,15 +174,9 @@ export function createSearchBar({
notifications: core.notifications,
});
- const dataSourceContainerRef = useCallback((node) => {
+ const dataSetContainerRef = useCallback((node) => {
if (node) {
- setDataSourceContainerRef(node);
- }
- }, []);
-
- const containerRef = useCallback((node) => {
- if (node) {
- setContainerRef(node);
+ setDataSetContainerRef(node);
}
}, []);
@@ -228,8 +220,7 @@ export function createSearchBar({
filters={filters}
query={query}
settings={settings}
- dataSourceContainerRef={dataSourceContainerRef}
- containerRef={containerRef}
+ dataSetContainerRef={dataSetContainerRef}
onFiltersUpdated={defaultFiltersUpdated(data.query)}
onRefreshChange={defaultOnRefreshChange(data.query)}
savedQuery={savedQuery}
diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx
index 11914f134443..0855a0fc6a4f 100644
--- a/src/plugins/data/public/ui/search_bar/search_bar.tsx
+++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx
@@ -80,8 +80,7 @@ export interface SearchBarOwnProps {
// Query bar - should be in SearchBarInjectedDeps
query?: Query;
settings?: Settings;
- dataSourceContainerRef?: React.RefCallback;
- containerRef?: React.RefCallback;
+ dataSetContainerRef?: React.RefCallback;
// Show when user has privileges to save
showSaveQuery?: boolean;
savedQuery?: SavedQuery;
@@ -491,8 +490,7 @@ class SearchBarUI extends Component {
queryEditor = (
;
SearchBar: React.ComponentType;
SuggestionsComponent: React.ComponentType;
+ /**
+ * @experimental - Subject to change
+ */
Settings: Settings;
- dataSourceContainer$: Observable;
- container$: Observable;
+ DataSetNavigator: React.ComponentType;
+ dataSetContainer$: Observable;
+ onSelectDataSet?: () => void | undefined;
}
diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts
index 1e0e6be8b78c..a55fe1ccf847 100644
--- a/src/plugins/data/public/ui/ui_service.ts
+++ b/src/plugins/data/public/ui/ui_service.ts
@@ -9,6 +9,7 @@ import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public';
import { ConfigSchema } from '../../config';
import { DataPublicPluginStart } from '../types';
import { createIndexPatternSelect } from './index_pattern_select';
+import { createDataSetNavigator } from './dataset_navigator/create_dataset_navigator';
import { QueryEditorExtensionConfig } from './query_editor';
import { createSearchBar } from './search_bar/create_search_bar';
import { createSettings } from './settings';
@@ -29,9 +30,8 @@ export class UiService implements Plugin {
enhancementsConfig: ConfigSchema['enhancements'];
private queryEnhancements: Map = new Map();
private queryEditorExtensionMap: Record = {};
- private dataSourceContainer$ = new BehaviorSubject(null);
- private container$ = new BehaviorSubject(null);
-
+ private dataSetContainer$ = new BehaviorSubject(null);
+ private onSelectDataSet?: () => void;
constructor(initializerContext: PluginInitializerContext) {
const { enhancements } = initializerContext.config.get();
@@ -62,12 +62,12 @@ export class UiService implements Plugin {
queryEditorExtensionMap: this.queryEditorExtensionMap,
});
- const setDataSourceContainerRef = (ref: HTMLDivElement | null) => {
- this.dataSourceContainer$.next(ref);
+ const setDataSetContainerRef = (ref: HTMLDivElement | null) => {
+ this.dataSetContainer$.next(ref);
};
- const setContainerRef = (ref: HTMLDivElement | null) => {
- this.container$.next(ref);
+ const setOnDataSetSelect = (onSelectDataSet: () => void) => {
+ this.onSelectDataSet = onSelectDataSet;
};
const SearchBar = createSearchBar({
@@ -75,17 +75,18 @@ export class UiService implements Plugin {
data: dataServices,
storage,
settings: Settings,
- setDataSourceContainerRef,
- setContainerRef,
+ setDataSetContainerRef,
+ setOnDataSetSelect,
});
return {
IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client),
+ DataSetNavigator: createDataSetNavigator(core.savedObjects.client),
SearchBar,
SuggestionsComponent,
Settings,
- dataSourceContainer$: this.dataSourceContainer$,
- container$: this.container$,
+ dataSetContainer$: this.dataSetContainer$,
+ onSelectDataSet: this.onSelectDataSet,
};
}
diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx
index eea1860dc950..9e7ed63b2009 100644
--- a/src/plugins/data_explorer/public/components/sidebar/index.tsx
+++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx
@@ -30,6 +30,8 @@ export const Sidebar: FC = ({ children }) => {
},
} = useOpenSearchDashboards();
+ const { DataSetNavigator } = ui;
+
useEffect(() => {
const subscriptions = ui.Settings.getEnabledQueryEnhancementsUpdated$().subscribe(
(enabledQueryEnhancements) => {
@@ -48,17 +50,17 @@ export const Sidebar: FC = ({ children }) => {
useEffect(() => {
if (!isEnhancementsEnabled) return;
- const subscriptions = ui.container$.subscribe((container) => {
- if (container === null) return;
+ const subscriptions = ui.dataSetContainer$.subscribe((dataSetContainer) => {
+ if (dataSetContainer === null) return;
if (containerRef.current) {
- setContainerRef(container);
+ setContainerRef(dataSetContainer);
}
});
return () => {
subscriptions.unsubscribe();
};
- }, [ui.container$, containerRef, setContainerRef, isEnhancementsEnabled]);
+ }, [ui.dataSetContainer$, containerRef, setContainerRef, isEnhancementsEnabled]);
useEffect(() => {
let isMounted = true;
@@ -130,23 +132,17 @@ export const Sidebar: FC = ({ children }) => {
[toasts]
);
+ const handleDataSetSelection = useCallback(
+ (dataSet: any) => {
+ dispatch(setIndexPattern(dataSet.id!));
+ },
+ [dispatch]
+ );
+
const memorizedReload = useCallback(() => {
dataSources.dataSourceService.reload();
}, [dataSources.dataSourceService]);
- const dataSourceSelector = (
-
- );
-
return (
{
containerRef.current = node;
}}
>
- {dataSourceSelector}
+
)}
{!isEnhancementsEnabled && (
@@ -171,7 +167,16 @@ export const Sidebar: FC = ({ children }) => {
color="transparent"
className="deSidebar_dataSource"
>
- {dataSourceSelector}
+
)}
diff --git a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts
index e9fe84713120..234951359952 100644
--- a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts
+++ b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts
@@ -6,10 +6,26 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { DataExplorerServices } from '../../types';
+interface DataSourceMeta {
+ ref?: string; // MDS ID
+ dsName?: string; // flint datasource
+}
+
+export interface DataSet {
+ id: string; // index pattern ID, index name, or flintdatasource.database.table
+ dataSource?: DataSourceMeta;
+ meta?: {
+ timestampField: string;
+ mapping?: any;
+ };
+ type?: 'dataset' | 'temporary';
+}
+
export interface MetadataState {
indexPattern?: string;
originatingApp?: string;
view?: string;
+ dataSet?: DataSet;
}
const initialState: MetadataState = {};
@@ -40,6 +56,9 @@ export const slice = createSlice({
setIndexPattern: (state, action: PayloadAction) => {
state.indexPattern = action.payload;
},
+ setDataSet: (state, action: PayloadAction) => {
+ state.dataSet = action.payload;
+ },
setOriginatingApp: (state, action: PayloadAction) => {
state.originatingApp = action.payload;
},
@@ -53,4 +72,4 @@ export const slice = createSlice({
});
export const { reducer } = slice;
-export const { setIndexPattern, setOriginatingApp, setView, setState } = slice.actions;
+export const { setIndexPattern, setDataSet, setOriginatingApp, setView, setState } = slice.actions;
diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts
index daf0b3d7e369..9d320de4b54b 100644
--- a/src/plugins/data_explorer/public/utils/state_management/store.ts
+++ b/src/plugins/data_explorer/public/utils/state_management/store.ts
@@ -116,4 +116,4 @@ export type RenderState = Omit; // Remaining state after
export type Store = ReturnType;
export type AppDispatch = Store['dispatch'];
-export { MetadataState, setIndexPattern, setOriginatingApp } from './metadata_slice';
+export { MetadataState, setIndexPattern, setDataSet, setOriginatingApp } from './metadata_slice';
diff --git a/src/plugins/query_enhancements/opensearch_dashboards.json b/src/plugins/query_enhancements/opensearch_dashboards.json
index b09494aab0ca..69d8fd3bd667 100644
--- a/src/plugins/query_enhancements/opensearch_dashboards.json
+++ b/src/plugins/query_enhancements/opensearch_dashboards.json
@@ -3,7 +3,7 @@
"version": "opensearchDashboards",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "opensearchDashboardsReact", "opensearchDashboardsUtils", "dataSourceManagement", "savedObjects", "uiActions"],
+ "requiredPlugins": ["data", "opensearchDashboardsReact", "opensearchDashboardsUtils", "savedObjects", "uiActions"],
"optionalPlugins": ["dataSource"]
}
diff --git a/src/plugins/query_enhancements/public/data_source_connection/components/connections_bar.tsx b/src/plugins/query_enhancements/public/data_source_connection/components/connections_bar.tsx
deleted file mode 100644
index d7590b278220..000000000000
--- a/src/plugins/query_enhancements/public/data_source_connection/components/connections_bar.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React, { useEffect, useRef, useState } from 'react';
-import { EuiPortal } from '@elastic/eui';
-import { distinctUntilChanged } from 'rxjs/operators';
-import { ToastsSetup } from 'opensearch-dashboards/public';
-import { DataPublicPluginStart, QueryEditorExtensionDependencies } from '../../../../data/public';
-import { DataSourceSelector } from '../../../../data_source_management/public';
-import { ConnectionsService } from '../services';
-
-interface ConnectionsProps {
- dependencies: QueryEditorExtensionDependencies;
- toasts: ToastsSetup;
- connectionsService: ConnectionsService;
-}
-
-export const ConnectionsBar: React.FC = ({ connectionsService, toasts }) => {
- const [isDataSourceEnabled, setIsDataSourceEnabled] = useState(false);
- const [uiService, setUiService] = useState(undefined);
- const containerRef = useRef(null);
-
- useEffect(() => {
- const uiServiceSubscription = connectionsService.getUiService().subscribe(setUiService);
- const dataSourceEnabledSubscription = connectionsService
- .getIsDataSourceEnabled$()
- .subscribe(setIsDataSourceEnabled);
-
- return () => {
- uiServiceSubscription.unsubscribe();
- dataSourceEnabledSubscription.unsubscribe();
- };
- }, [connectionsService]);
-
- useEffect(() => {
- if (!uiService || !isDataSourceEnabled || !containerRef.current) return;
- const subscriptions = uiService.dataSourceContainer$.subscribe((container) => {
- if (container && containerRef.current) {
- container.append(containerRef.current);
- }
- });
-
- return () => subscriptions.unsubscribe();
- }, [uiService, isDataSourceEnabled]);
-
- useEffect(() => {
- const selectedConnectionSubscription = connectionsService
- .getSelectedConnection$()
- .pipe(distinctUntilChanged())
- .subscribe((connection) => {
- if (connection) {
- // Assuming setSelectedConnection$ is meant to update some state or perform an action outside this component
- connectionsService.setSelectedConnection$(connection);
- }
- });
-
- return () => selectedConnectionSubscription.unsubscribe();
- }, [connectionsService]);
-
- const handleSelectedConnection = (id: string | undefined) => {
- if (!id) {
- connectionsService.setSelectedConnection$(undefined);
- return;
- }
- connectionsService.getConnectionById(id).subscribe((connection) => {
- connectionsService.setSelectedConnection$(connection);
- });
- };
-
- return (
- {
- containerRef.current = node;
- }}
- >
-
-
- handleSelectedConnection(dataSource[0]?.id || undefined)
- }
- />
-
-
- );
-};
diff --git a/src/plugins/query_enhancements/public/data_source_connection/components/index.ts b/src/plugins/query_enhancements/public/data_source_connection/components/index.ts
deleted file mode 100644
index 1ee969a1d079..000000000000
--- a/src/plugins/query_enhancements/public/data_source_connection/components/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export { ConnectionsBar } from './connections_bar';
diff --git a/src/plugins/query_enhancements/public/data_source_connection/index.ts b/src/plugins/query_enhancements/public/data_source_connection/index.ts
deleted file mode 100644
index e334163d91d4..000000000000
--- a/src/plugins/query_enhancements/public/data_source_connection/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export { createDataSourceConnectionExtension } from './utils';
-export * from './services';
diff --git a/src/plugins/query_enhancements/public/data_source_connection/services/index.ts b/src/plugins/query_enhancements/public/data_source_connection/services/index.ts
deleted file mode 100644
index 08eeda5a7aa1..000000000000
--- a/src/plugins/query_enhancements/public/data_source_connection/services/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export { ConnectionsService } from './connections_service';
diff --git a/src/plugins/query_enhancements/public/data_source_connection/utils/create_extension.tsx b/src/plugins/query_enhancements/public/data_source_connection/utils/create_extension.tsx
deleted file mode 100644
index e5822c4b378e..000000000000
--- a/src/plugins/query_enhancements/public/data_source_connection/utils/create_extension.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React from 'react';
-import { ToastsSetup } from 'opensearch-dashboards/public';
-import { QueryEditorExtensionConfig } from '../../../../data/public';
-import { ConfigSchema } from '../../../common/config';
-import { ConnectionsBar } from '../components';
-import { ConnectionsService } from '../services';
-
-export const createDataSourceConnectionExtension = (
- connectionsService: ConnectionsService,
- toasts: ToastsSetup,
- config: ConfigSchema
-): QueryEditorExtensionConfig => {
- return {
- id: 'data-source-connection',
- order: 2000,
- isEnabled$: (dependencies) => {
- return connectionsService.getIsDataSourceEnabled$();
- },
- getComponent: (dependencies) => {
- return (
-
- );
- },
- };
-};
diff --git a/src/plugins/query_enhancements/public/data_source_connection/utils/index.ts b/src/plugins/query_enhancements/public/data_source_connection/utils/index.ts
deleted file mode 100644
index 9eccc9e6f35a..000000000000
--- a/src/plugins/query_enhancements/public/data_source_connection/utils/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-export * from './create_extension';
diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx
index 0ea557db8ce2..9baec694f1a9 100644
--- a/src/plugins/query_enhancements/public/plugin.tsx
+++ b/src/plugins/query_enhancements/public/plugin.tsx
@@ -7,10 +7,9 @@ import moment from 'moment';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '../../../core/public';
import { IStorageWrapper, Storage } from '../../opensearch_dashboards_utils/public';
import { ConfigSchema } from '../common/config';
-import { ConnectionsService, createDataSourceConnectionExtension } from './data_source_connection';
+import { ConnectionsService, setData, setStorage } from './services';
import { createQueryAssistExtension } from './query_assist';
import { PPLSearchInterceptor, SQLAsyncSearchInterceptor, SQLSearchInterceptor } from './search';
-import { setData, setStorage } from './services';
import {
QueryEnhancementsPluginSetup,
QueryEnhancementsPluginSetupDependencies,
@@ -89,7 +88,7 @@ export class QueryEnhancementsPlugin
initialTo: moment().add(2, 'days').toISOString(),
},
showFilterBar: false,
- showDataSetsSelector: false,
+ showDataSetsSelector: true,
showDataSourcesSelector: true,
},
fields: {
@@ -109,7 +108,7 @@ export class QueryEnhancementsPlugin
searchBar: {
showDatePicker: false,
showFilterBar: false,
- showDataSetsSelector: false,
+ showDataSetsSelector: true,
showDataSourcesSelector: true,
queryStringInput: { initialValue: 'SELECT * FROM ' },
},
@@ -155,16 +154,6 @@ export class QueryEnhancementsPlugin
},
});
- data.__enhance({
- ui: {
- queryEditorExtension: createDataSourceConnectionExtension(
- this.connectionsService,
- core.notifications.toasts,
- this.config
- ),
- },
- });
-
return {};
}
diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts
index bca9961fea3b..0d1e5292b5df 100644
--- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts
+++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts
@@ -34,7 +34,7 @@ import {
fetchDataFrame,
} from '../../common';
import { QueryEnhancementsPluginStartDependencies } from '../types';
-import { ConnectionsService } from '../data_source_connection';
+import { ConnectionsService } from '../services';
export class PPLSearchInterceptor extends SearchInterceptor {
protected queryService!: DataPublicPluginStart['query'];
@@ -166,7 +166,7 @@ export class PPLSearchInterceptor extends SearchInterceptor {
queryConfig: {
...dataFrame.meta.queryConfig,
...(this.connectionsService.getSelectedConnection() && {
- dataSourceId: this.connectionsService.getSelectedConnection()?.id,
+ dataSourceId: this.connectionsService.getSelectedConnection()?.dataSource.id,
}),
},
};
diff --git a/src/plugins/query_enhancements/public/services.ts b/src/plugins/query_enhancements/public/services.ts
deleted file mode 100644
index d11233be2dca..000000000000
--- a/src/plugins/query_enhancements/public/services.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { createGetterSetter } from '../../opensearch_dashboards_utils/common';
-import { IStorageWrapper } from '../../opensearch_dashboards_utils/public';
-import { DataPublicPluginStart } from '../../data/public';
-
-export const [getStorage, setStorage] = createGetterSetter('storage');
-export const [getData, setData] = createGetterSetter('data');
diff --git a/src/plugins/query_enhancements/public/data_source_connection/services/connections_service.ts b/src/plugins/query_enhancements/public/services/connections_service.ts
similarity index 95%
rename from src/plugins/query_enhancements/public/data_source_connection/services/connections_service.ts
rename to src/plugins/query_enhancements/public/services/connections_service.ts
index 6afec4b51a99..97a59c2cd94a 100644
--- a/src/plugins/query_enhancements/public/data_source_connection/services/connections_service.ts
+++ b/src/plugins/query_enhancements/public/services/connections_service.ts
@@ -6,8 +6,8 @@
import { BehaviorSubject, Observable, from } from 'rxjs';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { CoreStart } from 'opensearch-dashboards/public';
-import { API } from '../../../common';
-import { Connection, ConnectionsServiceDeps } from '../../types';
+import { API } from '../../common';
+import { Connection, ConnectionsServiceDeps } from '../types';
export class ConnectionsService {
protected http!: ConnectionsServiceDeps['http'];
diff --git a/src/plugins/query_enhancements/public/services/index.ts b/src/plugins/query_enhancements/public/services/index.ts
new file mode 100644
index 000000000000..bb0284408faa
--- /dev/null
+++ b/src/plugins/query_enhancements/public/services/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { createGetterSetter } from '../../../opensearch_dashboards_utils/common';
+import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public';
+import { DataPublicPluginStart } from '../../../data/public';
+
+export const [getStorage, setStorage] = createGetterSetter('storage');
+export const [getData, setData] = createGetterSetter('data');
+
+export { ConnectionsService } from './connections_service';
diff --git a/src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts b/src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts
index f4fe42779dae..162cc7e8f103 100644
--- a/src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts
+++ b/src/plugins/query_enhancements/server/routes/data_source_connection/routes.ts
@@ -5,7 +5,6 @@
import { schema } from '@osd/config-schema';
import { IRouter } from 'opensearch-dashboards/server';
-import { DataSourceAttributes } from '../../../../data_source/common/data_sources';
import { API } from '../../../common';
export function registerDataSourceConnectionsRoutes(router: IRouter) {
@@ -18,7 +17,7 @@ export function registerDataSourceConnectionsRoutes(router: IRouter) {
},
async (context, request, response) => {
const fields = ['id', 'title', 'auth.type'];
- const resp = await context.core.savedObjects.client.find({
+ const resp = await context.core.savedObjects.client.find({
type: 'data-source',
fields,
perPage: 10000,
@@ -38,7 +37,7 @@ export function registerDataSourceConnectionsRoutes(router: IRouter) {
},
},
async (context, request, response) => {
- const resp = await context.core.savedObjects.client.get(
+ const resp = await context.core.savedObjects.client.get(
'data-source',
request.params.dataSourceId
);
diff --git a/src/plugins/query_enhancements/server/types.ts b/src/plugins/query_enhancements/server/types.ts
index 2ab716b98928..dd264568386d 100644
--- a/src/plugins/query_enhancements/server/types.ts
+++ b/src/plugins/query_enhancements/server/types.ts
@@ -4,7 +4,7 @@
*/
import { PluginSetup } from 'src/plugins/data/server';
-import { DataSourcePluginSetup } from '../../data_source/server';
+import { DataSourcePluginSetup } from 'src/plugins/data_source/server';
import { Logger } from '../../../core/server';
import { ConfigSchema } from '../common/config';