From dc79df329c8ae5a991641677eef822f3de76703e Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 19 Aug 2024 15:47:41 -0400 Subject: [PATCH] Adds page headers for updated UX (#2083) * Adds initial commit to add header components and modifies get started and user list pages Signed-off-by: Darshit Chanpura * Updates auth-view page Signed-off-by: Darshit Chanpura * Updates user edit and create pages Signed-off-by: Darshit Chanpura * Updates user permissions page Signed-off-by: Darshit Chanpura * Updates dashboards tenancy page Signed-off-by: Darshit Chanpura * Updates dashboards audit logs page Signed-off-by: Darshit Chanpura * Updates roles and related pages Signed-off-by: Darshit Chanpura * Updates variable name and fixes indentation Signed-off-by: Darshit Chanpura * Push logic into new component Signed-off-by: Derek Ho * Lint Signed-off-by: Derek Ho * Migrate audit logging and tenant tabs to new page header Signed-off-by: Derek Ho * Migrate all tabs to new component Signed-off-by: Derek Ho * Remove prop and fix some test failures Signed-off-by: Derek Ho * fix most tests Signed-off-by: Derek Ho * Fix existing tests Signed-off-by: Derek Ho * Push breadcrumb population into child components Signed-off-by: Derek Ho * Migrate breadcrumbs into the page header component for the top level of all pages Signed-off-by: Derek Ho * Lint Signed-off-by: Derek Ho * Update all instances of breadcrumbs to new component and all existing tests pass Signed-off-by: Derek Ho * Fixes target _blank redirects Signed-off-by: Darshit Chanpura * Fixes unit tests Signed-off-by: Darshit Chanpura * Add tests for new component and breadcrumb function Signed-off-by: Derek Ho * Address PR feedback Signed-off-by: Derek Ho * Update tests and snapshots Signed-off-by: Derek Ho --------- Signed-off-by: Darshit Chanpura Signed-off-by: Derek Ho Co-authored-by: Derek Ho --- public/apps/configuration/app-router.tsx | 78 +--- .../header/header-components.tsx | 62 +++ .../configuration/header/header-props.tsx | 37 ++ .../header/test/header-components.test.tsx | 93 ++++ .../audit-logging-edit-settings.tsx | 37 +- .../panels/audit-logging/audit-logging.tsx | 18 +- .../__snapshots__/audit-logging.test.tsx.snap | 82 +++- .../audit-logging/test/audit-logging.test.tsx | 25 +- .../panels/auth-view/auth-view.tsx | 58 ++- .../panels/auth-view/instruction-view.tsx | 6 +- .../__snapshots__/auth-view.test.tsx.snap | 30 +- .../panels/auth-view/test/auth-view.test.tsx | 8 +- .../auth-view/test/instruction-view.test.tsx | 3 - .../apps/configuration/panels/get-started.tsx | 33 +- .../internal-user-edit/internal-user-edit.tsx | 59 ++- .../test/internal-user-edit.test.tsx | 2 - .../permission-list/permission-list.tsx | 110 +++-- .../permission-list.test.tsx.snap | 158 ++++++- .../test/permission-list.test.tsx | 14 +- .../panels/role-edit/role-edit.tsx | 53 ++- .../test/role-edit-filtering.test.tsx | 13 +- .../panels/role-edit/test/role-edit.test.tsx | 8 +- .../apps/configuration/panels/role-list.tsx | 123 ++++-- .../role-mapping/role-edit-mapped-user.tsx | 41 +- .../test/role-edit-mapped-user.test.tsx | 6 - .../panels/role-view/role-view.tsx | 80 +++- .../__snapshots__/role-view.test.tsx.snap | 411 +++++++++++++----- .../panels/role-view/test/role-view.test.tsx | 28 +- .../panels/tenant-list/manage_tab.tsx | 28 +- .../panels/tenant-list/tenant-list.tsx | 51 ++- .../tenant-list/test/tenant-list.test.tsx | 32 +- .../__snapshots__/get-started.test.tsx.snap | 98 +++-- .../__snapshots__/role-list.test.tsx.snap | 109 ++++- .../__snapshots__/user-list.test.tsx.snap | 111 ++++- .../panels/test/role-list.test.tsx | 23 +- .../panels/test/user-list.test.tsx | 23 +- .../apps/configuration/panels/user-list.tsx | 121 ++++-- .../__snapshots__/app-router.test.tsx.snap | 70 +++ .../configuration/test/app-router.test.tsx | 3 + public/apps/configuration/types.ts | 1 + .../apps/configuration/utils/context-menu.tsx | 9 +- .../configuration/utils/resource-utils.tsx | 51 +++ .../utils/test/resource-utils.test.tsx | 61 ++- 43 files changed, 1899 insertions(+), 568 deletions(-) create mode 100644 public/apps/configuration/header/header-components.tsx create mode 100644 public/apps/configuration/header/header-props.tsx create mode 100644 public/apps/configuration/header/test/header-components.test.tsx diff --git a/public/apps/configuration/app-router.tsx b/public/apps/configuration/app-router.tsx index f0dc0cae2..eb987e7a5 100644 --- a/public/apps/configuration/app-router.tsx +++ b/public/apps/configuration/app-router.tsx @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -import { EuiBreadcrumb, EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui'; -import { flow, partial } from 'lodash'; +import { EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui'; +import { flow } from 'lodash'; import React, { createContext, useState } from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; @@ -38,31 +38,37 @@ import { TenantList } from './panels/tenant-list/tenant-list'; import { UserList } from './panels/user-list'; import { Action, RouteItem, SubAction } from './types'; import { ResourceType } from '../../../common'; -import { buildHashUrl, buildUrl } from './utils/url-builder'; +import { buildUrl } from './utils/url-builder'; import { CrossPageToast } from './cross-page-toast'; import { getDataSourceFromUrl, LocalCluster } from '../../utils/datasource-utils'; +import { getBreadcrumbs } from './utils/resource-utils'; const LANDING_PAGE_URL = '/getstarted'; export const ROUTE_MAP: { [key: string]: RouteItem } = { getStarted: { name: 'Get Started', + breadCrumbDisplayNameWithoutSecurityBase: 'Get started with access control', href: LANDING_PAGE_URL, }, [ResourceType.roles]: { name: 'Roles', + breadCrumbDisplayNameWithoutSecurityBase: 'Roles', href: buildUrl(ResourceType.roles), }, [ResourceType.users]: { name: 'Internal users', + breadCrumbDisplayNameWithoutSecurityBase: 'Internal users', href: buildUrl(ResourceType.users), }, [ResourceType.permissions]: { name: 'Permissions', + breadCrumbDisplayNameWithoutSecurityBase: 'Permissions', href: buildUrl(ResourceType.permissions), }, [ResourceType.tenants]: { name: 'Tenants', + breadCrumbDisplayNameWithoutSecurityBase: 'Dashboard multi-tenancy', href: buildUrl(ResourceType.tenants), }, [ResourceType.tenantsConfigureTab]: { @@ -71,10 +77,12 @@ export const ROUTE_MAP: { [key: string]: RouteItem } = { }, [ResourceType.auth]: { name: 'Authentication', + breadCrumbDisplayNameWithoutSecurityBase: 'Authentication and authorization', href: buildUrl(ResourceType.auth), }, [ResourceType.auditLogging]: { name: 'Audit logs', + breadCrumbDisplayNameWithoutSecurityBase: 'Audit logs', href: buildUrl(ResourceType.auditLogging), }, }; @@ -100,39 +108,6 @@ export const allNavPanelUrls = (multitenancyEnabled: boolean) => ...(multitenancyEnabled ? [buildUrl(ResourceType.tenantsConfigureTab)] : []), ]); -export function getBreadcrumbs( - resourceType?: ResourceType, - pageTitle?: string, - subAction?: string -): EuiBreadcrumb[] { - const breadcrumbs: EuiBreadcrumb[] = [ - { - text: 'Security', - href: buildHashUrl(), - }, - ]; - - if (resourceType) { - breadcrumbs.push({ - text: ROUTE_MAP[resourceType].name, - href: buildHashUrl(resourceType), - }); - } - - if (pageTitle) { - breadcrumbs.push({ - text: pageTitle, - }); - } - - if (subAction) { - breadcrumbs.push({ - text: subAction, - }); - } - return breadcrumbs; -} - function decodeParams(params: { [k: string]: string }): any { return Object.keys(params).reduce((obj: { [k: string]: string }, key: string) => { obj[key] = decodeURIComponent(params[key]); @@ -154,6 +129,7 @@ export function AppRouter(props: AppDependencies) { const dataSourceFromUrl = dataSourceEnabled ? getDataSourceFromUrl() : LocalCluster; const [dataSource, setDataSource] = useState(dataSourceFromUrl); + const includeSecurityBase = !props.coreStart.uiSettings.get('home:useNewHomePage'); return ( @@ -173,92 +149,72 @@ export function AppRouter(props: AppDependencies) { ( - + )} /> ( - + )} /> ( - + )} /> { - setGlobalBreadcrumbs(ResourceType.roles); return ; }} /> { - setGlobalBreadcrumbs(ResourceType.auth); return ; }} /> ( - + )} /> { - setGlobalBreadcrumbs(ResourceType.users); return ; }} /> { - setGlobalBreadcrumbs(ResourceType.auditLogging, 'General settings'); return ; }} /> { - setGlobalBreadcrumbs(ResourceType.auditLogging, 'Compliance settings'); return ; }} /> { - setGlobalBreadcrumbs(ResourceType.auditLogging); return ; }} /> { - setGlobalBreadcrumbs(ResourceType.permissions); return ; }} /> { - setGlobalBreadcrumbs(); return ; }} /> @@ -266,7 +222,6 @@ export function AppRouter(props: AppDependencies) { { - setGlobalBreadcrumbs(ResourceType.tenants); return ; }} /> @@ -275,7 +230,6 @@ export function AppRouter(props: AppDependencies) { { - setGlobalBreadcrumbs(ResourceType.tenants); return ; }} /> diff --git a/public/apps/configuration/header/header-components.tsx b/public/apps/configuration/header/header-components.tsx new file mode 100644 index 000000000..dde2bbbce --- /dev/null +++ b/public/apps/configuration/header/header-components.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 { flow } from 'lodash'; +import { ControlProps, DescriptionProps, HeaderProps } from './header-props'; +import { getBreadcrumbs } from '../utils/resource-utils'; + +export const HeaderButtonOrLink = React.memo((props: HeaderProps & ControlProps) => { + const { HeaderControl } = props.navigation.ui; + + return ( + + ); +}); + +export const PageHeader = (props: HeaderProps & DescriptionProps & ControlProps) => { + const { HeaderControl } = props.navigation.ui; // need to get this from SecurityPluginStartDependencies + const useNewUx = props.coreStart.uiSettings.get('home:useNewHomePage'); + flow(getBreadcrumbs, props.coreStart.chrome.setBreadcrumbs)( + !useNewUx, + props.resourceType, + props.pageTitle, + props.subAction, + props.count + ); + if (useNewUx) { + return ( + <> + {props.descriptionControls ? ( + + ) : null} + {props.appRightControls ? ( + + ) : null} + + ); + } else { + return props.fallBackComponent; + } +}; diff --git a/public/apps/configuration/header/header-props.tsx b/public/apps/configuration/header/header-props.tsx new file mode 100644 index 000000000..f9411e20a --- /dev/null +++ b/public/apps/configuration/header/header-props.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 { CoreStart } from 'opensearch-dashboards/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { TopNavControlData } from 'src/plugins/navigation/public/top_nav_menu/top_nav_control_data'; +import { ResourceType } from '../../../../common'; + +export interface HeaderProps { + navigation: NavigationPublicPluginStart; + coreStart: CoreStart; + fallBackComponent: JSX.Element; + resourceType?: ResourceType; + pageTitle?: string; + subAction?: string; + count?: number; +} + +export interface ControlProps { + appRightControls?: TopNavControlData[]; +} + +export interface DescriptionProps { + descriptionControls?: TopNavControlData[]; +} diff --git a/public/apps/configuration/header/test/header-components.test.tsx b/public/apps/configuration/header/test/header-components.test.tsx new file mode 100644 index 000000000..e9a677f52 --- /dev/null +++ b/public/apps/configuration/header/test/header-components.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 { mount } from 'enzyme'; +import { PageHeader } from '../header-components'; // Adjust the import path as needed +import { getBreadcrumbs } from '../../utils/resource-utils'; + +jest.mock('../../utils/resource-utils', () => ({ + getBreadcrumbs: jest.fn(), +})); + +describe('PageHeader', () => { + let props; + + beforeEach(() => { + jest.resetAllMocks(); + props = { + navigation: { + ui: { + HeaderControl: jest.fn(({ setMountPoint, controls }) => null), + }, + }, + coreStart: { + chrome: { + navGroup: { + getNavGroupEnabled: jest.fn(), + }, + setBreadcrumbs: jest.fn(), + }, + application: { + setAppRightControls: jest.fn(), + setAppDescriptionControls: jest.fn(), + }, + uiSettings: { + get: jest.fn(), + }, + }, + resourceType: 'test-resource', + pageTitle: 'Test Page Title', + subAction: 'test-sub-action', + count: 5, + descriptionControls: ['control-1'], + appRightControls: ['control-2'], + fallBackComponent:
Fallback Component
, + }; + }); + + it('renders with the feature flag off', () => { + props.coreStart.uiSettings.get.mockReturnValueOnce(false); + const wrapper = mount(); + + expect(getBreadcrumbs).toHaveBeenCalledWith( + true, + 'test-resource', + 'Test Page Title', + 'test-sub-action', + 5 + ); + expect(wrapper.contains(props.fallBackComponent)).toBe(true); + + expect(props.navigation.ui.HeaderControl).not.toBeCalled(); + }); + + it('renders with the feature flag on', () => { + props.coreStart.uiSettings.get.mockReturnValueOnce(true); + const wrapper = mount(); + + expect(getBreadcrumbs).toHaveBeenCalledWith( + false, + 'test-resource', + 'Test Page Title', + 'test-sub-action', + 5 + ); + expect(wrapper.contains(props.fallBackComponent)).toBe(false); + // Verifies that the HeaderControl is called with both controls passed as props + expect(props.navigation.ui.HeaderControl.mock.calls[0][0].controls).toEqual(['control-1']); + expect(props.navigation.ui.HeaderControl.mock.calls[1][0].controls).toEqual(['control-2']); + }); +}); diff --git a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx index 7f2ab07aa..4de73094c 100644 --- a/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx +++ b/public/apps/configuration/panels/audit-logging/audit-logging-edit-settings.tsx @@ -38,6 +38,7 @@ import { setCrossPageToast } from '../../utils/storage-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; import { getClusterInfo } from '../../../../utils/datasource-utils'; +import { PageHeader } from '../../header/header-components'; interface AuditLoggingEditSettingProps extends AppDependencies { setting: 'general' | 'compliance'; @@ -155,11 +156,19 @@ export function AuditLoggingEditSettings(props: AuditLoggingEditSettingProps) { const renderComplianceSetting = () => { return ( <> - - -

Compliance settings

-
-
+ + +

Compliance settings

+
+ + } + resourceType={ResourceType.auditLogging} + pageTitle="Compliance settings" + /> { return ( <> - - -

General settings

-
-
+ + +

General settings

+
+ + } + resourceType={ResourceType.auditLogging} + pageTitle="General settings" + /> - - -

Audit logging

-
-
+ + +

Audit logging

+
+ + } + resourceType={ResourceType.auditLogging} + /> {loading ? : content} diff --git a/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap b/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap index c5b689b96..fa1575630 100644 --- a/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap +++ b/public/apps/configuration/panels/audit-logging/test/__snapshots__/audit-logging.test.tsx.snap @@ -245,10 +245,17 @@ exports[`Audit logs render when AuditLoggingSettings.enabled is true 1`] = ` coreStart={ Object { "http": 1, + "uiSettings": Object { + "get": [MockFunction], + }, } } dataSourcePickerReadOnly={false} - navigation={Object {}} + depsStart={ + Object { + "navigation": Object {}, + } + } selectedDataSource={ Object { "id": "test", @@ -256,15 +263,29 @@ exports[`Audit logs render when AuditLoggingSettings.enabled is true 1`] = ` } setDataSource={[MockFunction]} /> - - -

- Audit logging -

-
-
+ + +

+ Audit logging +

+
+ + } + navigation={Object {}} + resourceType="auditLogging" + /> @@ -655,10 +676,17 @@ exports[`Audit logs should load access error component 1`] = ` coreStart={ Object { "http": 1, + "uiSettings": Object { + "get": [MockFunction], + }, } } dataSourcePickerReadOnly={false} - navigation={Object {}} + depsStart={ + Object { + "navigation": Object {}, + } + } selectedDataSource={ Object { "id": "test", @@ -666,15 +694,29 @@ exports[`Audit logs should load access error component 1`] = ` } setDataSource={[MockFunction]} /> - - -

- Audit logging -

-
-
+ + +

+ Audit logging +

+
+ + } + navigation={Object {}} + resourceType="auditLogging" + /> { const setState = jest.fn(); const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; beforeEach(() => { @@ -58,7 +61,7 @@ describe('Audit logs', () => { mockAuditLoggingUtils.getAuditLogging = jest.fn().mockReturnValue(mockAuditLoggingData); const component = shallow( - + ); const switchFound = component.find(EuiCompressedSwitch); @@ -76,7 +79,9 @@ describe('Audit logs', () => { mockAuditLoggingUtils.getAuditLogging = jest.fn().mockReturnValue(mockAuditLoggingData); - shallow(); + shallow( + + ); process.nextTick(() => { expect(mockAuditLoggingUtils.getAuditLogging).toHaveBeenCalledTimes(1); @@ -94,7 +99,9 @@ describe('Audit logs', () => { throw Error(); }); - shallow(); + shallow( + + ); process.nextTick(() => { expect(mockAuditLoggingUtils.getAuditLogging).toHaveBeenCalledTimes(1); @@ -120,7 +127,7 @@ describe('Audit logs', () => { it('audit logging switch change', () => { const component = shallow( - + ); component.find('[data-test-subj="audit-logging-enabled-switch"]').simulate('change'); expect(mockAuditLoggingUtils.updateAuditLogging).toHaveBeenCalledTimes(1); @@ -134,7 +141,7 @@ describe('Audit logs', () => { throw Error(); }); const component = shallow( - + ); component.find('[data-test-subj="audit-logging-enabled-switch"]').simulate('change'); @@ -150,7 +157,7 @@ describe('Audit logs', () => { .mockImplementationOnce(() => [auditLoggingSettings, setState]) .mockImplementationOnce(() => [false, jest.fn()]); const component = shallow( - + ); expect(component).toMatchSnapshot(); }); @@ -163,7 +170,7 @@ describe('Audit logs', () => { .mockImplementationOnce(() => [auditLoggingSettings, setState]) .mockImplementationOnce(() => [false, jest.fn()]); const component = shallow( - + ); component.find('[data-test-subj="general-settings-configure"]').simulate('click'); expect(window.location.hash).toBe( @@ -179,7 +186,7 @@ describe('Audit logs', () => { .mockImplementationOnce(() => [auditLoggingSettings, setState]) .mockImplementationOnce(() => [false, jest.fn()]); const component = shallow( - + ); component.find('[data-test-subj="compliance-settings-configure"]').simulate('click'); expect(window.location.hash).toBe( @@ -198,7 +205,7 @@ describe('Audit logs', () => { .mockImplementationOnce(() => [false, jest.fn()]) .mockImplementationOnce(() => [true, jest.fn()]); const component = shallow( - + ); expect(component).toMatchSnapshot(); }); diff --git a/public/apps/configuration/panels/auth-view/auth-view.tsx b/public/apps/configuration/panels/auth-view/auth-view.tsx index 8022ca260..6c19a9814 100644 --- a/public/apps/configuration/panels/auth-view/auth-view.tsx +++ b/public/apps/configuration/panels/auth-view/auth-view.tsx @@ -26,6 +26,8 @@ import { InstructionView } from './instruction-view'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { AccessErrorComponent } from '../../access-error-component'; +import { PageHeader } from '../../header/header-components'; +import { ResourceType } from '../../../../../common'; export function AuthView(props: AppDependencies) { const [authentication, setAuthentication] = React.useState([]); @@ -60,6 +62,18 @@ export function AuthView(props: AppDependencies) { fetchData(); }, [props.coreStart.http, dataSource]); + const buttonData = [ + { + label: 'Manage via config.yml', + isLoading: false, + href: DocLinks.BackendConfigurationDoc, + iconType: 'popout', + iconSide: 'right', + type: 'button', + target: '_blank', + }, + ]; + if (isEmpty(authentication) && !loading) { return ( <> @@ -69,11 +83,15 @@ export function AuthView(props: AppDependencies) { setDataSource={setDataSource} selectedDataSource={dataSource} /> - {accessErrorFlag ? ( - -

Authentication and authorization

-
- ) : null} + +

Authentication and authorization

+ + } + /> {accessErrorFlag ? ( - - -

Authentication and authorization

-
- {!loading && !errorFlag && props.config.ui.backend_configurable && ( - - )} -
+ + +

Authentication and authorization

+
+ {!loading && !errorFlag && props.config.ui.backend_configurable && ( + + )} + + } + resourceType={ResourceType.auth} + /> {loading ? ( ) : accessErrorFlag ? ( diff --git a/public/apps/configuration/panels/auth-view/instruction-view.tsx b/public/apps/configuration/panels/auth-view/instruction-view.tsx index c06383e23..0e53938e8 100644 --- a/public/apps/configuration/panels/auth-view/instruction-view.tsx +++ b/public/apps/configuration/panels/auth-view/instruction-view.tsx @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import { EuiCode, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiCode, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; import { ClientConfigType } from '../../../../types'; import { ExternalLinkButton } from '../../utils/display-utils'; @@ -22,10 +22,6 @@ import { DocLinks } from '../../constants'; export function InstructionView(props: { config: ClientConfigType }) { return ( <> - -

Authentication and authorization

-
- diff --git a/public/apps/configuration/panels/auth-view/test/__snapshots__/auth-view.test.tsx.snap b/public/apps/configuration/panels/auth-view/test/__snapshots__/auth-view.test.tsx.snap index aff22a5fb..6ede754fb 100644 --- a/public/apps/configuration/panels/auth-view/test/__snapshots__/auth-view.test.tsx.snap +++ b/public/apps/configuration/panels/auth-view/test/__snapshots__/auth-view.test.tsx.snap @@ -9,7 +9,11 @@ exports[`Auth view should load access error component 1`] = ` } } dataSourcePickerReadOnly={false} - navigation={Object {}} + depsStart={ + Object { + "navigation": Object {}, + } + } selectedDataSource={ Object { "id": "test", @@ -17,13 +21,23 @@ exports[`Auth view should load access error component 1`] = ` } setDataSource={[MockFunction]} /> - -

- Authentication and authorization -

-
+ +

+ Authentication and authorization +

+ + } + navigation={Object {}} + /> diff --git a/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx b/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx index 5643f86fa..91d020da3 100644 --- a/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx +++ b/public/apps/configuration/panels/auth-view/test/auth-view.test.tsx @@ -62,7 +62,7 @@ describe('Auth view', () => { it('valid data', (done) => { mockAuthViewUtils.getSecurityConfig = jest.fn().mockReturnValue(config); - shallow(); + shallow(); process.nextTick(() => { expect(mockAuthViewUtils.getSecurityConfig).toHaveBeenCalledTimes(1); @@ -81,7 +81,7 @@ describe('Auth view', () => { jest.spyOn(console, 'log').mockImplementationOnce(() => {}); - shallow(); + shallow(); process.nextTick(() => { expect(mockAuthViewUtils.getSecurityConfig).toHaveBeenCalledTimes(1); @@ -104,7 +104,9 @@ describe('Auth view', () => { mockAuthViewUtils.getSecurityConfig = jest .fn() .mockRejectedValue({ response: { status: 403 } }); - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); }); diff --git a/public/apps/configuration/panels/auth-view/test/instruction-view.test.tsx b/public/apps/configuration/panels/auth-view/test/instruction-view.test.tsx index 16a8c664f..9ab4902a0 100644 --- a/public/apps/configuration/panels/auth-view/test/instruction-view.test.tsx +++ b/public/apps/configuration/panels/auth-view/test/instruction-view.test.tsx @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { InstructionView } from '../instruction-view'; -import { EuiTitle } from '@elastic/eui'; import React from 'react'; import { ExternalLinkButton } from '../../../utils/display-utils'; @@ -27,8 +26,6 @@ describe('Instruction view', () => { }, }; const component = shallow(); - - expect(component.find(EuiTitle).find('h1').text()).toBe('Authentication and authorization'); expect(component.find(ExternalLinkButton).prop('text')).toBe('Create config.yml'); }); }); diff --git a/public/apps/configuration/panels/get-started.tsx b/public/apps/configuration/panels/get-started.tsx index 4918bc165..d1a1fa722 100644 --- a/public/apps/configuration/panels/get-started.tsx +++ b/public/apps/configuration/panels/get-started.tsx @@ -39,6 +39,7 @@ import { createSuccessToast, createUnknownErrorToast, useToastState } from '../u import { SecurityPluginTopNavMenu } from '../top-nav-menu'; import { DataSourceContext } from '../app-router'; import { getClusterInfo } from '../../../utils/datasource-utils'; +import { PageHeader } from '../header/header-components'; const addBackendStep = { title: 'Add backends', @@ -173,6 +174,17 @@ export function GetStarted(props: AppDependencies) { } const [toasts, addToast, removeToast] = useToastState(); + const buttonData = [ + { + label: 'Open in new window', + isLoading: false, + href: buildHashUrl(), + iconType: 'popout', + iconSide: 'right', + type: 'button', + target: '_blank', + }, + ]; return ( <>
@@ -182,13 +194,20 @@ export function GetStarted(props: AppDependencies) { setDataSource={setDataSource} selectedDataSource={dataSource} /> - - -

Get started

-
- -
- + + +

Get started

+
+ + + } + resourceType={'getStarted'} + />

diff --git a/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx b/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx index 5b122f384..6546978c3 100644 --- a/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx +++ b/public/apps/configuration/panels/internal-user-edit/internal-user-edit.tsx @@ -49,6 +49,7 @@ import { constructErrorMessageAndLog } from '../../../error-utils'; import { BackendRolePanel } from './backend-role-panel'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { PageHeader } from '../../header/header-components'; interface InternalUserEditDeps extends BreadcrumbsPageDependencies { action: 'create' | 'edit' | 'duplicate'; @@ -147,6 +148,18 @@ export function InternalUserEdit(props: InternalUserEditDeps) { } }; + const descriptionData = [ + { + renderComponent: ( + + The security plugin includes an internal user database. Use this database in place of, or + in addition to, an external
authentication system such as LDAP or Active Directory.{' '} + +
+ ), + }, + ]; + return ( <> - {props.buildBreadcrumbs(TITLE_TEXT_DICT[props.action])} - - - - - -

{TITLE_TEXT_DICT[props.action]}

- - - - - The security plugin includes an internal user database. Use this database in place of, - or in addition to, an external authentication system such as LDAP or Active Directory.{' '} - - - - - + + + + + + +

{TITLE_TEXT_DICT[props.action]}

+
+
+ + + The security plugin includes an internal user database. Use this database in + place of, or in addition to, an external authentication system such as LDAP or + Active Directory. + + +
+
+ + } + resourceType={ResourceType.users} + subAction={TITLE_TEXT_DICT[props.action]} + /> { { /> ); - expect(buildBreadcrumbs).toBeCalledTimes(1); expect(component.find(AttributePanel).length).toBe(1); }); diff --git a/public/apps/configuration/panels/permission-list/permission-list.tsx b/public/apps/configuration/panels/permission-list/permission-list.tsx index 17116dd3e..7981abb24 100644 --- a/public/apps/configuration/panels/permission-list/permission-list.tsx +++ b/public/apps/configuration/panels/permission-list/permission-list.tsx @@ -65,6 +65,8 @@ import { DocLinks } from '../../constants'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; import { AccessErrorComponent } from '../../access-error-component'; +import { PageHeader } from '../../header/header-components'; +import { ResourceType } from '../../../../../common'; export function renderBooleanToCheckMark(value: boolean): React.ReactNode { return value ? : ''; @@ -353,12 +355,41 @@ export function PermissionList(props: AppDependencies) { , ]; + const useUpdatedUX = props.coreStart.uiSettings.get('home:useNewHomePage'); + const [createActionGroupMenu] = useContextMenuState( 'Create action group', { fill: true }, - createActionGroupMenuItems + createActionGroupMenuItems, + useUpdatedUX ); + const buttonData = [ + { + isLoading: loading, + renderComponent: {createActionGroupMenu}, + }, + ]; + + const descriptionData = [ + { + isLoading: loading, + renderComponent: ( + + Permissions are individual actions, such as cluster:admin/snapshot/restore, which lets you + restore snapshots. Action groups
+ are reusable collections of permissions, such as MANAGE_SNAPSHOTS, which lets you view, + take, delete, and restore
+ snapshots. You can often meet your security needs using the default action groups, but you + might find it convenient to create
+ your own. +
+ ), + }, + ]; + + const permissionLen = Query.execute(query || '', permissionList).length; + return ( <> - - -

Permissions

-
-
+ + +

Permissions

+
+ + } + resourceType={ResourceType.permissions} + count={permissionList.length} + /> {loading ? ( ) : accessErrorFlag ? ( ) : ( - - - -

- Permissions - - {' '} - ({Query.execute(query || '', permissionList).length}) - -

-
- - Permissions are individual actions, such as cluster:admin/snapshot/restore, which - lets you restore snapshots. Action groups are reusable collections of permissions, - such as MANAGE_SNAPSHOTS, which lets you view, take, delete, and restore snapshots. - You can often meet your security needs using the default action groups, but you - might find it convenient to create your own.{' '} - - -
- - - {actionsMenu} - {createActionGroupMenu} - - -
+ {useUpdatedUX ? null : ( + + + +

+ Permissions + + {' '} + ({Query.execute(query || '', permissionList).length}) + +

+
+ + Permissions are individual actions, such as cluster:admin/snapshot/restore, which + lets you restore snapshots. Action groups are reusable collections of permissions, + such as MANAGE_SNAPSHOTS, which lets you view, take, delete, and restore + snapshots. You can often meet your security needs using the default action groups, + but you might find it convenient to create your own.{' '} + + +
+ + + {actionsMenu} + {createActionGroupMenu} + + +
+ )} {actionsMenu}] : undefined, }} selection={{ onSelectionChange: setSelection }} sorting={{ sort: { field: 'type', direction: 'asc' } }} diff --git a/public/apps/configuration/panels/permission-list/test/__snapshots__/permission-list.test.tsx.snap b/public/apps/configuration/panels/permission-list/test/__snapshots__/permission-list.test.tsx.snap index 4b86b4973..2dc58bac1 100644 --- a/public/apps/configuration/panels/permission-list/test/__snapshots__/permission-list.test.tsx.snap +++ b/public/apps/configuration/panels/permission-list/test/__snapshots__/permission-list.test.tsx.snap @@ -7,6 +7,49 @@ exports[`Permission list page AccessError component should load access error co coreStart={ Object { "http": 1, + "uiSettings": Object { + "get": [MockFunction] { + "calls": Array [ + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + ], + }, + }, } } dataSourcePickerReadOnly={false} @@ -19,15 +62,112 @@ exports[`Permission list page AccessError component should load access error co } setDataSource={[MockFunction]} /> - - -

- Permissions -

-
-
+ + + Create from blank + + + Create from selection + + , + }, + ] + } + coreStart={ + Object { + "http": 1, + "uiSettings": Object { + "get": [MockFunction] { + "calls": Array [ + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + Array [ + "home:useNewHomePage", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + ], + }, + }, + } + } + count={0} + descriptionControls={ + Array [ + Object { + "isLoading": false, + "renderComponent": + Permissions are individual actions, such as cluster:admin/snapshot/restore, which lets you restore snapshots. Action groups +
+ are reusable collections of permissions, such as MANAGE_SNAPSHOTS, which lets you view, take, delete, and restore +
+ snapshots. You can often meet your security needs using the default action groups, but you might find it convenient to create +
+ your own. + +
, + }, + ] + } + fallBackComponent={ + + +

+ Permissions +

+
+
+ } + resourceType="permissions" + /> diff --git a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx index daa6a86fe..cb26109e9 100644 --- a/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx +++ b/public/apps/configuration/panels/permission-list/test/permission-list.test.tsx @@ -100,12 +100,15 @@ describe('Permission list page ', () => { describe('PermissionList', () => { const mockCoreStart = { + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, http: 1, }; it('render empty', () => { const component = shallow( { jest.spyOn(console, 'log').mockImplementationOnce(() => {}); shallow( { }); const component = shallow( { jest.spyOn(React, 'useState').mockImplementation(() => [[sampleActionGroup], jest.fn()]); const component = shallow( { describe('AccessError component', () => { const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; let component; beforeEach(() => { diff --git a/public/apps/configuration/panels/role-edit/role-edit.tsx b/public/apps/configuration/panels/role-edit/role-edit.tsx index e025cda07..6006600fc 100644 --- a/public/apps/configuration/panels/role-edit/role-edit.tsx +++ b/public/apps/configuration/panels/role-edit/role-edit.tsx @@ -59,6 +59,7 @@ import { generateResourceName } from '../../utils/resource-utils'; import { NameRow } from '../../utils/name-row'; import { DataSourceContext } from '../../app-router'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { PageHeader } from '../../header/header-components'; interface RoleEditDeps extends BreadcrumbsPageDependencies { action: 'create' | 'edit' | 'duplicate'; @@ -233,6 +234,21 @@ export function RoleEdit(props: RoleEditDeps) { const tenantOptions = tenantNames.map(stringToComboBoxOption); + const descriptionData = [ + { + renderComponent: ( + + Roles are the core way of controlling access to your cluster. Roles contain any + combination of cluster-wide permission, index- +
+ specific permissions, document- and field-level security, and tenants. Then you map users + to these roles so that users
+ gain those permissions. +
+ ), + }, + ]; + return ( <> - {props.buildBreadcrumbs(TITLE_TEXT_DICT[props.action])} - - - -

{TITLE_TEXT_DICT[props.action]}

-
- Roles are the core way of controlling access to your cluster. Roles contain any - combination of cluster-wide permission, index-specific permissions, document- and - field-level security, and tenants. Once you've created the role, you can map users to - the roles so that users gain those permissions.{' '} - -
-
+ + {' '} + + + +

{TITLE_TEXT_DICT[props.action]}

+
+ Roles are the core way of controlling access to your cluster. Roles contain any + combination of cluster-wide permission, index-specific permissions, document- and + field-level security, and tenants. Once you've created the role, you can map + users to the roles so that users gain those permissions.{' '} + +
+
+ + } + navigation={props.depsStart.navigation} + coreStart={props.coreStart} + descriptionControls={descriptionData} + resourceType={ResourceType.roles} + subAction={TITLE_TEXT_DICT[props.action]} + /> { const sampleSourceRole = 'role'; const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, + chrome: { + navGroup: { + getNavGroupEnabled: jest.fn().mockReturnValue(false), + }, + setBreadcrumbs: jest.fn(), + }, }; (fetchActionGroups as jest.Mock).mockResolvedValue({ @@ -100,7 +109,7 @@ describe('Role edit filtering', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - depsStart={{} as any} + depsStart={{ navigation: { ui: {} } } as any} params={{} as any} config={{} as any} /> @@ -156,7 +165,7 @@ describe('Role edit filtering', () => { sourceRoleName={sampleSourceRole} buildBreadcrumbs={buildBreadcrumbs} coreStart={mockCoreStart as any} - depsStart={{} as any} + depsStart={{ navigation: { ui: {} } } as any} params={{} as any} config={{} as any} /> diff --git a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx index d5051809e..fca458cb6 100644 --- a/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx +++ b/public/apps/configuration/panels/role-edit/test/role-edit.test.tsx @@ -47,6 +47,9 @@ describe('Role edit', () => { const sampleSourceRole = 'role'; const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; const useEffect = jest.spyOn(React, 'useEffect'); @@ -55,13 +58,11 @@ describe('Role edit', () => { it('basic rendering', () => { const action = 'create'; - const buildBreadcrumbs = jest.fn(); const component = shallow( { /> ); - expect(buildBreadcrumbs).toBeCalledTimes(1); expect(component.find(ClusterPermissionPanel).length).toBe(1); expect(component.find(IndexPermissionPanel).length).toBe(1); expect(component.find(TenantPanel).length).toBe(1); @@ -81,13 +81,11 @@ describe('Role edit', () => { useEffect.mockImplementationOnce((f) => f()); useState.mockImplementation((initialValue) => [initialValue, jest.fn()]); const action = 'edit'; - const buildBreadcrumbs = jest.fn(); const component = shallow( > = [ { @@ -198,6 +199,7 @@ export function RoleList(props: AppDependencies) { const [searchOptions, setSearchOptions] = useState({}); const [query, setQuery] = useState(null); + useEffect(() => { setSearchOptions({ onChange: (arg) => { @@ -260,6 +262,37 @@ export function RoleList(props: AppDependencies) { }); }, [roleData]); + const useUpdatedUX = props.coreStart.uiSettings.get('home:useNewHomePage'); + const buttonData = [ + { + label: 'Create role', + isLoading: false, + href: buildHashUrl(ResourceType.roles, Action.create), + fill: true, + iconType: 'plus', + iconSide: 'left', + type: 'button', + testId: 'create-role', + }, + ]; + const descriptionData = [ + { + isLoading: loading, + renderComponent: ( + + Roles are the core way of controlling access to your cluster. Roles contain any + combination of cluster-wide permission, index- +
+ specific permissions, document- and field-level security, and tenants. Then you map users + to these roles so that users
+ gain those permissions. +
+ ), + }, + ]; + + const roleLen = Query.execute(query || '', roleData).length; + return ( <> - - -

Roles

-
-
+ + +

Roles

+
+ + } + resourceType={ResourceType.roles} + count={roleData.length} + /> {loading ? ( ) : accessErrorFlag ? ( ) : ( - - - -

- Roles - - {' '} - ({Query.execute(query || '', roleData).length}) - -

-
- - Roles are the core way of controlling access to your cluster. Roles contain any - combination of cluster-wide permission, index-specific permissions, document- and - field-level security, and tenants. Then you map users to these roles so that users - gain those permissions. - -
- - - {actionsMenu} - - - Create role - - - - -
+ {useUpdatedUX ? null : ( + + + +

+ Roles + ({roleLen}) +

+
+ + Roles are the core way of controlling access to your cluster. Roles contain any + combination of cluster-wide permission, index-specific permissions, document- and + field-level security, and tenants. Then you map users to these roles so that users + gain those permissions. + +
+ + + {actionsMenu} + + + Create role + + + + +
+ )} {actionsMenu}] : undefined, + }} error={errorFlag ? 'Load data failed, please check console log for more detail.' : ''} message={showTableStatusMessage(loading, roleData)} /> diff --git a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx index a99689fd7..956f9ebe4 100644 --- a/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx +++ b/public/apps/configuration/panels/role-mapping/role-edit-mapped-user.tsx @@ -46,6 +46,7 @@ import { ExternalLink } from '../../utils/display-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; import { DataSourceContext } from '../../app-router'; import { getClusterInfo } from '../../../../utils/datasource-utils'; +import { PageHeader } from '../../header/header-components'; interface RoleEditMappedUserProps extends BreadcrumbsPageDependencies { roleName: string; @@ -55,6 +56,17 @@ const TITLE_TEXT_DICT = { mapuser: 'Map user', }; +const descriptionData = [ + { + renderComponent: ( + + Map users to this role to inherit role permissions. Two types of users are supported: user, + and backend role. + + ), + }, +]; + export function RoleEditMappedUser(props: RoleEditMappedUserProps) { const [internalUsers, setInternalUsers] = React.useState([]); const [externalIdentities, setExternalIdentities] = React.useState( @@ -150,16 +162,25 @@ export function RoleEditMappedUser(props: RoleEditMappedUserProps) { setDataSource={setDataSource} selectedDataSource={dataSource} /> - {props.buildBreadcrumbs(props.roleName, TITLE_TEXT_DICT[SubAction.mapuser])} - - - -

Map user

-
- Map users to this role to inherit role permissions. Two types of users are supported: - user, and backend role. -
-
+ + + +

Map user

+
+ Map users to this role to inherit role permissions. Two types of users are supported: + user, and backend role. +
+ + } + resourceType={ResourceType.roles} + subAction={TITLE_TEXT_DICT[SubAction.mapuser]} + pageTitle={props.roleName} + descriptionControls={descriptionData} + /> { const mockCoreStart = { http: 1, }; - const buildBreadcrumbs = jest.fn(); const useEffect = jest.spyOn(React, 'useEffect'); const useState = jest.spyOn(React, 'useState'); @@ -54,7 +53,6 @@ describe('Role mapping edit', () => { const component = shallow( { /> ); - expect(buildBreadcrumbs).toBeCalledTimes(1); expect(component.find(InternalUsersPanel).length).toBe(1); expect(component.find(ExternalIdentitiesPanel).length).toBe(1); }); @@ -79,7 +76,6 @@ describe('Role mapping edit', () => { shallow( { const component = shallow( { const component = shallow( , ]; const [actionsMenu] = useContextMenuState('Actions', {}, actionsMenuItems); + const useUpdatedUX = props.coreStart.uiSettings.get('home:useNewHomePage'); if (isReserved) { pageActions = Duplicate role; @@ -394,6 +396,54 @@ export function RoleView(props: RoleViewProps) { ); } + const reservedRoleButtons = [ + { + label: 'Duplicate role', + isLoading: false, + href: buildHashUrl(ResourceType.roles, Action.edit, props.roleName), + type: 'button', + fill: true, + }, + ]; + const roleButtons = [ + { + isLoading: false, + run: async () => { + try { + await requestDeleteRoles(props.coreStart.http, [props.roleName], dataSource.id); + setCrossPageToast(buildUrl(ResourceType.roles), { + id: 'deleteRole', + color: 'success', + title: `${props.roleName} deleted ${getClusterInfo(dataSourceEnabled, dataSource)}`, + }); + window.location.href = buildHashUrl(ResourceType.roles); + } catch (e) { + addToast(createUnknownErrorToast('deleteRole', 'delete role')); + } + }, + iconType: 'trash', + color: 'danger', + type: 'button', // this should be icon, but icons current do not support a border currently + testId: 'delete', + ariaLabel: 'delete', + }, + { + label: 'Duplicate', + isLoading: false, + href: duplicateRoleLink, + type: 'button', + }, + { + label: 'Edit role', + isLoading: false, + href: buildHashUrl(ResourceType.roles, Action.edit, props.roleName), + fill: true, + type: 'button', + }, + ]; + + const roleView = isReserved ? reservedRoleButtons : roleButtons; + return ( <> - {props.buildBreadcrumbs(props.roleName)} - - - - -

{props.roleName}

-
-
- - {pageActions} -
+ + + + +

{props.roleName}

+
+
+ {pageActions} +
+ + } + resourceType={ResourceType.roles} + subAction={props.roleName} + /> - - - -

- role -

-
-
- - - - - duplicate - - - delete - - - - - Edit role - - - - -
+ + + + +

+ role +

+
+
+ + + + + duplicate + + + delete + + + + + Edit role + + + + +
+ + } + resourceType="roles" + subAction="role" + /> - - - -

- role -

-
-
- - - - - duplicate - - - delete - - - - - Edit role - - - - -
+ + + + +

+ role +

+
+
+ + + + + duplicate + + + delete + + + + + Edit role + + + + +
+ + } + resourceType="roles" + subAction="role" + /> { const sampleRole = 'role'; const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, + chrome: { + navGroup: { getNavGroupEnabled: jest.fn().mockReturnValue(false) }, + setBreadcrumbs: jest.fn(), + }, }; const buildBreadcrumbs = jest.fn(); @@ -98,7 +105,6 @@ describe('Role view', () => { { /> ); - expect(buildBreadcrumbs).toBeCalledTimes(1); expect(component.find(EuiTabbedContent).length).toBe(1); const tabs = component.find(EuiTabbedContent).dive(); @@ -121,7 +126,6 @@ describe('Role view', () => { { throw new Error(); }); const spy = jest.spyOn(console, 'log').mockImplementationOnce(() => {}); - shallow( + render( @@ -265,18 +269,18 @@ describe('Role view', () => { }); it('delete role', () => { - const component = shallow( + const component = mount( ); - component.find('[data-test-subj="delete"]').simulate('click'); + component.find('[data-test-subj="delete"]').first().simulate('click'); expect(requestDeleteRoles).toBeCalled(); }); @@ -285,18 +289,18 @@ describe('Role view', () => { (requestDeleteRoles as jest.Mock).mockImplementationOnce(() => { throw new Error(); }); - const component = shallow( + const component = mount( ); - component.find('[data-test-subj="delete"]').simulate('click'); + component.find('[data-test-subj="delete"]').first().simulate('click'); expect(createUnknownErrorToast).toBeCalled(); }); }); diff --git a/public/apps/configuration/panels/tenant-list/manage_tab.tsx b/public/apps/configuration/panels/tenant-list/manage_tab.tsx index 1d9a4918f..adce0ec79 100644 --- a/public/apps/configuration/panels/tenant-list/manage_tab.tsx +++ b/public/apps/configuration/panels/tenant-list/manage_tab.tsx @@ -66,15 +66,15 @@ import { PageId } from '../../types'; import { useDeleteConfirmState } from '../../utils/delete-confirm-modal-utils'; import { showTableStatusMessage } from '../../utils/loading-spinner-utils'; import { useContextMenuState } from '../../utils/context-menu'; -import { generateResourceName } from '../../utils/resource-utils'; +import { generateResourceName, getBreadcrumbs } from '../../utils/resource-utils'; import { DocLinks } from '../../constants'; import { TenantList } from './tenant-list'; -import { getBreadcrumbs } from '../../app-router'; import { LocalCluster } from '../../../../utils/datasource-utils'; import { buildUrl } from '../../utils/url-builder'; import { CrossPageToast } from '../../cross-page-toast'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; import { AccessErrorComponent } from '../../access-error-component'; +import { HeaderButtonOrLink } from '../../header/header-components'; export function ManageTab(props: AppDependencies) { const setGlobalBreadcrumbs = flow(getBreadcrumbs, props.coreStart.chrome.setBreadcrumbs); @@ -503,6 +503,19 @@ export function ManageTab(props: AppDependencies) { /> ); } + const useUpdatedUX = props.coreStart.uiSettings.get('home:useNewHomePage'); + const createTenantButton = [ + { + id: 'createTenant', + label: 'Creat tenant', + iconType: 'plus', + iconSide: 'left', + isLoading: false, + run: () => showEditModal('', Action.create, ''), + type: 'button', + fill: true, + }, + ]; /* eslint-disable */ return ( <> @@ -528,6 +541,15 @@ export function ManageTab(props: AppDependencies) { {actionsMenu} + { useUpdatedUX ? + + : Create tenant - + } diff --git a/public/apps/configuration/panels/tenant-list/tenant-list.tsx b/public/apps/configuration/panels/tenant-list/tenant-list.tsx index f804f09d9..aa5ee9e40 100644 --- a/public/apps/configuration/panels/tenant-list/tenant-list.tsx +++ b/public/apps/configuration/panels/tenant-list/tenant-list.tsx @@ -32,6 +32,8 @@ import { DocLinks } from '../../constants'; import { getDashboardsInfo } from '../../../../utils/dashboards-info-utils'; import { LocalCluster } from '../../../../utils/datasource-utils'; import { SecurityPluginTopNavMenu } from '../../top-nav-menu'; +import { PageHeader } from '../../header/header-components'; +import { ResourceType } from '../../../../../common'; interface TenantListProps extends AppDependencies { tabID: string; @@ -127,6 +129,21 @@ export function TenantList(props: TenantListProps) { )); }; + const descriptionData = [ + { + renderComponent: ( + + Tenants in OpenSearch Dashboards are spaces for saving index patterns, visualizations, + dashboards, and other OpenSearch +
Dashboards objects. Tenants are useful for safely sharing your work with other + OpenSearch Dashboards users. You can control
+ which roles have access to a tenant and whether those roles have read or write access.{' '} + +
+ ), + }, + ]; + return ( <> {}} selectedDataSource={LocalCluster} /> - - -

Dashboards multi-tenancy

-
-
- - Tenants in OpenSearch Dashboards are spaces for saving index patterns, visualizations, - dashboards, and other OpenSearch Dashboards objects. Tenants are useful for safely sharing - your work with other OpenSearch Dashboards users. You can control which roles have access to - a tenant and whether those roles have read or write access.{' '} - - + + + +

Dashboards multi-tenancy

+
+
+ + Tenants in OpenSearch Dashboards are spaces for saving index patterns, visualizations, + dashboards, and other OpenSearch Dashboards objects. Tenants are useful for safely + sharing your work with other OpenSearch Dashboards users. You can control which roles + have access to a tenant and whether those roles have read or write access.{' '} + + + + } + resourceType={ResourceType.tenants} + /> {renderTabs()} {!isMultiTenancyEnabled && selectedTabId === 'Manage' && tenancyDisabledWarning} diff --git a/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx b/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx index 66117c398..a602f507c 100644 --- a/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx +++ b/public/apps/configuration/panels/tenant-list/test/tenant-list.test.tsx @@ -60,13 +60,13 @@ describe('Tenant list', () => { const setState = jest.fn(); const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, chrome: { setBreadcrumbs() { return 1; }, - navGroup: { - getNavGroupEnabled: jest.fn().mockReturnValue(false), - }, }, }; const config = { @@ -101,7 +101,7 @@ describe('Tenant list', () => { const component = shallow( @@ -121,7 +121,7 @@ describe('Tenant list', () => { shallow( @@ -151,7 +151,7 @@ describe('Tenant list', () => { shallow( @@ -171,7 +171,7 @@ describe('Tenant list', () => { shallow( @@ -196,7 +196,7 @@ describe('Tenant list', () => { shallow( @@ -215,7 +215,7 @@ describe('Tenant list', () => { const component = shallow( @@ -237,7 +237,7 @@ describe('Tenant list', () => { const component = shallow( @@ -290,7 +290,7 @@ describe('Tenant list', () => { const component = shallow( @@ -319,7 +319,7 @@ describe('Tenant list', () => { const component = shallow( @@ -352,7 +352,7 @@ describe('Tenant list', () => { const component = shallow( @@ -397,7 +397,7 @@ describe('Tenant list', () => { component = shallow( @@ -487,7 +487,7 @@ describe('Tenant list', () => { component = shallow( @@ -499,7 +499,7 @@ describe('Tenant list', () => { component = shallow( diff --git a/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap index d380f6153..33a5d8009 100644 --- a/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/get-started.test.tsx.snap @@ -31,19 +31,42 @@ exports[`Get started (landing page) renders when backend configuration is disabl } setDataSource={[MockFunction]} /> - - -

- Get started -

-
- -
+ + +

+ Get started +

+
+ + + } + resourceType="getStarted" + /> @@ -310,19 +333,42 @@ exports[`Get started (landing page) renders when backend configuration is enable } setDataSource={[MockFunction]} /> - - -

- Get started -

-
- -
+ + +

+ Get started +

+
+ + + } + resourceType="getStarted" + /> diff --git a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap index 52233ddc7..cdc9a6f17 100644 --- a/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/role-list.test.tsx.snap @@ -7,10 +7,29 @@ exports[`Role list AccessError component should load access error component 1`] coreStart={ Object { "http": 1, + "uiSettings": Object { + "get": [MockFunction] { + "calls": Array [ + Array [ + "home:useNewHomePage", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + ], + }, + }, } } dataSourcePickerReadOnly={false} - navigation={Object {}} + depsStart={ + Object { + "navigation": Object {}, + } + } params={Object {}} selectedDataSource={ Object { @@ -19,15 +38,76 @@ exports[`Role list AccessError component should load access error component 1`] } setDataSource={[MockFunction]} /> - - -

- Roles -

-
-
+ + Roles are the core way of controlling access to your cluster. Roles contain any combination of cluster-wide permission, index- +
+ specific permissions, document- and field-level security, and tenants. Then you map users to these roles so that users +
+ gain those permissions. + +
, + }, + ] + } + fallBackComponent={ + + +

+ Roles +

+
+
+ } + navigation={Object {}} + resourceType="roles" + /> - - ( + ( 0 ) @@ -150,7 +229,11 @@ exports[`Role list AccessError component should load access error component 1`] message="No items found" pagination={true} responsive={true} - search={Object {}} + search={ + Object { + "toolsRight": undefined, + } + } selection={ Object { "onSelectionChange": [MockFunction], diff --git a/public/apps/configuration/panels/test/__snapshots__/user-list.test.tsx.snap b/public/apps/configuration/panels/test/__snapshots__/user-list.test.tsx.snap index 82f4af425..105e6dd38 100644 --- a/public/apps/configuration/panels/test/__snapshots__/user-list.test.tsx.snap +++ b/public/apps/configuration/panels/test/__snapshots__/user-list.test.tsx.snap @@ -11,10 +11,29 @@ exports[`User list AccessError component should load access error component 1`] "serverBasePath": "", }, }, + "uiSettings": Object { + "get": [MockFunction] { + "calls": Array [ + Array [ + "home:useNewHomePage", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + ], + }, + }, } } dataSourcePickerReadOnly={false} - navigation={Object {}} + depsStart={ + Object { + "navigation": Object {}, + } + } params={Object {}} selectedDataSource={ Object { @@ -23,15 +42,87 @@ exports[`User list AccessError component should load access error component 1`] } setDataSource={[MockFunction]} /> - - -

- Internal users -

-
-
+ + The Security plugin includes an internal user database. Use this database in place of, or in addition to, an external +
+ authentication system such as LDAP server or Active Directory. You can map an internal user to a role from + + + Roles + + . First, click +
+ into the detail page of the role. Then, under “Mapped users”, click “Manage mapping” + + , + }, + ] + } + fallBackComponent={ + + +

+ Internal users +

+
+
+ } + navigation={Object {}} + resourceType="users" + /> diff --git a/public/apps/configuration/panels/test/role-list.test.tsx b/public/apps/configuration/panels/test/role-list.test.tsx index c5f693633..4fd5e7ce6 100644 --- a/public/apps/configuration/panels/test/role-list.test.tsx +++ b/public/apps/configuration/panels/test/role-list.test.tsx @@ -48,6 +48,9 @@ describe('Role list', () => { const setState = jest.fn(); const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; beforeEach(() => { @@ -72,7 +75,7 @@ describe('Role list', () => { const component = shallow( @@ -91,8 +94,8 @@ describe('Role list', () => { shallow( @@ -125,7 +128,7 @@ describe('Role list', () => { shallow( @@ -144,7 +147,7 @@ describe('Role list', () => { shallow( @@ -167,7 +170,7 @@ describe('Role list', () => { shallow( @@ -200,7 +203,7 @@ describe('Role list', () => { component = shallow( @@ -236,7 +239,7 @@ describe('Role list', () => { const wrapper = shallow( @@ -255,7 +258,7 @@ describe('Role list', () => { const wrapper = shallow( @@ -285,7 +288,7 @@ describe('Role list', () => { component = shallow( diff --git a/public/apps/configuration/panels/test/user-list.test.tsx b/public/apps/configuration/panels/test/user-list.test.tsx index 980e8d21c..3b2e062c2 100644 --- a/public/apps/configuration/panels/test/user-list.test.tsx +++ b/public/apps/configuration/panels/test/user-list.test.tsx @@ -90,6 +90,9 @@ describe('User list', () => { describe('UserList', () => { const mockCoreStart = { http: 1, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; const setState = jest.fn(); jest.spyOn(React, 'useState').mockImplementation((initValue) => [initValue, setState]); @@ -98,7 +101,7 @@ describe('User list', () => { const component = shallow( @@ -112,7 +115,7 @@ describe('User list', () => { shallow( @@ -132,7 +135,7 @@ describe('User list', () => { shallow( @@ -146,7 +149,7 @@ describe('User list', () => { shallow( @@ -171,7 +174,7 @@ describe('User list', () => { shallow( @@ -194,6 +197,9 @@ describe('User list', () => { serverBasePath: '', }, }, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; let component; const mockUserListingData: InternalUsersListing = { @@ -215,7 +221,7 @@ describe('User list', () => { component = shallow( @@ -244,6 +250,9 @@ describe('User list', () => { serverBasePath: '', }, }, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; let component; beforeEach(() => { @@ -260,7 +269,7 @@ describe('User list', () => { component = shallow( diff --git a/public/apps/configuration/panels/user-list.tsx b/public/apps/configuration/panels/user-list.tsx index 1d1aedf3d..545f9071b 100644 --- a/public/apps/configuration/panels/user-list.tsx +++ b/public/apps/configuration/panels/user-list.tsx @@ -52,6 +52,7 @@ import { buildHashUrl } from '../utils/url-builder'; import { DataSourceContext } from '../app-router'; import { SecurityPluginTopNavMenu } from '../top-nav-menu'; import { AccessErrorComponent } from '../access-error-component'; +import { PageHeader } from '../header/header-components'; export function dictView(items: Dictionary) { if (isEmpty(items)) { @@ -204,6 +205,36 @@ export function UserList(props: AppDependencies) { const [actionsMenu, closeActionsMenu] = useContextMenuState('Actions', {}, actionsMenuItems); + const useUpdatedUX = props.coreStart.uiSettings.get('home:useNewHomePage'); + const buttonData = [ + { + label: 'Create internal user', + isLoading: false, + href: buildHashUrl(ResourceType.users, Action.create), + fill: true, + iconType: 'plus', + iconSide: 'left', + type: 'button', + testId: 'create-user', + }, + ]; + const descriptionData = [ + { + isLoading: loading, + renderComponent: ( + + The Security plugin includes an internal user database. Use this database in place of, or + in addition to, an external
authentication system such as LDAP server or Active + Directory. You can map an internal user to a role from{' '} + Roles + . First, click
into the detail page of the role. Then, under “Mapped users”, click + “Manage mapping” +
+ ), + }, + ]; + + const userLen = Query.execute(query || '', userData).length; return ( <> - - -

Internal users

-
-
+ + +

Internal users

+
+ + } + resourceType={ResourceType.users} + count={userData.length} + /> {loading ? ( ) : accessErrorFlag ? ( ) : ( - - - -

- Internal users - - {' '} - ({Query.execute(query || '', userData).length}) - -

-
- - The Security plugin includes an internal user database. Use this database in place - of, or in addition to, an external authentication system such as LDAP server or - Active Directory. You can map an internal user to a role from{' '} - Roles - . First, click into the detail page of the role. Then, under “Mapped users”, click - “Manage mapping” - -
- - - {actionsMenu} - - - Create internal user - - - - -
+ {useUpdatedUX ? null : ( + + + +

+ Internal users + ({userLen}) +

+
+ + The Security plugin includes an internal user database. Use this database in place + of, or in addition to, an external authentication system such as LDAP server or + Active Directory. You can map an internal user to a role from{' '} + Roles + . First, click into the detail page of the role. Then, under “Mapped users”, click + “Manage mapping” + +
+ + + {actionsMenu} + + + Create internal user + + + + +
+ )} {actionsMenu}] : undefined, }} // @ts-ignore selection={{ onSelectionChange: setSelection }} diff --git a/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap b/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap index d5ee503be..85c8181a7 100644 --- a/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap +++ b/public/apps/configuration/test/__snapshots__/app-router.test.tsx.snap @@ -24,30 +24,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -66,30 +73,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -108,30 +122,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -150,30 +171,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -192,30 +220,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -234,30 +269,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -276,30 +318,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -318,30 +367,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -360,30 +416,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, @@ -402,30 +465,37 @@ exports[`SecurityPluginTopNavMenu renders DataSourceMenu when dataSource is enab items={ Array [ Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Get started with access control", "href": "/getstarted", "name": "Get Started", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Authentication and authorization", "href": "/auth", "name": "Authentication", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Roles", "href": "/roles", "name": "Roles", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Internal users", "href": "/users", "name": "Internal users", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Permissions", "href": "/permissions", "name": "Permissions", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Dashboard multi-tenancy", "href": "/tenants", "name": "Tenants", }, Object { + "breadCrumbDisplayNameWithoutSecurityBase": "Audit logs", "href": "/auditLogging", "name": "Audit logs", }, diff --git a/public/apps/configuration/test/app-router.test.tsx b/public/apps/configuration/test/app-router.test.tsx index 4bb61888d..ff3ededf0 100644 --- a/public/apps/configuration/test/app-router.test.tsx +++ b/public/apps/configuration/test/app-router.test.tsx @@ -35,6 +35,9 @@ describe('SecurityPluginTopNavMenu', () => { getNavGroupEnabled: jest.fn().mockReturnValue(false), }, }, + uiSettings: { + get: jest.fn().mockReturnValue(false), + }, }; const securityPluginConfigMock = { diff --git a/public/apps/configuration/types.ts b/public/apps/configuration/types.ts index 967072d18..6736d8e12 100644 --- a/public/apps/configuration/types.ts +++ b/public/apps/configuration/types.ts @@ -46,6 +46,7 @@ export enum TenantPermissionType { export interface RouteItem { name: string; href: string; + breadCrumbDisplayNameWithoutSecurityBase: string; } export interface DataObject { diff --git a/public/apps/configuration/utils/context-menu.tsx b/public/apps/configuration/utils/context-menu.tsx index b0a199a89..aea6ae5a8 100644 --- a/public/apps/configuration/utils/context-menu.tsx +++ b/public/apps/configuration/utils/context-menu.tsx @@ -27,17 +27,18 @@ import React from 'react'; export function useContextMenuState( buttonText: string, buttonProps: EuiButtonProps, - children: React.ReactElement[] + children: React.ReactElement[], + useUpdatedUX?: boolean ): [React.ReactElement, () => void] { const [isContextMenuOpen, setContextMenuOpen] = useState(false); const closeContextMenu = () => setContextMenuOpen(false); const button = ( { - setContextMenuOpen(true); + setContextMenuOpen(!isContextMenuOpen); }} {...buttonProps} > diff --git a/public/apps/configuration/utils/resource-utils.tsx b/public/apps/configuration/utils/resource-utils.tsx index ac30e4251..e5d1156ad 100644 --- a/public/apps/configuration/utils/resource-utils.tsx +++ b/public/apps/configuration/utils/resource-utils.tsx @@ -13,6 +13,11 @@ * permissions and limitations under the License. */ +import { EuiBreadcrumb } from '@elastic/eui'; +import { ResourceType } from '../../../../common'; +import { buildHashUrl } from './url-builder'; +import { ROUTE_MAP } from '../app-router'; + export function generateResourceName(action: string, sourceResourceName: string): string { switch (action) { case 'edit': @@ -27,3 +32,49 @@ export function generateResourceName(action: string, sourceResourceName: string) export function getResourceUrl(endpoint: string, resourceName: string) { return endpoint + '/' + encodeURIComponent(resourceName); } + +export function getBreadcrumbs( + includeSecurityBase: boolean, + resourceType?: ResourceType, + pageTitle?: string, + subAction?: string, + count?: number +): EuiBreadcrumb[] { + const breadcrumbs: EuiBreadcrumb[] = includeSecurityBase + ? [ + { + text: 'Security', + href: buildHashUrl(), + }, + ] + : []; + + if (resourceType) { + if (includeSecurityBase) { + breadcrumbs.push({ + text: ROUTE_MAP[resourceType].name, + href: buildHashUrl(resourceType), + }); + } else { + breadcrumbs.push({ + text: count + ? `${ROUTE_MAP[resourceType].breadCrumbDisplayNameWithoutSecurityBase} (${count})` + : ROUTE_MAP[resourceType].breadCrumbDisplayNameWithoutSecurityBase, + href: buildHashUrl(resourceType), + }); + } + } + + if (pageTitle) { + breadcrumbs.push({ + text: pageTitle, + }); + } + + if (subAction) { + breadcrumbs.push({ + text: subAction, + }); + } + return breadcrumbs; +} diff --git a/public/apps/configuration/utils/test/resource-utils.test.tsx b/public/apps/configuration/utils/test/resource-utils.test.tsx index ab0dac6b7..b69b899d0 100644 --- a/public/apps/configuration/utils/test/resource-utils.test.tsx +++ b/public/apps/configuration/utils/test/resource-utils.test.tsx @@ -13,24 +13,75 @@ * permissions and limitations under the License. */ -import { generateResourceName } from '../resource-utils'; +import { ResourceType } from '../../../../../common'; +import { generateResourceName, getBreadcrumbs } from '../resource-utils'; -describe('generateResourceName', () => { - it('edit should return same name', () => { +describe('ResourceUtilsTests', () => { + it('generateResourceName edit should return same name', () => { const result = generateResourceName('edit', 'user1'); expect(result).toBe('user1'); }); - it('duplicate should append _copy suffix', () => { + it('generateResourceName duplicate should append _copy suffix', () => { const result = generateResourceName('duplicate', 'role1'); expect(result).toBe('role1_copy'); }); - it('other action should return empty string', () => { + it('generateResourceName other action should return empty string', () => { const result = generateResourceName('create', 'tenant1'); expect(result).toBe(''); }); + + it('getBreadcrumbs with security base should include security breadcrumb', () => { + const breadcrumbs = getBreadcrumbs(true); + expect(breadcrumbs).toEqual([ + { + href: '#/', + text: 'Security', + }, + ]); + }); + + it('getBreadcrumbs without security base should not include security breadcrumb', () => { + const breadcrumbs = getBreadcrumbs(false); + expect(breadcrumbs).toEqual([]); + }); + + it('getBreadcrumbs without security base should use display name', () => { + const breadcrumbs = getBreadcrumbs(false, ResourceType.auth); + expect(breadcrumbs).toEqual([{ href: '#/auth', text: 'Authentication and authorization' }]); + }); + + it('getBreadcrumbs with security base should use regular name and have security base', () => { + const breadcrumbs = getBreadcrumbs(true, ResourceType.auth); + expect(breadcrumbs).toEqual([ + { href: '#/', text: 'Security' }, + { href: '#/auth', text: 'Authentication' }, + ]); + }); + + it('getBreadcrumbs with title and subactions should include those breadcrumbs', () => { + const breadcrumbs = getBreadcrumbs(false, ResourceType.roles, 'Derek', 'Map user'); + expect(breadcrumbs).toEqual([ + { href: '#/roles', text: 'Roles' }, + { text: 'Derek' }, + { text: 'Map user' }, + ]); + }); + + it('getBreadcrumbs should show breadcrumb with count when security base is not included', () => { + const breadcrumbs = getBreadcrumbs(false, ResourceType.roles, undefined, undefined, 30); + expect(breadcrumbs).toEqual([{ href: '#/roles', text: 'Roles (30)' }]); + }); + + it('getBreadcrumbs should not show breadcrumb with count when security base is included', () => { + const breadcrumbs = getBreadcrumbs(true, ResourceType.roles, undefined, undefined, 30); + expect(breadcrumbs).toEqual([ + { href: '#/', text: 'Security' }, + { href: '#/roles', text: 'Roles' }, + ]); + }); });