From 0831c99576694960c82a29f57bce585e403d3718 Mon Sep 17 00:00:00 2001
From: Qxisylolo
Date: Tue, 1 Oct 2024 17:26:06 +0800
Subject: [PATCH 01/31] [workspace]feat/add workspace selector to left nav bar.
(#8364)
* feat/add_workspace_selector
Signed-off-by: Qxisylolo
* Add_workspace_selector_to_left_nav, adjust style
Signed-off-by: Qxisylolo
* Add_workspace_selector_to_left_nav, adjust styles
Signed-off-by: Qxisylolo
* Add_workspace_selector_to_left_nav, keep the origin width of nav bar
Signed-off-by: Qxisylolo
* Add_workspace_selector_to_left_nav, keep the origin width of nav bar, and adjust width
Signed-off-by: Qxisylolo
* fix/the_UI_of_recent_assets, resolve comments
Signed-off-by: Qxisylolo
* fix/the_UI_of_recent_assets, add tests
Signed-off-by: Qxisylolo
* fix/the_UI_of_recent_assets, set scss
Signed-off-by: Qxisylolo
* fix/the_UI_of_recent_assets, resolve conflicts
Signed-off-by: Qxisylolo
* fix/the_UI_of_recent_assets, delete workspace list
Signed-off-by: Qxisylolo
* feat: show nav group when not in a workspace
Signed-off-by: SuZhou-Joe
* feat: format scss file
Signed-off-by: SuZhou-Joe
* feat: optimize code
Signed-off-by: SuZhou-Joe
* Update src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
Co-authored-by: Yulong Ruan
Signed-off-by: SuZhou-Joe
* fix: unit test
Signed-off-by: SuZhou-Joe
---------
Signed-off-by: Qxisylolo
Signed-off-by: SuZhou-Joe
Co-authored-by: SuZhou-Joe
Co-authored-by: Yulong Ruan
---
.../header/collapsible_nav_group_enabled.tsx | 15 +-
.../collapsible_nav_group_enabled_top.tsx | 24 +-
.../workspace_menu/workspace_menu.scss | 3 +
.../workspace_menu/workspace_menu.test.tsx | 28 +--
.../workspace_menu/workspace_menu.tsx | 201 +++++++--------
.../workspace_picker_content.tsx | 183 +++++++++-----
.../workspace_selector.test.tsx | 231 ++++++++++++++++++
.../workspace_selector/workspace_selector.tsx | 206 ++++++++++++++++
src/plugins/workspace/public/plugin.test.ts | 16 +-
src/plugins/workspace/public/plugin.ts | 22 +-
10 files changed, 722 insertions(+), 207 deletions(-)
create mode 100644 src/plugins/workspace/public/components/workspace_menu/workspace_menu.scss
create mode 100644 src/plugins/workspace/public/components/workspace_selector/workspace_selector.test.tsx
create mode 100644 src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx
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 c1c9d518a219..4387eb6c1769 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
@@ -8,7 +8,6 @@ import {
EuiFlyout,
EuiPanel,
EuiHorizontalRule,
- EuiSpacer,
EuiHideFor,
EuiFlyoutProps,
EuiShowFor,
@@ -79,6 +78,7 @@ export function CollapsibleNavGroupEnabled({
id,
isNavOpen,
storage = window.localStorage,
+ currentWorkspace$,
closeNav,
navigateToApp,
navigateToUrl,
@@ -94,7 +94,6 @@ export function CollapsibleNavGroupEnabled({
const appId = useObservable(observables.appId$, '');
const navGroupsMap = useObservable(observables.navGroupsMap$, {});
const currentNavGroup = useObservable(observables.currentNavGroup$, undefined);
-
const visibleUseCases = useMemo(() => getVisibleUseCases(navGroupsMap), [navGroupsMap]);
const currentNavGroupId = useMemo(() => {
@@ -115,9 +114,6 @@ export function CollapsibleNavGroupEnabled({
? !currentNavGroupId
: currentNavGroupId === ALL_USE_CASE_ID;
- const shouldShowCollapsedNavHeaderContent =
- isNavOpen && !!collapsibleNavHeaderRender && !currentNavGroupId;
-
const navLinksForRender: ChromeNavLink[] = useMemo(() => {
const getSystemNavGroups = () => {
const result: ChromeNavLink[] = [];
@@ -300,6 +296,7 @@ export function CollapsibleNavGroupEnabled({
)}
@@ -320,12 +317,6 @@ export function CollapsibleNavGroupEnabled({
hasShadow={false}
className="eui-yScroll flex-1-container"
>
- {shouldShowCollapsedNavHeaderContent && collapsibleNavHeaderRender ? (
- <>
- {collapsibleNavHeaderRender()}
-
- >
- ) : null}
JSX.Element | null;
homeLink?: ChromeNavLink;
navGroupsMap: Record;
currentNavGroup?: NavGroupItemInMap;
@@ -39,6 +40,7 @@ export interface CollapsibleNavTopProps {
}
export const CollapsibleNavTop = ({
+ collapsibleNavHeaderRender,
currentNavGroup,
navigateToApp,
logos,
@@ -52,7 +54,6 @@ export const CollapsibleNavTop = ({
navLinks,
}: CollapsibleNavTopProps) => {
const currentWorkspace = useObservable(currentWorkspace$);
-
const firstVisibleNavLinkInFirstVisibleUseCase = useMemo(
() =>
fulfillRegistrationLinksToChromeNavLinks(
@@ -148,12 +149,19 @@ export const CollapsibleNavTop = ({
/>
- {currentNavGroup?.title && (
- <>
-
- {currentNavGroup?.title}
- >
- )}
+ {
+ // Nav groups with type are system(global) nav group and we should show title for those nav groups
+ (currentNavGroup?.type || collapsibleNavHeaderRender) && (
+ <>
+
+ {currentNavGroup?.type ? (
+ {currentNavGroup?.title}
+ ) : (
+ collapsibleNavHeaderRender?.()
+ )}
+ >
+ )
+ }
);
};
diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.scss b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.scss
new file mode 100644
index 000000000000..7fe456168b68
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.scss
@@ -0,0 +1,3 @@
+.workspaceMenuHeader {
+ padding: $ouiSizeL;
+}
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 a0afcf3eb8bb..71a7632d6f08 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
@@ -5,7 +5,7 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
-
+import moment from 'moment';
import { WorkspaceMenu } from './workspace_menu';
import { coreMock } from '../../../../../core/public/mocks';
import { CoreStart, DEFAULT_NAV_GROUPS } from '../../../../../core/public';
@@ -64,16 +64,14 @@ describe('', () => {
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();
+ expect(screen.getByTestId('workspace-menu-item-workspace-1')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-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 },
- ]);
+ it('should display viewed xx ago for recent workspaces', () => {
+ jest
+ .spyOn(recentWorkspaceManager, 'getRecentWorkspaces')
+ .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]);
coreStartMock.workspaces.workspaceList$.next([
{ id: 'workspace-1', name: 'workspace 1', features: [] },
@@ -85,9 +83,7 @@ describe('', () => {
const selectButton = screen.getByTestId('workspace-select-button');
fireEvent.click(selectButton);
- 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();
+ expect(screen.getByText(`viewed ${moment(1234567890).fromNow()}`)).toBeInTheDocument();
});
it('should be able to display empty state when the workspace list is empty', () => {
@@ -98,7 +94,7 @@ describe('', () => {
expect(screen.getByText(/no workspace available/i)).toBeInTheDocument();
});
- it('should be able to perform search and filter and the results will be shown in both all and recent section', () => {
+ it('should be able to perform search and filter and the results will be shown', () => {
coreStartMock.workspaces.workspaceList$.next([
{ id: 'workspace-1', name: 'workspace 1', features: [] },
{ id: 'test-2', name: 'test 2', features: [] },
@@ -113,8 +109,8 @@ describe('', () => {
const searchInput = screen.getByRole('searchbox');
fireEvent.change(searchInput, { target: { value: 'works' } });
- expect(screen.getByTestId('workspace-menu-item-recent-workspace-1')).toBeInTheDocument();
- expect(screen.getByTestId('workspace-menu-item-recent-workspace-1')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-workspace-1')).toBeInTheDocument();
+ expect(screen.queryByText('workspace-menu-item-workspace-1')).not.toBeInTheDocument();
});
it('should be able to display empty state when seach is not found', () => {
@@ -145,7 +141,7 @@ describe('', () => {
fireEvent.click(screen.getByTestId('workspace-select-button'));
expect(screen.getByTestId('workspace-menu-current-workspace-name')).toBeInTheDocument();
- expect(screen.getByTestId('workspace-menu-current-use-case')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-current-workspace-use-case')).toBeInTheDocument();
expect(screen.getByTestId('current-workspace-icon-wsObservability')).toBeInTheDocument();
expect(screen.getByText('Observability')).toBeInTheDocument();
});
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 64c8fb583802..a68aea576ae1 100644
--- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
+++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx
@@ -2,18 +2,17 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
-
import { i18n } from '@osd/i18n';
import React, { useState } from 'react';
import { useObservable } from 'react-use';
import {
EuiText,
EuiPanel,
- EuiButton,
EuiPopover,
EuiButtonIcon,
EuiFlexItem,
EuiIcon,
+ EuiSpacer,
EuiFlexGroup,
EuiHorizontalRule,
EuiButtonEmpty,
@@ -26,6 +25,7 @@ import { getFirstUseCaseOfFeatureConfigs } from '../../utils';
import { WorkspaceUseCase } from '../../types';
import { validateWorkspaceColor } from '../../../common/utils';
import { WorkspacePickerContent } from '../workspace_picker_content/workspace_picker_content';
+import './workspace_menu.scss';
const defaultHeaderName = i18n.translate('workspace.menu.defaultHeaderName', {
defaultMessage: 'Workspaces',
@@ -93,106 +93,115 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => {
button={currentWorkspaceButton}
isOpen={isPopoverOpen}
closePopover={closePopover}
- panelPaddingSize="s"
anchorPosition="downCenter"
+ panelPaddingSize="s"
repositionOnScroll={true}
>
-
-
- {currentWorkspace ? (
- <>
-
-
-
-
- {currentWorkspaceName}
-
+
+
+ {currentWorkspace ? (
+ <>
+
+
+
+
+
+ {currentWorkspaceName}
+
+
+
+
+ {getUseCase(currentWorkspace)?.title ?? ''}
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+ {currentWorkspaceName}
+
+ >
+ )}
+
+
+
+
+ setPopover(false)}
+ isInTwoLines={false}
+ />
+
+
+ {isDashboardAdmin && (
+
+
+
+
+
+ {
+ closePopover();
+ coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
+ }}
>
- {getUseCase(currentWorkspace)?.title ?? ''}
-
-
- >
- ) : (
- <>
-
-
+ {manageWorkspacesButton}
+
-
- {currentWorkspaceName}
-
- >
- )}
-
-
-
-
- setPopover(false)}
- />
-
-
- {isDashboardAdmin && (
-
-
-
-
- {
- closePopover();
- coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
- }}
- >
- {manageWorkspacesButton}
-
-
-
- {
- closePopover();
- coreStart.application.navigateToApp(WORKSPACE_CREATE_APP_ID);
- }}
- >
- {createWorkspaceButton}
-
-
-
-
- )}
+
+ {
+ closePopover();
+ coreStart.application.navigateToApp(WORKSPACE_CREATE_APP_ID);
+ }}
+ >
+ {createWorkspaceButton}
+
+
+
+
+ )}
+
);
};
diff --git a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx
index 72fdd67b7367..389ffb04ea0d 100644
--- a/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx
+++ b/src/plugins/workspace/public/components/workspace_picker_content/workspace_picker_content.tsx
@@ -5,8 +5,9 @@
import { i18n } from '@osd/i18n';
import React, { useMemo, useState } from 'react';
import { useObservable } from 'react-use';
+import moment from 'moment';
import {
- EuiTitle,
+ EuiHorizontalRule,
EuiIcon,
EuiPanel,
EuiSpacer,
@@ -15,6 +16,8 @@ import {
EuiListGroup,
EuiListGroupItem,
EuiEmptyPrompt,
+ EuiFlexItem,
+ EuiFlexGroup,
} from '@elastic/eui';
import { BehaviorSubject } from 'rxjs';
@@ -24,65 +27,80 @@ import { WorkspaceUseCase } from '../../types';
import { validateWorkspaceColor } from '../../../common/utils';
import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils';
-const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces', {
- defaultMessage: 'All workspaces',
-});
+function sortBy(getkey: (item: T) => number | undefined) {
+ return (a: T, b: T): number => {
+ const aValue = getkey(a);
+ const bValue = getkey(b);
+
+ if (aValue === undefined) return 1;
+ if (bValue === undefined) return -1;
+
+ return bValue - aValue;
+ };
+}
const searchFieldPlaceholder = i18n.translate('workspace.menu.search.placeholder', {
defaultMessage: 'Search workspace name',
});
-const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', {
- defaultMessage: 'Recent workspaces',
-});
-
const getValidWorkspaceColor = (color?: string) =>
validateWorkspaceColor(color) ? color : undefined;
+interface UpdatedWorkspaceObject extends WorkspaceObject {
+ accessTimeStamp?: number;
+ accessTime?: string;
+}
interface Props {
coreStart: CoreStart;
registeredUseCases$: BehaviorSubject;
onClickWorkspace?: () => void;
+ isInTwoLines?: boolean;
}
export const WorkspacePickerContent = ({
coreStart,
registeredUseCases$,
onClickWorkspace,
+ isInTwoLines,
}: Props) => {
- const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []);
const isDashboardAdmin = coreStart.application.capabilities?.dashboards?.isDashboardAdmin;
const availableUseCases = useObservable(registeredUseCases$, []);
const [search, setSearch] = useState('');
+ const workspaceList = useObservable(coreStart.workspaces.workspaceList$, []);
- const recentWorkspaces = useMemo(() => {
- return recentWorkspaceManager
- .getRecentWorkspaces()
- .map((workspace) => workspaceList.find((ws) => ws.id === workspace.id))
- .filter((workspace): workspace is WorkspaceObject => workspace !== undefined);
+ const updatedRecentWorkspaceList: UpdatedWorkspaceObject[] = useMemo(() => {
+ const recentWorkspaces = recentWorkspaceManager.getRecentWorkspaces();
+ const updatedList = workspaceList.map((workspace) => {
+ const recentWorkspace = recentWorkspaces.find((recent) => recent.id === workspace.id);
+
+ if (recentWorkspace) {
+ return {
+ ...workspace,
+ accessTimeStamp: recentWorkspace.timestamp,
+ accessTime: `viewed ${moment(recentWorkspace.timestamp).fromNow()}`,
+ };
+ }
+ return workspace as UpdatedWorkspaceObject;
+ });
+
+ return updatedList.sort(sortBy((workspace) => workspace.accessTimeStamp));
}, [workspaceList]);
- const queryFromList = ({ list, query }: { list: WorkspaceObject[]; query: string }) => {
+ const queryFromList = ({ list, query }: { list: UpdatedWorkspaceObject[]; query: string }) => {
if (!list || list.length === 0) {
return [];
}
-
if (query && query.trim() !== '') {
const normalizedQuery = query.toLowerCase();
const result = list.filter((item) => item.name.toLowerCase().includes(normalizedQuery));
return result;
}
-
return list;
};
const queriedWorkspace = useMemo(() => {
- return queryFromList({ list: workspaceList, query: search });
- }, [workspaceList, search]);
-
- const queriedRecentWorkspace = useMemo(() => {
- return queryFromList({ list: recentWorkspaces, query: search });
- }, [recentWorkspaces, search]);
+ return queryFromList({ list: updatedRecentWorkspaceList, query: search });
+ }, [updatedRecentWorkspaceList, search]);
const getUseCase = (workspace: WorkspaceObject) => {
if (!workspace.features) {
@@ -125,42 +143,91 @@ export const WorkspacePickerContent = ({
);
};
- const getWorkspaceListGroup = (filterWorkspaceList: WorkspaceObject[], itemType: string) => {
- const listItems = filterWorkspaceList.map((workspace: WorkspaceObject) => {
+ const getWorkspaceLists = (filterWorkspaceList: UpdatedWorkspaceObject[]) => {
+ const listItems = filterWorkspaceList.map((workspace: UpdatedWorkspaceObject) => {
const useCase = getUseCase(workspace);
const useCaseURL = getUseCaseUrl(useCase, workspace, coreStart.application, coreStart.http);
return (
-
- }
- label={workspace.name}
- onClick={() => {
- onClickWorkspace?.();
- window.location.assign(useCaseURL);
- }}
- />
+ <>
+
+
+ }
+ label={
+ !isInTwoLines ? (
+
+
+
+ {workspace.name}
+
+
+
+
+
+
+ {useCase?.title}
+
+
+
+
+ {workspace.accessTime}
+
+
+
+
+
+ ) : (
+
+
+
+ {workspace.name}
+
+
+
+
+ {useCase?.title}
+
+
+
+
+ {workspace.accessTime}
+
+
+
+ )
+ }
+ onClick={() => {
+ onClickWorkspace?.();
+ window.location.assign(useCaseURL);
+ }}
+ />
+ >
);
});
+ return listItems;
+ };
+
+ const getWorkspaceListGroup = (filterWorkspaceList: UpdatedWorkspaceObject[]) => {
+ const listItems = getWorkspaceLists(filterWorkspaceList);
return (
<>
-
- {itemType === 'all' ? allWorkspacesTitle : recentWorkspacesTitle}
-
-
-
- {listItems}
-
-
+ {listItems}
>
);
};
@@ -174,18 +241,18 @@ export const WorkspacePickerContent = ({
onChange={(e) => setSearch(e.target.value)}
placeholder={searchFieldPlaceholder}
/>
-
+
+
- {queriedRecentWorkspace.length > 0 &&
- getWorkspaceListGroup(queriedRecentWorkspace, 'recent')}
-
- {queriedWorkspace.length > 0 && getWorkspaceListGroup(queriedWorkspace, 'all')}
+ {queriedWorkspace.length > 0 && getWorkspaceListGroup(queriedWorkspace)}
{queriedWorkspace.length === 0 && getEmptyStatePrompt()}
diff --git a/src/plugins/workspace/public/components/workspace_selector/workspace_selector.test.tsx b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.test.tsx
new file mode 100644
index 000000000000..5b8b05a6506a
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.test.tsx
@@ -0,0 +1,231 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import moment from 'moment';
+import { WorkspaceSelector } from './workspace_selector';
+import { coreMock } from '../../../../../core/public/mocks';
+import { CoreStart, DEFAULT_NAV_GROUPS, WorkspaceObject } from '../../../../../core/public';
+import { BehaviorSubject } from 'rxjs';
+import { IntlProvider } from 'react-intl';
+import { recentWorkspaceManager } from '../../recent_workspace_manager';
+jest.mock('@osd/i18n', () => ({
+ i18n: {
+ translate: (id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage,
+ },
+}));
+describe('', () => {
+ let coreStartMock: CoreStart;
+ const navigateToApp = jest.fn();
+ const registeredUseCases$ = new BehaviorSubject([
+ { ...DEFAULT_NAV_GROUPS.observability, features: [{ id: 'discover', title: 'Discover' }] },
+ ]);
+
+ beforeEach(() => {
+ coreStartMock = coreMock.createStart();
+ coreStartMock.application.capabilities = {
+ navLinks: {},
+ management: {},
+ catalogue: {},
+ savedObjectsManagement: {},
+ workspaces: { permissionEnabled: true },
+ dashboards: { isDashboardAdmin: true },
+ };
+
+ coreStartMock.application = {
+ ...coreStartMock.application,
+ navigateToApp,
+ };
+
+ const mockCurrentWorkspace = [{ id: 'workspace-1', name: 'workspace 1' }];
+ coreStartMock.workspaces.currentWorkspace$ = new BehaviorSubject(
+ mockCurrentWorkspace
+ );
+
+ jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => {
+ return `https://test.com/app/${appId}`;
+ });
+ });
+
+ const WorkspaceSelectorCreatorComponent = () => {
+ return (
+
+
+
+ );
+ };
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+
+ it('should display the current workspace name', () => {
+ render();
+ expect(screen.getByTestId('workspace-selector-current-title')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-selector-current-name')).toBeInTheDocument();
+ });
+ it('should display a list of workspaces in the dropdown', () => {
+ jest
+ .spyOn(recentWorkspaceManager, 'getRecentWorkspaces')
+ .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]);
+
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: [] },
+ { id: 'workspace-2', name: 'workspace 2', features: [] },
+ ]);
+
+ render();
+ const selectButton = screen.getByTestId('workspace-selector-button');
+ fireEvent.click(selectButton);
+
+ expect(screen.getByTestId('workspace-menu-item-workspace-1')).toBeInTheDocument();
+ expect(screen.getByTestId('workspace-menu-item-workspace-2')).toBeInTheDocument();
+ });
+
+ it('should display viewed xx ago for recent workspaces', () => {
+ jest
+ .spyOn(recentWorkspaceManager, 'getRecentWorkspaces')
+ .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]);
+
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: [] },
+ { id: 'workspace-2', name: 'workspace 2', features: [] },
+ ]);
+
+ render();
+
+ const selectButton = screen.getByTestId('workspace-selector-button');
+ fireEvent.click(selectButton);
+
+ expect(screen.getByText(`viewed ${moment(1234567890).fromNow()}`)).toBeInTheDocument();
+ });
+
+ it('should be able to display empty state when the workspace list is empty', () => {
+ coreStartMock.workspaces.workspaceList$.next([]);
+ render();
+ const selectButton = screen.getByTestId('workspace-selector-button');
+ fireEvent.click(selectButton);
+ expect(screen.getByText(/no workspace available/i)).toBeInTheDocument();
+ });
+
+ it('should be able to perform search and filter and the results will be shown', () => {
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: [] },
+ { id: 'test-2', name: 'test 2', features: [] },
+ ]);
+ jest
+ .spyOn(recentWorkspaceManager, 'getRecentWorkspaces')
+ .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]);
+ render();
+
+ const selectButton = screen.getByTestId('workspace-selector-button');
+ fireEvent.click(selectButton);
+
+ const searchInput = screen.getByRole('searchbox');
+ fireEvent.change(searchInput, { target: { value: 'works' } });
+ expect(screen.getByTestId('workspace-menu-item-workspace-1')).toBeInTheDocument();
+ expect(screen.queryByText('workspace-menu-item-workspace-1')).not.toBeInTheDocument();
+ });
+
+ it('should be able to display empty state when seach is not found', () => {
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: [] },
+ { id: 'test-2', name: 'test 2', features: [] },
+ ]);
+ jest
+ .spyOn(recentWorkspaceManager, 'getRecentWorkspaces')
+ .mockReturnValue([{ id: 'workspace-1', timestamp: 1234567890 }]);
+ render();
+
+ const selectButton = screen.getByTestId('workspace-selector-button');
+ fireEvent.click(selectButton);
+
+ const searchInput = screen.getByRole('searchbox');
+ fireEvent.change(searchInput, { target: { value: 'noitems' } });
+ expect(screen.getByText(/no workspace available/i)).toBeInTheDocument();
+ });
+
+ it('should navigate to the first feature of workspace use case', () => {
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
+ ]);
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: {
+ assign: jest.fn(),
+ },
+ });
+
+ render();
+ fireEvent.click(screen.getByTestId('workspace-selector-button'));
+ fireEvent.click(screen.getByText(/workspace 1/i));
+
+ expect(window.location.assign).toHaveBeenCalledWith(
+ 'https://test.com/w/workspace-1/app/discover'
+ );
+
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ });
+ });
+
+ it('should navigate to the workspace detail page when use case is all', () => {
+ coreStartMock.workspaces.workspaceList$.next([
+ { id: 'workspace-1', name: 'workspace 1', features: ['use-case-all'] },
+ ]);
+
+ const originalLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ value: {
+ assign: jest.fn(),
+ },
+ });
+
+ render();
+ fireEvent.click(screen.getByTestId('workspace-selector-button'));
+ fireEvent.click(screen.getByText(/workspace 1/i));
+
+ expect(window.location.assign).toHaveBeenCalledWith(
+ 'https://test.com/w/workspace-1/app/workspace_detail'
+ );
+
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ });
+ });
+
+ it('should navigate to create workspace page', () => {
+ render();
+ fireEvent.click(screen.getByTestId('workspace-selector-button'));
+ fireEvent.click(screen.getByText(/create workspace/i));
+ expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_create');
+ });
+
+ it('should navigate to workspace list page', () => {
+ render();
+
+ fireEvent.click(screen.getByTestId('workspace-selector-button'));
+ fireEvent.click(screen.getByText(/manage/i));
+ expect(coreStartMock.application.navigateToApp).toHaveBeenCalledWith('workspace_list');
+ });
+
+ it('should hide create workspace button for non dashboard admin', () => {
+ coreStartMock.application.capabilities = {
+ ...coreStartMock.application.capabilities,
+ dashboards: {
+ ...coreStartMock.application.capabilities.dashboards,
+ isDashboardAdmin: false,
+ },
+ };
+ render();
+
+ fireEvent.click(screen.getByTestId('workspace-selector-button'));
+ expect(screen.queryByText(/manage/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/create workspaces/i)).toBeNull();
+ });
+});
diff --git a/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx
new file mode 100644
index 000000000000..ca6431da1e1d
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_selector/workspace_selector.tsx
@@ -0,0 +1,206 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { useObservable } from 'react-use';
+import { i18n } from '@osd/i18n';
+import {
+ EuiButton,
+ EuiIcon,
+ EuiPopover,
+ EuiPanel,
+ EuiHorizontalRule,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+import { BehaviorSubject } from 'rxjs';
+import { WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID } from '../../../common/constants';
+import { CoreStart, WorkspaceObject } from '../../../../../core/public';
+import { getFirstUseCaseOfFeatureConfigs } from '../../utils';
+import { WorkspaceUseCase } from '../../types';
+import { validateWorkspaceColor } from '../../../common/utils';
+import { WorkspacePickerContent } from '../workspace_picker_content/workspace_picker_content';
+
+const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorkspace', {
+ defaultMessage: 'Create workspace',
+});
+
+const manageWorkspacesButton = i18n.translate('workspace.menu.button.manageWorkspaces', {
+ defaultMessage: 'Manage',
+});
+
+const getValidWorkspaceColor = (color?: string) =>
+ validateWorkspaceColor(color) ? color : undefined;
+
+interface Props {
+ coreStart: CoreStart;
+ registeredUseCases$: BehaviorSubject;
+}
+
+export const WorkspaceSelector = ({ coreStart, registeredUseCases$ }: Props) => {
+ const [isPopoverOpen, setPopover] = useState(false);
+ const currentWorkspace = useObservable(coreStart.workspaces.currentWorkspace$, null);
+ const availableUseCases = useObservable(registeredUseCases$, []);
+ const isDashboardAdmin = coreStart.application.capabilities?.dashboards?.isDashboardAdmin;
+
+ const getUseCase = (workspace: WorkspaceObject) => {
+ if (!workspace.features) {
+ return;
+ }
+ const useCaseId = getFirstUseCaseOfFeatureConfigs(workspace.features);
+ return availableUseCases.find((useCase) => useCase.id === useCaseId);
+ };
+
+ const onButtonClick = () => {
+ setPopover(!isPopoverOpen);
+ };
+
+ const closePopover = () => {
+ setPopover(false);
+ };
+ const button = currentWorkspace ? (
+
+
+
+ {i18n.translate('workspace.left.nav.selector.label', {
+ defaultMessage: 'WORKSPACE',
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {currentWorkspace.name}
+
+
+
+
+
+ {i18n.translate('workspace.left.nav.selector.title', {
+ defaultMessage: getUseCase(currentWorkspace)?.title || '',
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ Select a Workspace
+ );
+
+ return (
+
+
+
+
+ setPopover(false)}
+ isInTwoLines={false}
+ />
+
+
+
+ {isDashboardAdmin && (
+
+
+
+
+
+ {
+ closePopover();
+ coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID);
+ }}
+ >
+ {manageWorkspacesButton}
+
+
+
+
+ {
+ closePopover();
+ 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 0bbf636f6397..2fba6a457322 100644
--- a/src/plugins/workspace/public/plugin.test.ts
+++ b/src/plugins/workspace/public/plugin.test.ts
@@ -221,7 +221,20 @@ describe('Workspace plugin', () => {
);
});
- it('#setup should register registerCollapsibleNavHeader when new left nav is turned on', async () => {
+ it('#setup should register registerCollapsibleNavHeader when enter a workspace', async () => {
+ const windowSpy = jest.spyOn(window, 'window', 'get');
+ windowSpy.mockImplementation(
+ () =>
+ ({
+ location: {
+ href: 'http://localhost/w/workspaceId/app',
+ },
+ } as any)
+ );
+ workspaceClientMock.enterWorkspace.mockResolvedValueOnce({
+ success: true,
+ error: 'error',
+ });
const setupMock = coreMock.createSetup();
let collapsibleNavHeaderImplementation = () => null;
setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
@@ -234,6 +247,7 @@ describe('Workspace plugin', () => {
const startMock = coreMock.createStart();
await workspacePlugin.start(startMock, getMockDependencies());
expect(collapsibleNavHeaderImplementation()).not.toEqual(null);
+ windowSpy.mockRestore();
});
it('#setup should register workspace essential use case when new home is disabled', async () => {
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index 42a0d154cecb..ff6ce2c75898 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -60,6 +60,7 @@ import { UseCaseService } from './services/use_case_service';
import { WorkspaceListCard } from './components/service_card';
import { NavigationPublicPluginStart } from '../../../plugins/navigation/public';
import { WorkspacePickerContent } from './components/workspace_picker_content/workspace_picker_content';
+import { WorkspaceSelector } from './components/workspace_selector/workspace_selector';
import { HOME_CONTENT_AREAS } from '../../../plugins/content_management/public';
import {
registerEssentialOverviewContent,
@@ -122,7 +123,6 @@ export class WorkspacePlugin
*/
private filterNavLinks = (core: CoreStart) => {
const currentWorkspace$ = core.workspaces.currentWorkspace$;
-
this.workspaceAndUseCasesCombineSubscription?.unsubscribe();
this.workspaceAndUseCasesCombineSubscription = combineLatest([
currentWorkspace$,
@@ -536,25 +536,15 @@ export class WorkspacePlugin
},
]);
- if (core.chrome.navGroup.getNavGroupEnabled()) {
- /**
- * Show workspace picker content when outside of workspace and not in any nav group
- */
+ if (workspaceId) {
core.chrome.registerCollapsibleNavHeader(() => {
if (!this.coreStart) {
return null;
}
- return React.createElement(EuiPanel, {
- hasShadow: false,
- hasBorder: false,
- paddingSize: 's',
- children: [
- React.createElement(WorkspacePickerContent, {
- key: 'workspacePickerContent',
- coreStart: this.coreStart,
- registeredUseCases$: this.registeredUseCases$,
- }),
- ],
+ return React.createElement(WorkspaceSelector, {
+ key: 'workspaceSelector',
+ coreStart: this.coreStart,
+ registeredUseCases$: this.registeredUseCases$,
});
});
}
From a7f3e9d3f2b6e9c4cf935ac305ad564a02122116 Mon Sep 17 00:00:00 2001
From: Sean Li
Date: Tue, 1 Oct 2024 04:55:27 -0700
Subject: [PATCH 02/31] [Discover] Display Cache Time and Clear Cache Button
(#8214)
* initial commit for cache time and clearing cache
Signed-off-by: Sean Li
* Changeset file for PR #8214 created/updated
* updating UI to address some comments
Signed-off-by: Sean Li
* addressing comments, adding tests
Signed-off-by: Sean Li
* removing dynamic default message, following i18n best practice
Signed-off-by: Sean Li
---------
Signed-off-by: Sean Li
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8214.yml | 2 +
src/plugins/data/common/storage/storage.ts | 4 +
.../dataset_service/dataset_service.test.ts | 71 +++++++++------
.../dataset_service/dataset_service.ts | 13 +++
.../ui/dataset_selector/dataset_explorer.tsx | 91 ++++++++++++++-----
5 files changed, 130 insertions(+), 51 deletions(-)
create mode 100644 changelogs/fragments/8214.yml
diff --git a/changelogs/fragments/8214.yml b/changelogs/fragments/8214.yml
new file mode 100644
index 000000000000..ce9d1ee6ca32
--- /dev/null
+++ b/changelogs/fragments/8214.yml
@@ -0,0 +1,2 @@
+feat:
+- Add last updated time and cache refresh button to Discover Advanced Dataset Selector ([#8214](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8214))
\ No newline at end of file
diff --git a/src/plugins/data/common/storage/storage.ts b/src/plugins/data/common/storage/storage.ts
index 8ec5f6a13b8d..752e0b29c92e 100644
--- a/src/plugins/data/common/storage/storage.ts
+++ b/src/plugins/data/common/storage/storage.ts
@@ -61,6 +61,10 @@ export class DataStorage {
if (ourKey != null) ours.push(ourKey);
});
}
+
+ clear(): void {
+ this.engine.clear();
+ }
}
export function createStorage(deps: { engine: IStorageEngine; prefix: string }) {
diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.test.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.test.ts
index f72fd0ea5b46..02b9eb0759fc 100644
--- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.test.ts
+++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.test.ts
@@ -23,17 +23,26 @@ describe('DatasetService', () => {
service = new DatasetService(uiSettings, sessionStorage);
});
- test('registerType and getType', () => {
- const mockType = {
- id: 'test-type',
- title: 'Test Type',
- meta: { icon: { type: 'test' } },
- toDataset: jest.fn(),
- fetch: jest.fn(),
- fetchFields: jest.fn(),
- supportedLanguages: jest.fn(),
- };
+ const mockResult = {
+ id: 'test-structure',
+ title: 'Test Structure',
+ type: 'test-type',
+ children: [{ id: 'child1', title: 'Child 1', type: 'test-type' }],
+ };
+
+ const mockPath: DataStructure[] = [{ id: 'root', title: 'Root', type: 'root' }];
+
+ const mockType = {
+ id: 'test-type',
+ title: 'Test Type',
+ meta: { icon: { type: 'test' } },
+ toDataset: jest.fn(),
+ fetch: jest.fn().mockResolvedValue(mockResult),
+ fetchFields: jest.fn(),
+ supportedLanguages: jest.fn(),
+ };
+ test('registerType and getType', () => {
service.registerType(mockType);
expect(service.getType('test-type')).toBe(mockType);
});
@@ -52,25 +61,9 @@ describe('DatasetService', () => {
});
test('fetchOptions caches and returns data structures', async () => {
- const mockType = {
- id: 'test-type',
- title: 'Test Type',
- meta: { icon: { type: 'test' } },
- toDataset: jest.fn(),
- fetch: jest.fn().mockResolvedValue({
- id: 'test-structure',
- title: 'Test Structure',
- type: 'test-type',
- children: [{ id: 'child1', title: 'Child 1', type: 'test-type' }],
- }),
- fetchFields: jest.fn(),
- supportedLanguages: jest.fn(),
- };
-
service.registerType(mockType);
- const path: DataStructure[] = [{ id: 'root', title: 'Root', type: 'root' }];
- const result = await service.fetchOptions(mockDataPluginServices, path, 'test-type');
+ const result = await service.fetchOptions(mockDataPluginServices, mockPath, 'test-type');
expect(result).toEqual({
id: 'test-structure',
@@ -79,8 +72,30 @@ describe('DatasetService', () => {
children: [{ id: 'child1', title: 'Child 1', type: 'test-type' }],
});
- const cachedResult = await service.fetchOptions(mockDataPluginServices, path, 'test-type');
+ const cachedResult = await service.fetchOptions(mockDataPluginServices, mockPath, 'test-type');
expect(cachedResult).toEqual(result);
expect(mockType.fetch).toHaveBeenCalledTimes(2);
});
+
+ test('clear cache', async () => {
+ service.registerType(mockType);
+
+ await service.fetchOptions(mockDataPluginServices, mockPath, 'test-type');
+ expect(sessionStorage.keys().length === 1);
+
+ service.clearCache();
+ expect(sessionStorage.keys().length === 0);
+ });
+
+ test('caching object correctly sets last cache time', async () => {
+ service.registerType(mockType);
+
+ const time = Date.now();
+
+ Date.now = jest.fn(() => time);
+
+ await service.fetchOptions(mockDataPluginServices, mockPath, 'test-type');
+
+ expect(service.getLastCacheTime()).toEqual(time);
+ });
});
diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts
index 2f9a0884442f..36faead38308 100644
--- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts
+++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts
@@ -138,6 +138,7 @@ export class DatasetService {
}
private cacheDataStructure(dataType: string, dataStructure: DataStructure) {
+ this.setLastCacheTime(Date.now());
const cachedDataStructure: CachedDataStructure = {
id: dataStructure.id,
title: dataStructure.title,
@@ -164,6 +165,18 @@ export class DatasetService {
});
}
+ public clearCache(): void {
+ this.sessionStorage.clear();
+ }
+
+ public getLastCacheTime(): number | undefined {
+ return Number(this.sessionStorage.get('lastCacheTime')) || undefined;
+ }
+
+ private setLastCacheTime(time: number): void {
+ this.sessionStorage.set('lastCacheTime', time);
+ }
+
private async fetchDefaultDataset(): Promise {
const defaultIndexPatternId = this.uiSettings.get('defaultIndex');
if (!defaultIndexPatternId) {
diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
index f393884bc599..dbcf7fe5acfc 100644
--- a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
+++ b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
@@ -7,6 +7,8 @@ import React, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
EuiIcon,
EuiLink,
EuiModalBody,
@@ -19,6 +21,7 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@osd/i18n/react';
+import moment from 'moment';
import { BaseDataset, DATA_STRUCTURE_META_TYPES, DataStructure } from '../../../common';
import { QueryStringContract } from '../../query';
import { IDataPluginServices } from '../../types';
@@ -38,6 +41,7 @@ export const DatasetExplorer = ({
onNext: (dataset: BaseDataset) => void;
onCancel: () => void;
}) => {
+ const uiSettings = services.uiSettings;
const [explorerDataset, setExplorerDataset] = useState(undefined);
const [loading, setLoading] = useState(false);
@@ -68,31 +72,72 @@ export const DatasetExplorer = ({
return (
<>
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {queryString.getDatasetService().getLastCacheTime() && (
+
+
+
+
+
+
+
+
+ {
+ queryString.getDatasetService().clearCache();
+ onCancel();
+ }}
+ size="xs"
+ iconSide="left"
+ iconType="refresh"
+ iconGap="s"
+ flush="both"
+ >
+
+
+
+
+
+ )}
+
Date: Wed, 2 Oct 2024 02:07:19 +0800
Subject: [PATCH 03/31] add Hailong-am as maintainer (#8415)
* feat: add Hailong-am as maintainer
Signed-off-by: SuZhou-Joe
* Changeset file for PR #8415 created/updated
* Changeset file for PR #8415 created/updated
---------
Signed-off-by: SuZhou-Joe
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
MAINTAINERS.md | 1 +
changelogs/fragments/8415.yml | 2 ++
2 files changed, 3 insertions(+)
create mode 100644 changelogs/fragments/8415.yml
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 9f964d82a66c..c75a1404b7b9 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -30,6 +30,7 @@ This document contains a list of maintainers in this repo. See [opensearch-proje
| Sean Li | [sejli](https://github.com/sejli) | Amazon |
| Joshua Li | [joshuali925](https://github.com/joshuali925) | Amazon |
| Huy Nguyen | [huyaboo](https://github.com/huyaboo) | Amazon |
+| Hailong Cui | [Hailong-am](https://github.com/Hailong-am) | Amazon |
## Emeritus
diff --git a/changelogs/fragments/8415.yml b/changelogs/fragments/8415.yml
new file mode 100644
index 000000000000..261821962a13
--- /dev/null
+++ b/changelogs/fragments/8415.yml
@@ -0,0 +1,2 @@
+doc:
+- Add Hailong-am as maintainer ([#8415](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8415))
\ No newline at end of file
From 0f126da2c8e7bdbf8b8b915e197a4a0aa90ed1ee Mon Sep 17 00:00:00 2001
From: Miki
Date: Tue, 1 Oct 2024 11:32:28 -0700
Subject: [PATCH 04/31] Fix unprefixed and duplicate i18n identifiers in
workspace plugin (#8410)
Signed-off-by: Miki
---
.../home_get_start_card/use_case_card_title.tsx | 6 +++---
.../components/workspace_column/workspace_column.tsx | 2 +-
.../workspace/public/components/workspace_creator_app.tsx | 2 +-
.../components/workspace_detail/workspace_detail.tsx | 8 ++++----
.../workspace/public/components/workspace_detail_app.tsx | 2 +-
.../workspace_fatal_error/workspace_fatal_error.tsx | 6 +++---
.../workspace_form/fields/workspace_name_field.tsx | 2 +-
.../workspace/public/components/workspace_form/utils.ts | 4 ++--
.../workspace_form/workspace_form_error_callout.tsx | 6 +++---
.../workspace_form/workspace_permission_setting_input.tsx | 6 +++---
.../workspace_form/workspace_permission_setting_panel.tsx | 6 +++---
src/plugins/workspace/server/permission_control/client.ts | 2 +-
.../workspace_saved_objects_client_wrapper.ts | 6 +++---
13 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx
index 95400ee336e7..379039700dbc 100644
--- a/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx
+++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx
@@ -51,8 +51,8 @@ export const UseCaseCardTitle = ({ filterWorkspaces, useCase, core }: UseCaseCar
const iconButton = (
- {i18n.translate('workspace.getStartCard.popover.title.', {
+ {i18n.translate('workspace.getStartCard.popover.title', {
defaultMessage: 'SELECT WORKSPACE',
})}
diff --git a/src/plugins/workspace/public/components/workspace_column/workspace_column.tsx b/src/plugins/workspace/public/components/workspace_column/workspace_column.tsx
index 4391f8d2c858..035fa3b6ebd4 100644
--- a/src/plugins/workspace/public/components/workspace_column/workspace_column.tsx
+++ b/src/plugins/workspace/public/components/workspace_column/workspace_column.tsx
@@ -35,7 +35,7 @@ export function getWorkspaceColumn(
euiColumn: {
align: 'left',
field: 'workspaces',
- name: i18n.translate('savedObjectsManagement.objectsTable.table.columnWorkspacesName', {
+ name: i18n.translate('workspace.objectsTable.table.columnWorkspacesName', {
defaultMessage: 'Workspace',
}),
render: (workspaces: string[]) => {
diff --git a/src/plugins/workspace/public/components/workspace_creator_app.tsx b/src/plugins/workspace/public/components/workspace_creator_app.tsx
index 007d0d7cb151..5a4671c4fd05 100644
--- a/src/plugins/workspace/public/components/workspace_creator_app.tsx
+++ b/src/plugins/workspace/public/components/workspace_creator_app.tsx
@@ -20,7 +20,7 @@ export const WorkspaceCreatorApp = (props: WorkspaceCreatorProps) => {
*/
useEffect(() => {
const homeBreadcrumb = {
- text: i18n.translate('core.breadcrumbs.homeTitle', { defaultMessage: 'Home' }),
+ text: i18n.translate('workspace.breadcrumbs.homeTitle', { defaultMessage: 'Home' }),
onClick: () => {
application?.navigateToApp('home');
},
diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx
index 674b1e31ed47..e027c7d5f586 100644
--- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx
+++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.tsx
@@ -369,7 +369,7 @@ export const WorkspaceDetail = (props: WorkspaceDetailPropsWithFormSubmitting) =
{modalVisible && (
setModalVisible(false)}
@@ -379,14 +379,14 @@ export const WorkspaceDetail = (props: WorkspaceDetailPropsWithFormSubmitting) =
history.push(`?tab=${tabId}`);
setSelectedTabId(tabId);
}}
- cancelButtonText={i18n.translate('workspace.form.cancelButtonText', {
+ cancelButtonText={i18n.translate('workspace.detail.cancelButtonText', {
defaultMessage: 'Cancel',
})}
- confirmButtonText={i18n.translate('workspace.form.confirmButtonText', {
+ confirmButtonText={i18n.translate('workspace.detail.confirmButtonText', {
defaultMessage: 'Confirm',
})}
>
- {i18n.translate('workspace.form.cancelModal.body', {
+ {i18n.translate('workspace.detail.cancelModal.body', {
defaultMessage: 'Any unsaved changes will be lost.',
})}
diff --git a/src/plugins/workspace/public/components/workspace_detail_app.tsx b/src/plugins/workspace/public/components/workspace_detail_app.tsx
index f0867b8d59e7..66ea4f3f8b9c 100644
--- a/src/plugins/workspace/public/components/workspace_detail_app.tsx
+++ b/src/plugins/workspace/public/components/workspace_detail_app.tsx
@@ -99,7 +99,7 @@ export const WorkspaceDetailApp = (props: WorkspaceDetailPropsWithOnAppLeave) =>
}
if (!currentWorkspace) {
notifications?.toasts.addDanger({
- title: i18n.translate('Cannot find current workspace', {
+ title: i18n.translate('workspace.detail.notFoundError', {
defaultMessage: 'Cannot update workspace',
}),
});
diff --git a/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx
index a75e8bc45976..3a6e3fae9cd7 100644
--- a/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx
+++ b/src/plugins/workspace/public/components/workspace_fatal_error/workspace_fatal_error.tsx
@@ -35,7 +35,7 @@ export function WorkspaceFatalError(props: { error?: string }) {
title={
@@ -43,7 +43,7 @@ export function WorkspaceFatalError(props: { error?: string }) {
body={
@@ -51,7 +51,7 @@ export function WorkspaceFatalError(props: { error?: string }) {
actions={[
,
diff --git a/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx b/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx
index 768cb4b41c27..eae05968d8e8 100644
--- a/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx
+++ b/src/plugins/workspace/public/components/workspace_form/fields/workspace_name_field.tsx
@@ -53,7 +53,7 @@ export const WorkspaceNameField = ({
{showDescription && (
<>
- {i18n.translate('workspace.form.workspaceDetails.name.helpText', {
+ {i18n.translate('workspace.form.workspaceDetails.name.helpTextLong', {
defaultMessage:
'Use a unique name for the workspace. Valid characters are a-z, A-Z, 0-9, (), [], _ (underscore), - (hyphen) and (space).',
})}
diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts
index f6497315a262..5bbc1125fbeb 100644
--- a/src/plugins/workspace/public/components/workspace_form/utils.ts
+++ b/src/plugins/workspace/public/components/workspace_form/utils.ts
@@ -331,7 +331,7 @@ export const validateWorkspaceForm = (
if (!features || !features.some((featureConfig) => isUseCaseFeatureConfig(featureConfig))) {
formErrors.features = {
code: WorkspaceFormErrorCode.UseCaseMissing,
- message: i18n.translate('workspace.form.features.empty', {
+ message: i18n.translate('workspace.form.features.emptyUseCase', {
defaultMessage: 'Use case is required. Select a use case.',
}),
};
@@ -339,7 +339,7 @@ export const validateWorkspaceForm = (
if (color && !validateWorkspaceColor(color)) {
formErrors.color = {
code: WorkspaceFormErrorCode.InvalidColor,
- message: i18n.translate('workspace.form.features.empty', {
+ message: i18n.translate('workspace.form.features.invalidColor', {
defaultMessage: 'Color is invalid. Enter a valid color.',
}),
};
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form_error_callout.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form_error_callout.tsx
index 1d90235ed82c..2c32fde1d100 100644
--- a/src/plugins/workspace/public/components/workspace_form/workspace_form_error_callout.tsx
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_form_error_callout.tsx
@@ -31,11 +31,11 @@ const getSuggestionFromErrorCode = (error: WorkspaceFormError) => {
defaultMessage: 'Enter a user group.',
});
case WorkspaceFormErrorCode.DuplicateUserIdPermissionSetting:
- return i18n.translate('workspace.form.errorCallout.duplicatePermission', {
+ return i18n.translate('workspace.form.errorCallout.duplicateUserPermission', {
defaultMessage: 'Enter a unique user.',
});
case WorkspaceFormErrorCode.DuplicateUserGroupPermissionSetting:
- return i18n.translate('workspace.form.errorCallout.duplicatePermission', {
+ return i18n.translate('workspace.form.errorCallout.duplicateGroupPermission', {
defaultMessage: 'Enter a unique user group.',
});
case WorkspaceFormErrorCode.PermissionSettingOwnerMissing:
@@ -112,7 +112,7 @@ export const WorkspaceFormErrorCallout = ({ errors }: WorkspaceFormErrorCalloutP
)}
{errors.color && (
- {i18n.translate('workspaceForm.permissionSetting.typeLabel', {
+ {i18n.translate('workspace.form.permissionSetting.typeLabel', {
defaultMessage: 'Type',
})}
- {i18n.translate('workspaceForm.permissionSetting.collaboratorLabel', {
+ {i18n.translate('workspace.form.permissionSetting.collaboratorLabel', {
defaultMessage: 'Collaborator',
})}
- {i18n.translate('workspaceForm.permissionSetting.accessLevelLabel', {
+ {i18n.translate('workspace.form.permissionSetting.accessLevelLabel', {
defaultMessage: 'Access level',
})}
diff --git a/src/plugins/workspace/server/permission_control/client.ts b/src/plugins/workspace/server/permission_control/client.ts
index 3d052c3fe2f6..c6e147f3609c 100644
--- a/src/plugins/workspace/server/permission_control/client.ts
+++ b/src/plugins/workspace/server/permission_control/client.ts
@@ -184,7 +184,7 @@ export class SavedObjectsPermissionControl {
if (!savedObjectsGet.length) {
return {
success: false,
- error: i18n.translate('savedObjects.permission.notFound', {
+ error: i18n.translate('workspace.savedObjects.permission.notFound', {
defaultMessage: 'Can not find target saved objects.',
}),
};
diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts
index 091644d46efc..876f333be95e 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts
@@ -48,7 +48,7 @@ const generateWorkspacePermissionError = () =>
const generateSavedObjectsPermissionError = () =>
SavedObjectsErrorHelpers.decorateForbiddenError(
new Error(
- i18n.translate('saved_objects.permission.invalidate', {
+ i18n.translate('workspace.saved_objects.permission.invalidate', {
defaultMessage: 'Invalid saved objects permission',
})
)
@@ -57,7 +57,7 @@ const generateSavedObjectsPermissionError = () =>
const generateDataSourcePermissionError = () =>
SavedObjectsErrorHelpers.decorateForbiddenError(
new Error(
- i18n.translate('saved_objects.data_source.invalidate', {
+ i18n.translate('workspace.saved_objects.data_source.invalidate', {
defaultMessage: 'Invalid data source permission, please associate it to current workspace',
})
)
@@ -66,7 +66,7 @@ const generateDataSourcePermissionError = () =>
const generateOSDAdminPermissionError = () =>
SavedObjectsErrorHelpers.decorateForbiddenError(
new Error(
- i18n.translate('dashboard.admin.permission.invalidate', {
+ i18n.translate('workspace.admin.permission.invalidate', {
defaultMessage: 'Invalid permission, please contact OSD admin',
})
)
From 2e7e235fe6047a12720463d4a0e4f7408d3c8532 Mon Sep 17 00:00:00 2001
From: Miki
Date: Tue, 1 Oct 2024 11:49:44 -0700
Subject: [PATCH 05/31] Fix duplicate i18n identifiers in visualize plugin
(#8407)
* Fix duplicate i18n identifiers in visualize plugin
Signed-off-by: Miki
* Changeset file for PR #8407 created/updated
---------
Signed-off-by: Miki
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8407.yml | 2 ++
.../public/application/components/visualize_listing.tsx | 4 ++--
.../public/application/components/visualize_top_nav.tsx | 2 +-
.../public/application/utils/get_top_nav_config.tsx | 8 ++++----
4 files changed, 9 insertions(+), 7 deletions(-)
create mode 100644 changelogs/fragments/8407.yml
diff --git a/changelogs/fragments/8407.yml b/changelogs/fragments/8407.yml
new file mode 100644
index 000000000000..27029ebbe418
--- /dev/null
+++ b/changelogs/fragments/8407.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix duplicate i18n identifiers in visualize plugin ([#8407](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8407))
\ No newline at end of file
diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx
index 17ddd2c7bfe5..b904bdfe1b00 100644
--- a/src/plugins/visualize/public/application/components/visualize_listing.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx
@@ -89,7 +89,7 @@ export const VisualizeListing = () => {
if (showUpdatedUx) {
chrome.setBreadcrumbs([
{
- text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', {
+ text: i18n.translate('visualize.listingBreadcrumbsTitle', {
defaultMessage: 'Visualizations',
}),
},
@@ -97,7 +97,7 @@ export const VisualizeListing = () => {
} else {
chrome.setBreadcrumbs([
{
- text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', {
+ text: i18n.translate('visualize.legacy.listingBreadcrumbsTitle', {
defaultMessage: 'Visualize',
}),
},
diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
index f1c47016d048..e2bc639e79f0 100644
--- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
@@ -261,7 +261,7 @@ const TopNav = ({
indexPatterns={indexPatterns}
screenTitle={
vis.title ||
- i18n.translate('discover.savedSearch.newTitle', {
+ i18n.translate('visualize.savedSearch.newTitle', {
defaultMessage: 'New visualization',
})
}
diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
index cffd251b3eb4..78d1dbb07a30 100644
--- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
+++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx
@@ -106,7 +106,7 @@ export const getLegacyTopNavConfig = (
{
id: 'inspector',
label: i18n.translate('visualize.topNavMenu.openInspectorButtonLabel', {
- defaultMessage: 'inspect',
+ defaultMessage: 'Inspect',
}),
description: i18n.translate('visualize.topNavMenu.openInspectorButtonAriaLabel', {
defaultMessage: 'Open Inspector for visualization',
@@ -127,7 +127,7 @@ export const getLegacyTopNavConfig = (
{
id: 'share',
label: i18n.translate('visualize.topNavMenu.shareVisualizationButtonLabel', {
- defaultMessage: 'share',
+ defaultMessage: 'Share',
}),
description: i18n.translate('visualize.topNavMenu.shareVisualizationButtonAriaLabel', {
defaultMessage: 'Share Visualization',
@@ -168,10 +168,10 @@ export const getLegacyTopNavConfig = (
label:
savedVis?.id && originatingApp
? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', {
- defaultMessage: 'save as',
+ defaultMessage: 'Save as',
})
: i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', {
- defaultMessage: 'save',
+ defaultMessage: 'Save',
}),
emphasize: (savedVis && !savedVis.id) || !originatingApp,
description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', {
From daa59deb5e243191a62926ec19a40fb92af9bf4c Mon Sep 17 00:00:00 2001
From: Suchit Sahoo <38322563+LDrago27@users.noreply.github.com>
Date: Tue, 1 Oct 2024 16:10:15 -0700
Subject: [PATCH 06/31] Add Ui Metric for Discover 2.0 (#8345)
Signed-off-by: Suchit Sahoo
---
.../index_pattern_select.test.tsx | 2 +-
.../data_explorer/opensearch_dashboards.json | 3 +-
.../public/components/app_container.tsx | 15 +++++++-
src/plugins/data_explorer/public/plugin.ts | 4 +-
src/plugins/data_explorer/public/services.ts | 10 +++++
src/plugins/data_explorer/public/types.ts | 2 +
.../public/ui_metric/constants.ts | 8 ++++
.../data_explorer/public/ui_metric/index.ts | 7 ++++
.../public/ui_metric/report_ui_metric.ts | 23 ++++++++++++
.../discover/opensearch_dashboards.json | 3 +-
.../view_components/utils/use_search.ts | 9 ++++-
.../public/opensearch_dashboards_services.ts | 5 +++
src/plugins/discover/public/plugin.ts | 4 ++
.../discover/public/ui_metric/constants.ts | 8 ++++
.../discover/public/ui_metric/index.ts | 7 ++++
.../public/ui_metric/report_ui_metric.ts | 37 +++++++++++++++++++
16 files changed, 141 insertions(+), 6 deletions(-)
create mode 100644 src/plugins/data_explorer/public/services.ts
create mode 100644 src/plugins/data_explorer/public/ui_metric/constants.ts
create mode 100644 src/plugins/data_explorer/public/ui_metric/index.ts
create mode 100644 src/plugins/data_explorer/public/ui_metric/report_ui_metric.ts
create mode 100644 src/plugins/discover/public/ui_metric/constants.ts
create mode 100644 src/plugins/discover/public/ui_metric/index.ts
create mode 100644 src/plugins/discover/public/ui_metric/report_ui_metric.ts
diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx
index 9186aa361a0e..dcf008fcf2d7 100644
--- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx
+++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx
@@ -49,7 +49,7 @@ describe('IndexPatternSelect', () => {
bulkGetMock.mockResolvedValue({ savedObjects: [{ attributes: { title: 'test1' } }] });
compInstance.debouncedFetch('');
- await new Promise((resolve) => setTimeout(resolve, 300));
+ await new Promise((resolve) => setTimeout(resolve, 600));
await nextTick();
expect(bulkGetMock).toBeCalledWith([{ id: 'testDataSourceId', type: 'data-source' }]);
});
diff --git a/src/plugins/data_explorer/opensearch_dashboards.json b/src/plugins/data_explorer/opensearch_dashboards.json
index 23db353b2cc8..bed97d60d323 100644
--- a/src/plugins/data_explorer/opensearch_dashboards.json
+++ b/src/plugins/data_explorer/opensearch_dashboards.json
@@ -8,7 +8,8 @@
"data",
"navigation",
"embeddable",
- "expressions"
+ "expressions",
+ "usageCollection"
],
"optionalPlugins": [],
"requiredBundles": [
diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx
index 9326c4097f0f..d555316dd21d 100644
--- a/src/plugins/data_explorer/public/components/app_container.tsx
+++ b/src/plugins/data_explorer/public/components/app_container.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { memo, useRef } from 'react';
+import React, { memo, useEffect, useRef } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -23,6 +23,13 @@ import './app_container.scss';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';
import { IDataPluginServices } from '../../../data/public';
import { QUERY_ENHANCEMENT_ENABLED_SETTING } from './constants';
+import {
+ DISCOVER_LOAD_EVENT,
+ NEW_DISCOVER_LOAD_EVENT,
+ NEW_DISCOVER_OPT_IN,
+ NEW_DISCOVER_OPT_OUT,
+ trackUiMetric,
+} from '../ui_metric';
export const AppContainer = React.memo(
({ view, params }: { view?: View; params: AppMountParameters }) => {
@@ -35,10 +42,16 @@ export const AppContainer = React.memo(
const topLinkRef = useRef(null);
const datePickerRef = useRef(null);
+
if (!view) {
return ;
}
+ trackUiMetric(DISCOVER_LOAD_EVENT);
+ if (isEnhancementsEnabled) {
+ trackUiMetric(NEW_DISCOVER_LOAD_EVENT);
+ }
+
const { Canvas, Panel, Context } = view;
const MemoizedPanel = memo(Panel);
diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts
index d2c8da53a697..28e4237d6773 100644
--- a/src/plugins/data_explorer/public/plugin.ts
+++ b/src/plugins/data_explorer/public/plugin.ts
@@ -31,6 +31,7 @@ import {
} from '../../opensearch_dashboards_utils/public';
import { getPreloadedStore } from './utils/state_management';
import { opensearchFilters } from '../../data/public';
+import { setUsageCollector } from './services';
export class DataExplorerPlugin
implements
@@ -47,10 +48,11 @@ export class DataExplorerPlugin
public setup(
core: CoreSetup,
- { data }: DataExplorerPluginSetupDependencies
+ { data, usageCollection }: DataExplorerPluginSetupDependencies
): DataExplorerPluginSetup {
const viewService = this.viewService;
+ setUsageCollector(usageCollection);
const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({
baseUrl: core.http.basePath.prepend(`/app/${PLUGIN_ID}`),
defaultSubUrl: '#/',
diff --git a/src/plugins/data_explorer/public/services.ts b/src/plugins/data_explorer/public/services.ts
new file mode 100644
index 000000000000..13c6e57b6646
--- /dev/null
+++ b/src/plugins/data_explorer/public/services.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { createGetterSetter } from '../../opensearch_dashboards_utils/public';
+import { UsageCollectionSetup } from '../../usage_collection/public';
+
+export const [getUsageCollector, setUsageCollector] = createGetterSetter(
+ 'UsageCollector'
+);
diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts
index 5f677fb46cfd..d63396155b55 100644
--- a/src/plugins/data_explorer/public/types.ts
+++ b/src/plugins/data_explorer/public/types.ts
@@ -10,6 +10,7 @@ import { ViewServiceStart, ViewServiceSetup } from './services/view_service';
import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public';
import { Store } from './utils/state_management';
+import { UsageCollectionSetup } from '../../usage_collection/public';
export type DataExplorerPluginSetup = ViewServiceSetup;
@@ -18,6 +19,7 @@ export interface DataExplorerPluginStart {}
export interface DataExplorerPluginSetupDependencies {
data: DataPublicPluginSetup;
+ usageCollection: UsageCollectionSetup;
}
export interface DataExplorerPluginStartDependencies {
diff --git a/src/plugins/data_explorer/public/ui_metric/constants.ts b/src/plugins/data_explorer/public/ui_metric/constants.ts
new file mode 100644
index 000000000000..769dbe12e954
--- /dev/null
+++ b/src/plugins/data_explorer/public/ui_metric/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const NEW_DISCOVER_LOAD_EVENT = 'new_discover_load_count';
+export const DISCOVER_LOAD_EVENT = 'discover_load_count';
+export const NEW_DISCOVER_APP_NAME = 'New_Discover';
diff --git a/src/plugins/data_explorer/public/ui_metric/index.ts b/src/plugins/data_explorer/public/ui_metric/index.ts
new file mode 100644
index 000000000000..eceec571b67d
--- /dev/null
+++ b/src/plugins/data_explorer/public/ui_metric/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './constants';
+export * from './report_ui_metric';
diff --git a/src/plugins/data_explorer/public/ui_metric/report_ui_metric.ts b/src/plugins/data_explorer/public/ui_metric/report_ui_metric.ts
new file mode 100644
index 000000000000..1d53ec4e3336
--- /dev/null
+++ b/src/plugins/data_explorer/public/ui_metric/report_ui_metric.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { UiStatsMetricType } from 'packages/osd-analytics/target/types';
+import { METRIC_TYPE } from '../../../usage_collection/public';
+import { NEW_DISCOVER_APP_NAME } from './constants';
+import { getUsageCollector } from '../services';
+
+export const trackUiMetric = (
+ eventName: string,
+ appName: string = NEW_DISCOVER_APP_NAME,
+ metricType: UiStatsMetricType = METRIC_TYPE.COUNT
+) => {
+ try {
+ const usageCollector = getUsageCollector();
+ usageCollector.reportUiStats(appName, metricType, eventName);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }
+};
diff --git a/src/plugins/discover/opensearch_dashboards.json b/src/plugins/discover/opensearch_dashboards.json
index bcbfc2096731..c49528d2e227 100644
--- a/src/plugins/discover/opensearch_dashboards.json
+++ b/src/plugins/discover/opensearch_dashboards.json
@@ -13,7 +13,8 @@
"urlForwarding",
"navigation",
"uiActions",
- "visualizations"
+ "visualizations",
+ "usageCollection"
],
"optionalPlugins": ["home", "share"],
"requiredBundles": [
diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts
index 88300cc570fa..34264540c5ec 100644
--- a/src/plugins/discover/public/application/view_components/utils/use_search.ts
+++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts
@@ -8,7 +8,7 @@ import { BehaviorSubject, Subject, merge } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { i18n } from '@osd/i18n';
import { useEffect } from 'react';
-import { cloneDeep } from 'lodash';
+import { cloneDeep, isEqual } from 'lodash';
import { useLocation } from 'react-router-dom';
import { RequestAdapter } from '../../../../../inspector/public';
import { DiscoverViewServices } from '../../../build_services';
@@ -33,6 +33,7 @@ import {
} from '../../../opensearch_dashboards_services';
import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../common';
import { syncQueryStateWithUrl } from '../../../../../data/public';
+import { trackQueryMetric } from '../../../ui_metric';
export enum ResultStatus {
UNINITIALIZED = 'uninitialized',
@@ -184,6 +185,12 @@ export const useSearch = (services: DiscoverViewServices) => {
inspectorRequest.json(body);
});
+ // Track the dataset type and language used
+ const query = searchSource.getField('query');
+ if (query && query.dataset?.type && query.language) {
+ trackQueryMetric(query);
+ }
+
// Execute the search
const fetchResp = await searchSource.fetch({
abortSignal: fetchStateRef.current.abortController.signal,
diff --git a/src/plugins/discover/public/opensearch_dashboards_services.ts b/src/plugins/discover/public/opensearch_dashboards_services.ts
index 85d8ba7976cc..25e948ec6d6f 100644
--- a/src/plugins/discover/public/opensearch_dashboards_services.ts
+++ b/src/plugins/discover/public/opensearch_dashboards_services.ts
@@ -37,6 +37,7 @@ import { createGetterSetter } from '../../opensearch_dashboards_utils/public';
import { search } from '../../data/public';
import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry';
+import { UsageCollectionSetup } from '../../usage_collection/public';
let services: DiscoverServices | null = null;
let uiActions: UiActionsStart;
@@ -67,6 +68,10 @@ export const [getDocViewsLinksRegistry, setDocViewsLinksRegistry] = createGetter
DocViewsLinksRegistry
>('DocViewsLinksRegistry');
+export const [getUsageCollector, setUsageCollector] = createGetterSetter(
+ 'UsageCollector'
+);
+
/**
* Makes sure discover and context are using one instance of history.
*/
diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts
index 3a083f05b805..2e1f76aea6ad 100644
--- a/src/plugins/discover/public/plugin.ts
+++ b/src/plugins/discover/public/plugin.ts
@@ -51,6 +51,7 @@ import {
setScopedHistory,
syncHistoryLocations,
getServices,
+ setUsageCollector,
} from './opensearch_dashboards_services';
import { createSavedSearchesLoader } from './saved_searches';
import { buildServices } from './build_services';
@@ -75,6 +76,7 @@ declare module '../../share/public' {
[DISCOVER_APP_URL_GENERATOR]: UrlGeneratorState;
}
}
+import { UsageCollectionSetup } from '../../usage_collection/public';
/**
* @public
@@ -128,6 +130,7 @@ export interface DiscoverSetupPlugins {
visualizations: VisualizationsSetup;
data: DataPublicPluginSetup;
dataExplorer: DataExplorerPluginSetup;
+ usageCollection: UsageCollectionSetup;
}
/**
@@ -174,6 +177,7 @@ export class DiscoverPlugin
);
}
+ setUsageCollector(plugins.usageCollection);
this.docViewsRegistry = new DocViewsRegistry();
setDocViewsRegistry(this.docViewsRegistry);
this.docViewsRegistry.addDocView({
diff --git a/src/plugins/discover/public/ui_metric/constants.ts b/src/plugins/discover/public/ui_metric/constants.ts
new file mode 100644
index 000000000000..e6d4fc20ae71
--- /dev/null
+++ b/src/plugins/discover/public/ui_metric/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const DATASET_METRIC_SUFFIX = 'dataset_queries_count';
+export const LANGUAGE_METRIC_SUFFIX = 'language_queries_count';
+export const NEW_DISCOVER_APP_NAME = 'New_Discover';
diff --git a/src/plugins/discover/public/ui_metric/index.ts b/src/plugins/discover/public/ui_metric/index.ts
new file mode 100644
index 000000000000..eceec571b67d
--- /dev/null
+++ b/src/plugins/discover/public/ui_metric/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './constants';
+export * from './report_ui_metric';
diff --git a/src/plugins/discover/public/ui_metric/report_ui_metric.ts b/src/plugins/discover/public/ui_metric/report_ui_metric.ts
new file mode 100644
index 000000000000..9768de74d3df
--- /dev/null
+++ b/src/plugins/discover/public/ui_metric/report_ui_metric.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { UiStatsMetricType } from 'packages/osd-analytics/target/types';
+import { METRIC_TYPE } from '../../../usage_collection/public';
+import { DATASET_METRIC_SUFFIX, LANGUAGE_METRIC_SUFFIX, NEW_DISCOVER_APP_NAME } from './constants';
+import { getUsageCollector } from '../opensearch_dashboards_services';
+import { Query } from '../../../data/public';
+
+export const getDatasetTypeMetricEventName = (datasource: string) => {
+ return `${datasource}_${DATASET_METRIC_SUFFIX}`;
+};
+
+export const getLanguageMetricEventName = (language: string) => {
+ return `${language}_${LANGUAGE_METRIC_SUFFIX}`;
+};
+
+export const trackUiMetric = (
+ eventName: string,
+ appName: string = NEW_DISCOVER_APP_NAME,
+ metricType: UiStatsMetricType = METRIC_TYPE.COUNT
+) => {
+ try {
+ const usageCollector = getUsageCollector();
+ usageCollector.reportUiStats(appName, metricType, eventName);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }
+};
+
+export const trackQueryMetric = (query: Query) => {
+ trackUiMetric(getDatasetTypeMetricEventName(query.dataset?.type!));
+ trackUiMetric(getLanguageMetricEventName(query.language));
+};
From b6eb1a042244a1fcecb45ecd30f3dc9c8151da38 Mon Sep 17 00:00:00 2001
From: SuZhou-Joe
Date: Wed, 2 Oct 2024 09:08:51 +0800
Subject: [PATCH 07/31] [workspace]feat: validate workspace when find objects
(#8268)
* feat: validate workspace when find objects
Signed-off-by: SuZhou-Joe
* Changeset file for PR #8268 created/updated
* fix: type error
Signed-off-by: SuZhou-Joe
* feat: add unit test
Signed-off-by: SuZhou-Joe
* feat: address some comments
Signed-off-by: SuZhou-Joe
* feat: optimize performance
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>
---
changelogs/fragments/8268.yml | 2 +
src/plugins/workspace/server/plugin.ts | 2 +-
.../workspace/server/routes/duplicate.ts | 1 -
.../workspace/server/routes/index.test.ts | 8 ++-
src/plugins/workspace/server/routes/index.ts | 5 --
.../workspace_id_consumer_wrapper.test.ts | 8 +++
...space_saved_objects_client_wrapper.test.ts | 23 +++----
.../workspace_id_consumer_wrapper.test.ts | 68 ++++++++++++++++++-
.../workspace_id_consumer_wrapper.ts | 60 +++++++++++++---
src/plugins/workspace/server/types.ts | 1 -
.../workspace/server/workspace_client.mock.ts | 20 +++---
11 files changed, 154 insertions(+), 44 deletions(-)
create mode 100644 changelogs/fragments/8268.yml
diff --git a/changelogs/fragments/8268.yml b/changelogs/fragments/8268.yml
new file mode 100644
index 000000000000..dc7bc64538a8
--- /dev/null
+++ b/changelogs/fragments/8268.yml
@@ -0,0 +1,2 @@
+feat:
+- Validate if the workspaces param is valid or not when consume it in the wrapper. ([#8268](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8268))
\ No newline at end of file
diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts
index 7b0702b41f71..42ca77e922e4 100644
--- a/src/plugins/workspace/server/plugin.ts
+++ b/src/plugins/workspace/server/plugin.ts
@@ -200,7 +200,7 @@ export class WorkspacePlugin implements Plugin>;
const mockDynamicConfigService = dynamicConfigServiceMock.createInternalStartContract();
@@ -19,15 +20,18 @@ const mockDynamicConfigService = dynamicConfigServiceMock.createInternalStartCon
describe(`Workspace routes`, () => {
let server: SetupServerReturn['server'];
let httpSetup: SetupServerReturn['httpSetup'];
+ let mockedWorkspaceClient: IWorkspaceClientImpl;
beforeEach(async () => {
({ server, httpSetup } = await setupServer());
const router = httpSetup.createRouter('');
+ mockedWorkspaceClient = workspaceClientMock.create();
+
registerRoutes({
router,
- client: workspaceClientMock,
+ client: mockedWorkspaceClient,
logger: loggingSystemMock.create().get(),
maxImportExportSize: Number.MAX_SAFE_INTEGER,
isPermissionControlEnabled: false,
@@ -51,7 +55,7 @@ describe(`Workspace routes`, () => {
})
.expect(200);
expect(result.body).toEqual({ id: expect.any(String) });
- expect(workspaceClientMock.create).toHaveBeenCalledWith(
+ expect(mockedWorkspaceClient.create).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
name: 'Observability',
diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts
index d7be4fb8b846..599bc1e82f59 100644
--- a/src/plugins/workspace/server/routes/index.ts
+++ b/src/plugins/workspace/server/routes/index.ts
@@ -141,7 +141,6 @@ export function registerRoutes({
const result = await client.list(
{
request: req,
- logger,
},
req.body
);
@@ -180,7 +179,6 @@ export function registerRoutes({
const result = await client.get(
{
request: req,
- logger,
},
id
);
@@ -225,7 +223,6 @@ export function registerRoutes({
const result = await client.create(
{
request: req,
- logger,
},
createPayload
);
@@ -252,7 +249,6 @@ export function registerRoutes({
const result = await client.update(
{
request: req,
- logger,
},
id,
{
@@ -280,7 +276,6 @@ export function registerRoutes({
const result = await client.delete(
{
request: req,
- logger,
},
id
);
diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts
index 1586a7dfa9b2..c8212d9cc6b1 100644
--- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts
@@ -277,6 +277,14 @@ describe('workspace_id_consumer integration test', () => {
);
});
+ it('should return error when find with a not existing workspace', async () => {
+ const findResult = await osdTestServer.request
+ .get(root, `/w/not_exist_workspace_id/api/saved_objects/_find?type=${dashboard.type}`)
+ .expect(400);
+
+ expect(findResult.body.message).toEqual('Invalid workspaces');
+ });
+
it('import within workspace', async () => {
await clearFooAndBar();
diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts
index b5ab4210f1ba..82c943545aca 100644
--- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts
@@ -243,19 +243,14 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
describe('find', () => {
it('should return empty result if user not permitted', async () => {
- const result = await notPermittedSavedObjectedClient.find({
- type: 'dashboard',
- workspaces: ['workspace-1'],
- perPage: 999,
- page: 1,
- });
-
- expect(result).toEqual({
- saved_objects: [],
- total: 0,
- page: 1,
- per_page: 999,
- });
+ await expect(
+ notPermittedSavedObjectedClient.find({
+ type: 'dashboard',
+ workspaces: ['workspace-1'],
+ perPage: 999,
+ page: 1,
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Invalid workspaces]`);
});
it('should return consistent inner workspace data when user permitted', async () => {
@@ -758,7 +753,7 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
{
id: deleteWorkspaceId,
permissions: {
- library_read: { users: ['foo'] },
+ read: { users: ['foo'] },
library_write: { users: ['foo'] },
},
}
diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts
index 2db8d146822f..570d701d7c63 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts
@@ -7,10 +7,12 @@ import { updateWorkspaceState } from '../../../../core/server/utils';
import { SavedObject } from '../../../../core/public';
import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks';
import { WorkspaceIdConsumerWrapper } from './workspace_id_consumer_wrapper';
+import { workspaceClientMock } from '../workspace_client.mock';
describe('WorkspaceIdConsumerWrapper', () => {
const requestHandlerContext = coreMock.createRequestHandlerContext();
- const wrapperInstance = new WorkspaceIdConsumerWrapper();
+ const mockedWorkspaceClient = workspaceClientMock.create();
+ const wrapperInstance = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
const mockedClient = savedObjectsClientMock.create();
const workspaceEnabledMockRequest = httpServerMock.createOpenSearchDashboardsRequest();
updateWorkspaceState(workspaceEnabledMockRequest, {
@@ -103,6 +105,29 @@ describe('WorkspaceIdConsumerWrapper', () => {
describe('find', () => {
beforeEach(() => {
mockedClient.find.mockClear();
+ mockedWorkspaceClient.get.mockImplementation((requestContext, id) => {
+ if (id === 'foo') {
+ return {
+ success: true,
+ };
+ }
+
+ return {
+ success: false,
+ };
+ });
+ mockedWorkspaceClient.list.mockResolvedValue({
+ success: true,
+ result: {
+ workspaces: [
+ {
+ id: 'foo',
+ },
+ ],
+ },
+ });
+ mockedWorkspaceClient.get.mockClear();
+ mockedWorkspaceClient.list.mockClear();
});
it(`Should add workspaces parameters when find`, async () => {
@@ -113,10 +138,48 @@ describe('WorkspaceIdConsumerWrapper', () => {
type: 'dashboard',
workspaces: ['foo'],
});
+ expect(mockedWorkspaceClient.get).toBeCalledTimes(1);
+ expect(mockedWorkspaceClient.list).toBeCalledTimes(0);
});
it(`Should pass a empty workspace array`, async () => {
- const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper();
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, {});
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+ await mockedWrapperClient.find({
+ type: ['dashboard', 'visualization'],
+ });
+ expect(mockedClient.find).toBeCalledWith({
+ type: ['dashboard', 'visualization'],
+ });
+ });
+
+ it(`Should throw error when passing in invalid workspaces`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, {});
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+ expect(
+ mockedWrapperClient.find({
+ type: ['dashboard', 'visualization'],
+ workspaces: ['foo', 'not-exist'],
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Invalid workspaces]`);
+ expect(mockedWorkspaceClient.get).toBeCalledTimes(0);
+ expect(mockedWorkspaceClient.list).toBeCalledTimes(1);
+ });
+
+ it(`Should not throw error when passing in '*'`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
updateWorkspaceState(mockRequest, {});
const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
@@ -126,6 +189,7 @@ describe('WorkspaceIdConsumerWrapper', () => {
});
await mockedWrapperClient.find({
type: ['dashboard', 'visualization'],
+ workspaces: ['*'],
});
expect(mockedClient.find).toBeCalledWith({
type: ['dashboard', 'visualization'],
diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts
index 1871a3a9b9f1..90820c835d47 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts
@@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
+import { i18n } from '@osd/i18n';
import { getWorkspaceState } from '../../../../core/server/utils';
import {
@@ -12,8 +13,9 @@ import {
SavedObjectsCheckConflictsObject,
OpenSearchDashboardsRequest,
SavedObjectsFindOptions,
- SavedObject,
+ SavedObjectsErrorHelpers,
} from '../../../../core/server';
+import { IWorkspaceClientImpl } from '../types';
const UI_SETTINGS_SAVED_OBJECTS_TYPE = 'config';
@@ -74,15 +76,55 @@ export class WorkspaceIdConsumerWrapper {
this.formatWorkspaceIdParams(wrapperOptions.request, options)
),
delete: wrapperOptions.client.delete,
- find: (options: SavedObjectsFindOptions) => {
- return wrapperOptions.client.find(
- // Based on https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts#L49
- // we need to make sure the find call for upgrade config should be able to find all the global configs as it was before.
- // It is a workaround for 2.17, should be optimized in the upcoming 2.18 release.
+ find: async (options: SavedObjectsFindOptions) => {
+ // Based on https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts#L49
+ // we need to make sure the find call for upgrade config should be able to find all the global configs as it was before.
+ // It is a workaround for 2.17, should be optimized in the upcoming 2.18 release.
+ const finalOptions =
this.isConfigType(options.type as string) && options.sortField === 'buildNum'
? options
- : this.formatWorkspaceIdParams(wrapperOptions.request, options)
- );
+ : this.formatWorkspaceIdParams(wrapperOptions.request, options);
+ if (finalOptions.workspaces?.length) {
+ let isAllTargetWorkspaceExisting = false;
+ // If only has one workspace, we should use get to optimize performance
+ if (finalOptions.workspaces.length === 1) {
+ const workspaceGet = await this.workspaceClient.get(
+ { request: wrapperOptions.request },
+ finalOptions.workspaces[0]
+ );
+ if (workspaceGet.success) {
+ isAllTargetWorkspaceExisting = true;
+ }
+ } else {
+ const workspaceList = await this.workspaceClient.list(
+ {
+ request: wrapperOptions.request,
+ },
+ {
+ perPage: 9999,
+ }
+ );
+ if (workspaceList.success) {
+ const workspaceIdsSet = new Set(
+ workspaceList.result.workspaces.map((workspace) => workspace.id)
+ );
+ isAllTargetWorkspaceExisting = finalOptions.workspaces.every((targetWorkspace) =>
+ workspaceIdsSet.has(targetWorkspace)
+ );
+ }
+ }
+
+ if (!isAllTargetWorkspaceExisting) {
+ throw SavedObjectsErrorHelpers.decorateBadRequestError(
+ new Error(
+ i18n.translate('workspace.id_consumer.invalid', {
+ defaultMessage: 'Invalid workspaces',
+ })
+ )
+ );
+ }
+ }
+ return wrapperOptions.client.find(finalOptions);
},
bulkGet: wrapperOptions.client.bulkGet,
get: wrapperOptions.client.get,
@@ -94,5 +136,5 @@ export class WorkspaceIdConsumerWrapper {
};
};
- constructor() {}
+ constructor(private readonly workspaceClient: IWorkspaceClientImpl) {}
}
diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts
index ed056bedf4e3..d9b998cc3344 100644
--- a/src/plugins/workspace/server/types.ts
+++ b/src/plugins/workspace/server/types.ts
@@ -32,7 +32,6 @@ export interface WorkspaceFindOptions {
export interface IRequestDetail {
request: OpenSearchDashboardsRequest;
- logger: Logger;
}
export interface IWorkspaceClientImpl {
diff --git a/src/plugins/workspace/server/workspace_client.mock.ts b/src/plugins/workspace/server/workspace_client.mock.ts
index b6dce61cedcf..c11a6245e792 100644
--- a/src/plugins/workspace/server/workspace_client.mock.ts
+++ b/src/plugins/workspace/server/workspace_client.mock.ts
@@ -4,13 +4,15 @@
*/
export const workspaceClientMock = {
- setup: jest.fn(),
- setSavedObjects: jest.fn(),
- setUiSettings: jest.fn(),
- create: jest.fn().mockResolvedValue({ id: 'mock-workspace-id' }),
- list: jest.fn(),
- get: jest.fn(),
- update: jest.fn(),
- delete: jest.fn(),
- destroy: jest.fn(),
+ create: () => ({
+ setup: jest.fn(),
+ setSavedObjects: jest.fn(),
+ setUiSettings: jest.fn(),
+ create: jest.fn().mockResolvedValue({ id: 'mock-workspace-id' }),
+ list: jest.fn(),
+ get: jest.fn(),
+ update: jest.fn(),
+ delete: jest.fn(),
+ destroy: jest.fn(),
+ }),
};
From 8dc3611298f3aebac7f3fe95aa0ac74c507f8332 Mon Sep 17 00:00:00 2001
From: Miki
Date: Tue, 1 Oct 2024 18:53:12 -0700
Subject: [PATCH 08/31] Fix duplicate i18n identifier in savedObjectsManagement
plugin (#8405)
Signed-off-by: Miki
---
.../components/__snapshots__/table.test.tsx.snap | 8 ++++----
.../objects_table/components/flyout.tsx | 2 +-
.../management_section/objects_table/components/table.tsx | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap
index 20c945c5ee4e..ac3e7baa5220 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap
@@ -84,7 +84,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
checked={true}
label={
{
isLegacyFile={isLegacyFile}
updateSelection={(newValues: ImportMode) => this.changeImportMode(newValues)}
optionLabel={i18n.translate(
- 'savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle',
+ 'savedObjectsManagement.objectsTable.importModeControl.conflictManagementTitle',
{ defaultMessage: 'Conflict management' }
)}
useUpdatedUX={this.props.useUpdatedUX}
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx
index 5f7ee1235378..c71774244f59 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx
@@ -487,7 +487,7 @@ export class Table extends PureComponent {
label={
Date: Tue, 1 Oct 2024 18:53:35 -0700
Subject: [PATCH 09/31] Fix dynamic uses of i18n in indexPatternManagement
plugin (#8398)
* Fix dynamic uses of i18n in indexPatternManagement plugin
Signed-off-by: Miki
* Changeset file for PR #8398 created/updated
---------
Signed-off-by: Miki
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8398.yml | 2 ++
.../public/components/breadcrumbs.ts | 10 +++++++---
.../index_pattern_table.tsx | 18 +++++++++---------
3 files changed, 18 insertions(+), 12 deletions(-)
create mode 100644 changelogs/fragments/8398.yml
diff --git a/changelogs/fragments/8398.yml b/changelogs/fragments/8398.yml
new file mode 100644
index 000000000000..649798050b3e
--- /dev/null
+++ b/changelogs/fragments/8398.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix dynamic uses of i18n in indexPatternManagement plugin ([#8398](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8398))
\ No newline at end of file
diff --git a/src/plugins/index_pattern_management/public/components/breadcrumbs.ts b/src/plugins/index_pattern_management/public/components/breadcrumbs.ts
index 4811f41a4c0a..69034a221573 100644
--- a/src/plugins/index_pattern_management/public/components/breadcrumbs.ts
+++ b/src/plugins/index_pattern_management/public/components/breadcrumbs.ts
@@ -34,9 +34,13 @@ import { IndexPattern } from '../../../data/public';
export function getListBreadcrumbs(currentWorkspaceName?: string) {
return [
{
- text: i18n.translate('indexPatternManagement.indexPatterns.listBreadcrumb', {
- defaultMessage: currentWorkspaceName ? 'Workspace index patterns' : 'Index patterns',
- }),
+ text: currentWorkspaceName
+ ? i18n.translate('indexPatternManagement.inWorkspace.indexPatterns.listBreadcrumb', {
+ defaultMessage: 'Workspace index patterns',
+ })
+ : i18n.translate('indexPatternManagement.indexPatterns.listBreadcrumb', {
+ defaultMessage: 'Index patterns',
+ }),
href: `/`,
},
];
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
index eaffaf4feacf..6cda80c2e464 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
@@ -245,21 +245,21 @@ export const IndexPatternTable = ({ canSave, history }: Props) => {
);
})();
- const description = i18n.translate(
- 'indexPatternManagement.indexPatternTable.indexPatternExplanation',
- currentWorkspace
- ? {
+ const description = currentWorkspace
+ ? i18n.translate(
+ 'indexPatternManagement.indexPatternTable.indexPatternExplanationWithWorkspace',
+ {
defaultMessage:
'Create and manage the index patterns that help you retrieve your data from OpenSearch for {name} workspace.',
values: {
name: currentWorkspace.name,
},
}
- : {
- defaultMessage:
- 'Create and manage the index patterns that help you retrieve your data from OpenSearch.',
- }
- );
+ )
+ : i18n.translate('indexPatternManagement.indexPatternTable.indexPatternExplanation', {
+ defaultMessage:
+ 'Create and manage the index patterns that help you retrieve your data from OpenSearch.',
+ });
const pageTitleAndDescription = useUpdatedUX ? (
Date: Wed, 2 Oct 2024 10:45:53 +0800
Subject: [PATCH 10/31] [navigation] feat: Render dev tools inside a modal
(#7938)
* feat: render the content inside a modal
Signed-off-by: SuZhou-Joe
* Changeset file for PR #7938 created/updated
* feat: use memory router when opened in modal
Signed-off-by: SuZhou-Joe
* feat: update
Signed-off-by: SuZhou-Joe
* feat: update buttons
Signed-off-by: SuZhou-Joe
* feat: update
Signed-off-by: SuZhou-Joe
* feat: optimize layout
Signed-off-by: SuZhou-Joe
* feat: optimize layout
Signed-off-by: SuZhou-Joe
* fix: vertical scrollbar issue
Signed-off-by: SuZhou-Joe
* feat: update test
Signed-off-by: SuZhou-Joe
* feat: update
Signed-off-by: SuZhou-Joe
* feat: update
Signed-off-by: SuZhou-Joe
* feat: update order
Signed-off-by: SuZhou-Joe
* fix: update snapshot
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>
---
changelogs/fragments/7938.yml | 2 +
.../__snapshots__/top_nav_menu.test.tsx.snap | 76 ++++++++
.../components/top_nav_menu.test.tsx | 37 ++++
.../application/components/top_nav_menu.tsx | 61 ++++++-
.../application/containers/editor/editor.tsx | 10 +-
.../containers/main/get_top_nav.ts | 115 ------------
.../containers/main/get_top_nav.tsx | 166 ++++++++++++++++++
.../application/containers/main/main.tsx | 47 +++--
src/plugins/console/public/styles/_app.scss | 4 +
src/plugins/dev_tools/public/application.tsx | 104 ++++++++---
.../dev_tools/public/dev_tools_icon.scss | 32 ++++
.../dev_tools/public/dev_tools_icon.test.tsx | 49 +++++-
.../dev_tools/public/dev_tools_icon.tsx | 143 +++++++++++++--
src/plugins/dev_tools/public/plugin.ts | 5 +
14 files changed, 669 insertions(+), 182 deletions(-)
create mode 100644 changelogs/fragments/7938.yml
create mode 100644 src/plugins/console/public/application/components/__snapshots__/top_nav_menu.test.tsx.snap
create mode 100644 src/plugins/console/public/application/components/top_nav_menu.test.tsx
delete mode 100644 src/plugins/console/public/application/containers/main/get_top_nav.ts
create mode 100644 src/plugins/console/public/application/containers/main/get_top_nav.tsx
create mode 100644 src/plugins/dev_tools/public/dev_tools_icon.scss
diff --git a/changelogs/fragments/7938.yml b/changelogs/fragments/7938.yml
new file mode 100644
index 000000000000..12162f9a219b
--- /dev/null
+++ b/changelogs/fragments/7938.yml
@@ -0,0 +1,2 @@
+feat:
+- Change dev tools to a modal ([#7938](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7938))
\ No newline at end of file
diff --git a/src/plugins/console/public/application/components/__snapshots__/top_nav_menu.test.tsx.snap b/src/plugins/console/public/application/components/__snapshots__/top_nav_menu.test.tsx.snap
new file mode 100644
index 000000000000..e297409a5060
--- /dev/null
+++ b/src/plugins/console/public/application/components/__snapshots__/top_nav_menu.test.tsx.snap
@@ -0,0 +1,76 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TopNavMenu Component should render correctly when not use updatedUX 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`TopNavMenu Component should render correctly when use updatedUX 1`] = `
+
+`;
diff --git a/src/plugins/console/public/application/components/top_nav_menu.test.tsx b/src/plugins/console/public/application/components/top_nav_menu.test.tsx
new file mode 100644
index 000000000000..7cd1d761c82f
--- /dev/null
+++ b/src/plugins/console/public/application/components/top_nav_menu.test.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { MenuItemPosition, TopNavMenu, TopNavMenuItem } from './top_nav_menu';
+import { render } from '@testing-library/react';
+
+describe('TopNavMenu Component', () => {
+ const mockedItems: TopNavMenuItem[] = [
+ {
+ id: 'foo',
+ label: 'foo',
+ description: 'foo',
+ onClick: jest.fn(),
+ testId: 'foo',
+ position: MenuItemPosition.LEFT,
+ },
+ {
+ id: 'bar',
+ label: 'bar',
+ description: 'bar',
+ onClick: jest.fn(),
+ testId: 'bar',
+ position: MenuItemPosition.RIGHT,
+ },
+ ];
+ it('should render correctly when not use updatedUX', () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+ it('should render correctly when use updatedUX', () => {
+ const { container } = render();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/plugins/console/public/application/components/top_nav_menu.tsx b/src/plugins/console/public/application/components/top_nav_menu.tsx
index bd99098ec21a..90a3c34535ec 100644
--- a/src/plugins/console/public/application/components/top_nav_menu.tsx
+++ b/src/plugins/console/public/application/components/top_nav_menu.tsx
@@ -29,7 +29,18 @@
*/
import React, { FunctionComponent } from 'react';
-import { EuiTabs, EuiTab } from '@elastic/eui';
+import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+
+interface CommonProps {
+ disabled: boolean;
+ onClick: () => void;
+ ['data-test-subj']: string;
+}
+
+export enum MenuItemPosition {
+ LEFT = 'left',
+ RIGHT = 'right',
+}
export interface TopNavMenuItem {
id: string;
@@ -37,14 +48,60 @@ export interface TopNavMenuItem {
description: string;
onClick: () => void;
testId: string;
+ render?: (commonProps: CommonProps) => React.JSX.Element;
+ position: MenuItemPosition;
}
interface Props {
disabled?: boolean;
items: TopNavMenuItem[];
+ useUpdatedUX?: boolean;
+ rightContainerChildren?: React.ReactNode;
}
-export const TopNavMenu: FunctionComponent = ({ items, disabled }) => {
+export const TopNavMenu: FunctionComponent = ({
+ items,
+ disabled,
+ useUpdatedUX,
+ rightContainerChildren,
+}) => {
+ if (useUpdatedUX) {
+ const leftMenus = items.filter((item) => item.position === MenuItemPosition.LEFT);
+ const rightMenus = items.filter((item) => item.position === MenuItemPosition.RIGHT);
+ const renderMenus = (item: TopNavMenuItem, idx: number) => {
+ const commonProps: CommonProps = {
+ disabled: !!disabled,
+ onClick: item.onClick,
+ ['data-test-subj']: item.testId,
+ };
+
+ return (
+
+ {item.render?.(commonProps) || null}
+
+ );
+ };
+ return (
+ <>
+
+
+
+
+ {leftMenus.map((item, index) => renderMenus(item, index))}
+
+
+
+
+ {rightContainerChildren}
+ {rightMenus.map((item, index) => renderMenus(item, index))}
+
+
+
+
+ >
+ );
+ }
+
return (
{items.map((item, idx) => {
diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx
index 4d5723b123d4..5ed67118e419 100644
--- a/src/plugins/console/public/application/containers/editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/editor.tsx
@@ -49,9 +49,10 @@ const PANEL_MIN_WIDTH = '100px';
interface Props {
loading: boolean;
dataSourceId?: string;
+ useUpdatedUX?: boolean;
}
-export const Editor = memo(({ loading, dataSourceId }: Props) => {
+export const Editor = memo(({ loading, dataSourceId, useUpdatedUX }: Props) => {
const {
services: { storage },
} = useServicesContext();
@@ -101,7 +102,12 @@ export const Editor = memo(({ loading, dataSourceId }: Props) => {
)}
{loading ? : }
diff --git a/src/plugins/console/public/application/containers/main/get_top_nav.ts b/src/plugins/console/public/application/containers/main/get_top_nav.ts
deleted file mode 100644
index cd21321993bb..000000000000
--- a/src/plugins/console/public/application/containers/main/get_top_nav.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * SPDX-License-Identifier: Apache-2.0
- *
- * The OpenSearch Contributors require contributions made to
- * this file be licensed under the Apache-2.0 license or a
- * compatible open source license.
- *
- * Any modifications Copyright OpenSearch Contributors. See
- * GitHub history for details.
- */
-
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { i18n } from '@osd/i18n';
-
-interface Props {
- onClickHistory: () => void;
- onClickSettings: () => void;
- onClickHelp: () => void;
- onClickExport: () => void;
- onClickImport: () => void;
-}
-
-export function getTopNavConfig({
- onClickHistory,
- onClickSettings,
- onClickHelp,
- onClickExport,
- onClickImport,
-}: Props) {
- return [
- {
- id: 'history',
- label: i18n.translate('console.topNav.historyTabLabel', {
- defaultMessage: 'History',
- }),
- description: i18n.translate('console.topNav.historyTabDescription', {
- defaultMessage: 'History',
- }),
- onClick: () => {
- onClickHistory();
- },
- testId: 'consoleHistoryButton',
- },
- {
- id: 'settings',
- label: i18n.translate('console.topNav.settingsTabLabel', {
- defaultMessage: 'Settings',
- }),
- description: i18n.translate('console.topNav.settingsTabDescription', {
- defaultMessage: 'Settings',
- }),
- onClick: () => {
- onClickSettings();
- },
- testId: 'consoleSettingsButton',
- },
- {
- id: 'help',
- label: i18n.translate('console.topNav.helpTabLabel', {
- defaultMessage: 'Help',
- }),
- description: i18n.translate('console.topNav.helpTabDescription', {
- defaultMessage: 'Help',
- }),
- onClick: () => {
- onClickHelp();
- },
- testId: 'consoleHelpButton',
- },
- {
- id: 'export',
- label: i18n.translate('console.topNav.exportTabLabel', {
- defaultMessage: 'Export',
- }),
- description: i18n.translate('console.topNav.exportTabDescription', {
- defaultMessage: 'Export',
- }),
- onClick: () => {
- onClickExport();
- },
- testId: 'consoleExportButton',
- },
- {
- id: 'import',
- label: i18n.translate('console.topNav.importTabLabel', {
- defaultMessage: 'Import',
- }),
- description: i18n.translate('console.topNav.importTabDescription', {
- defaultMessage: 'Import',
- }),
- onClick: () => {
- onClickImport();
- },
- testId: 'consoleImportButton',
- },
- ];
-}
diff --git a/src/plugins/console/public/application/containers/main/get_top_nav.tsx b/src/plugins/console/public/application/containers/main/get_top_nav.tsx
new file mode 100644
index 000000000000..d9da29f1de3c
--- /dev/null
+++ b/src/plugins/console/public/application/containers/main/get_top_nav.tsx
@@ -0,0 +1,166 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { EuiSmallButton, EuiSmallButtonEmpty, EuiSmallButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { TopNavMenuItem } from '../../components';
+import { MenuItemPosition } from '../../components/top_nav_menu';
+
+interface Props {
+ onClickHistory: () => void;
+ onClickSettings: () => void;
+ onClickHelp: () => void;
+ onClickExport: () => void;
+ onClickImport: () => void;
+ useUpdatedUX?: boolean;
+}
+
+export function getTopNavConfig({
+ onClickHistory,
+ onClickSettings,
+ onClickHelp,
+ onClickExport,
+ onClickImport,
+ useUpdatedUX,
+}: Props): TopNavMenuItem[] {
+ const helpItem: TopNavMenuItem = {
+ id: 'help',
+ label: i18n.translate('console.topNav.helpTabLabel', {
+ defaultMessage: 'Help',
+ }),
+ description: i18n.translate('console.topNav.helpTabDescription', {
+ defaultMessage: 'Help',
+ }),
+ onClick: () => {
+ onClickHelp();
+ },
+ testId: 'consoleHelpButton',
+ render: (commonProps) => (
+
+
+
+ ),
+ position: MenuItemPosition.RIGHT,
+ };
+ const settingsItem = {
+ id: 'settings',
+ label: i18n.translate('console.topNav.settingsTabLabel', {
+ defaultMessage: 'Settings',
+ }),
+ description: i18n.translate('console.topNav.settingsTabDescription', {
+ defaultMessage: 'Settings',
+ }),
+ onClick: () => {
+ onClickSettings();
+ },
+ testId: 'consoleSettingsButton',
+ render: (commonProps) => (
+
+
+
+ ),
+ position: MenuItemPosition.RIGHT,
+ };
+ return [
+ {
+ id: 'history',
+ label: i18n.translate('console.topNav.historyTabLabel', {
+ defaultMessage: 'History',
+ }),
+ description: i18n.translate('console.topNav.historyTabDescription', {
+ defaultMessage: 'History',
+ }),
+ onClick: () => {
+ onClickHistory();
+ },
+ testId: 'consoleHistoryButton',
+ render: (commonProps) => (
+
+ {i18n.translate('console.topNav.historyTabLabel', {
+ defaultMessage: 'History',
+ })}
+
+ ),
+ position: MenuItemPosition.LEFT,
+ },
+ ...(useUpdatedUX ? [helpItem, settingsItem] : [settingsItem, helpItem]),
+ {
+ id: 'export',
+ label: i18n.translate('console.topNav.exportTabLabel', {
+ defaultMessage: 'Export',
+ }),
+ description: i18n.translate('console.topNav.exportTabDescription', {
+ defaultMessage: 'Export',
+ }),
+ onClick: () => {
+ onClickExport();
+ },
+ testId: 'consoleExportButton',
+ render: (commonProps) => (
+
+ {i18n.translate('console.topNav.exportTabLabel', {
+ defaultMessage: 'Export',
+ })}
+
+ ),
+ position: MenuItemPosition.RIGHT,
+ },
+ {
+ id: 'import',
+ label: i18n.translate('console.topNav.importTabLabel', {
+ defaultMessage: 'Import',
+ }),
+ description: i18n.translate('console.topNav.importTabDescription', {
+ defaultMessage: 'Import',
+ }),
+ onClick: () => {
+ onClickImport();
+ },
+ testId: 'consoleImportButton',
+ render: (commonProps) => (
+
+ {i18n.translate('console.topNav.importButtonLabel', {
+ defaultMessage: 'Import query',
+ })}
+
+ ),
+ position: MenuItemPosition.RIGHT,
+ },
+ ];
+}
diff --git a/src/plugins/console/public/application/containers/main/main.tsx b/src/plugins/console/public/application/containers/main/main.tsx
index bbe5bd9856eb..071229495294 100644
--- a/src/plugins/console/public/application/containers/main/main.tsx
+++ b/src/plugins/console/public/application/containers/main/main.tsx
@@ -57,7 +57,7 @@ interface MainProps {
export function Main({ dataSourceId }: MainProps) {
const {
- services: { storage, objectStorageClient },
+ services: { storage, objectStorageClient, uiSettings },
} = useServicesContext();
const { ready: editorsReady } = useEditorReadContext();
@@ -98,10 +98,31 @@ export function Main({ dataSourceId }: MainProps) {
const lastDatum = requestData?.[requestData.length - 1] ?? requestError;
+ const useUpdatedUX = uiSettings.get('home:useNewHomePage');
+
+ const networkRequestStatusBarContent = (
+
+
+
+ );
+
return (
setShowHistory(!showingHistory),
onClickSettings: () => setShowSettings(true),
onClickHelp: () => setShowHelp(!showHelp),
onClickExport: () => onExport(),
onClickImport: () => setShowImportFlyout(!showImportFlyout),
})}
+ rightContainerChildren={networkRequestStatusBarContent}
/>
-
-
-
+ {useUpdatedUX ? null : networkRequestStatusBarContent}
{showingHistory ?
{renderConsoleHistory()} : null}
-
+
diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss
index 161d1f200afd..706a4b8cf025 100644
--- a/src/plugins/console/public/styles/_app.scss
+++ b/src/plugins/console/public/styles/_app.scss
@@ -11,6 +11,10 @@
.consoleContainer {
padding: $euiSizeS;
+
+ &.useUpdatedUX-true {
+ padding: $euiSizeS 0;
+ }
}
.conApp {
diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx
index 38d444665852..e57f11bee617 100644
--- a/src/plugins/dev_tools/public/application.tsx
+++ b/src/plugins/dev_tools/public/application.tsx
@@ -39,6 +39,7 @@ import {
ApplicationStart,
ChromeStart,
CoreStart,
+ MountPoint,
NotificationsStart,
SavedObjectsStart,
ScopedHistory,
@@ -56,6 +57,8 @@ interface DevToolsWrapperProps {
notifications: NotificationsStart;
dataSourceEnabled: boolean;
dataSourceManagement?: DataSourceManagementPluginSetup;
+ useUpdatedUX?: boolean;
+ setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
}
interface MountedDevToolDescriptor {
@@ -69,10 +72,13 @@ function DevToolsWrapper({
activeDevTool,
updateRoute,
savedObjects,
- notifications: { toasts },
+ notifications,
dataSourceEnabled,
dataSourceManagement,
+ useUpdatedUX,
+ setMenuMountPoint,
}: DevToolsWrapperProps) {
+ const { toasts } = notifications;
const mountedTool = useRef
(null);
const [isLoading, setIsLoading] = React.useState(true);
@@ -115,6 +121,21 @@ function DevToolsWrapper({
};
const renderDataSourceSelector = () => {
+ if (useUpdatedUX) {
+ const DataSourceMenu = dataSourceManagement!.ui.getDataSourceMenu();
+ return (
+
+ );
+ }
const DataSourceSelector = dataSourceManagement!.ui.DataSourceSelector;
return (
@@ -218,26 +239,33 @@ function setBreadcrumbs(chrome: ChromeStart) {
]);
}
-export function renderApp(
- { application, chrome, docLinks, savedObjects, notifications }: CoreStart,
- element: HTMLElement,
- history: ScopedHistory,
- devTools: readonly DevToolApp[],
- { dataSourceManagement, dataSource }: DevToolsSetupDependencies
+export function MainApp(
+ props: {
+ devTools: readonly DevToolApp[];
+ RouterComponent?: React.ComponentClass;
+ } & Pick<
+ DevToolsWrapperProps,
+ | 'savedObjects'
+ | 'notifications'
+ | 'dataSourceEnabled'
+ | 'dataSourceManagement'
+ | 'useUpdatedUX'
+ | 'setMenuMountPoint'
+ >
) {
- const dataSourceEnabled = !!dataSource;
- if (redirectOnMissingCapabilities(application)) {
- return () => {};
- }
-
- addHelpMenuToAppChrome(chrome, docLinks);
- setBadge(application, chrome);
- setBreadcrumbs(chrome);
- setTitle(chrome);
-
- ReactDOM.render(
+ const {
+ devTools,
+ savedObjects,
+ notifications,
+ dataSourceEnabled,
+ dataSourceManagement,
+ useUpdatedUX,
+ setMenuMountPoint,
+ RouterComponent = Router,
+ } = props;
+ return (
-
+
{devTools
// Only create routes for devtools that are not disabled
@@ -247,15 +275,17 @@ export function renderApp(
key={devTool.id}
path={`/${devTool.id}`}
exact={!devTool.enableRouting}
- render={(props) => (
+ render={(routeProps) => (
)}
/>
@@ -264,8 +294,36 @@ export function renderApp(
-
- ,
+
+
+ );
+}
+
+export function renderApp(
+ { application, chrome, docLinks, savedObjects, notifications }: CoreStart,
+ element: HTMLElement,
+ history: ScopedHistory,
+ devTools: readonly DevToolApp[],
+ { dataSourceManagement, dataSource }: DevToolsSetupDependencies
+) {
+ const dataSourceEnabled = !!dataSource;
+ if (redirectOnMissingCapabilities(application)) {
+ return () => {};
+ }
+
+ addHelpMenuToAppChrome(chrome, docLinks);
+ setBadge(application, chrome);
+ setBreadcrumbs(chrome);
+ setTitle(chrome);
+
+ ReactDOM.render(
+
,
element
);
diff --git a/src/plugins/dev_tools/public/dev_tools_icon.scss b/src/plugins/dev_tools/public/dev_tools_icon.scss
new file mode 100644
index 000000000000..69e6e524a86d
--- /dev/null
+++ b/src/plugins/dev_tools/public/dev_tools_icon.scss
@@ -0,0 +1,32 @@
+.devToolsOverlayMask {
+ padding-bottom: 0;
+
+ .devApp__container {
+ overflow-y: auto;
+ flex-shrink: 1;
+ }
+
+ // Make the parents of devApp__container as overflow hidden
+ // so that devApp__container can be scrollable
+ .devAppWrapper,
+ .devApp {
+ overflow: hidden;
+ }
+
+ .devToolsCloseButton {
+ align-self: flex-end;
+ }
+}
+
+.devToolsModalContent {
+ padding-left: $euiSize;
+ padding-right: $euiSize;
+ margin-top: 0;
+}
+
+// When there are multiple maskOverlays in the page and once a focused overlay get unmounted
+// it will remove the hasOverlay class in the root element, making the whole page scrollable.
+// This class is used for keep the root element unscrollable when the dev tools model is mount.
+.noScrollByDevTools {
+ overflow: hidden;
+}
diff --git a/src/plugins/dev_tools/public/dev_tools_icon.test.tsx b/src/plugins/dev_tools/public/dev_tools_icon.test.tsx
index 3a3eef74b11f..b465ec3c8b07 100644
--- a/src/plugins/dev_tools/public/dev_tools_icon.test.tsx
+++ b/src/plugins/dev_tools/public/dev_tools_icon.test.tsx
@@ -7,13 +7,52 @@ import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { DevToolsIcon } from './dev_tools_icon';
import { coreMock } from '../../../core/public/mocks';
+import { urlForwardingPluginMock } from 'src/plugins/url_forwarding/public/mocks';
+
+const createDepsMock = () => {
+ return {
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ };
+};
+
+jest.mock('./application', () => ({
+ MainApp: () =>
,
+}));
describe('
', () => {
- it('should call chrome.navGroup.setCurrentNavGroup and application.navigateToApp methods from core service when click', () => {
+ it('should call chrome.navGroup.setCurrentNavGroup and application.navigateToApp methods from core service when click', async () => {
const coreStartMock = coreMock.createStart();
- const { container } = render(
);
- const component = container.children[0].children[0];
- fireEvent.click(component);
- expect(coreStartMock.application.navigateToApp).toBeCalledWith('foo');
+ const { container, getByTestId, findByText } = render(
+
+ );
+ expect(container).toMatchInlineSnapshot(`
+
+
+
+
+
+ `);
+
+ fireEvent.click(getByTestId('openDevToolsModal'));
+ await findByText('Dev tools title');
});
});
diff --git a/src/plugins/dev_tools/public/dev_tools_icon.tsx b/src/plugins/dev_tools/public/dev_tools_icon.tsx
index d198cd2b0638..57593cdbd517 100644
--- a/src/plugins/dev_tools/public/dev_tools_icon.tsx
+++ b/src/plugins/dev_tools/public/dev_tools_icon.tsx
@@ -3,25 +3,136 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React from 'react';
-import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import React, { useRef, useCallback, useState, useEffect } from 'react';
+import {
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiOverlayMask,
+ EuiPanel,
+ EuiSmallButton,
+ EuiSpacer,
+ EuiText,
+ EuiToolTip,
+} from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { CoreStart } from 'opensearch-dashboards/public';
+import { MemoryRouter } from 'react-router-dom';
+import { MainApp } from './application';
+import { DevToolApp } from './dev_tool';
+import { DevToolsSetupDependencies } from './plugin';
+import './dev_tools_icon.scss';
+
+export function DevToolsIcon({
+ core,
+ devTools,
+ deps,
+ title,
+}: {
+ core: CoreStart;
+ devTools: readonly DevToolApp[];
+ deps: DevToolsSetupDependencies;
+ title: string;
+}) {
+ const [modalVisible, setModalVisible] = useState(false);
+ const elementRef = useRef
(null);
+ const setMountPoint = useCallback((renderFn) => {
+ renderFn(elementRef.current);
+ return () => {};
+ }, []);
+
+ useEffect(() => {
+ if (modalVisible) {
+ document.body.classList.add('noScrollByDevTools');
+ } else {
+ document.body.classList.remove('noScrollByDevTools');
+ }
+
+ return () => {
+ document.body.classList.remove('noScrollByDevTools');
+ };
+ }, [modalVisible]);
-export function DevToolsIcon({ core, appId }: { core: CoreStart; appId: string }) {
return (
-
- {
- core.application.navigateToApp(appId);
- }}
- />
-
+ <>
+
+ {
+ setModalVisible(true);
+ }}
+ />
+
+ {modalVisible ? (
+ /**
+ * We can not use OuiModal component here because OuiModal uses OuiOverlayMask as its parent node
+ * but overlay mask has a default padding bottom that prevent the modal from covering the whole page.
+ */
+
+
+
setModalVisible(false)}
+ className="euiModal__closeIcon"
+ color="text"
+ aria-label="close modal"
+ />
+
+
+
+
+
+
+
+ {title}
+
+
+
+ (elementRef.current = element)} />
+
+
+
+
+
+
+ setModalVisible(false)}
+ className="devToolsCloseButton"
+ minWidth="unset"
+ >
+ {i18n.translate('dev_tools.modal.close.label', {
+ defaultMessage: 'Close',
+ })}
+
+
+
+
+
+
+ ) : null}
+ >
);
}
diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts
index 34bc74c8d518..e6478807da7a 100644
--- a/src/plugins/dev_tools/public/plugin.ts
+++ b/src/plugins/dev_tools/public/plugin.ts
@@ -75,6 +75,7 @@ export interface DevToolsSetup {
export class DevToolsPlugin implements Plugin {
private readonly devTools = new Map();
private appStateUpdater = new BehaviorSubject(() => ({}));
+ private setupDeps: DevToolsSetupDependencies | undefined;
private getSortedDevTools(): readonly DevToolApp[] {
return sortBy([...this.devTools.values()], 'order');
@@ -89,6 +90,7 @@ export class DevToolsPlugin implements Plugin {
public setup(coreSetup: CoreSetup, deps: DevToolsSetupDependencies) {
const { application: applicationSetup, getStartServices } = coreSetup;
const { urlForwarding, managementOverview } = deps;
+ this.setupDeps = deps;
applicationSetup.register({
id: this.id,
@@ -145,6 +147,9 @@ export class DevToolsPlugin implements Plugin {
React.createElement(DevToolsIcon, {
core,
appId: this.id,
+ devTools: this.getSortedDevTools(),
+ deps: this.setupDeps as DevToolsSetupDependencies,
+ title: this.title,
})
),
});
From 774909e3eba292de73a2ea29529790b69ce67b8d Mon Sep 17 00:00:00 2001
From: Miki
Date: Tue, 1 Oct 2024 20:10:58 -0700
Subject: [PATCH 11/31] Fix flaky IndexPatternSelect test in data plugin
(#8429)
Signed-off-by: Miki
---
.../index_pattern_select.test.tsx | 127 ++++++++++++++----
1 file changed, 103 insertions(+), 24 deletions(-)
diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx
index dcf008fcf2d7..3b87700e664e 100644
--- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx
+++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.test.tsx
@@ -3,54 +3,133 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { shallow } from 'enzyme';
-import { SavedObjectsClientContract } from '../../../../../core/public';
import React from 'react';
import IndexPatternSelect from './index_pattern_select';
+import { savedObjectsServiceMock } from '../../../../../core/public/mocks';
describe('IndexPatternSelect', () => {
- let client: SavedObjectsClientContract;
- const bulkGetMock = jest.fn();
+ const savedObjectsClient = savedObjectsServiceMock.createStartContract().client;
+ const onChangeMock = jest.fn();
- const nextTick = () => new Promise((res) => process.nextTick(res));
+ jest.useFakeTimers();
beforeEach(() => {
- client = {
- find: jest.fn().mockResolvedValue({
+ onChangeMock.mockReset();
+
+ jest.spyOn(savedObjectsClient, 'get').mockReturnValue(
+ // @ts-ignore
+ Promise.resolve({
+ id: '3',
+ type: 'data-source',
+ references: [{ id: 'testDataSourceId3', type: 'data-source' }],
+ attributes: { title: 'testTitle3' },
+ })
+ );
+
+ jest.spyOn(savedObjectsClient, 'bulkGet').mockReturnValue(
+ // @ts-ignore
+ Promise.resolve({
savedObjects: [
{
- references: [{ id: 'testDataSourceId', type: 'data-source' }],
+ id: '4',
+ type: 'data-source',
+ attributes: { title: 'testTitle4' },
+ },
+ ],
+ })
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should use the data-source IDs to make a bulkGet call', async () => {
+ jest.spyOn(savedObjectsClient, 'find').mockReturnValue(
+ // @ts-ignore
+ Promise.resolve({
+ total: 2,
+ perPage: 10,
+ page: 1,
+ savedObjects: [
+ {
+ id: '1',
+ type: 'data-source',
+ references: [{ id: 'testDataSourceId1', type: 'data-source' }],
attributes: { title: 'testTitle1' },
},
{
- references: [{ id: 'testDataSourceId', type: 'data-source' }],
+ id: '2',
+ type: 'data-source',
+ references: [{ id: 'testDataSourceId2', type: 'data-source' }],
attributes: { title: 'testTitle2' },
},
],
- }),
- bulkGet: bulkGetMock,
- get: jest.fn().mockResolvedValue({
- references: [{ id: 'someId', type: 'data-source' }],
- attributes: { title: 'testTitle' },
- }),
- } as any;
+ })
+ );
+
+ const compInstance = shallow(
+
+ ).instance();
+
+ const call = compInstance.debouncedFetch('');
+ jest.advanceTimersByTime(10000);
+ await call;
+ await compInstance.debouncedFetch.flush();
+
+ expect(savedObjectsClient.bulkGet).toBeCalledWith([
+ { id: 'testDataSourceId1', type: 'data-source' },
+ { id: 'testDataSourceId2', type: 'data-source' },
+ ]);
});
- it('should render index pattern select', async () => {
- const onChangeMock = jest.fn();
- const compInstance = shallow(
+ it('should combine saved-objects with common data-source IDs when making a bulkGet call', async () => {
+ jest.spyOn(savedObjectsClient, 'find').mockReturnValue(
+ // @ts-ignore
+ Promise.resolve({
+ total: 2,
+ perPage: 10,
+ page: 1,
+ savedObjects: [
+ {
+ id: '1',
+ type: 'data-source',
+ references: [{ id: 'testDataSourceId0', type: 'data-source' }],
+ attributes: { title: 'testTitle1' },
+ },
+ {
+ id: '2',
+ type: 'data-source',
+ references: [{ id: 'testDataSourceId0', type: 'data-source' }],
+ attributes: { title: 'testTitle2' },
+ },
+ ],
+ })
+ );
+
+ const compInstance = shallow(
).instance();
- bulkGetMock.mockResolvedValue({ savedObjects: [{ attributes: { title: 'test1' } }] });
- compInstance.debouncedFetch('');
- await new Promise((resolve) => setTimeout(resolve, 600));
- await nextTick();
- expect(bulkGetMock).toBeCalledWith([{ id: 'testDataSourceId', type: 'data-source' }]);
+ const call = compInstance.debouncedFetch('');
+ jest.advanceTimersByTime(10000);
+ await call;
+ await compInstance.debouncedFetch.flush();
+
+ expect(savedObjectsClient.bulkGet).toBeCalledWith([
+ { id: 'testDataSourceId0', type: 'data-source' },
+ ]);
});
});
From 96d436edc57c4ef80ce9b7c86a7c1a4c48c79bbf Mon Sep 17 00:00:00 2001
From: Miki
Date: Tue, 1 Oct 2024 21:19:16 -0700
Subject: [PATCH 12/31] Fix dynamic uses of i18n in home plugin (#8403)
* Fix dynamic uses of i18n in home plugin
Signed-off-by: Miki
* Changeset file for PR #8403 created/updated
---------
Signed-off-by: Miki
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8403.yml | 2 ++
src/plugins/home/server/ui_settings.ts | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
create mode 100644 changelogs/fragments/8403.yml
diff --git a/changelogs/fragments/8403.yml b/changelogs/fragments/8403.yml
new file mode 100644
index 000000000000..34d00d2e2651
--- /dev/null
+++ b/changelogs/fragments/8403.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix dynamic uses of i18n in home plugin ([#8403](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8403))
\ No newline at end of file
diff --git a/src/plugins/home/server/ui_settings.ts b/src/plugins/home/server/ui_settings.ts
index 7df590ba5480..ff0fb0ab1420 100644
--- a/src/plugins/home/server/ui_settings.ts
+++ b/src/plugins/home/server/ui_settings.ts
@@ -17,11 +17,11 @@ import { USE_NEW_HOME_PAGE } from '../common/constants';
export const uiSettings: Record = {
[USE_NEW_HOME_PAGE]: {
- name: i18n.translate('core.ui_settings.params.useNewHomePage', {
+ name: i18n.translate('home.ui_settings.useNewHomePage.label', {
defaultMessage: 'Use New Home Page',
}),
value: false,
- description: i18n.translate('core.ui_settings.params.useNewHomePage', {
+ description: i18n.translate('home.ui_settings.useNewHomePage.description', {
defaultMessage: 'Try the new home page',
}),
schema: schema.boolean(),
From cec8a1e9cfc3adaee8917d652a31852b9b30ae51 Mon Sep 17 00:00:00 2001
From: Miki
Date: Tue, 1 Oct 2024 21:19:27 -0700
Subject: [PATCH 13/31] Fix unprefixed i18n identifiers in management plugin
(#8408)
* Fix unprefixed i18n identifiers in management plugin
Signed-off-by: Miki
* Changeset file for PR #8408 created/updated
---------
Signed-off-by: Miki
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8408.yml | 2 ++
src/plugins/management/public/components/settings_icon.tsx | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 changelogs/fragments/8408.yml
diff --git a/changelogs/fragments/8408.yml b/changelogs/fragments/8408.yml
new file mode 100644
index 000000000000..3fac7577a5db
--- /dev/null
+++ b/changelogs/fragments/8408.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix unprefixed i18n identifiers in management plugin ([#8408](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8408))
\ No newline at end of file
diff --git a/src/plugins/management/public/components/settings_icon.tsx b/src/plugins/management/public/components/settings_icon.tsx
index aef9bc28b52a..ecae0394d963 100644
--- a/src/plugins/management/public/components/settings_icon.tsx
+++ b/src/plugins/management/public/components/settings_icon.tsx
@@ -53,7 +53,7 @@ export function SettingsIcon({ core }: { core: CoreStart }) {
id="popoverForSettingsIcon"
button={
From fd31398a41de3f801316a12c9bd1629eea314dde Mon Sep 17 00:00:00 2001
From: Joshua Li
Date: Tue, 1 Oct 2024 21:54:32 -0700
Subject: [PATCH 14/31] [Discover] Support data connections and multi-select
table in dataset picker (#8255)
* initial implementation of cloud watch data set type
Signed-off-by: Joshua Li
* wip-- support selecting multiple datasets
Signed-off-by: Joshua Li
* Revert "wip-- support selecting multiple datasets"
This reverts commit 4977cc96573bee9c5bff30601bf1f69bd74d2e0c.
Signed-off-by: Joshua Li
* add multi-select dataset table
Signed-off-by: Joshua Li
* add token based pagination in dataset table
Signed-off-by: Joshua Li
* display data connection in dataset title
Signed-off-by: Joshua Li
* avoid overlap of dataset title and language selector
Signed-off-by: Joshua Li
* Revert "avoid overlap of dataset title and language selector"
This reverts commit 87d599db892ccbe501322e231da81c67bd6a5fa0.
Wait for https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8204
Signed-off-by: Joshua Li
* revert query enhancement changes for cloudwatch
Signed-off-by: Joshua Li
* Changeset file for PR #8255 created/updated
* Revert "revert query enhancement changes for cloudwatch"
This reverts commit 5eb065a5623935e40a3da93e748355b0f1144f50.
Signed-off-by: Joshua Li
* address comments
Signed-off-by: Joshua Li
* revert query enhancement changes for cloudwatch
Signed-off-by: Joshua Li
* address comments
Signed-off-by: Joshua Li
---------
Signed-off-by: Joshua Li
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8255.yml | 2 +
src/plugins/data/common/datasets/types.ts | 2 +
.../dataset_service/dataset_service.ts | 16 ++-
.../query_string/dataset_service/types.ts | 21 ++-
.../data/public/query/query_string/index.ts | 7 +-
.../dataset_selector/_dataset_explorer.scss | 12 ++
.../public/ui/dataset_selector/_index.scss | 5 +
.../ui/dataset_selector/dataset_explorer.tsx | 101 ++++++++------
.../dataset_selector/dataset_table.test.tsx | 128 ++++++++++++++++++
.../ui/dataset_selector/dataset_table.tsx | 101 ++++++++++++++
.../data_source/opensearch_dashboards.json | 2 +-
11 files changed, 348 insertions(+), 49 deletions(-)
create mode 100644 changelogs/fragments/8255.yml
create mode 100644 src/plugins/data/public/ui/dataset_selector/dataset_table.test.tsx
create mode 100644 src/plugins/data/public/ui/dataset_selector/dataset_table.tsx
diff --git a/changelogs/fragments/8255.yml b/changelogs/fragments/8255.yml
new file mode 100644
index 000000000000..3f2d5361e70e
--- /dev/null
+++ b/changelogs/fragments/8255.yml
@@ -0,0 +1,2 @@
+feat:
+- Support data connections and multi-select table in dataset picker ([#8255](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8255))
\ No newline at end of file
diff --git a/src/plugins/data/common/datasets/types.ts b/src/plugins/data/common/datasets/types.ts
index b9dbcdc3173b..58a5dc65932d 100644
--- a/src/plugins/data/common/datasets/types.ts
+++ b/src/plugins/data/common/datasets/types.ts
@@ -120,6 +120,8 @@ export interface DataStructure {
/** Optional array of child data structures */
children?: DataStructure[];
hasNext?: boolean;
+ paginationToken?: string;
+ multiSelect?: boolean;
columnHeader?: string;
/** Optional metadata for the data structure */
meta?: DataStructureMeta;
diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts
index 36faead38308..5f10b3d67193 100644
--- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts
+++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts
@@ -14,7 +14,7 @@ import {
DataStorage,
CachedDataStructure,
} from '../../../../common';
-import { DatasetTypeConfig } from './types';
+import { DatasetTypeConfig, DataStructureFetchOptions } from './types';
import { indexPatternTypeConfig, indexTypeConfig } from './lib';
import { IndexPatternsContract } from '../../../index_patterns';
import { IDataPluginServices } from '../../../types';
@@ -91,7 +91,8 @@ export class DatasetService {
public async fetchOptions(
services: IDataPluginServices,
path: DataStructure[],
- dataType: string
+ dataType: string,
+ options?: DataStructureFetchOptions
): Promise {
const type = this.typesRegistry.get(dataType);
if (!type) {
@@ -99,14 +100,19 @@ export class DatasetService {
}
const lastPathItem = path[path.length - 1];
- const cacheKey = `${dataType}.${lastPathItem.id}`;
+ const fetchOptionsKey = Object.entries(options || {})
+ .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
+ .map(([key, value]) => `${key}=${value}`)
+ .join('&');
+ const cacheKey =
+ `${dataType}.${lastPathItem.id}` + (fetchOptionsKey.length ? `?${fetchOptionsKey}` : '');
const cachedDataStructure = this.sessionStorage.get(cacheKey);
if (cachedDataStructure?.children?.length > 0) {
return this.cacheToDataStructure(dataType, cachedDataStructure);
}
- const fetchedDataStructure = await type.fetch(services, path);
+ const fetchedDataStructure = await type.fetch(services, path, options);
this.cacheDataStructure(dataType, fetchedDataStructure);
return fetchedDataStructure;
}
@@ -146,6 +152,8 @@ export class DatasetService {
parent: dataStructure.parent?.id || '',
children: dataStructure.children?.map((child) => child.id) || [],
hasNext: dataStructure.hasNext,
+ paginationToken: dataStructure.paginationToken,
+ multiSelect: dataStructure.multiSelect,
columnHeader: dataStructure.columnHeader,
meta: dataStructure.meta,
};
diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts
index c68aed8b6459..36a6cc3eb598 100644
--- a/src/plugins/data/public/query/query_string/dataset_service/types.ts
+++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts
@@ -6,6 +6,16 @@ import { EuiIconProps } from '@elastic/eui';
import { Dataset, DatasetField, DatasetSearchOptions, DataStructure } from '../../../../common';
import { IDataPluginServices } from '../../../types';
+/**
+ * Options for fetching the data structure.
+ */
+export interface DataStructureFetchOptions {
+ /** Search string to filter results */
+ search?: string;
+ /** Token for paginated results */
+ paginationToken?: string;
+}
+
/**
* Configuration for handling dataset operations.
*/
@@ -28,12 +38,17 @@ export interface DatasetTypeConfig {
*/
toDataset: (path: DataStructure[]) => Dataset;
/**
- * Fetches child options for a given DataStructure.
+ * Fetches child data structures and populates corresponding properties for a given DataStructure.
* @param {IDataPluginServices} services - The data plugin services.
* @param {DataStructure} dataStructure - The parent DataStructure.
- * @returns {Promise} A promise that resolves to a DatasetHandlerFetchResponse.
+ * @param {DataStructureFetchOptions} options - The fetch options for pagination and search.
+ * @returns {Promise} A promise that resolves to the updated DataStructure.
*/
- fetch: (services: IDataPluginServices, path: DataStructure[]) => Promise;
+ fetch: (
+ services: IDataPluginServices,
+ path: DataStructure[],
+ options?: DataStructureFetchOptions
+ ) => Promise;
/**
* Fetches fields for the dataset.
* @returns {Promise} A promise that resolves to an array of DatasetFields.
diff --git a/src/plugins/data/public/query/query_string/index.ts b/src/plugins/data/public/query/query_string/index.ts
index a004103e971b..0a1ad963ef9e 100644
--- a/src/plugins/data/public/query/query_string/index.ts
+++ b/src/plugins/data/public/query/query_string/index.ts
@@ -29,7 +29,12 @@
*/
export { QueryStringContract, QueryStringManager } from './query_string_manager';
-export { DatasetServiceContract, DatasetService, DatasetTypeConfig } from './dataset_service';
+export {
+ DataStructureFetchOptions,
+ DatasetService,
+ DatasetServiceContract,
+ DatasetTypeConfig,
+} from './dataset_service';
export {
LanguageServiceContract,
LanguageService,
diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss
index 704a49f010a6..5908e9cc8f33 100644
--- a/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss
+++ b/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss
@@ -1,3 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
.datasetExplorer {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 240px)) minmax(300px, 1fr);
@@ -26,4 +31,11 @@
padding: $euiSizeS;
border-bottom: $euiBorderThin;
}
+
+ .datasetTable {
+ &__loadMore {
+ text-align: center;
+ padding: $euiSizeS;
+ }
+ }
}
diff --git a/src/plugins/data/public/ui/dataset_selector/_index.scss b/src/plugins/data/public/ui/dataset_selector/_index.scss
index a16a39a501c7..cdbb0bee469e 100644
--- a/src/plugins/data/public/ui/dataset_selector/_index.scss
+++ b/src/plugins/data/public/ui/dataset_selector/_index.scss
@@ -1,3 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
@import "./dataset_explorer";
@import "./dataset_selector";
@import "./dataset_configurator";
diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
index dbcf7fe5acfc..9d430b2bb312 100644
--- a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
+++ b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
@@ -23,8 +23,9 @@ import {
import { FormattedMessage } from '@osd/i18n/react';
import moment from 'moment';
import { BaseDataset, DATA_STRUCTURE_META_TYPES, DataStructure } from '../../../common';
-import { QueryStringContract } from '../../query';
+import { DataStructureFetchOptions, QueryStringContract } from '../../query';
import { IDataPluginServices } from '../../types';
+import { DatasetTable } from './dataset_table';
export const DatasetExplorer = ({
services,
@@ -44,12 +45,23 @@ export const DatasetExplorer = ({
const uiSettings = services.uiSettings;
const [explorerDataset, setExplorerDataset] = useState(undefined);
const [loading, setLoading] = useState(false);
+ const datasetService = queryString.getDatasetService();
- const selectDataStructure = async (item: DataStructure, newPath: DataStructure[]) => {
+ const fetchNextDataStructure = async (
+ nextPath: DataStructure[],
+ dataType: string,
+ options?: DataStructureFetchOptions
+ ) => datasetService.fetchOptions(services, nextPath, dataType, options);
+
+ const selectDataStructure = async (item: DataStructure | undefined, newPath: DataStructure[]) => {
+ if (!item) {
+ setExplorerDataset(undefined);
+ return;
+ }
const lastPathItem = newPath[newPath.length - 1];
const nextPath = [...newPath, item];
- const typeConfig = queryString.getDatasetService().getType(nextPath[1].id);
+ const typeConfig = datasetService.getType(nextPath[1].id);
if (!typeConfig) return;
if (!lastPathItem.hasNext) {
@@ -59,9 +71,7 @@ export const DatasetExplorer = ({
}
setLoading(true);
- const nextDataStructure = await queryString
- .getDatasetService()
- .fetchOptions(services, nextPath, typeConfig.id);
+ const nextDataStructure = await fetchNextDataStructure(nextPath, typeConfig.id);
setLoading(false);
setPath([...newPath, nextDataStructure]);
@@ -161,41 +171,52 @@ export const DatasetExplorer = ({
{current.columnHeader}
- ({
- label: child.parent ? `${child.parent.title}::${child.title}` : child.title,
- value: child.id,
- prepend: child.meta?.type === DATA_STRUCTURE_META_TYPES.TYPE &&
- child.meta?.icon && ,
- append: appendIcon(child),
- checked: isChecked(child, index, path, explorerDataset),
- }))}
- onChange={(options) => {
- const selected = options.find((option) => option.checked);
- if (selected) {
- const item = current.children?.find((child) => child.id === selected.value);
- if (item) {
- selectDataStructure(item, path.slice(0, index + 1));
+ {current.multiSelect ? (
+
+ ) : (
+ ({
+ label: child.parent ? `${child.parent.title}::${child.title}` : child.title,
+ value: child.id,
+ prepend: child.meta?.type === DATA_STRUCTURE_META_TYPES.TYPE &&
+ child.meta?.icon && ,
+ append: appendIcon(child),
+ checked: isChecked(child, index, path, explorerDataset),
+ }))}
+ onChange={(options) => {
+ const selected = options.find((option) => option.checked);
+ if (selected) {
+ const item = current.children?.find((child) => child.id === selected.value);
+ if (item) {
+ selectDataStructure(item, path.slice(0, index + 1));
+ }
}
- }
- }}
- singleSelection
- {...(isFinal && {
- searchProps: {
- compressed: true,
- },
- searchable: true,
- })}
- height="full"
- className="datasetExplorer__selectable"
- >
- {(list, search) => (
- <>
- {isFinal && search}
- {list}
- >
- )}
-
+ }}
+ singleSelection
+ {...(isFinal && {
+ searchProps: {
+ compressed: true,
+ },
+ searchable: true,
+ })}
+ height="full"
+ className="datasetExplorer__selectable"
+ >
+ {(list, search) => (
+ <>
+ {isFinal && search}
+ {list}
+ >
+ )}
+
+ )}
);
})}
diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_table.test.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_table.test.tsx
new file mode 100644
index 000000000000..164a665ba33a
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_selector/dataset_table.test.tsx
@@ -0,0 +1,128 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import React, { ComponentProps } from 'react';
+import { IntlProvider } from 'react-intl';
+import { DataStructure } from '../../../common';
+import { queryServiceMock } from '../../query/mocks';
+import { getQueryService } from '../../services';
+import { DatasetTable } from './dataset_table';
+
+jest.mock('../../services', () => ({
+ getQueryService: jest.fn(),
+}));
+
+describe('DataSetTable', () => {
+ const mockQueryService = queryServiceMock.createSetupContract();
+ const mockedTypeId = mockQueryService.queryString.getDatasetService().getType('test-type')?.id;
+
+ const mockPath: DataStructure[] = [
+ { id: 'root', title: 'Root', type: 'root' },
+ { id: 'type1', title: 'Type 1', type: 'indexes' },
+ {
+ id: 'parent',
+ title: 'Parent',
+ type: 'cluster',
+ children: [
+ { id: 'child1', title: 'Child 1', type: 'index' },
+ { id: 'child2', title: 'Child 2', type: 'index' },
+ ],
+ paginationToken: 'token',
+ multiSelect: true,
+ },
+ ];
+
+ const mockProps: ComponentProps = {
+ path: mockPath,
+ setPath: jest.fn(),
+ index: 2,
+ explorerDataset: undefined,
+ selectDataStructure: jest.fn(),
+ fetchNextDataStructure: jest.fn().mockResolvedValue([]),
+ };
+
+ const renderWithIntl = (component: React.ReactNode) =>
+ render({component});
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (getQueryService as jest.Mock).mockReturnValue(mockQueryService);
+ });
+
+ it('renders the DataSetTable component', () => {
+ renderWithIntl();
+
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
+ expect(screen.getByText('Child 2')).toBeInTheDocument();
+ expect(screen.getByText('Load more')).toBeInTheDocument();
+ });
+
+ it('calls selectDataStructure when an index is selected', async () => {
+ renderWithIntl();
+
+ const checkbox = screen.getByTestId('checkboxSelectRow-child1');
+ fireEvent.click(checkbox);
+
+ await waitFor(() => {
+ expect(mockProps.selectDataStructure).toHaveBeenCalledWith(
+ {
+ id: 'child1',
+ title: 'Child 1',
+ type: 'index',
+ },
+ mockPath.slice(0, mockPath.length)
+ );
+ });
+ });
+
+ it('calls selectDataStructure with undefined when all items are deselected', async () => {
+ const propsWithSelection = {
+ ...mockProps,
+ explorerDataset: { id: 'child1,child2', title: 'Child 1,Child 2', type: 'index' },
+ };
+ renderWithIntl();
+
+ const checkbox1 = screen.getByTestId('checkboxSelectRow-child1');
+ const checkbox2 = screen.getByTestId('checkboxSelectRow-child2');
+
+ fireEvent.click(checkbox1);
+ fireEvent.click(checkbox2);
+
+ await waitFor(() => {
+ expect(mockProps.selectDataStructure).toHaveBeenCalledWith(undefined, mockPath.slice(0, 3));
+ });
+ });
+
+ it('calls onTableChange when search is performed', async () => {
+ renderWithIntl();
+ const searchInput = screen.getByRole('searchbox');
+ fireEvent.change(searchInput, { target: { value: 'test' } });
+ fireEvent.keyDown(searchInput, { key: 'Enter', code: 'Enter' });
+
+ expect(mockedTypeId).toBeDefined();
+
+ await waitFor(() => {
+ expect(mockProps.fetchNextDataStructure).toHaveBeenCalledWith(
+ mockPath,
+ mockedTypeId,
+ expect.objectContaining({ search: 'test' })
+ );
+ });
+ });
+
+ it('calls onTableChange when Load more is clicked', async () => {
+ renderWithIntl();
+ fireEvent.click(screen.getByText('Load more'));
+
+ await waitFor(() => {
+ expect(mockProps.fetchNextDataStructure).toHaveBeenCalledWith(
+ mockPath,
+ mockedTypeId,
+ expect.objectContaining({ paginationToken: 'token' })
+ );
+ });
+ });
+});
diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_table.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_table.tsx
new file mode 100644
index 000000000000..148f25691987
--- /dev/null
+++ b/src/plugins/data/public/ui/dataset_selector/dataset_table.tsx
@@ -0,0 +1,101 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { EuiBasicTable, EuiFieldSearch, EuiLink, EuiText } from '@elastic/eui';
+import React, { useRef, useState } from 'react';
+import { FormattedMessage } from '@osd/i18n/react';
+import { DataStructure } from '../../../common';
+import { getQueryService } from '../../services';
+import { DataStructureFetchOptions } from '../../query';
+
+interface DatasetTableProps {
+ path: DataStructure[];
+ setPath: (newPath: DataStructure[]) => void;
+ index: number;
+ explorerDataset: DataStructure | undefined;
+ selectDataStructure: (item: DataStructure | undefined, newPath: DataStructure[]) => Promise;
+ fetchNextDataStructure: (
+ nextPath: DataStructure[],
+ dataType: string,
+ options?: DataStructureFetchOptions
+ ) => Promise;
+}
+
+export const DatasetTable: React.FC = (props) => {
+ const datasetService = getQueryService().queryString.getDatasetService();
+ const [loading, setLoading] = useState(false);
+ const searchRef = useRef(null);
+
+ const initialSelectedIds = props.explorerDataset?.id.split(',');
+ const dataStructures = props.path[props.index].children || [];
+ const paginationToken = props.path[props.index].paginationToken;
+
+ const onTableChange = async (options: DataStructureFetchOptions) => {
+ const typeConfig = datasetService.getType(props.path[1].id);
+ if (!typeConfig) return;
+
+ setLoading(true);
+ await props
+ .fetchNextDataStructure(props.path, typeConfig.id, options)
+ .then((newDataStructure) => {
+ props.setPath([...props.path.slice(0, props.index), newDataStructure]);
+ })
+ .finally(() => setLoading(false));
+ };
+
+ return (
+
+
(searchRef.current = node)}
+ onSearch={(value) => onTableChange({ search: value })}
+ />
+
+ {
+ if (items.length === 0) {
+ props.selectDataStructure(undefined, props.path.slice(0, props.index + 1));
+ return;
+ }
+ if (!items.every((item) => item.type === items[0].type)) {
+ throw new Error('All items must be of the same type');
+ }
+ const newItem: DataStructure = {
+ id: items.map((item) => item.id).join(','),
+ title: items.map((item) => item.title).join(','),
+ type: items[0].type,
+ };
+ props.selectDataStructure(newItem, props.path.slice(0, props.index + 1));
+ },
+ initialSelected:
+ (initialSelectedIds?.length &&
+ dataStructures?.filter((item) => initialSelectedIds.includes(item.id))) ||
+ [],
+ }}
+ />
+
+ {paginationToken && (
+
+ onTableChange({ paginationToken, search: searchRef.current?.value })}
+ >
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/plugins/data_source/opensearch_dashboards.json b/src/plugins/data_source/opensearch_dashboards.json
index b67bf5ff429c..e22a99ac3eca 100644
--- a/src/plugins/data_source/opensearch_dashboards.json
+++ b/src/plugins/data_source/opensearch_dashboards.json
@@ -6,5 +6,5 @@
"ui": true,
"requiredPlugins": ["opensearchDashboardsUtils"],
"optionalPlugins": [],
- "extraPublicDirs": ["common","common/data_sources", "common/util"]
+ "extraPublicDirs": ["common","common/data_sources", "common/data_connections", "common/util"]
}
From 26dca198cbe8328a6fdc0122e81f9d47e4b1378c Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Wed, 2 Oct 2024 13:00:29 +0800
Subject: [PATCH 15/31] [Workspace] Remove whats new in workspace overview page
(#8435)
* remove whats new
Signed-off-by: Hailong Cui
* Changeset file for PR #8435 created/updated
---------
Signed-off-by: Hailong Cui
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8435.yml | 2 ++
.../components/home_list_card.test.tsx | 34 +++----------------
.../application/components/home_list_card.tsx | 18 ++--------
.../public/application/home_render.test.tsx | 12 ++-----
.../home/public/application/home_render.tsx | 16 ++-------
5 files changed, 13 insertions(+), 69 deletions(-)
create mode 100644 changelogs/fragments/8435.yml
diff --git a/changelogs/fragments/8435.yml b/changelogs/fragments/8435.yml
new file mode 100644
index 000000000000..959d3e9b9cf2
--- /dev/null
+++ b/changelogs/fragments/8435.yml
@@ -0,0 +1,2 @@
+fix:
+- [Workspace]Remove what's new card in workspace overview page ([#8435](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8435))
\ No newline at end of file
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
index 1492d82dc544..161bd66a8324 100644
--- a/src/plugins/home/public/application/components/home_list_card.test.tsx
+++ b/src/plugins/home/public/application/components/home_list_card.test.tsx
@@ -75,21 +75,9 @@ describe('Register HomeListCardToPages', () => {
it('register to use case overview page', () => {
registerHomeListCardToPage(contentManagementStartMock, docLinkMock);
- expect(contentManagementStartMock.registerContentProvider).toHaveBeenCalledTimes(4);
+ expect(contentManagementStartMock.registerContentProvider).toHaveBeenCalledTimes(2);
- let whatsNewCall = registerContentProviderFn.mock.calls[0];
- expect(whatsNewCall[0].getTargetArea()).toEqual('essentials_overview/service_cards');
- expect(whatsNewCall[0].getContent()).toMatchInlineSnapshot(`
- Object {
- "id": "whats_new",
- "kind": "custom",
- "order": 10,
- "render": [Function],
- "width": 24,
- }
- `);
-
- let learnOpenSearchCall = registerContentProviderFn.mock.calls[1];
+ let learnOpenSearchCall = registerContentProviderFn.mock.calls[0];
expect(learnOpenSearchCall[0].getTargetArea()).toEqual('essentials_overview/service_cards');
expect(learnOpenSearchCall[0].getContent()).toMatchInlineSnapshot(`
Object {
@@ -97,23 +85,11 @@ describe('Register HomeListCardToPages', () => {
"kind": "custom",
"order": 20,
"render": [Function],
- "width": 24,
- }
- `);
-
- whatsNewCall = registerContentProviderFn.mock.calls[2];
- expect(whatsNewCall[0].getTargetArea()).toEqual('all_overview/service_cards');
- expect(whatsNewCall[0].getContent()).toMatchInlineSnapshot(`
- Object {
- "id": "whats_new",
- "kind": "custom",
- "order": 30,
- "render": [Function],
- "width": undefined,
+ "width": 48,
}
`);
- learnOpenSearchCall = registerContentProviderFn.mock.calls[3];
+ learnOpenSearchCall = registerContentProviderFn.mock.calls[1];
expect(learnOpenSearchCall[0].getTargetArea()).toEqual('all_overview/service_cards');
expect(learnOpenSearchCall[0].getContent()).toMatchInlineSnapshot(`
Object {
@@ -121,7 +97,7 @@ describe('Register HomeListCardToPages', () => {
"kind": "custom",
"order": 40,
"render": [Function],
- "width": undefined,
+ "width": 16,
}
`);
});
diff --git a/src/plugins/home/public/application/components/home_list_card.tsx b/src/plugins/home/public/application/components/home_list_card.tsx
index 745291dae6e7..7c7fbb3c725b 100644
--- a/src/plugins/home/public/application/components/home_list_card.tsx
+++ b/src/plugins/home/public/application/components/home_list_card.tsx
@@ -165,27 +165,12 @@ export const registerHomeListCardToPage = (
contentManagement: ContentManagementPluginStart,
docLinks: DocLinksStart
) => {
- registerHomeListCard(contentManagement, {
- id: 'whats_new',
- order: 10,
- config: getWhatsNewConfig(docLinks),
- target: ESSENTIAL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS,
- width: 24,
- });
-
registerHomeListCard(contentManagement, {
id: 'learn_opensearch_new',
order: 20,
config: getLearnOpenSearchConfig(docLinks),
target: ESSENTIAL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS,
- width: 24,
- });
-
- registerHomeListCard(contentManagement, {
- id: 'whats_new',
- order: 30,
- config: getWhatsNewConfig(docLinks),
- target: ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS,
+ width: 48,
});
registerHomeListCard(contentManagement, {
@@ -193,5 +178,6 @@ export const registerHomeListCardToPage = (
order: 40,
config: getLearnOpenSearchConfig(docLinks),
target: ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS,
+ width: 16,
});
};
diff --git a/src/plugins/home/public/application/home_render.test.tsx b/src/plugins/home/public/application/home_render.test.tsx
index 63cd57b3915d..8f3037d99097 100644
--- a/src/plugins/home/public/application/home_render.test.tsx
+++ b/src/plugins/home/public/application/home_render.test.tsx
@@ -130,22 +130,14 @@ describe('initHome', () => {
};
initHome(contentManagementStartMock, core);
- expect(registerHomeListCard).toHaveBeenCalledTimes(2);
-
- expect(registerHomeListCard).toHaveBeenCalledWith(contentManagementStartMock, {
- id: 'whats_new',
- order: 10,
- config: getWhatsNewConfig(core.docLinks),
- target: HOME_CONTENT_AREAS.SERVICE_CARDS,
- width: 16,
- });
+ expect(registerHomeListCard).toHaveBeenCalledTimes(1);
expect(registerHomeListCard).toHaveBeenCalledWith(contentManagementStartMock, {
id: 'learn_opensearch_new',
order: 11,
config: getLearnOpenSearchConfig(core.docLinks),
target: HOME_CONTENT_AREAS.SERVICE_CARDS,
- width: 16,
+ width: 48,
});
});
});
diff --git a/src/plugins/home/public/application/home_render.tsx b/src/plugins/home/public/application/home_render.tsx
index d4d247ec1652..5f714a719481 100644
--- a/src/plugins/home/public/application/home_render.tsx
+++ b/src/plugins/home/public/application/home_render.tsx
@@ -16,11 +16,7 @@ import {
OBSERVABILITY_OVERVIEW_PAGE_ID,
SECURITY_ANALYTICS_OVERVIEW_PAGE_ID,
} from '../../../../plugins/content_management/public';
-import {
- getWhatsNewConfig,
- getLearnOpenSearchConfig,
- registerHomeListCard,
-} from './components/home_list_card';
+import { getLearnOpenSearchConfig, registerHomeListCard } from './components/home_list_card';
import { registerUseCaseCard } from './components/use_case_card';
@@ -90,19 +86,11 @@ export const initHome = (contentManagement: ContentManagementPluginStart, core:
});
}
- registerHomeListCard(contentManagement, {
- id: 'whats_new',
- order: 10,
- config: getWhatsNewConfig(core.docLinks),
- target: HOME_CONTENT_AREAS.SERVICE_CARDS,
- width: 16,
- });
-
registerHomeListCard(contentManagement, {
id: 'learn_opensearch_new',
order: 11,
config: getLearnOpenSearchConfig(core.docLinks),
target: HOME_CONTENT_AREAS.SERVICE_CARDS,
- width: 16,
+ width: workspaceEnabled ? 32 : 48,
});
};
From 7d8d840b268cce58862a88a4ba98f3a1952f1806 Mon Sep 17 00:00:00 2001
From: Sumukh Swamy
Date: Wed, 2 Oct 2024 00:08:27 -0700
Subject: [PATCH 16/31] [BUG] fixed wrapping of date (#8247)
* fixed wrapping of date
Signed-off-by: sumukhswamy
* addressed comments
Signed-off-by: sumukhswamy
* addressed comments for all time-fields
Signed-off-by: sumukhswamy
---------
Signed-off-by: sumukhswamy
---
.../default_discover_table/table_row.tsx | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/src/plugins/discover/public/application/components/default_discover_table/table_row.tsx b/src/plugins/discover/public/application/components/default_discover_table/table_row.tsx
index beb3a0656f19..a326674789d6 100644
--- a/src/plugins/discover/public/application/components/default_discover_table/table_row.tsx
+++ b/src/plugins/discover/public/application/components/default_discover_table/table_row.tsx
@@ -9,16 +9,16 @@
* GitHub history for details.
*/
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSmallButtonIcon } from '@elastic/eui';
import { i18n } from '@osd/i18n';
-import React, { useState, useCallback } from 'react';
-import { EuiSmallButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import dompurify from 'dompurify';
-import { TableCell } from './table_cell';
-import { DocViewerLinks } from '../doc_viewer_links/doc_viewer_links';
-import { DocViewer } from '../doc_viewer/doc_viewer';
-import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types';
+import React, { useCallback, useState } from 'react';
import { IndexPattern } from '../../../opensearch_dashboards_services';
+import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types';
import { fetchSourceTypeDataCell } from '../data_grid/data_grid_table_cell_value';
+import { DocViewer } from '../doc_viewer/doc_viewer';
+import { DocViewerLinks } from '../doc_viewer_links/doc_viewer_links';
+import { TableCell } from './table_cell';
export interface TableRowProps {
row: OpenSearchSearchHit;
@@ -111,7 +111,11 @@ const TableRowUI = ({
{/* eslint-disable-next-line react/no-danger */}
From 584205f3b562e0eaedd14808134a3627f0882d5b Mon Sep 17 00:00:00 2001
From: Miki
Date: Wed, 2 Oct 2024 00:21:57 -0700
Subject: [PATCH 17/31] Fix unprefixed and duplicate i18n identifiers in
visAugmenter plugin (#8409)
* Fix unprefixed and duplicate i18n identifiers in visAugmenter plugin
Signed-off-by: Miki
* Changeset file for PR #8409 created/updated
---------
Signed-off-by: Miki
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8409.yml | 2 ++
.../public/actions/plugin_resource_delete_action.ts | 2 +-
.../public/actions/saved_object_delete_action.ts | 5 +++--
.../actions/open_events_flyout_action.ts | 2 +-
.../actions/view_events_option_action.tsx | 2 +-
src/plugins/vis_augmenter/server/plugin.ts | 8 ++++----
6 files changed, 12 insertions(+), 9 deletions(-)
create mode 100644 changelogs/fragments/8409.yml
diff --git a/changelogs/fragments/8409.yml b/changelogs/fragments/8409.yml
new file mode 100644
index 000000000000..3cdf6d787a77
--- /dev/null
+++ b/changelogs/fragments/8409.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix unprefixed and duplicate i18n identifiers in visAugmenter plugin ([#8409](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8409))
\ No newline at end of file
diff --git a/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts b/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts
index 6e3939820d28..1f29eb996315 100644
--- a/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts
+++ b/src/plugins/vis_augmenter/public/actions/plugin_resource_delete_action.ts
@@ -23,7 +23,7 @@ export class PluginResourceDeleteAction implements Action
}
public getDisplayName() {
- return i18n.translate('dashboard.actions.deleteSavedObject.name', {
- defaultMessage: 'Clean up augment-vis saved objects associated to a deleted vis',
+ return i18n.translate('visAugmenter.actions.deleteSavedObject.name', {
+ defaultMessage:
+ 'Clean up all augment-vis saved objects associated to the deleted visualization',
});
}
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts
index cb47e5d6a85c..dfc8888165f0 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/open_events_flyout_action.ts
@@ -32,7 +32,7 @@ export class OpenEventsFlyoutAction implements Action {
}
public getDisplayName() {
- return i18n.translate('dashboard.actions.viewEvents.displayName', {
+ return i18n.translate('visAugmenter.actions.viewEvents.displayName', {
defaultMessage: 'View Events',
});
}
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx
index 6410e8a13634..ac7f795c586e 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx
@@ -37,7 +37,7 @@ export class ViewEventsOptionAction implements Action {
}
public getDisplayName() {
- return i18n.translate('dashboard.actions.viewEvents.displayName', {
+ return i18n.translate('visAugmenter.actions.viewEvents.displayName', {
defaultMessage: 'View Events',
});
}
diff --git a/src/plugins/vis_augmenter/server/plugin.ts b/src/plugins/vis_augmenter/server/plugin.ts
index e482265ee290..17d492fac247 100644
--- a/src/plugins/vis_augmenter/server/plugin.ts
+++ b/src/plugins/vis_augmenter/server/plugin.ts
@@ -53,23 +53,23 @@ export class VisAugmenterPlugin
if (isAugmentationEnabled) {
core.uiSettings.register({
[PLUGIN_AUGMENTATION_ENABLE_SETTING]: {
- name: i18n.translate('visualization.enablePluginAugmentationTitle', {
+ name: i18n.translate('visAugmenter.enablePluginAugmentationTitle', {
defaultMessage: 'Enable plugin augmentation',
}),
value: true,
- description: i18n.translate('visualization.enablePluginAugmentationText', {
+ description: i18n.translate('visAugmenter.enablePluginAugmentationText', {
defaultMessage: 'Plugin functionality can be accessed from line chart visualizations',
}),
category: ['visualization'],
schema: schema.boolean(),
},
[PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING]: {
- name: i18n.translate('visualization.enablePluginAugmentation.maxPluginObjectsTitle', {
+ name: i18n.translate('visAugmenter.enablePluginAugmentation.maxPluginObjectsTitle', {
defaultMessage: 'Max number of associated augmentations',
}),
value: 10,
description: i18n.translate(
- 'visualization.enablePluginAugmentation.maxPluginObjectsText',
+ 'visAugmenter.enablePluginAugmentation.maxPluginObjectsText',
{
defaultMessage:
'Associating more than 10 plugin resources per visualization can lead to performance ' +
From 7b5a3797a885943d5b7037fc58dd8c9fb978c7dd Mon Sep 17 00:00:00 2001
From: Lin Wang
Date: Wed, 2 Oct 2024 15:56:45 +0800
Subject: [PATCH 18/31] [Workspace]Refactor use case selector in workspace
creation page (#8413)
* Refactor use case selector in workspace create
Signed-off-by: Lin Wang
* Add gap for details panel title
Signed-off-by: Lin Wang
* Update use case name and description
Signed-off-by: Lin Wang
* Filter out hidden nav links
Signed-off-by: Lin Wang
* Changeset file for PR #8413 created/updated
* Add test case for different feature details
Signed-off-by: Lin Wang
* Change back to "Analytics"
Signed-off-by: Lin Wang
* Add all features suffix for all use case
Signed-off-by: Lin Wang
* Fix failed unit tests
Signed-off-by: Lin Wang
* Filter out getting started links
Signed-off-by: Lin Wang
* Renaming isFlyoutVisible to isUseCaseFlyoutVisible
Signed-off-by: Lin Wang
---------
Signed-off-by: Lin Wang
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8413.yml | 2 +
src/core/utils/default_nav_groups.ts | 14 +-
.../use_case_overview/setup_overview.test.tsx | 2 +-
.../creator_details_panel.tsx | 2 +
.../public/components/workspace_form/index.ts | 1 +
.../public/components/workspace_form/types.ts | 5 +-
.../workspace_use_case.test.tsx | 58 +++---
.../workspace_form/workspace_use_case.tsx | 189 +++++++++---------
.../workspace_use_case_flyout.test.tsx | 81 ++++++++
.../workspace_use_case_flyout.tsx | 128 ++++++++++++
.../default_workspace.test.tsx.snap | 2 +-
.../__snapshots__/index.test.tsx.snap | 2 +-
.../workspace_list/default_workspace.test.tsx | 2 +-
.../components/workspace_list/index.test.tsx | 2 +-
.../public/services/use_case_service.test.ts | 23 +++
.../public/services/use_case_service.ts | 35 ++--
src/plugins/workspace/public/types.ts | 1 +
src/plugins/workspace/public/utils.test.ts | 180 +++++++++++++++--
src/plugins/workspace/public/utils.ts | 70 +++++--
19 files changed, 612 insertions(+), 187 deletions(-)
create mode 100644 changelogs/fragments/8413.yml
create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx
create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx
diff --git a/changelogs/fragments/8413.yml b/changelogs/fragments/8413.yml
new file mode 100644
index 000000000000..b23b994010e4
--- /dev/null
+++ b/changelogs/fragments/8413.yml
@@ -0,0 +1,2 @@
+feat:
+- [Workspace]Refactor use case selector in workspace creation page ([#8413](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8413))
\ No newline at end of file
diff --git a/src/core/utils/default_nav_groups.ts b/src/core/utils/default_nav_groups.ts
index 76fedcf803ea..b58bd43e5b08 100644
--- a/src/core/utils/default_nav_groups.ts
+++ b/src/core/utils/default_nav_groups.ts
@@ -38,10 +38,11 @@ const defaultNavGroups = {
all: {
id: ALL_USE_CASE_ID,
title: i18n.translate('core.ui.group.all.title', {
- defaultMessage: 'Analytics (All)',
+ defaultMessage: 'Analytics',
}),
description: i18n.translate('core.ui.group.all.description', {
- defaultMessage: 'This is a use case contains all the features.',
+ defaultMessage:
+ 'If you aren’t sure where to start with OpenSearch, or if you have needs that cut across multiple use cases.',
}),
order: 3000,
icon: 'wsAnalytics',
@@ -52,7 +53,8 @@ const defaultNavGroups = {
defaultMessage: 'Observability',
}),
description: i18n.translate('core.ui.group.observability.description', {
- defaultMessage: 'Gain visibility into your applications and infrastructure.',
+ defaultMessage:
+ 'Gain visibility into system health, performance, and reliability through monitoring of logs, metrics and traces.',
}),
order: 4000,
icon: 'wsObservability',
@@ -63,7 +65,8 @@ const defaultNavGroups = {
defaultMessage: 'Security Analytics',
}),
description: i18n.translate('core.ui.group.security.analytics.description', {
- defaultMessage: 'Enhance your security posture with advanced analytics.',
+ defaultMessage:
+ 'Detect and investigate potential security threats and vulnerabilities across your systems and data.',
}),
order: 5000,
icon: 'wsSecurityAnalytics',
@@ -86,7 +89,8 @@ const defaultNavGroups = {
defaultMessage: 'Search',
}),
description: i18n.translate('core.ui.group.search.description', {
- defaultMessage: 'Discover and query your data with ease.',
+ defaultMessage:
+ "Quickly find and explore relevant information across your organization's data sources.",
}),
order: 6000,
icon: 'wsSearch',
diff --git a/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx b/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx
index a895534e47a9..ea0a12b930cf 100644
--- a/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx
+++ b/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx
@@ -141,7 +141,7 @@ describe('Setup use case overview', () => {
"titleElement": "h4",
"titleSize": "s",
},
- "description": "Gain visibility into your applications and infrastructure.",
+ "description": "Gain visibility into system health, performance, and reliability through monitoring of logs, metrics and traces.",
"getIcon": [Function],
"id": "observability",
"kind": "card",
diff --git a/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx b/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx
index 64519d9a1bde..f654ea327fd4 100644
--- a/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx
+++ b/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx
@@ -13,6 +13,7 @@ import {
EuiFormControlLayout,
EuiFormRow,
EuiPanel,
+ EuiSpacer,
EuiText,
} from '@elastic/eui';
import { EuiColorPickerOutput } from '@elastic/eui/src/components/color_picker/color_picker';
@@ -46,6 +47,7 @@ export const CreatorDetailsPanel = ({
})}
+
diff --git a/src/plugins/workspace/public/components/workspace_form/index.ts b/src/plugins/workspace/public/components/workspace_form/index.ts
index a58b2918a1b3..fef18d7c8c5e 100644
--- a/src/plugins/workspace/public/components/workspace_form/index.ts
+++ b/src/plugins/workspace/public/components/workspace_form/index.ts
@@ -12,6 +12,7 @@ export { WorkspaceCancelModal } from './workspace_cancel_modal';
export { WorkspaceNameField, WorkspaceDescriptionField } from './fields';
export { ConnectionTypeIcon } from './connection_type_icon';
export { DataSourceConnectionTable } from './data_source_connection_table';
+export { WorkspaceUseCaseFlyout } from './workspace_use_case_flyout';
export { WorkspaceFormSubmitData, WorkspaceFormProps, WorkspaceFormDataState } from './types';
export {
diff --git a/src/plugins/workspace/public/components/workspace_form/types.ts b/src/plugins/workspace/public/components/workspace_form/types.ts
index f160f18b4357..9c8da46e5be9 100644
--- a/src/plugins/workspace/public/components/workspace_form/types.ts
+++ b/src/plugins/workspace/public/components/workspace_form/types.ts
@@ -88,7 +88,10 @@ export interface WorkspaceFormProps {
}
export interface AvailableUseCaseItem
- extends Pick {
+ extends Pick<
+ WorkspaceUseCase,
+ 'id' | 'title' | 'features' | 'description' | 'systematic' | 'icon'
+ > {
disabled?: boolean;
}
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx
index 8a0b14782e91..85725c452051 100644
--- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx
@@ -4,7 +4,7 @@
*/
import React from 'react';
-import { fireEvent, render, waitFor } from '@testing-library/react';
+import { fireEvent, render, waitFor, within } from '@testing-library/react';
import { DEFAULT_NAV_GROUPS } from '../../../../../core/public';
import { WorkspaceUseCase, WorkspaceUseCaseProps } from './workspace_use_case';
import { WorkspaceFormErrors } from './types';
@@ -76,13 +76,13 @@ describe('WorkspaceUseCase', () => {
],
});
await waitFor(() => {
- expect(renderResult.getByText('Essentials')).toHaveClass(
+ expect(renderResult.getByText('Essentials').closest('label')).toHaveClass(
'euiCheckableCard__label-isDisabled'
);
});
});
- it('should be able to toggle use case features', async () => {
+ it('should open flyout and expanded selected use cases', async () => {
const { renderResult } = setup({
availableUseCases: [
{
@@ -93,58 +93,46 @@ describe('WorkspaceUseCase', () => {
],
},
],
+ value: DEFAULT_NAV_GROUPS.observability.id,
});
+
+ fireEvent.click(renderResult.getByText('Learn more.'));
+
await waitFor(() => {
- expect(renderResult.getByText('See more....')).toBeInTheDocument();
- expect(renderResult.queryByText('Feature 1')).toBe(null);
- expect(renderResult.queryByText('Feature 2')).toBe(null);
+ expect(within(renderResult.getByRole('dialog')).getByText('Use cases')).toBeInTheDocument();
+ expect(
+ within(renderResult.getByRole('dialog')).getByText('Observability')
+ ).toBeInTheDocument();
+ expect(within(renderResult.getByRole('dialog')).getByText('Feature 1')).toBeInTheDocument();
+ expect(within(renderResult.getByRole('dialog')).getByText('Feature 2')).toBeInTheDocument();
});
+ });
- fireEvent.click(renderResult.getByText('See more....'));
+ it('should close flyout after close button clicked', async () => {
+ const { renderResult } = setup({});
+ fireEvent.click(renderResult.getByText('Learn more.'));
await waitFor(() => {
- expect(renderResult.getByText('See less....')).toBeInTheDocument();
- expect(renderResult.getByText('Feature 1')).toBeInTheDocument();
- expect(renderResult.getByText('Feature 2')).toBeInTheDocument();
+ expect(within(renderResult.getByRole('dialog')).getByText('Use cases')).toBeInTheDocument();
});
- fireEvent.click(renderResult.getByText('See less....'));
+ fireEvent.click(renderResult.getByTestId('euiFlyoutCloseButton'));
await waitFor(() => {
- expect(renderResult.getByText('See more....')).toBeInTheDocument();
- expect(renderResult.queryByText('Feature 1')).toBe(null);
- expect(renderResult.queryByText('Feature 2')).toBe(null);
+ expect(renderResult.queryByText('dialog')).toBeNull();
});
});
- it('should show static all use case features', async () => {
+ it('should render "(all features)" suffix for "all use case"', () => {
const { renderResult } = setup({
availableUseCases: [
{
...DEFAULT_NAV_GROUPS.all,
- features: [
- { id: 'feature1', title: 'Feature 1' },
- { id: 'feature2', title: 'Feature 2' },
- ],
+ features: [],
},
],
});
- fireEvent.click(renderResult.getByText('See more....'));
-
- await waitFor(() => {
- expect(renderResult.getByText('Discover')).toBeInTheDocument();
- expect(renderResult.getByText('Dashboards')).toBeInTheDocument();
- expect(renderResult.getByText('Visualize')).toBeInTheDocument();
- expect(
- renderResult.getByText('Observability services, metrics, traces, and more')
- ).toBeInTheDocument();
- expect(
- renderResult.getByText('Security analytics threat alerts, findings, correlations, and more')
- ).toBeInTheDocument();
- expect(
- renderResult.getByText('Search studio, relevance tuning, vector search, and more')
- ).toBeInTheDocument();
- });
+ expect(renderResult.getByText('(all features)')).toBeInTheDocument();
});
});
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx
index 5eebdc8fa369..94defbdaefad 100644
--- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useCallback, useState, useMemo } from 'react';
+import React, { useCallback, useState } from 'react';
import { i18n } from '@osd/i18n';
import {
EuiCheckableCard,
@@ -12,14 +12,19 @@ import {
EuiCompressedFormRow,
EuiText,
EuiLink,
+ EuiPanel,
+ EuiIcon,
+ EuiSpacer,
} from '@elastic/eui';
+import { ALL_USE_CASE_ID } from '../../../../../core/public';
-import { ALL_USE_CASE_ID, DEFAULT_NAV_GROUPS } from '../../../../../core/public';
import { WorkspaceFormErrors, AvailableUseCaseItem } from './types';
+import { WorkspaceUseCaseFlyout } from './workspace_use_case_flyout';
import './workspace_use_case.scss';
interface WorkspaceUseCaseCardProps {
id: string;
+ icon?: string;
title: string;
checked: boolean;
disabled?: boolean;
@@ -30,92 +35,57 @@ interface WorkspaceUseCaseCardProps {
const WorkspaceUseCaseCard = ({
id,
+ icon,
title,
- features,
description,
checked,
disabled,
onChange,
}: WorkspaceUseCaseCardProps) => {
- const [isExpanded, setIsExpanded] = useState(false);
- const featureItems = useMemo(() => {
- if (id === DEFAULT_NAV_GROUPS.essentials.id) {
- return [];
- }
- if (id === ALL_USE_CASE_ID) {
- return [
- i18n.translate('workspace.form.useCase.feature.all.discover', {
- defaultMessage: 'Discover',
- }),
- i18n.translate('workspace.form.useCase.feature.all.dashboards', {
- defaultMessage: 'Dashboards',
- }),
- i18n.translate('workspace.form.useCase.feature.all.visualize', {
- defaultMessage: 'Visualize',
- }),
- i18n.translate('workspace.form.useCase.feature.all.observability', {
- defaultMessage: 'Observability services, metrics, traces, and more',
- }),
- i18n.translate('workspace.form.useCase.feature.all.securityAnalytics', {
- defaultMessage: 'Security analytics threat alerts, findings, correlations, and more',
- }),
- i18n.translate('workspace.form.useCase.feature.all.search', {
- defaultMessage: 'Search studio, relevance tuning, vector search, and more',
- }),
- ];
- }
-
- const featureTitles = features.flatMap((feature) => (feature.title ? [feature.title] : []));
- return featureTitles;
- }, [features, id]);
-
const handleChange = useCallback(() => {
onChange(id);
}, [id, onChange]);
- const toggleExpanded = useCallback(() => {
- setIsExpanded((flag) => !flag);
- }, []);
return (
+ {icon && (
+
+
+
+ )}
+
+
+
+ {title}
+ {id === ALL_USE_CASE_ID && (
+ <>
+
+
+
+ {i18n.translate('workspace.forms.useCaseCard.allUseCaseSuffix', {
+ defaultMessage: '(all features)',
+ })}
+
+
+ >
+ )}
+
+
+
+
+ }
checked={checked}
className="workspace-use-case-item"
onChange={handleChange}
data-test-subj={`workspaceUseCase-${id}`}
disabled={disabled}
+ style={{ width: '100%' }}
>
{description}
- {featureItems.length > 0 && (
-
- {isExpanded && (
- <>
- {i18n.translate('workspace.form.useCase.featureExpandedTitle', {
- defaultMessage: 'Feature includes:',
- })}
-
- {featureItems.map((feature, index) => (
- - {feature}
- ))}
-
- >
- )}
-
-
- {isExpanded
- ? i18n.translate('workspace.form.useCase.showLessButton', {
- defaultMessage: 'See less....',
- })
- : i18n.translate('workspace.form.useCase.showMoreButton', {
- defaultMessage: 'See more....',
- })}
-
-
-
- )}
);
};
@@ -133,29 +103,68 @@ export const WorkspaceUseCase = ({
formErrors,
availableUseCases,
}: WorkspaceUseCaseProps) => {
+ const [isUseCaseFlyoutVisible, setIsUseCaseFlyoutVisible] = useState(false);
+ const handleLearnMoreClick = useCallback(() => {
+ setIsUseCaseFlyoutVisible(true);
+ }, []);
+ const handleFlyoutClose = useCallback(() => {
+ setIsUseCaseFlyoutVisible(false);
+ }, []);
+
return (
-
-
- {availableUseCases.map(({ id, title, description, features, disabled }) => (
-
-
-
- ))}
-
-
+
+
+
+ {i18n.translate('workspace.form.panels.useCase.title', {
+ defaultMessage: 'Use case and features',
+ })}
+
+
+
+ {i18n.translate('workspace.form.panels.useCase.description', {
+ defaultMessage:
+ 'The use case defines the set of features that will be available in the workspace. You can change the use case later only to one with more features than the current use case.',
+ })}
+
+
+ {i18n.translate('workspace.form.panels.useCase.learnMore', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ {isUseCaseFlyoutVisible && (
+
+ )}
+
+
+
+
+ {availableUseCases.map(({ id, icon, title, description, features, disabled }) => (
+
+
+
+ ))}
+
+
+
);
};
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx
new file mode 100644
index 000000000000..2cc4efd8cf6d
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { DEFAULT_NAV_GROUPS } from '../../../../../core/public';
+
+import { WorkspaceUseCaseFlyout } from './workspace_use_case_flyout';
+
+const mockAvailableUseCases = [
+ {
+ id: 'use-case-1',
+ icon: 'logoElasticsearch',
+ title: 'Use Case 1',
+ description: 'This is the description for Use Case 1',
+ features: [
+ {
+ id: 'feature-1',
+ title: 'Feature 1',
+ details: ['Detail 1', 'Detail 2'],
+ },
+ {
+ id: 'feature-2',
+ title: 'Feature 2',
+ details: [],
+ },
+ ],
+ },
+ {
+ id: 'use-case-2',
+ icon: 'logoKibana',
+ title: 'Use Case 2',
+ description: 'This is the description for Use Case 2',
+ features: [],
+ },
+];
+
+describe('WorkspaceUseCaseFlyout', () => {
+ it('should render the flyout with the correct title and available use cases', () => {
+ render(
+
+ );
+ const title = screen.getByText('Use cases');
+ expect(title).toBeInTheDocument();
+ expect(screen.getByText(mockAvailableUseCases[0].title)).toBeInTheDocument();
+ expect(screen.getByText(mockAvailableUseCases[0].title)).toBeInTheDocument();
+ });
+
+ it('should call the onClose callback when the close button is clicked', () => {
+ const onCloseMock = jest.fn();
+ render(
+
+ );
+ const closeButton = screen.getByTestId('euiFlyoutCloseButton');
+ fireEvent.click(closeButton);
+ expect(onCloseMock).toHaveBeenCalled();
+ });
+
+ it('should expand the default use case if provided', () => {
+ render(
+
+ );
+ const useCaseDescription = screen.getByText(/This is the description for Use Case 1/);
+ expect(useCaseDescription).toBeInTheDocument();
+ });
+
+ it('should render "(all features)" suffix for "all use case"', () => {
+ render(
+
+ );
+ expect(screen.getByText('(all features)')).toBeInTheDocument();
+ });
+});
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx
new file mode 100644
index 000000000000..7617f3ec14ea
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx
@@ -0,0 +1,128 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { i18n } from '@osd/i18n';
+import {
+ EuiFlyout,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlyoutBody,
+ EuiAccordion,
+ EuiSpacer,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+} from '@elastic/eui';
+import { ALL_USE_CASE_ID } from '../../../../../core/public';
+
+import { AvailableUseCaseItem } from './types';
+
+const WORKSPACE_USE_CASE_FLYOUT_TITLE_ID = 'workspaceUseCaseFlyoutTitle';
+
+export interface WorkspaceUseCaseFlyoutProps {
+ onClose: () => void;
+ availableUseCases: AvailableUseCaseItem[];
+ defaultExpandUseCase?: string;
+}
+
+export const WorkspaceUseCaseFlyout = ({
+ onClose,
+ availableUseCases,
+ defaultExpandUseCase,
+}: WorkspaceUseCaseFlyoutProps) => {
+ return (
+
+
+
+
+ {i18n.translate('workspace.forms.useCaseFlyout.title', { defaultMessage: 'Use cases' })}
+
+
+
+
+ {availableUseCases.map(({ id, icon, title, description, features }, index) => (
+
+
+ {icon && (
+
+
+
+ )}
+
+
+
+ {title}
+ {id === ALL_USE_CASE_ID && (
+ <>
+
+
+
+ {i18n.translate('workspace.forms.useCaseFlyout.allUseCaseSuffix', {
+ defaultMessage: '(all features)',
+ })}
+
+
+ >
+ )}
+
+
+
+
+ }
+ paddingSize="l"
+ initialIsOpen={id === defaultExpandUseCase}
+ >
+
+ {description}
+
+ {features && features.length > 0 && (
+ <>
+ {i18n.translate('workspace.forms.useCaseFlyout.featuresIncluded', {
+ defaultMessage: 'Features included:',
+ })}
+
+
+ {features.map(({ id: featureId, title: featureTitle, details }) => (
+ -
+
+ {i18n.translate('workspace.forms.useCaseFlyout.featureTitle', {
+ defaultMessage:
+ '{featureTitle}{detailsCount, plural, =0 {} other {: }}',
+ values: { featureTitle, detailsCount: details?.length ?? 0 },
+ })}
+
+ {details?.join(
+ i18n.translate(
+ 'workspace.forms.useCaseFlyout.featuresDetails.delimiter',
+ {
+ defaultMessage: ', ',
+ }
+ )
+ )}
+
+ ))}
+
+ >
+ )}
+
+
+ {index < availableUseCases.length - 1 && }
+
+ ))}
+
+
+ );
+};
diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap
index 0bb3f51ead3d..57c41452231e 100644
--- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap
+++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap
@@ -248,7 +248,7 @@ exports[`UserDefaultWorkspace should render title and table normally 1`] = `
- Analytics (All)
+ Analytics
|
diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap
index b32655bafd98..b30c67b37643 100644
--- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap
+++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap
@@ -411,7 +411,7 @@ exports[`WorkspaceList should render title and table normally 1`] = `
- Analytics (All)
+ Analytics
diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx
index f6a50b1c1350..eeeec9e06fb0 100644
--- a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx
+++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx
@@ -137,7 +137,7 @@ describe('UserDefaultWorkspace', () => {
expect(getByText('name2')).toBeInTheDocument();
// should display use case
- expect(getByText('Analytics (All)')).toBeInTheDocument();
+ expect(getByText('Analytics')).toBeInTheDocument();
expect(getByText('Observability')).toBeInTheDocument();
// owner column not display
diff --git a/src/plugins/workspace/public/components/workspace_list/index.test.tsx b/src/plugins/workspace/public/components/workspace_list/index.test.tsx
index a945866fd7bf..62709e4fe053 100644
--- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx
+++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx
@@ -153,7 +153,7 @@ describe('WorkspaceList', () => {
expect(getByText('name2')).toBeInTheDocument();
// should display use case
- expect(getByText('Analytics (All)')).toBeInTheDocument();
+ expect(getByText('Analytics')).toBeInTheDocument();
expect(getByText('Observability')).toBeInTheDocument();
});
diff --git a/src/plugins/workspace/public/services/use_case_service.test.ts b/src/plugins/workspace/public/services/use_case_service.test.ts
index bdd819145f82..d810dc570bb3 100644
--- a/src/plugins/workspace/public/services/use_case_service.test.ts
+++ b/src/plugins/workspace/public/services/use_case_service.test.ts
@@ -47,12 +47,18 @@ const setupUseCaseStart = (options?: { navGroupEnabled?: boolean }) => {
]);
const navGroupsMap$ = new BehaviorSubject>(mockNavGroupsMap);
const useCase = new UseCaseService();
+ const navLinks$ = new BehaviorSubject([
+ { id: 'dashboards', title: 'Dashboards', baseUrl: '', href: '' },
+ { id: 'searchRelevance', title: 'Search Relevance', baseUrl: '', href: '' },
+ ]);
chrome.navGroup.getNavGroupEnabled.mockImplementation(() => options?.navGroupEnabled ?? true);
chrome.navGroup.getNavGroupsMap$.mockImplementation(() => navGroupsMap$);
+ chrome.navLinks.getNavLinks$.mockImplementation(() => navLinks$);
return {
chrome,
+ navLinks$,
navGroupsMap$,
workspaceConfigurableApps$,
useCaseStart: useCase.start({
@@ -187,6 +193,23 @@ describe('UseCaseService', () => {
});
expect(fn).toHaveBeenCalledTimes(2);
});
+
+ it('should not emit after navLinks$ emit same value', async () => {
+ const { useCaseStart, navLinks$ } = setupUseCaseStart();
+ const registeredUseCases$ = useCaseStart.getRegisteredUseCases$();
+ const fn = jest.fn();
+
+ registeredUseCases$.subscribe(fn);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ navLinks$.next([...navLinks$.getValue()]);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ navLinks$.next([...navLinks$.getValue()].slice(1));
+ expect(fn).toHaveBeenCalledTimes(2);
+ });
+
it('should move all use case to the last one', async () => {
const { useCaseStart, navGroupsMap$ } = setupUseCaseStart();
diff --git a/src/plugins/workspace/public/services/use_case_service.ts b/src/plugins/workspace/public/services/use_case_service.ts
index 31e15977d9a5..ca4f2b55c223 100644
--- a/src/plugins/workspace/public/services/use_case_service.ts
+++ b/src/plugins/workspace/public/services/use_case_service.ts
@@ -118,12 +118,13 @@ export class UseCaseService {
return {
getRegisteredUseCases$: () => {
if (chrome.navGroup.getNavGroupEnabled()) {
- return chrome.navGroup
- .getNavGroupsMap$()
+ return combineLatest([chrome.navGroup.getNavGroupsMap$(), chrome.navLinks.getNavLinks$()])
.pipe(
- map((navGroupsMap) =>
- Object.values(navGroupsMap).map(convertNavGroupToWorkspaceUseCase)
- )
+ map(([navGroupsMap, allNavLinks]) => {
+ return Object.values(navGroupsMap).map((navGroup) =>
+ convertNavGroupToWorkspaceUseCase(navGroup, allNavLinks)
+ );
+ })
)
.pipe(
distinctUntilChanged((useCases, anotherUseCases) => {
@@ -169,17 +170,23 @@ export class UseCaseService {
.filter((useCase) => {
return useCase.features.some((featureId) => configurableAppsId.includes(featureId));
})
- .map((item) => ({
- ...item,
- features: item.features.map((featureId) => ({
- title: configurableApps.find((app) => app.id === featureId)?.title,
- id: featureId,
- })),
- }))
+ .map(
+ (item) =>
+ ({
+ ...item,
+ features: item.features.map((featureId) => ({
+ title: configurableApps.find((app) => app.id === featureId)?.title,
+ id: featureId,
+ })),
+ } as WorkspaceUseCase)
+ )
.concat({
...DEFAULT_NAV_GROUPS.all,
- features: configurableApps.map((app) => ({ id: app.id, title: app.title })),
- }) as WorkspaceUseCase[];
+ features: configurableApps.map((app) => ({
+ id: app.id,
+ title: app.title,
+ })),
+ } as WorkspaceUseCase);
})
);
},
diff --git a/src/plugins/workspace/public/types.ts b/src/plugins/workspace/public/types.ts
index 53f2be4dfb36..997e8c67fd05 100644
--- a/src/plugins/workspace/public/types.ts
+++ b/src/plugins/workspace/public/types.ts
@@ -20,6 +20,7 @@ export type Services = CoreStart & {
export interface WorkspaceUseCaseFeature {
id: string;
title?: string;
+ details?: string[];
}
export interface WorkspaceUseCase {
diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts
index 9f79beafea12..a5f2a9c435e2 100644
--- a/src/plugins/workspace/public/utils.test.ts
+++ b/src/plugins/workspace/public/utils.test.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { AppNavLinkStatus, NavGroupType, PublicAppInfo } from '../../../core/public';
+import { AppNavLinkStatus, ChromeNavLink, NavGroupType, PublicAppInfo } from '../../../core/public';
import {
featureMatchesConfig,
filterWorkspaceConfigurableApps,
@@ -19,7 +19,7 @@ import {
} from './utils';
import { WorkspaceAvailability } from '../../../core/public';
import { coreMock } from '../../../core/public/mocks';
-import { WORKSPACE_DETAIL_APP_ID, USE_CASE_PREFIX } from '../common/constants';
+import { USE_CASE_PREFIX } from '../common/constants';
import {
SigV4ServiceName,
DataSourceEngineType,
@@ -37,10 +37,23 @@ const useCaseMock = {
id: 'foo',
title: 'Foo',
description: 'Foo description',
- features: [{ id: 'bar' }],
+ features: [{ id: 'bar' }, { id: 'baz', title: 'Baz', details: ['Qux'] }],
systematic: false,
order: 1,
};
+const allNavLinksMock: ChromeNavLink[] = [
+ { id: 'foo', title: 'Foo', baseUrl: '', href: '' },
+ { id: 'bar', title: 'Bar', baseUrl: '', href: '' },
+ { id: 'baz', title: 'Baz', baseUrl: '', href: '' },
+ { id: 'qux', title: 'Qux', baseUrl: '', href: '' },
+ { id: 'observability_overview', title: 'Observability Overview', baseUrl: '', href: '' },
+ {
+ id: 'observability-gettingStarted',
+ title: 'Observability Getting Started',
+ baseUrl: '',
+ href: '',
+ },
+];
describe('workspace utils: featureMatchesConfig', () => {
it('feature configured with `*` should match any features', () => {
@@ -530,13 +543,16 @@ describe('workspace utils: getIsOnlyAllowEssentialUseCase', () => {
describe('workspace utils: convertNavGroupToWorkspaceUseCase', () => {
it('should convert nav group to consistent workspace use case', () => {
expect(
- convertNavGroupToWorkspaceUseCase({
- id: 'foo',
- title: 'Foo',
- description: 'Foo description',
- navLinks: [{ id: 'bar', title: 'Bar' }],
- icon: 'wsAnalytics',
- })
+ convertNavGroupToWorkspaceUseCase(
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ navLinks: [{ id: 'bar', title: 'Bar' }],
+ icon: 'wsAnalytics',
+ },
+ allNavLinksMock
+ )
).toEqual({
id: 'foo',
title: 'Foo',
@@ -547,13 +563,16 @@ describe('workspace utils: convertNavGroupToWorkspaceUseCase', () => {
});
expect(
- convertNavGroupToWorkspaceUseCase({
- id: 'foo',
- title: 'Foo',
- description: 'Foo description',
- navLinks: [{ id: 'bar', title: 'Bar' }],
- type: NavGroupType.SYSTEM,
- })
+ convertNavGroupToWorkspaceUseCase(
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ navLinks: [{ id: 'bar', title: 'Bar' }],
+ type: NavGroupType.SYSTEM,
+ },
+ allNavLinksMock
+ )
).toEqual({
id: 'foo',
title: 'Foo',
@@ -562,6 +581,111 @@ describe('workspace utils: convertNavGroupToWorkspaceUseCase', () => {
systematic: true,
});
});
+
+ it('should filter out overview features', () => {
+ expect(
+ convertNavGroupToWorkspaceUseCase(
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ navLinks: [{ id: 'observability_overview', title: 'Observability Overview' }],
+ icon: 'wsAnalytics',
+ },
+ allNavLinksMock
+ )
+ ).toEqual(
+ expect.objectContaining({
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ features: [],
+ systematic: false,
+ icon: 'wsAnalytics',
+ })
+ );
+ });
+
+ it('should filter out getting started features', () => {
+ expect(
+ convertNavGroupToWorkspaceUseCase(
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ navLinks: [
+ { id: 'observability-gettingStarted', title: 'Observability Getting Started' },
+ ],
+ icon: 'wsAnalytics',
+ },
+ allNavLinksMock
+ )
+ ).toEqual(
+ expect.objectContaining({
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ features: [],
+ systematic: false,
+ icon: 'wsAnalytics',
+ })
+ );
+ });
+
+ it('should grouped nav links by category', () => {
+ expect(
+ convertNavGroupToWorkspaceUseCase(
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ navLinks: [
+ { id: 'bar', title: 'Bar', category: { id: 'category-1', label: 'Category 1' } },
+ { id: 'baz', title: 'Baz' },
+ { id: 'qux', title: 'Qux', category: { id: 'category-1', label: 'Category 1' } },
+ ],
+ icon: 'wsAnalytics',
+ },
+ allNavLinksMock
+ )
+ ).toEqual(
+ expect.objectContaining({
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ features: [
+ { id: 'baz', title: 'Baz' },
+ { id: 'category-1', title: 'Category 1', details: ['Bar', 'Qux'] },
+ ],
+ systematic: false,
+ icon: 'wsAnalytics',
+ })
+ );
+ });
+
+ it('should filter out custom features', () => {
+ expect(
+ convertNavGroupToWorkspaceUseCase(
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ navLinks: [{ id: 'bar', title: 'Bar', category: { id: 'custom', label: 'Custom' } }],
+ icon: 'wsAnalytics',
+ },
+ allNavLinksMock
+ )
+ ).toEqual(
+ expect.objectContaining({
+ id: 'foo',
+ title: 'Foo',
+ description: 'Foo description',
+ features: [],
+ systematic: false,
+ icon: 'wsAnalytics',
+ })
+ );
+ });
});
describe('workspace utils: isEqualWorkspaceUseCase', () => {
@@ -661,6 +785,28 @@ describe('workspace utils: isEqualWorkspaceUseCase', () => {
)
).toEqual(true);
});
+
+ it('should return false for different feature details', () => {
+ const featureWithDetails = {
+ id: 'foo',
+ title: 'Foo',
+ details: ['Bar'],
+ };
+ const featureWithOtherDetails = {
+ id: 'foo',
+ title: 'Foo',
+ details: ['Baz'],
+ };
+ expect(
+ isEqualWorkspaceUseCase(
+ { ...useCaseMock, features: [featureWithDetails] },
+ {
+ ...useCaseMock,
+ features: [featureWithOtherDetails],
+ }
+ )
+ ).toEqual(false);
+ });
it('should return true when all properties equal', () => {
expect(
isEqualWorkspaceUseCase(useCaseMock, {
diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts
index 9cadc2f54054..77c005cd7eea 100644
--- a/src/plugins/workspace/public/utils.ts
+++ b/src/plugins/workspace/public/utils.ts
@@ -15,8 +15,8 @@ import {
ApplicationStart,
HttpSetup,
NotificationsStart,
-} from '../../../core/public';
-import {
+ fulfillRegistrationLinksToChromeNavLinks,
+ ChromeNavLink,
App,
AppCategory,
AppNavLinkStatus,
@@ -360,23 +360,53 @@ export const getIsOnlyAllowEssentialUseCase = async (client: SavedObjectsStart['
return false;
};
-export const convertNavGroupToWorkspaceUseCase = ({
- id,
- title,
- description,
- navLinks,
- type,
- order,
- icon,
-}: NavGroupItemInMap): WorkspaceUseCase => ({
- id,
- title,
- description,
- features: navLinks.map((item) => ({ id: item.id, title: item.title })),
- systematic: type === NavGroupType.SYSTEM || id === ALL_USE_CASE_ID,
- order,
- icon,
-});
+export const convertNavGroupToWorkspaceUseCase = (
+ { id, title, description, navLinks, type, order, icon }: NavGroupItemInMap,
+ allNavLinks: ChromeNavLink[]
+): WorkspaceUseCase => {
+ const visibleNavLinks = allNavLinks.filter((link) => !link.hidden);
+ const visibleNavLinksWithinNavGroup = fulfillRegistrationLinksToChromeNavLinks(
+ navLinks,
+ visibleNavLinks
+ );
+ const features: WorkspaceUseCaseFeature[] = [];
+ const category2NavLinks: { [key: string]: WorkspaceUseCaseFeature & { details: string[] } } = {};
+ for (const { id: featureId, title: featureTitle, category } of visibleNavLinksWithinNavGroup) {
+ const lowerFeatureId = featureId.toLowerCase();
+ // Filter out overview and getting started links
+ if (lowerFeatureId.endsWith('overview') || lowerFeatureId.endsWith('started')) {
+ continue;
+ }
+ if (!category) {
+ features.push({ id: featureId, title: featureTitle });
+ continue;
+ }
+ // Filter out custom features
+ if (category.id === 'custom') {
+ continue;
+ }
+ if (!category2NavLinks[category.id]) {
+ category2NavLinks[category.id] = {
+ id: category.id,
+ title: category.label,
+ details: [],
+ };
+ }
+ if (featureTitle) {
+ category2NavLinks[category.id].details.push(featureTitle);
+ }
+ }
+ features.push(...Object.values(category2NavLinks));
+ return {
+ id,
+ title,
+ description,
+ features,
+ systematic: type === NavGroupType.SYSTEM || id === ALL_USE_CASE_ID,
+ order,
+ icon,
+ };
+};
const compareFeatures = (
features1: WorkspaceUseCaseFeature[],
@@ -384,7 +414,7 @@ const compareFeatures = (
) => {
const featuresSerializer = (features: WorkspaceUseCaseFeature[]) =>
features
- .map(({ id, title }) => `${id}-${title}`)
+ .map(({ id, title, details }) => `${id}-${title}-${details?.join('')}`)
.sort()
.join();
return featuresSerializer(features1) === featuresSerializer(features2);
From 03ec9b9c314cac58d657ede7cb0a7ff2c1c5aecf Mon Sep 17 00:00:00 2001
From: Miki
Date: Wed, 2 Oct 2024 10:34:22 -0700
Subject: [PATCH 19/31] Temporary fix for a bug with Chrome 129 when handling
mask-image (#8340)
* Mitigate @elastic/charts' use of `mask-image`
Signed-off-by: Miki
---
src/core/public/styles/_base.scss | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss
index e3b92e861449..237d15d35faa 100644
--- a/src/core/public/styles/_base.scss
+++ b/src/core/public/styles/_base.scss
@@ -83,6 +83,8 @@ $euiCollapsibleNavWidth: $euiSize * 20;
.euiFlyoutBody .euiFlyoutBody__overflow.euiFlyoutBody__overflow--hasBanner,
.euiModalBody .euiModalBody__overflow,
.euiSelectableList__list,
+// From @elastic/charts: any ruleset containing mask-image
+.echLegend .echLegendListContainer,
// For OSD: consumers of eui?ScrollWithShadows
.osdQueryBar__textarea:not(:focus):not(:invalid),
.osdSavedQueryManagement__list,
From 391e2ac950456d91e3ab2b1d3205929e530f0d7d Mon Sep 17 00:00:00 2001
From: Miki
Date: Wed, 2 Oct 2024 10:54:30 -0700
Subject: [PATCH 20/31] Fix dynamic uses of i18n and correct duplicate i18n
identifiers in console plugin (#8393)
* Fix dynamic uses of i18n and correct duplicate i18n identifiers in console plugin
Signed-off-by: Miki
* Changeset file for PR #8393 created/updated
---------
Signed-off-by: Miki
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
---
changelogs/fragments/8393.yml | 2 ++
.../application/components/import_flyout.tsx | 2 +-
.../editor/legacy/console_editor/editor.tsx | 24 +++++++++----------
3 files changed, 14 insertions(+), 14 deletions(-)
create mode 100644 changelogs/fragments/8393.yml
diff --git a/changelogs/fragments/8393.yml b/changelogs/fragments/8393.yml
new file mode 100644
index 000000000000..e1b1031516af
--- /dev/null
+++ b/changelogs/fragments/8393.yml
@@ -0,0 +1,2 @@
+fix:
+- Fix dynamic uses of i18n and correct duplicate i18n identifiers in console plugin ([#8393](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8393))
\ No newline at end of file
diff --git a/src/plugins/console/public/application/components/import_flyout.tsx b/src/plugins/console/public/application/components/import_flyout.tsx
index 0dd0112048e1..ef16dbefd6ef 100644
--- a/src/plugins/console/public/application/components/import_flyout.tsx
+++ b/src/plugins/console/public/application/components/import_flyout.tsx
@@ -215,7 +215,7 @@ export const ImportFlyout = ({ close, refresh }: ImportFlyoutProps) => {
} else {
setStatus('error');
setError(
- i18n.translate('console.ImportFlyout.importFileErrorMessage', {
+ i18n.translate('console.ImportFlyout.importFileErrorMessage.notJSON', {
defaultMessage: 'The selected file is not valid. Please select a valid JSON file.',
})
);
diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
index 4422cbc4743f..e45cf876d1e4 100644
--- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
@@ -228,10 +228,14 @@ function EditorUI({ initialTextValue, dataSourceId }: EditorProps) {
});
}, [sendCurrentRequestToOpenSearch, openDocumentation]);
- const tooltipDefaultMessage =
- dataSourceId === undefined ? `Select a data source` : `Click to send request`;
-
- const toolTipButtonDiasbled = dataSourceId === undefined;
+ const toolTipButtonDisabled = dataSourceId === undefined;
+ const sendLabel = toolTipButtonDisabled
+ ? i18n.translate('console.sendRequestButtonTooltip.withoutDataSourceId', {
+ defaultMessage: 'Select a data source',
+ })
+ : i18n.translate('console.sendRequestButtonTooltip', {
+ defaultMessage: 'Send request',
+ });
return (
@@ -243,19 +247,13 @@ function EditorUI({ initialTextValue, dataSourceId }: EditorProps) {
responsive={false}
>
-
+
From 73787386ddb9b3459b4302232395f6a04d0fb7f1 Mon Sep 17 00:00:00 2001
From: Viraj Sanghvi
Date: Wed, 2 Oct 2024 11:27:05 -0700
Subject: [PATCH 21/31] fix: Show loading indicator in recents popover button
(#8430)
Signed-off-by: Viraj Sanghvi
---
.../header/__snapshots__/header.test.tsx.snap | 178 +++++++++++++++++-
.../__snapshots__/recent_items.test.tsx.snap | 42 +++++
src/core/public/chrome/ui/header/header.tsx | 1 +
.../public/chrome/ui/header/recent_items.scss | 9 +
.../chrome/ui/header/recent_items.test.tsx | 8 +
.../public/chrome/ui/header/recent_items.tsx | 53 ++++--
6 files changed, 272 insertions(+), 19 deletions(-)
diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
index 2efa9b705b84..28d88b9bf277 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap
@@ -9339,7 +9339,45 @@ exports[`Header renders application header without title and breadcrumbs 1`] = `
"closed": false,
"hasError": false,
"isStopped": false,
- "observers": Array [],
+ "observers": Array [
+ Subscriber {
+ "_parentOrParents": null,
+ "_subscriptions": Array [
+ SubjectSubscription {
+ "_parentOrParents": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "subject": [Circular],
+ "subscriber": [Circular],
+ },
+ ],
+ "closed": false,
+ "destination": SafeSubscriber {
+ "_complete": undefined,
+ "_context": [Circular],
+ "_error": undefined,
+ "_next": [Function],
+ "_parentOrParents": null,
+ "_parentSubscriber": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "destination": Object {
+ "closed": true,
+ "complete": [Function],
+ "error": [Function],
+ "next": [Function],
+ },
+ "isStopped": false,
+ "syncErrorThrowable": false,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ "isStopped": false,
+ "syncErrorThrowable": true,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ ],
"thrownError": null,
}
}
@@ -10263,6 +10301,55 @@ exports[`Header renders application header without title and breadcrumbs 1`] = `
"put": [MockFunction],
}
}
+ loadingCount$={
+ BehaviorSubject {
+ "_isScalar": false,
+ "_value": 0,
+ "closed": false,
+ "hasError": false,
+ "isStopped": false,
+ "observers": Array [
+ Subscriber {
+ "_parentOrParents": null,
+ "_subscriptions": Array [
+ SubjectSubscription {
+ "_parentOrParents": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "subject": [Circular],
+ "subscriber": [Circular],
+ },
+ ],
+ "closed": false,
+ "destination": SafeSubscriber {
+ "_complete": undefined,
+ "_context": [Circular],
+ "_error": undefined,
+ "_next": [Function],
+ "_parentOrParents": null,
+ "_parentSubscriber": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "destination": Object {
+ "closed": true,
+ "complete": [Function],
+ "error": [Function],
+ "next": [Function],
+ },
+ "isStopped": false,
+ "syncErrorThrowable": false,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ "isStopped": false,
+ "syncErrorThrowable": true,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ ],
+ "thrownError": null,
+ }
+ }
navLinks$={
BehaviorSubject {
"_isScalar": false,
@@ -18621,7 +18708,45 @@ exports[`Header renders page header with application title 1`] = `
"closed": false,
"hasError": false,
"isStopped": false,
- "observers": Array [],
+ "observers": Array [
+ Subscriber {
+ "_parentOrParents": null,
+ "_subscriptions": Array [
+ SubjectSubscription {
+ "_parentOrParents": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "subject": [Circular],
+ "subscriber": [Circular],
+ },
+ ],
+ "closed": false,
+ "destination": SafeSubscriber {
+ "_complete": undefined,
+ "_context": [Circular],
+ "_error": undefined,
+ "_next": [Function],
+ "_parentOrParents": null,
+ "_parentSubscriber": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "destination": Object {
+ "closed": true,
+ "complete": [Function],
+ "error": [Function],
+ "next": [Function],
+ },
+ "isStopped": false,
+ "syncErrorThrowable": false,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ "isStopped": false,
+ "syncErrorThrowable": true,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ ],
"thrownError": null,
}
}
@@ -19544,6 +19669,55 @@ exports[`Header renders page header with application title 1`] = `
"put": [MockFunction],
}
}
+ loadingCount$={
+ BehaviorSubject {
+ "_isScalar": false,
+ "_value": 0,
+ "closed": false,
+ "hasError": false,
+ "isStopped": false,
+ "observers": Array [
+ Subscriber {
+ "_parentOrParents": null,
+ "_subscriptions": Array [
+ SubjectSubscription {
+ "_parentOrParents": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "subject": [Circular],
+ "subscriber": [Circular],
+ },
+ ],
+ "closed": false,
+ "destination": SafeSubscriber {
+ "_complete": undefined,
+ "_context": [Circular],
+ "_error": undefined,
+ "_next": [Function],
+ "_parentOrParents": null,
+ "_parentSubscriber": [Circular],
+ "_subscriptions": null,
+ "closed": false,
+ "destination": Object {
+ "closed": true,
+ "complete": [Function],
+ "error": [Function],
+ "next": [Function],
+ },
+ "isStopped": false,
+ "syncErrorThrowable": false,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ "isStopped": false,
+ "syncErrorThrowable": true,
+ "syncErrorThrown": false,
+ "syncErrorValue": null,
+ },
+ ],
+ "thrownError": null,
+ }
+ }
navLinks$={
BehaviorSubject {
"_isScalar": false,
diff --git a/src/core/public/chrome/ui/header/__snapshots__/recent_items.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/recent_items.test.tsx.snap
index 17634ee1c3fd..5dd325c9dfed 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/recent_items.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/recent_items.test.tsx.snap
@@ -33,3 +33,45 @@ exports[`Recent items should render base element normally 1`] = `
+
+