From 9a275de0f99cb616048cdb1409eb1018daf4b196 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Fri, 4 Jun 2021 14:28:11 -0400 Subject: [PATCH 01/15] [App Search] Initial logic for Crawler Overview (#101176) * New CrawlerOverview component * CrawlerRouter should use CrawlerOverview in dev mode * New CrawlerOverviewLogic * New crawler route * Display domains data for CrawlerOverview in EuiCode * Update types * Clean up tests for Crawler utils * Better todo commenting for CrawlerOverview tests * Remove unused div from CrawlerOverview * Rename CrawlerOverviewLogic.actios.setCrawlerData to onFetchCrawlerData * Cleaning up CrawlerOverviewLogic * Cleaning up CrawlerOverviewLogic tests * Fix CrawlerPolicies capitalization * Add Loading UX * Cleaning up afterEachs across Crawler tests --- .../crawler/crawler_landing.test.tsx | 5 +- .../crawler/crawler_overview.test.tsx | 66 ++++++++++ .../components/crawler/crawler_overview.tsx | 41 ++++++ .../crawler/crawler_overview_logic.test.ts | 121 ++++++++++++++++++ .../crawler/crawler_overview_logic.ts | 64 +++++++++ .../crawler/crawler_router.test.tsx | 15 ++- .../components/crawler/crawler_router.tsx | 3 +- .../app_search/components/crawler/types.ts | 67 ++++++++++ .../components/crawler/utils.test.ts | 93 ++++++++++++++ .../app_search/components/crawler/utils.ts | 55 ++++++++ .../server/routes/app_search/crawler.test.ts | 35 +++++ .../server/routes/app_search/crawler.ts | 29 +++++ .../server/routes/app_search/index.ts | 2 + 13 files changed, 589 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.test.tsx index 9591b82773b9fe..132579bad8bdc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.test.tsx @@ -19,14 +19,11 @@ describe('CrawlerLanding', () => { let wrapper: ShallowWrapper; beforeEach(() => { + jest.clearAllMocks(); setMockValues({ ...mockEngineValues }); wrapper = shallow(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('contains an external documentation link', () => { const externalDocumentationLink = wrapper.find('[data-test-subj="CrawlerDocumentationLink"]'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx new file mode 100644 index 00000000000000..eb30ae867b4b6d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { rerender, setMockActions, setMockValues } from '../../../__mocks__'; +import '../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiCode } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; + +import { CrawlerOverview } from './crawler_overview'; + +const actions = { + fetchCrawlerData: jest.fn(), +}; + +const values = { + dataLoading: false, + domains: [], +}; + +describe('CrawlerOverview', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find(EuiCode)).toHaveLength(1); + }); + + it('calls fetchCrawlerData on page load', () => { + expect(actions.fetchCrawlerData).toHaveBeenCalledTimes(1); + }); + + // TODO after DomainsTable is built in a future PR + // it('contains a DomainsTable', () => {}) + + // TODO after CrawlRequestsTable is built in a future PR + // it('containss a CrawlRequestsTable,() => {}) + + // TODO after AddDomainForm is built in a future PR + // it('contains an AddDomainForm' () => {}) + + // TODO after empty state is added in a future PR + // it('has an empty state', () => {} ) + + it('shows an empty state when data is loading', () => { + setMockValues({ dataLoading: true }); + rerender(wrapper); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx new file mode 100644 index 00000000000000..5eeaaaef696058 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiCode, EuiPageHeader } from '@elastic/eui'; + +import { FlashMessages } from '../../../shared/flash_messages'; + +import { Loading } from '../../../shared/loading'; + +import { CRAWLER_TITLE } from './constants'; +import { CrawlerOverviewLogic } from './crawler_overview_logic'; + +export const CrawlerOverview: React.FC = () => { + const { dataLoading, domains } = useValues(CrawlerOverviewLogic); + + const { fetchCrawlerData } = useActions(CrawlerOverviewLogic); + + useEffect(() => { + fetchCrawlerData(); + }, []); + + if (dataLoading) { + return ; + } + + return ( + <> + + + {JSON.stringify(domains, null, 2)} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts new file mode 100644 index 00000000000000..766f5dcfa02dc3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import '../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { CrawlerOverviewLogic } from './crawler_overview_logic'; +import { CrawlerPolicies, CrawlerRules, CrawlRule } from './types'; + +const DEFAULT_VALUES = { + dataLoading: true, + domains: [], +}; + +const DEFAULT_CRAWL_RULE: CrawlRule = { + id: '-', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.regex, + pattern: '.*', +}; + +describe('CrawlerOverviewLogic', () => { + const { mount } = new LogicMounter(CrawlerOverviewLogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(CrawlerOverviewLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onFetchCrawlerData', () => { + const crawlerData = { + domains: [ + { + id: '507f1f77bcf86cd799439011', + createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', + url: 'moviedatabase.com', + documentCount: 13, + sitemaps: [], + entryPoints: [], + crawlRules: [], + defaultCrawlRule: DEFAULT_CRAWL_RULE, + }, + ], + }; + + beforeEach(() => { + CrawlerOverviewLogic.actions.onFetchCrawlerData(crawlerData); + }); + + it('should set all received data as top-level values', () => { + expect(CrawlerOverviewLogic.values.domains).toEqual(crawlerData.domains); + }); + + it('should set dataLoading to false', () => { + expect(CrawlerOverviewLogic.values.dataLoading).toEqual(false); + }); + }); + }); + + describe('listeners', () => { + describe('fetchCrawlerData', () => { + it('calls onFetchCrawlerData with retrieved data that has been converted from server to client', async () => { + jest.spyOn(CrawlerOverviewLogic.actions, 'onFetchCrawlerData'); + + http.get.mockReturnValue( + Promise.resolve({ + domains: [ + { + id: '507f1f77bcf86cd799439011', + name: 'moviedatabase.com', + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + }, + ], + }) + ); + CrawlerOverviewLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/crawler'); + expect(CrawlerOverviewLogic.actions.onFetchCrawlerData).toHaveBeenCalledWith({ + domains: [ + { + id: '507f1f77bcf86cd799439011', + createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', + url: 'moviedatabase.com', + documentCount: 13, + sitemaps: [], + entryPoints: [], + crawlRules: [], + }, + ], + }); + }); + + it('calls flashApiErrors when there is an error', async () => { + http.get.mockReturnValue(Promise.reject('error')); + CrawlerOverviewLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts new file mode 100644 index 00000000000000..6f04ade5962ebb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors } from '../../../shared/flash_messages'; + +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; + +import { CrawlerData, CrawlerDataFromServer, CrawlerDomain } from './types'; +import { crawlerDataServerToClient } from './utils'; + +interface CrawlerOverviewValues { + dataLoading: boolean; + domains: CrawlerDomain[]; +} + +interface CrawlerOverviewActions { + fetchCrawlerData(): void; + onFetchCrawlerData(data: CrawlerData): { data: CrawlerData }; +} + +export const CrawlerOverviewLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'], + actions: { + fetchCrawlerData: true, + onFetchCrawlerData: (data) => ({ data }), + }, + reducers: { + dataLoading: [ + true, + { + onFetchCrawlerData: () => false, + }, + ], + domains: [ + [], + { + onFetchCrawlerData: (_, { data: { domains } }) => domains, + }, + ], + }, + listeners: ({ actions }) => ({ + fetchCrawlerData: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}/crawler`); + const crawlerData = crawlerDataServerToClient(response as CrawlerDataFromServer); + actions.onFetchCrawlerData(crawlerData); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx index 6aa9ca8c4feb1d..351f5474478033 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx @@ -14,21 +14,32 @@ import { Switch } from 'react-router-dom'; import { shallow } from 'enzyme'; import { CrawlerLanding } from './crawler_landing'; +import { CrawlerOverview } from './crawler_overview'; import { CrawlerRouter } from './crawler_router'; describe('CrawlerRouter', () => { + const OLD_ENV = process.env; + beforeEach(() => { + jest.clearAllMocks(); setMockValues({ ...mockEngineValues }); }); afterEach(() => { - jest.clearAllMocks(); + process.env = OLD_ENV; }); - it('renders a landing page', () => { + it('renders a landing page by default', () => { const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); expect(wrapper.find(CrawlerLanding)).toHaveLength(1); }); + + it('renders a crawler overview in dev', () => { + process.env.NODE_ENV = 'development'; + const wrapper = shallow(); + + expect(wrapper.find(CrawlerOverview)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx index fcc949de7d8b4b..926c45b4379377 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx @@ -14,13 +14,14 @@ import { getEngineBreadcrumbs } from '../engine'; import { CRAWLER_TITLE } from './constants'; import { CrawlerLanding } from './crawler_landing'; +import { CrawlerOverview } from './crawler_overview'; export const CrawlerRouter: React.FC = () => { return ( - + {process.env.NODE_ENV === 'development' ? : } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts new file mode 100644 index 00000000000000..f895e8f01e399f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum CrawlerPolicies { + allow = 'allow', + deny = 'deny', +} + +export enum CrawlerRules { + beginsWith = 'begins', + endsWith = 'ends', + contains = 'contains', + regex = 'regex', +} + +export interface CrawlRule { + id: string; + policy: CrawlerPolicies; + rule: CrawlerRules; + pattern: string; +} + +export interface EntryPoint { + id: string; + value: string; +} + +export interface Sitemap { + id: string; + url: string; +} + +export interface CrawlerDomain { + createdOn: string; + documentCount: number; + id: string; + lastCrawl?: string; + url: string; + crawlRules: CrawlRule[]; + defaultCrawlRule?: CrawlRule; + entryPoints: EntryPoint[]; + sitemaps: Sitemap[]; +} + +export interface CrawlerDomainFromServer { + id: string; + name: string; + created_on: string; + last_visited_at?: string; + document_count: number; + crawl_rules: CrawlRule[]; + default_crawl_rule?: CrawlRule; + entry_points: EntryPoint[]; + sitemaps: Sitemap[]; +} + +export interface CrawlerData { + domains: CrawlerDomain[]; +} + +export interface CrawlerDataFromServer { + domains: CrawlerDomainFromServer[]; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts new file mode 100644 index 00000000000000..6e2dd7c826b70a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CrawlerPolicies, CrawlerRules, CrawlRule, CrawlerDomainFromServer } from './types'; + +import { crawlerDomainServerToClient, crawlerDataServerToClient } from './utils'; + +const DEFAULT_CRAWL_RULE: CrawlRule = { + id: '-', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.regex, + pattern: '.*', +}; + +describe('crawlerDomainServerToClient', () => { + it('converts the API payload into properties matching our code style', () => { + const id = '507f1f77bcf86cd799439011'; + const name = 'moviedatabase.com'; + + const defaultServerPayload = { + id, + name, + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + }; + + const defaultClientPayload = { + id, + createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', + url: name, + documentCount: 13, + sitemaps: [], + entryPoints: [], + crawlRules: [], + }; + + expect(crawlerDomainServerToClient(defaultServerPayload)).toStrictEqual(defaultClientPayload); + expect( + crawlerDomainServerToClient({ + ...defaultServerPayload, + last_visited_at: 'Mon, 31 Aug 2020 17:00:00 +0000', + }) + ).toStrictEqual({ ...defaultClientPayload, lastCrawl: 'Mon, 31 Aug 2020 17:00:00 +0000' }); + expect( + crawlerDomainServerToClient({ + ...defaultServerPayload, + default_crawl_rule: DEFAULT_CRAWL_RULE, + }) + ).toStrictEqual({ ...defaultClientPayload, defaultCrawlRule: DEFAULT_CRAWL_RULE }); + }); +}); + +describe('crawlerDataServerToClient', () => { + it('converts all domains from the server form to their client form', () => { + const domains: CrawlerDomainFromServer[] = [ + { + id: 'x', + name: 'moviedatabase.com', + document_count: 13, + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + sitemaps: [], + entry_points: [], + crawl_rules: [], + default_crawl_rule: DEFAULT_CRAWL_RULE, + }, + { + id: 'y', + name: 'swiftype.com', + last_visited_at: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 40, + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + sitemaps: [], + entry_points: [], + crawl_rules: [], + }, + ]; + + const output = crawlerDataServerToClient({ + domains, + }); + + expect(output.domains).toHaveLength(2); + expect(output.domains[0]).toEqual(crawlerDomainServerToClient(domains[0])); + expect(output.domains[1]).toEqual(crawlerDomainServerToClient(domains[1])); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts new file mode 100644 index 00000000000000..e89c549261fcae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CrawlerDomain, + CrawlerDomainFromServer, + CrawlerData, + CrawlerDataFromServer, +} from './types'; + +export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): CrawlerDomain { + const { + id, + name, + sitemaps, + created_on: createdOn, + last_visited_at: lastCrawl, + document_count: documentCount, + crawl_rules: crawlRules, + default_crawl_rule: defaultCrawlRule, + entry_points: entryPoints, + } = payload; + + const clientPayload: CrawlerDomain = { + id, + url: name, + documentCount, + createdOn, + crawlRules, + sitemaps, + entryPoints, + }; + + if (lastCrawl) { + clientPayload.lastCrawl = lastCrawl; + } + + if (defaultCrawlRule) { + clientPayload.defaultCrawlRule = defaultCrawlRule; + } + + return clientPayload; +} + +export function crawlerDataServerToClient(payload: CrawlerDataFromServer): CrawlerData { + const { domains } = payload; + + return { + domains: domains.map((domain) => crawlerDomainServerToClient(domain)), + }; +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts new file mode 100644 index 00000000000000..626a107b6942ba --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; + +import { registerCrawlerRoutes } from './crawler'; + +describe('crawler routes', () => { + describe('GET /api/app_search/engines/{name}/crawler', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/crawler', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts new file mode 100644 index 00000000000000..15b8340b07d4eb --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerCrawlerRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{name}/crawler', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler', + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 6ccdce0935d935..2442b61c632c1a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -9,6 +9,7 @@ import { RouteDependencies } from '../../plugin'; import { registerAnalyticsRoutes } from './analytics'; import { registerApiLogsRoutes } from './api_logs'; +import { registerCrawlerRoutes } from './crawler'; import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; @@ -42,4 +43,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerResultSettingsRoutes(dependencies); registerApiLogsRoutes(dependencies); registerOnboardingRoutes(dependencies); + registerCrawlerRoutes(dependencies); }; From 77533da2be7470238474faf7efe4ce351a014a4e Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 4 Jun 2021 14:32:17 -0400 Subject: [PATCH 02/15] [App Search] 100% code coverage plus fix console error (#101407) --- .../__mocks__/flash_messages_logic.mock.ts | 1 + .../app_search/components/library/library.tsx | 2 ++ .../applications/app_search/index.test.tsx | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts index 17e22e6f23daf3..6c31927cd75b09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts @@ -15,6 +15,7 @@ export const mockFlashMessagesActions = { clearFlashMessages: jest.fn(), setQueuedMessages: jest.fn(), clearQueuedMessages: jest.fn(), + dismissToastMessage: jest.fn(), }; export const mockFlashMessageHelpers = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 5d619297702994..b9d3dbd9ee4129 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -5,6 +5,8 @@ * 2.0. */ +/* istanbul ignore file */ + import React, { useState } from 'react'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 287d46c2dec75c..8d33bd2d130ec5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -26,6 +26,7 @@ import { EngineRouter, EngineNav } from './components/engine'; import { EngineCreation } from './components/engine_creation'; import { EnginesOverview } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; +import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; import { RoleMappingsRouter } from './components/role_mappings'; import { SetupGuide } from './components/setup_guide'; @@ -147,6 +148,28 @@ describe('AppSearchConfigured', () => { }); }); }); + + describe('library', () => { + it('renders a library page in development', () => { + const OLD_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + + rerender(wrapper); + + expect(wrapper.find(Library)).toHaveLength(1); + process.env.NODE_ENV = OLD_ENV; + }); + + it("doesn't in production", () => { + const OLD_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + rerender(wrapper); + + expect(wrapper.find(Library)).toHaveLength(0); + process.env.NODE_ENV = OLD_ENV; + }); + }); }); describe('AppSearchNav', () => { From e565b22ab372760bb13350768c9f624ce26dc981 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 4 Jun 2021 19:56:52 +0100 Subject: [PATCH 03/15] chore(NA): upgrade bazel rules nodejs to v3.5.1 (#101412) --- WORKSPACE.bazel | 6 +++--- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index d80ad948cbb553..acb62043a15ca7 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "10f534e1c80f795cffe1f2822becd4897754d18564612510c59b3c73544ae7c6", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.0/rules_nodejs-3.5.0.tar.gz"], + sha256 = "4a5d654a4ccd4a4c24eca5d319d85a88a650edf119601550c95bf400c8cc897e", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.5.1/rules_nodejs-3.5.1.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.5.0") +check_rules_nodejs_version(minimum_version_string = "3.5.1") # Setup the Node.js toolchain for the architectures we want to support # diff --git a/package.json b/package.json index a2499d85247d73..65cb1e51866df5 100644 --- a/package.json +++ b/package.json @@ -441,7 +441,7 @@ "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", "@bazel/ibazel": "^0.15.10", - "@bazel/typescript": "^3.5.0", + "@bazel/typescript": "^3.5.1", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/apm-rum": "^5.6.1", diff --git a/yarn.lock b/yarn.lock index c5255bc4d0d305..83ab15d1f68d1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1204,10 +1204,10 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f" integrity sha512-0v+OwCQ6fsGFa50r6MXWbUkSGuWOoZ22K4pMSdtWiL5LKFIE4kfmMmtQS+M7/ICNwk2EIYob+NRreyi/DGUz5A== -"@bazel/typescript@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.0.tgz#605493f4f0a5297df8a7fcccb86a1a80ea2090bb" - integrity sha512-BtGFp4nYFkQTmnONCzomk7dkmOwaINBL3piq+lykBlcc6UxLe9iCAnZpOyPypB1ReN3k3SRNAa53x6oGScQxMg== +"@bazel/typescript@^3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.1.tgz#c6027d683adeefa2c3cebfa3ed5efa17c405a63b" + integrity sha512-dU5sGgaGdFWV1dJ1B+9iFbttgcKtmob+BvlM8mY7Nxq4j7/wVbgPjiVLOBeOD7kpzYep8JHXfhAokHt486IG+Q== dependencies: protobufjs "6.8.8" semver "5.6.0" From 137778d1240d56ca6b5aab0eea82f97ef5936d44 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Fri, 4 Jun 2021 15:21:54 -0400 Subject: [PATCH 04/15] [Fleet] Show callout & CTA in add agent flyout if no enrollment keys (#100599) ## Summary Fixes https://github.com/elastic/kibana/issues/91454 ### If there are no enrollment tokens for a policy, show help text & button to create one. https://user-images.githubusercontent.com/57655/119555390-ce95b480-bd6b-11eb-8333-ce7b50c9fccd.mov ### Clicking "Create enrollment token" button in Agents list view now opens a modal instead of a flyout https://user-images.githubusercontent.com/57655/119556503-1e28b000-bd6d-11eb-8952-1da8e80e976e.mov ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../hooks/use_request/enrollment_api_keys.ts | 10 ++ .../agent_policy_selection.tsx | 120 +++++++++++++---- .../managed_instructions.tsx | 14 +- .../agent_enrollment_flyout/steps.tsx | 8 +- ...lyout.tsx => new_enrollment_key_modal.tsx} | 125 +++++++----------- .../enrollment_token_list_page/index.tsx | 14 +- .../public/applications/fleet/types/index.ts | 2 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 180 insertions(+), 115 deletions(-) rename x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/{new_enrollment_key_flyout.tsx => new_enrollment_key_modal.tsx} (50%) diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/enrollment_api_keys.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/enrollment_api_keys.ts index 601d54ec56c466..7b3ddaada80010 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/enrollment_api_keys.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/enrollment_api_keys.ts @@ -11,6 +11,8 @@ import type { GetOneEnrollmentAPIKeyResponse, GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, + PostEnrollmentAPIKeyRequest, + PostEnrollmentAPIKeyResponse, } from '../../types'; import { useRequest, sendRequest, useConditionalRequest } from './use_request'; @@ -65,3 +67,11 @@ export function useGetEnrollmentAPIKeys( ...options, }); } + +export function sendCreateEnrollmentAPIKey(body: PostEnrollmentAPIKeyRequest['body']) { + return sendRequest({ + method: 'post', + path: enrollmentAPIKeyRouteService.getCreatePath(), + body, + }); +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx index bcedb23b32d5d6..4edc1121b1091a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -8,21 +8,25 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButton, EuiCallOut, EuiSelect, EuiSpacer, EuiText } from '@elastic/eui'; import { SO_SEARCH_LIMIT } from '../../../../constants'; import type { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../../../types'; -import { sendGetEnrollmentAPIKeys, useStartServices } from '../../../../hooks'; +import { + sendGetEnrollmentAPIKeys, + useStartServices, + sendCreateEnrollmentAPIKey, +} from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; type Props = { agentPolicies?: AgentPolicy[]; - onAgentPolicyChange?: (key: string) => void; + onAgentPolicyChange?: (key?: string) => void; excludeFleetServer?: boolean; } & ( | { withKeySelection: true; - onKeyChange?: (key: string) => void; + onKeyChange?: (key?: string) => void; } | { withKeySelection: false; @@ -38,6 +42,8 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState( [] ); + const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); + const [selectedState, setSelectedState] = useState<{ agentPolicyId?: string; enrollmentAPIKeyId?: string; @@ -45,7 +51,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { useEffect( function triggerOnAgentPolicyChangeEffect() { - if (onAgentPolicyChange && selectedState.agentPolicyId) { + if (onAgentPolicyChange) { onAgentPolicyChange(selectedState.agentPolicyId); } }, @@ -58,7 +64,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { return; } - if (selectedState.enrollmentAPIKeyId) { + if (onKeyChange) { onKeyChange(selectedState.enrollmentAPIKeyId); } }, @@ -94,6 +100,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { return; } if (!selectedState.agentPolicyId) { + setIsAuthenticationSettingsOpen(true); setEnrollmentAPIKeys([]); return; } @@ -204,28 +211,89 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { {isAuthenticationSettingsOpen && ( <> - ({ - value: key.id, - text: key.name, - }))} - value={selectedState.enrollmentAPIKeyId || undefined} - prepend={ - + {enrollmentAPIKeys.length && selectedState.enrollmentAPIKeyId ? ( + ({ + value: key.id, + text: key.name, + }))} + value={selectedState.enrollmentAPIKeyId || undefined} + prepend={ + + + + } + onChange={(e) => { + setSelectedState({ + ...selectedState, + enrollmentAPIKeyId: e.target.value, + }); + }} + /> + ) : ( + +
+ +
+ + { + setIsLoadingEnrollmentKey(true); + if (selectedState.agentPolicyId) { + sendCreateEnrollmentAPIKey({ policy_id: selectedState.agentPolicyId }) + .then((res) => { + if (res.error) { + throw res.error; + } + setIsLoadingEnrollmentKey(false); + if (res.data?.item) { + setEnrollmentAPIKeys([res.data.item]); + setSelectedState({ + ...selectedState, + enrollmentAPIKeyId: res.data.item.id, + }); + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { + defaultMessage: 'Enrollment token created', + }) + ); + } + }) + .catch((error) => { + setIsLoadingEnrollmentKey(false); + notifications.toasts.addError(error, { + title: 'Error', + }); + }); + } + }} + > -
- } - onChange={(e) => { - setSelectedState({ - ...selectedState, - enrollmentAPIKeyId: e.target.value, - }); - }} - /> + + + )} )} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx index 0158af2d78470b..df1630abfab476 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx @@ -18,6 +18,8 @@ import { useLink, useFleetStatus, } from '../../../../hooks'; +import { NewEnrollmentTokenModal } from '../../enrollment_token_list_page/components/new_enrollment_key_modal'; + import { ManualInstructions } from '../../../../components/enrollment_instructions'; import { FleetServerRequirementPage, @@ -99,7 +101,7 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { defaultMessage: 'Enroll and start the Elastic Agent', }), - children: apiKey.data && ( + children: selectedAPIKeyId && apiKey.data && ( ), }); @@ -107,12 +109,18 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { return baseSteps; }, [ agentPolicies, + selectedAPIKeyId, apiKey.data, isFleetServerPolicySelected, settings.data?.item?.fleet_server_hosts, fleetServerInstructions, ]); + const [isModalOpen, setModalOpen] = useState(false); + const closeModal = () => { + setModalOpen(false); + }; + return ( <> {fleetStatus.isReady ? ( @@ -125,6 +133,10 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { + + {isModalOpen && ( + + )} ) : fleetStatus.missingRequirements?.length === 1 && fleetStatus.missingRequirements[0] === 'fleet_server' ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx index 6a446e888a19f0..8ba0098b3d2778 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx @@ -54,8 +54,8 @@ export const AgentPolicySelectionStep = ({ excludeFleetServer, }: { agentPolicies?: AgentPolicy[]; - setSelectedAPIKeyId?: (key: string) => void; - setSelectedPolicyId?: (policyId: string) => void; + setSelectedAPIKeyId?: (key?: string) => void; + setSelectedPolicyId?: (policyId?: string) => void; setIsFleetServerPolicySelected?: (selected: boolean) => void; excludeFleetServer?: boolean; }) => { @@ -67,11 +67,11 @@ export const AgentPolicySelectionStep = ({ : []; const onAgentPolicyChange = useCallback( - async (policyId: string) => { + async (policyId?: string) => { if (setSelectedPolicyId) { setSelectedPolicyId(policyId); } - if (setIsFleetServerPolicySelected) { + if (policyId && setIsFleetServerPolicySelected) { const agentPolicyRequest = await sendGetOneAgentPolicy(policyId); if ( agentPolicyRequest.data?.item && diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_modal.tsx similarity index 50% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_modal.tsx index 7fae295d0d5b4a..29e130f5583ab5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_modal.tsx @@ -7,32 +7,16 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, - EuiFlyoutFooter, - EuiForm, - EuiFormRow, - EuiFieldText, - EuiSelect, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiConfirmModal, EuiForm, EuiFormRow, EuiFieldText, EuiSelect } from '@elastic/eui'; -import type { AgentPolicy } from '../../../../types'; -import { useInput, useStartServices, sendRequest } from '../../../../hooks'; -import { enrollmentAPIKeyRouteService } from '../../../../services'; +import type { AgentPolicy, EnrollmentAPIKey } from '../../../../types'; +import { useInput, useStartServices, sendCreateEnrollmentAPIKey } from '../../../../hooks'; function useCreateApiKeyForm( policyIdDefaultValue: string | undefined, - onSuccess: (keyId: string) => void + onSuccess: (key: EnrollmentAPIKey) => void, + onError: (error: Error) => void ) { - const { notifications } = useStartServices(); const [isLoading, setIsLoading] = useState(false); const apiKeyNameInput = useInput(''); const policyIdInput = useInput(policyIdDefaultValue); @@ -41,31 +25,23 @@ function useCreateApiKeyForm( event.preventDefault(); setIsLoading(true); try { - const res = await sendRequest({ - method: 'post', - path: enrollmentAPIKeyRouteService.getCreatePath(), - body: JSON.stringify({ - name: apiKeyNameInput.value, - policy_id: policyIdInput.value, - }), + const res = await sendCreateEnrollmentAPIKey({ + name: apiKeyNameInput.value, + policy_id: policyIdInput.value, }); + if (res.error) { throw res.error; } policyIdInput.clear(); apiKeyNameInput.clear(); setIsLoading(false); - onSuccess(res.data.item.id); - notifications.toasts.addSuccess( - i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { - defaultMessage: 'Enrollment token created.', - }) - ); - } catch (err) { - notifications.toasts.addError(err as Error, { - title: 'Error', - }); + if (res.data?.item) { + onSuccess(res.data.item); + } + } catch (error) { setIsLoading(false); + onError(error); } }; @@ -78,18 +54,32 @@ function useCreateApiKeyForm( } interface Props { - onClose: () => void; - agentPolicies: AgentPolicy[]; + onClose: (key?: EnrollmentAPIKey) => void; + agentPolicies?: AgentPolicy[]; } -export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({ +export const NewEnrollmentTokenModal: React.FunctionComponent = ({ onClose, agentPolicies = [], }) => { + const { notifications } = useStartServices(); const policyIdDefaultValue = agentPolicies.find((agentPolicy) => agentPolicy.is_default)?.id; - const form = useCreateApiKeyForm(policyIdDefaultValue, () => { - onClose(); - }); + const form = useCreateApiKeyForm( + policyIdDefaultValue, + (key: EnrollmentAPIKey) => { + onClose(key); + notifications.toasts.addSuccess( + i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { + defaultMessage: 'Enrollment token created', + }) + ); + }, + (error: Error) => { + notifications.toasts.addError(error, { + title: 'Error', + }); + } + ); const body = ( @@ -124,41 +114,26 @@ export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({ }))} /> - - - ); return ( - - - -

- -

-
-
- {body} - - - - - - - - - -
+ onClose()} + cancelButtonText={i18n.translate('xpack.fleet.newEnrollmentKey.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + onConfirm={form.onSubmit} + confirmButtonText={i18n.translate('xpack.fleet.newEnrollmentKey.submitButton', { + defaultMessage: 'Create enrollment token', + })} + > + {body} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx index 6d141b0c9ebf10..66e0c338dbbbcd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx @@ -34,7 +34,7 @@ import { import type { EnrollmentAPIKey, GetAgentPoliciesResponseItem } from '../../../types'; import { SearchBar } from '../../../components/search_bar'; -import { NewEnrollmentTokenFlyout } from './components/new_enrollment_key_flyout'; +import { NewEnrollmentTokenModal } from './components/new_enrollment_key_modal'; import { ConfirmEnrollmentTokenDelete } from './components/confirm_delete_modal'; const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId }) => { @@ -156,7 +156,7 @@ const DeleteButton: React.FunctionComponent<{ apiKey: EnrollmentAPIKey; refresh: export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { useBreadcrumbs('fleet_enrollment_tokens'); - const [flyoutOpen, setFlyoutOpen] = useState(false); + const [isModalOpen, setModalOpen] = useState(false); const [search, setSearch] = useState(''); const { pagination, setPagination, pageSizeOptions } = usePagination(); @@ -270,11 +270,11 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { return ( <> - {flyoutOpen && ( - { - setFlyoutOpen(false); + onClose={(key?: EnrollmentAPIKey) => { + setModalOpen(false); enrollmentAPIKeysRequest.resendRequest(); }} /> @@ -301,7 +301,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { /> - setFlyoutOpen(true)}> + setModalOpen(true)}> Date: Fri, 4 Jun 2021 14:22:31 -0500 Subject: [PATCH 05/15] [Enterprise Search] Convert Role mappings for both apps to use flyouts (#101198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add RoleOptionLabel component * Refactor RoleSelector to use EuiRadioGroup Previously, we used individual radio buttons in a map in the component. However the new designs have a shared label and work best in the EuiRadioGroup component. * Add reducer and actions to logic file for flyout visibility * Remove redirects in favor of refreshing lists With the existing multi-page view, we redirect after creating, editing or deleting a mapping. We now simply refresh the list after the action. Also as a part of this commit, we show a hard-coded error message if a user tries to navigate to a non-existant mapping, instead of redirecting to a 404 page * Add RoleMappingFlyout component * Refactor AttributeSelector No longer uses a panel or has the need for flex groups - Also added a test for 100% coverage * Refactor RoleMappingsTable - Use EuiButtonIcons instead of Link - Manage button now triggers flyout instead of linking to route * Remove AddRoleMappingButton We can just use an EuiButton to trigger the flyout * Convert to use RoleSelector syntax - Passes the entire array to the component instead of mapping. - Uses ‘id’ instead of ‘type’ to match EUI component - For App Search, as per design and PM direction, dropping labels for advanced and standard roles and showing them all in the same list. - Removed unused constant and i18ns * Move constants to shared Will do a lot more of this in a future PR * Remove DeleteMappingCallout - This now an action in the row - Also added tests for correct titles for 100% test coverage * Remove routers and routes - SPA FTW * No longer pass isNew as prop - Determine based on existence of Role Mapping instead * No longer need to initialze role mapping in the component This will become a flyout and the intialization will be triggered when the button in the table is clicked. * Remove flash messages This will be handled globally in the main component. * Wrap components with flyout Also add to main RoleMappings views * Add form row validation for App Search * Remove unnecessary layout components - Don’t need the panel, headings, spacer, and Flex components - Also removed constants and i18n from unused headings * Wire up handleDeleteMapping to take ID param The method now passes the ID directly from the table row action item * Add EuiPortal wrapper for flyout Without this, the flyout was was under the overlay. Hide whitespace changes on this commit * Add spacer to better match design * Update constants for new copy from design * Replace all engines/groups radio and group/engine selectors - The designs call for a radio group and a combo box, instead of separate radios and a list of checkboxes - Also added a spacer to each layout * Remove util that is no longer needed - This was used for generating routes that are no longer there - Also removed unused test file from a component deleted in an earlier PR - Fix test since spacer was added * Add missing i18n constant * Add back missing scoped engine check * Rename roleId -> roleMappingId * Use shared constant for “Cancel” Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/role_mappings/constants.ts | 60 ++-- .../components/role_mappings/index.ts | 2 +- .../role_mappings/role_mapping.test.tsx | 51 +-- .../components/role_mappings/role_mapping.tsx | 291 ++++++------------ .../role_mappings/role_mappings.test.tsx | 22 +- .../role_mappings/role_mappings.tsx | 33 +- .../role_mappings/role_mappings_logic.test.ts | 88 ++++-- .../role_mappings/role_mappings_logic.ts | 106 ++++--- .../role_mappings_router.test.tsx | 26 -- .../role_mappings/role_mappings_router.tsx | 29 -- .../components/role_mappings/utils.test.ts | 16 - .../components/role_mappings/utils.ts | 12 - .../applications/app_search/index.test.tsx | 6 +- .../public/applications/app_search/index.tsx | 4 +- .../public/applications/app_search/routes.ts | 2 - .../app_search/utils/role/types.ts | 2 +- .../add_role_mapping_button.test.tsx | 22 -- .../role_mapping/add_role_mapping_button.tsx | 22 -- .../role_mapping/attribute_selector.test.tsx | 8 + .../role_mapping/attribute_selector.tsx | 140 ++++----- .../shared/role_mapping/constants.ts | 59 ++++ .../delete_mapping_callout.test.tsx | 31 -- .../role_mapping/delete_mapping_callout.tsx | 29 -- .../applications/shared/role_mapping/index.ts | 4 +- .../role_mapping/role_mapping_flyout.test.tsx | 64 ++++ .../role_mapping/role_mapping_flyout.tsx | 90 ++++++ .../role_mapping/role_mappings_table.test.tsx | 20 +- .../role_mapping/role_mappings_table.tsx | 29 +- .../role_mapping/role_option_label.test.tsx | 24 ++ .../shared/role_mapping/role_option_label.tsx | 26 ++ .../role_mapping/role_selector.test.tsx | 30 +- .../shared/role_mapping/role_selector.tsx | 68 ++-- .../applications/workplace_search/index.tsx | 4 +- .../workplace_search/routes.test.tsx | 8 - .../applications/workplace_search/routes.ts | 3 - .../views/role_mappings/constants.ts | 42 ++- .../views/role_mappings/index.ts | 2 +- .../views/role_mappings/role_mapping.test.tsx | 51 ++- .../views/role_mappings/role_mapping.tsx | 226 +++++--------- .../role_mappings/role_mappings.test.tsx | 22 +- .../views/role_mappings/role_mappings.tsx | 29 +- .../role_mappings/role_mappings_logic.test.ts | 79 +++-- .../role_mappings/role_mappings_logic.ts | 93 ++++-- .../role_mappings_router.test.tsx | 26 -- .../role_mappings/role_mappings_router.tsx | 34 -- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- 47 files changed, 1043 insertions(+), 1020 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_option_label.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index 2f9ff707f96317..59010cb9ab8b66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -9,15 +9,6 @@ import { i18n } from '@kbn/i18n'; import { AdvanceRoleType } from '../../types'; -export const SAVE_ROLE_MAPPING = i18n.translate( - 'xpack.enterpriseSearch.appSearch.roleMapping.saveRoleMappingButtonLabel', - { defaultMessage: 'Save role mapping' } -); -export const UPDATE_ROLE_MAPPING = i18n.translate( - 'xpack.enterpriseSearch.appSearch.roleMapping.updateRoleMappingButtonLabel', - { defaultMessage: 'Update role mapping' } -); - export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate( 'xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody', { @@ -126,74 +117,71 @@ export const ADMIN_ROLE_TYPE_DESCRIPTION = i18n.translate( } ); -export const ADVANCED_ROLE_SELECTORS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.advancedRoleSelectorsTitle', +export const ENGINE_REQUIRED_ERROR = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engineRequiredError', { - defaultMessage: 'Full or limited engine access', + defaultMessage: 'At least one assigned engine is required.', } ); -export const ROLE_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.roleTitle', { - defaultMessage: 'Role', -}); - -export const FULL_ENGINE_ACCESS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.fullEngineAccessTitle', +export const ALL_ENGINES_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.allEnginesLabel', { - defaultMessage: 'Full engine access', + defaultMessage: 'Assign to all engines', } ); -export const FULL_ENGINE_ACCESS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.appSearch.fullEngineAccessDescription', +export const ALL_ENGINES_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.allEnginesDescription', { - defaultMessage: 'Access to all current and future engines.', + defaultMessage: + 'Assigning to all engines includes all current and future engines as created and administered at a later date.', } ); -export const LIMITED_ENGINE_ACCESS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.limitedEngineAccessTitle', +export const SPECIFIC_ENGINES_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.specificEnginesLabel', { - defaultMessage: 'Limited engine access', + defaultMessage: 'Assign to specific engines', } ); -export const LIMITED_ENGINE_ACCESS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.appSearch.limitedEngineAccessDescription', +export const SPECIFIC_ENGINES_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.specificEnginesDescription', { - defaultMessage: 'Limit user access to specific engines:', + defaultMessage: 'Assign to a select set of engines statically.', } ); -export const ENGINE_ACCESS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engineAccessTitle', +export const ENGINE_ASSIGNMENT_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engineAssignmentLabel', { - defaultMessage: 'Engine access', + defaultMessage: 'Engine assignment', } ); export const ADVANCED_ROLE_TYPES = [ { - type: 'dev', + id: 'dev', description: DEV_ROLE_TYPE_DESCRIPTION, }, { - type: 'editor', + id: 'editor', description: EDITOR_ROLE_TYPE_DESCRIPTION, }, { - type: 'analyst', + id: 'analyst', description: ANALYST_ROLE_TYPE_DESCRIPTION, }, ] as AdvanceRoleType[]; export const STANDARD_ROLE_TYPES = [ { - type: 'owner', + id: 'owner', description: OWNER_ROLE_TYPE_DESCRIPTION, }, { - type: 'admin', + id: 'admin', description: ADMIN_ROLE_TYPE_DESCRIPTION, }, ] as AdvanceRoleType[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts index ce4b1de6e399d8..19062cf44c17af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { RoleMappingsRouter } from './role_mappings_router'; +export { RoleMappings } from './role_mappings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx index f50fc21d5ba58d..2e179dc2b6ab3c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.test.tsx @@ -12,18 +12,16 @@ import { engines } from '../../__mocks__/engines.mock'; import React from 'react'; +import { waitFor } from '@testing-library/dom'; import { shallow } from 'enzyme'; -import { EuiCheckbox } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiRadioGroup } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; -import { - AttributeSelector, - DeleteMappingCallout, - RoleSelector, -} from '../../../shared/role_mapping'; +import { AttributeSelector, RoleSelector } from '../../../shared/role_mapping'; import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { STANDARD_ROLE_TYPES } from './constants'; + import { RoleMapping } from './role_mapping'; describe('RoleMapping', () => { @@ -68,39 +66,44 @@ describe('RoleMapping', () => { }); it('renders', () => { + setMockValues({ ...mockValues, roleMapping: asRoleMapping }); const wrapper = shallow(); expect(wrapper.find(AttributeSelector)).toHaveLength(1); - expect(wrapper.find(RoleSelector)).toHaveLength(5); + expect(wrapper.find(RoleSelector)).toHaveLength(1); }); - it('returns Loading when loading', () => { - setMockValues({ ...mockValues, dataLoading: true }); + it('only passes standard role options for non-advanced roles', () => { + setMockValues({ ...mockValues, hasAdvancedRoles: false }); const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.find(RoleSelector).prop('roleOptions')).toHaveLength(STANDARD_ROLE_TYPES.length); }); - it('renders DeleteMappingCallout for existing mapping', () => { - setMockValues({ ...mockValues, roleMapping: asRoleMapping }); + it('sets initial selected state when accessAllEngines is true', () => { + setMockValues({ ...mockValues, accessAllEngines: true }); const wrapper = shallow(); - expect(wrapper.find(DeleteMappingCallout)).toHaveLength(1); + expect(wrapper.find(EuiRadioGroup).prop('idSelected')).toBe('all'); }); - it('hides DeleteMappingCallout for new mapping', () => { - const wrapper = shallow(); + it('handles all/specific engines radio change', () => { + const wrapper = shallow(); + const radio = wrapper.find(EuiRadioGroup); + radio.simulate('change', { target: { checked: false } }); - expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0); + expect(actions.handleAccessAllEnginesChange).toHaveBeenCalledWith(false); }); - it('handles engine checkbox click', () => { + it('handles engine checkbox click', async () => { const wrapper = shallow(); - wrapper - .find(EuiCheckbox) - .first() - .simulate('change', { target: { checked: true } }); - - expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith(engines[0].name, true); + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: engines[0].name, value: engines[0].name }]) + ); + wrapper.update(); + + expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith([engines[0].name]); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index 610ceae8856f24..0f201889b2f05c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -5,65 +5,36 @@ * 2.0. */ -import React, { useEffect } from 'react'; - -import { useParams } from 'react-router-dom'; +import React from 'react'; import { useActions, useValues } from 'kea'; -import { - EuiButton, - EuiCheckbox, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPageContentBody, - EuiPageHeader, - EuiPanel, - EuiRadio, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; +import { EuiComboBox, EuiFormRow, EuiHorizontalRule, EuiRadioGroup, EuiSpacer } from '@elastic/eui'; + import { AttributeSelector, - DeleteMappingCallout, RoleSelector, + RoleOptionLabel, + RoleMappingFlyout, } from '../../../shared/role_mapping'; -import { - ROLE_MAPPINGS_TITLE, - ADD_ROLE_MAPPING_TITLE, - MANAGE_ROLE_MAPPING_TITLE, -} from '../../../shared/role_mapping/constants'; import { AppLogic } from '../../app_logic'; +import { AdvanceRoleType } from '../../types'; import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; -import { Engine } from '../engine/types'; import { - SAVE_ROLE_MAPPING, - UPDATE_ROLE_MAPPING, ADVANCED_ROLE_TYPES, STANDARD_ROLE_TYPES, - ADVANCED_ROLE_SELECTORS_TITLE, - ROLE_TITLE, - FULL_ENGINE_ACCESS_TITLE, - FULL_ENGINE_ACCESS_DESCRIPTION, - LIMITED_ENGINE_ACCESS_TITLE, - LIMITED_ENGINE_ACCESS_DESCRIPTION, - ENGINE_ACCESS_TITLE, + ENGINE_REQUIRED_ERROR, + ALL_ENGINES_LABEL, + ALL_ENGINES_DESCRIPTION, + SPECIFIC_ENGINES_LABEL, + SPECIFIC_ENGINES_DESCRIPTION, + ENGINE_ASSIGNMENT_LABEL, } from './constants'; import { RoleMappingsLogic } from './role_mappings_logic'; -interface RoleMappingProps { - isNew?: boolean; -} - -export const RoleMapping: React.FC = ({ isNew }) => { - const { roleId } = useParams() as { roleId: string }; +export const RoleMapping: React.FC = () => { const { myRole } = useValues(AppLogic); const { @@ -71,12 +42,10 @@ export const RoleMapping: React.FC = ({ isNew }) => { handleAttributeSelectorChange, handleAttributeValueChange, handleAuthProviderChange, - handleDeleteMapping, handleEngineSelectionChange, handleRoleChange, handleSaveMapping, - initializeRoleMapping, - resetState, + closeRoleMappingFlyout, } = useActions(RoleMappingsLogic); const { @@ -86,7 +55,6 @@ export const RoleMapping: React.FC = ({ isNew }) => { attributes, availableAuthProviders, availableEngines, - dataLoading, elasticsearchRoles, hasAdvancedRoles, multipleAuthProvidersConfig, @@ -94,154 +62,97 @@ export const RoleMapping: React.FC = ({ isNew }) => { roleType, selectedEngines, selectedAuthProviders, + selectedOptions, } = useValues(RoleMappingsLogic); - useEffect(() => { - initializeRoleMapping(roleId); - return resetState; - }, []); - - if (dataLoading) return ; - - const SAVE_ROLE_MAPPING_LABEL = isNew ? SAVE_ROLE_MAPPING : UPDATE_ROLE_MAPPING; - const TITLE = isNew ? ADD_ROLE_MAPPING_TITLE : MANAGE_ROLE_MAPPING_TITLE; - - const saveRoleMappingButton = ( - - {SAVE_ROLE_MAPPING_LABEL} - - ); - - const engineSelector = (engine: Engine) => ( - { - handleEngineSelectionChange(engine.name, e.target.checked); - }} - label={engine.name} - /> - ); - - const advancedRoleSelectors = ( - <> - - -

{ADVANCED_ROLE_SELECTORS_TITLE}

-
- - {ADVANCED_ROLE_TYPES.map(({ type, description }) => ( - 0 || accessAllEngines; + + const mapRoleOptions = ({ id, description }: AdvanceRoleType) => ({ + id, + description, + disabled: !myRole.availableRoleTypes.includes(id), + }); + + const standardRoleOptions = STANDARD_ROLE_TYPES.map(mapRoleOptions); + const advancedRoleOptions = ADVANCED_ROLE_TYPES.map(mapRoleOptions); + + const roleOptions = hasAdvancedRoles + ? [...standardRoleOptions, ...advancedRoleOptions] + : standardRoleOptions; + + const engineOptions = [ + { + id: 'all', + label: , + }, + { + id: 'specific', + label: ( + - ))} - - ); + ), + }, + ]; return ( - <> - - - - - - - - - - - -

{ROLE_TITLE}

-
- - -

{FULL_ENGINE_ACCESS_TITLE}

-
- - {STANDARD_ROLE_TYPES.map(({ type, description }) => ( - - ))} - {hasAdvancedRoles && advancedRoleSelectors} -
-
- {hasAdvancedRoles && ( - - - -

{ENGINE_ACCESS_TITLE}

-
- - - - -

{FULL_ENGINE_ACCESS_TITLE}

-
-

{FULL_ENGINE_ACCESS_DESCRIPTION}

- - } - /> -
- - <> - - -

{LIMITED_ENGINE_ACCESS_TITLE}

-
-

{LIMITED_ENGINE_ACCESS_DESCRIPTION}

- - } - /> - {!accessAllEngines && ( -
- {availableEngines.map((engine) => engineSelector(engine))} -
- )} - -
-
-
- )} -
- - {roleMapping && } -
- + + + + + + {hasAdvancedRoles && ( + <> + + + handleAccessAllEnginesChange(id === 'all')} + legend={{ + children: {ENGINE_ASSIGNMENT_LABEL}, + }} + /> + + + ({ label: name, value: name }))} + onChange={(options) => { + handleEngineSelectionChange(options.map(({ value }) => value as string)); + }} + fullWidth + isDisabled={accessAllEngines || !roleHasScopedEngines(roleType)} + /> + + + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx index c6da903e209128..4ccb1fec0f034f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx @@ -12,16 +12,19 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; import { RoleMappingsTable } from '../../../shared/role_mapping'; import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { RoleMapping } from './role_mapping'; import { RoleMappings } from './role_mappings'; describe('RoleMappings', () => { const initializeRoleMappings = jest.fn(); + const initializeRoleMapping = jest.fn(); + const handleDeleteMapping = jest.fn(); const mockValues = { roleMappings: [wsRoleMapping], dataLoading: false, @@ -31,6 +34,8 @@ describe('RoleMappings', () => { beforeEach(() => { setMockActions({ initializeRoleMappings, + initializeRoleMapping, + handleDeleteMapping, }); setMockValues(mockValues); }); @@ -54,4 +59,19 @@ describe('RoleMappings', () => { expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); }); + + it('renders RoleMapping flyout', () => { + setMockValues({ ...mockValues, roleMappingFlyoutOpen: true }); + const wrapper = shallow(); + + expect(wrapper.find(RoleMapping)).toHaveLength(1); + }); + + it('handles button click', () => { + setMockValues({ ...mockValues, roleMappings: [] }); + const wrapper = shallow(); + wrapper.find(EuiEmptyPrompt).dive().find(EuiButton).simulate('click'); + + expect(initializeRoleMapping).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index 86e2e51d29a7d2..61ed70f515f6f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -10,6 +10,7 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { + EuiButton, EuiEmptyPrompt, EuiPageContent, EuiPageContentBody, @@ -20,22 +21,31 @@ import { import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Loading } from '../../../shared/loading'; -import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping'; +import { RoleMappingsTable } from '../../../shared/role_mapping'; import { EMPTY_ROLE_MAPPINGS_TITLE, + ROLE_MAPPING_ADD_BUTTON, ROLE_MAPPINGS_TITLE, ROLE_MAPPINGS_DESCRIPTION, } from '../../../shared/role_mapping/constants'; -import { ROLE_MAPPING_NEW_PATH } from '../../routes'; - import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING, EMPTY_ROLE_MAPPINGS_BODY } from './constants'; +import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; -import { generateRoleMappingPath } from './utils'; export const RoleMappings: React.FC = () => { - const { initializeRoleMappings, resetState } = useActions(RoleMappingsLogic); - const { roleMappings, multipleAuthProvidersConfig, dataLoading } = useValues(RoleMappingsLogic); + const { + initializeRoleMappings, + initializeRoleMapping, + handleDeleteMapping, + resetState, + } = useActions(RoleMappingsLogic); + const { + roleMappings, + multipleAuthProvidersConfig, + dataLoading, + roleMappingFlyoutOpen, + } = useValues(RoleMappingsLogic); useEffect(() => { initializeRoleMappings(); @@ -44,7 +54,11 @@ export const RoleMappings: React.FC = () => { if (dataLoading) return ; - const addMappingButton = ; + const addMappingButton = ( + initializeRoleMapping()}> + {ROLE_MAPPING_ADD_BUTTON} + + ); const roleMappingEmptyState = ( @@ -63,8 +77,9 @@ export const RoleMappings: React.FC = () => { accessItemKey="engines" accessHeader={ROLE_MAPPINGS_ENGINE_ACCESS_HEADING} addMappingButton={addMappingButton} - getRoleMappingPath={generateRoleMappingPath} + initializeRoleMapping={initializeRoleMapping} shouldShowAuthProvider={multipleAuthProvidersConfig} + handleDeleteMapping={handleDeleteMapping} /> ); @@ -72,6 +87,8 @@ export const RoleMappings: React.FC = () => { <> + + {roleMappingFlyoutOpen && } 0}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index ada17fc9a732a9..d0534a2a0be59f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; +import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; import { LogicMounter } from '../../../__mocks__/kea.mock'; import { engines } from '../../__mocks__/engines.mock'; @@ -13,20 +13,25 @@ import { engines } from '../../__mocks__/engines.mock'; import { nextTick } from '@kbn/test/jest'; import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; -import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants'; import { RoleMappingsLogic } from './role_mappings_logic'; describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; - const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { + clearFlashMessages, + flashAPIErrors, + setSuccessMessage, + setErrorMessage, + } = mockFlashMessageHelpers; const { mount } = new LogicMounter(RoleMappingsLogic); const DEFAULT_VALUES = { attributes: [], availableAuthProviders: [], elasticsearchRoles: [], roleMapping: null, + roleMappingFlyoutOpen: false, roleMappings: [], roleType: 'owner', attributeValue: '', @@ -38,6 +43,7 @@ describe('RoleMappingsLogic', () => { selectedEngines: new Set(), accessAllEngines: true, selectedAuthProviders: [ANY_AUTH_PROVIDER], + selectedOptions: [], }; const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] }; @@ -87,6 +93,10 @@ describe('RoleMappingsLogic', () => { attributeValue: 'superuser', elasticsearchRoles: mappingServerProps.elasticsearchRoles, selectedEngines: new Set(engines.map((e) => e.name)), + selectedOptions: [ + { label: engines[0].name, value: engines[0].name }, + { label: engines[1].name, value: engines[1].name }, + ], }); }); @@ -134,21 +144,21 @@ describe('RoleMappingsLogic', () => { }); it('handles adding an engine to selected engines', () => { - RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, true); + RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name, otherEngine.name]); expect(RoleMappingsLogic.values.selectedEngines).toEqual( new Set([engine.name, otherEngine.name]) ); }); it('handles removing an engine from selected engines', () => { - RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, false); + RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name]); expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name])); }); }); it('handleAccessAllEnginesChange', () => { - RoleMappingsLogic.actions.handleAccessAllEnginesChange(); + RoleMappingsLogic.actions.handleAccessAllEnginesChange(false); expect(RoleMappingsLogic.values).toEqual({ ...DEFAULT_VALUES, @@ -250,6 +260,25 @@ describe('RoleMappingsLogic', () => { expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES); expect(clearFlashMessages).toHaveBeenCalled(); }); + + it('openRoleMappingFlyout', () => { + mount(mappingServerProps); + RoleMappingsLogic.actions.openRoleMappingFlyout(); + + expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + + it('closeRoleMappingFlyout', () => { + mount({ + ...mappingServerProps, + roleMappingFlyoutOpen: true, + }); + RoleMappingsLogic.actions.closeRoleMappingFlyout(); + + expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false); + expect(clearFlashMessages).toHaveBeenCalled(); + }); }); describe('listeners', () => { @@ -302,12 +331,12 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); - it('redirects when there is a 404 status', async () => { + it('shows error when there is a 404 status', async () => { http.get.mockReturnValue(Promise.reject({ status: 404 })); RoleMappingsLogic.actions.initializeRoleMapping(); await nextTick(); - expect(navigateToUrl).toHaveBeenCalled(); + expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND); }); }); @@ -322,8 +351,12 @@ describe('RoleMappingsLogic', () => { engines: [], }; - it('calls API and navigates when new mapping', async () => { + it('calls API and refreshes list when new mapping', async () => { mount(mappingsServerProps); + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); http.post.mockReturnValue(Promise.resolve(mappingServerProps)); RoleMappingsLogic.actions.handleSaveMapping(); @@ -333,11 +366,15 @@ describe('RoleMappingsLogic', () => { }); await nextTick(); - expect(navigateToUrl).toHaveBeenCalled(); + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); }); - it('calls API and navigates when existing mapping', async () => { + it('calls API and refreshes list when existing mapping', async () => { mount(mappingServerProps); + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); http.put.mockReturnValue(Promise.resolve(mappingServerProps)); RoleMappingsLogic.actions.handleSaveMapping(); @@ -347,7 +384,7 @@ describe('RoleMappingsLogic', () => { }); await nextTick(); - expect(navigateToUrl).toHaveBeenCalled(); + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); expect(setSuccessMessage).toHaveBeenCalled(); }); @@ -383,6 +420,7 @@ describe('RoleMappingsLogic', () => { describe('handleDeleteMapping', () => { let confirmSpy: any; + const roleMappingId = 'r1'; beforeEach(() => { confirmSpy = jest.spyOn(window, 'confirm'); @@ -393,30 +431,26 @@ describe('RoleMappingsLogic', () => { confirmSpy.mockRestore(); }); - it('returns when no mapping', () => { - RoleMappingsLogic.actions.handleDeleteMapping(); - - expect(http.delete).not.toHaveBeenCalled(); - }); - - it('calls API and navigates', async () => { + it('calls API and refreshes list', async () => { mount(mappingServerProps); + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); http.delete.mockReturnValue(Promise.resolve({})); - RoleMappingsLogic.actions.handleDeleteMapping(); + RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); - expect(http.delete).toHaveBeenCalledWith( - `/api/app_search/role_mappings/${asRoleMapping.id}` - ); + expect(http.delete).toHaveBeenCalledWith(`/api/app_search/role_mappings/${roleMappingId}`); await nextTick(); - expect(navigateToUrl).toHaveBeenCalled(); + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); expect(setSuccessMessage).toHaveBeenCalled(); }); it('handles error', async () => { mount(mappingServerProps); http.delete.mockReturnValue(Promise.reject('this is an error')); - RoleMappingsLogic.actions.handleDeleteMapping(); + RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); @@ -425,7 +459,7 @@ describe('RoleMappingsLogic', () => { it('will do nothing if not confirmed', () => { mount(mappingServerProps); jest.spyOn(window, 'confirm').mockReturnValueOnce(false); - RoleMappingsLogic.actions.handleDeleteMapping(); + RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); expect(http.delete).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index 00b944d91cbcba..6981f48159a4ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -7,16 +7,17 @@ import { kea, MakeLogicType } from 'kea'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; + import { clearFlashMessages, flashAPIErrors, setSuccessMessage, + setErrorMessage, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; -import { KibanaLogic } from '../../../shared/kibana'; -import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants'; import { AttributeName } from '../../../shared/types'; -import { ROLE_MAPPINGS_PATH } from '../../routes'; import { ASRoleMapping, RoleTypes } from '../../types'; import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; import { Engine } from '../engine/types'; @@ -49,28 +50,24 @@ const getFirstAttributeValue = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][1] as AttributeName; interface RoleMappingsActions { - handleAccessAllEnginesChange(): void; + handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; handleAuthProviderChange(value: string[]): { value: string[] }; handleAttributeSelectorChange( value: AttributeName, firstElasticsearchRole: string ): { value: AttributeName; firstElasticsearchRole: string }; handleAttributeValueChange(value: string): { value: string }; - handleDeleteMapping(): void; - handleEngineSelectionChange( - engineName: string, - selected: boolean - ): { - engineName: string; - selected: boolean; - }; + handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; + handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; handleSaveMapping(): void; - initializeRoleMapping(roleId?: string): { roleId?: string }; + initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; initializeRoleMappings(): void; resetState(): void; setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; + openRoleMappingFlyout(): void; + closeRoleMappingFlyout(): void; } interface RoleMappingsValues { @@ -89,6 +86,8 @@ interface RoleMappingsValues { roleType: RoleTypes; selectedAuthProviders: string[]; selectedEngines: Set; + roleMappingFlyoutOpen: boolean; + selectedOptions: EuiComboBoxOptionOption[]; } export const RoleMappingsLogic = kea>({ @@ -98,21 +97,20 @@ export const RoleMappingsLogic = kea data, handleAuthProviderChange: (value: string) => ({ value }), handleRoleChange: (roleType: RoleTypes) => ({ roleType }), - handleEngineSelectionChange: (engineName: string, selected: boolean) => ({ - engineName, - selected, - }), + handleEngineSelectionChange: (engineNames: string[]) => ({ engineNames }), handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ value, firstElasticsearchRole, }), handleAttributeValueChange: (value: string) => ({ value }), - handleAccessAllEnginesChange: true, + handleAccessAllEnginesChange: (selected: boolean) => ({ selected }), resetState: true, initializeRoleMappings: true, - initializeRoleMapping: (roleId) => ({ roleId }), - handleDeleteMapping: true, + initializeRoleMapping: (roleMappingId) => ({ roleMappingId }), + handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }), handleSaveMapping: true, + openRoleMappingFlyout: true, + closeRoleMappingFlyout: false, }, reducers: { dataLoading: [ @@ -169,6 +167,7 @@ export const RoleMappingsLogic = kea roleMapping || null, resetState: () => null, + closeRoleMappingFlyout: () => null, }, ], roleType: [ @@ -185,7 +184,7 @@ export const RoleMappingsLogic = kea roleMapping ? roleMapping.accessAllEngines : true, handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType), - handleAccessAllEnginesChange: (accessAllEngines) => !accessAllEngines, + handleAccessAllEnginesChange: (_, { selected }) => selected, }, ], attributeValue: [ @@ -197,6 +196,7 @@ export const RoleMappingsLogic = kea value, resetState: () => '', + closeRoleMappingFlyout: () => '', }, ], attributeName: [ @@ -206,6 +206,7 @@ export const RoleMappingsLogic = kea value, resetState: () => 'username', + closeRoleMappingFlyout: () => 'username', }, ], selectedEngines: [ @@ -214,13 +215,9 @@ export const RoleMappingsLogic = kea roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(), handleAccessAllEnginesChange: () => new Set(), - handleEngineSelectionChange: (engines, { engineName, selected }) => { - const newSelectedEngineNames = new Set(engines as Set); - if (selected) { - newSelectedEngineNames.add(engineName); - } else { - newSelectedEngineNames.delete(engineName); - } + handleEngineSelectionChange: (_, { engineNames }) => { + const newSelectedEngineNames = new Set() as Set; + engineNames.forEach((engineName) => newSelectedEngineNames.add(engineName)); return newSelectedEngineNames; }, @@ -250,7 +247,27 @@ export const RoleMappingsLogic = kea true, + closeRoleMappingFlyout: () => false, + initializeRoleMappings: () => false, + initializeRoleMapping: () => true, + }, + ], }, + selectors: ({ selectors }) => ({ + selectedOptions: [ + () => [selectors.selectedEngines, selectors.availableEngines], + (selectedEngines, availableEngines) => { + const selectedNames = Array.from(selectedEngines.values()); + return availableEngines + .filter(({ name }: { name: string }) => selectedNames.includes(name)) + .map(({ name }: { name: string }) => ({ label: name, value: name })); + }, + ], + }), listeners: ({ actions, values }) => ({ initializeRoleMappings: async () => { const { http } = HttpLogic.values; @@ -263,33 +280,31 @@ export const RoleMappingsLogic = kea { + initializeRoleMapping: async ({ roleMappingId }) => { const { http } = HttpLogic.values; - const { navigateToUrl } = KibanaLogic.values; - const route = roleId - ? `/api/app_search/role_mappings/${roleId}` + const route = roleMappingId + ? `/api/app_search/role_mappings/${roleMappingId}` : '/api/app_search/role_mappings/new'; try { const response = await http.get(route); actions.setRoleMappingData(response); } catch (e) { - navigateToUrl(ROLE_MAPPINGS_PATH); - flashAPIErrors(e); + if (e.status === 404) { + setErrorMessage(ROLE_MAPPING_NOT_FOUND); + } else { + flashAPIErrors(e); + } } }, - handleDeleteMapping: async () => { - const { roleMapping } = values; - if (!roleMapping) return; - + handleDeleteMapping: async ({ roleMappingId }) => { const { http } = HttpLogic.values; - const { navigateToUrl } = KibanaLogic.values; - const route = `/api/app_search/role_mappings/${roleMapping.id}`; + const route = `/api/app_search/role_mappings/${roleMappingId}`; if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { try { await http.delete(route); - navigateToUrl(ROLE_MAPPINGS_PATH); + actions.initializeRoleMappings(); setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); } catch (e) { flashAPIErrors(e); @@ -298,7 +313,6 @@ export const RoleMappingsLogic = kea { const { http } = HttpLogic.values; - const { navigateToUrl } = KibanaLogic.values; const { attributeName, @@ -330,7 +344,7 @@ export const RoleMappingsLogic = kea { clearFlashMessages(); }, + closeRoleMappingFlyout: () => { + clearFlashMessages(); + }, + openRoleMappingFlyout: () => { + clearFlashMessages(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx deleted file mode 100644 index e9fc40ba1dbb43..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -import { shallow } from 'enzyme'; - -import { RoleMapping } from './role_mapping'; -import { RoleMappings } from './role_mappings'; -import { RoleMappingsRouter } from './role_mappings_router'; - -describe('RoleMappingsRouter', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(3); - expect(wrapper.find(RoleMapping)).toHaveLength(2); - expect(wrapper.find(RoleMappings)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx deleted file mode 100644 index 7aa8b4067d9e5b..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_router.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { Route, Switch } from 'react-router-dom'; - -import { ROLE_MAPPING_NEW_PATH, ROLE_MAPPING_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; - -import { RoleMapping } from './role_mapping'; -import { RoleMappings } from './role_mappings'; - -export const RoleMappingsRouter: React.FC = () => ( - - - - - - - - - - - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts deleted file mode 100644 index e72f2b90758ac5..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { generateRoleMappingPath } from './utils'; - -describe('generateRoleMappingPath', () => { - it('generates paths with roleId filled', () => { - const roleId = 'role123'; - - expect(generateRoleMappingPath(roleId)).toEqual(`/role_mappings/${roleId}`); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts deleted file mode 100644 index 109d3de1b86db6..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ROLE_MAPPING_PATH } from '../../routes'; -import { generateEncodedPath } from '../../utils/encode_path_params'; - -export const generateRoleMappingPath = (roleId: string) => - generateEncodedPath(ROLE_MAPPING_PATH, { roleId }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 8d33bd2d130ec5..08aab7af164e31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -28,7 +28,7 @@ import { EnginesOverview } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; -import { RoleMappingsRouter } from './components/role_mappings'; +import { RoleMappings } from './components/role_mappings'; import { SetupGuide } from './components/setup_guide'; import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; @@ -106,13 +106,13 @@ describe('AppSearchConfigured', () => { it('renders RoleMappings when canViewRoleMappings is true', () => { setMockValues({ myRole: { canViewRoleMappings: true } }); rerender(wrapper); - expect(wrapper.find(RoleMappingsRouter)).toHaveLength(1); + expect(wrapper.find(RoleMappings)).toHaveLength(1); }); it('does not render RoleMappings when user canViewRoleMappings is false', () => { setMockValues({ myRole: { canManageEngines: false } }); rerender(wrapper); - expect(wrapper.find(RoleMappingsRouter)).toHaveLength(0); + expect(wrapper.find(RoleMappings)).toHaveLength(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 9b59e0e19a5da1..a491efcb234dca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -28,7 +28,7 @@ import { ErrorConnecting } from './components/error_connecting'; import { KibanaHeaderActions } from './components/layout/kibana_header_actions'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; -import { RoleMappingsRouter } from './components/role_mappings'; +import { RoleMappings } from './components/role_mappings'; import { Settings, SETTINGS_TITLE } from './components/settings'; import { SetupGuide } from './components/setup_guide'; import { @@ -112,7 +112,7 @@ export const AppSearchConfigured: React.FC> = (props) = {canViewRoleMappings && ( - + )} {canManageEngines && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 872db3e149b609..c8fb009fb31da2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -16,8 +16,6 @@ export const SETTINGS_PATH = '/settings'; export const CREDENTIALS_PATH = '/credentials'; export const ROLE_MAPPINGS_PATH = '/role_mappings'; -export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`; -export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`; export const ENGINES_PATH = '/engines'; export const ENGINE_CREATION_PATH = '/engine_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts index 8aa58d08b96dd7..f125a9dd13aa5e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/types.ts @@ -52,6 +52,6 @@ export interface ASRoleMapping extends RoleMapping { } export interface AdvanceRoleType { - type: RoleTypes; + id: RoleTypes; description: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx deleted file mode 100644 index a02f6c43225c08..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiButtonTo } from '../react_router_helpers'; - -import { AddRoleMappingButton } from './add_role_mapping_button'; - -describe('AddRoleMappingButton', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiButtonTo)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx deleted file mode 100644 index 097302e0aa5f12..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/add_role_mapping_button.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiButtonTo } from '../react_router_helpers'; - -import { ADD_ROLE_MAPPING_BUTTON } from './constants'; - -interface Props { - path: string; -} - -export const AddRoleMappingButton: React.FC = ({ path }) => ( - - {ADD_ROLE_MAPPING_BUTTON} - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index 504acf9ae1c6a2..2258496464ef55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -114,6 +114,14 @@ describe('AttributeSelector', () => { expect(handleAuthProviderChange).toHaveBeenCalledWith(['kbn_saml']); }); + it('should call the "handleAuthProviderChange" prop with fallback when a value not present', () => { + const wrapper = shallow(); + const select = findAuthProvidersSelect(wrapper); + select.simulate('change', [{ label: 'kbn_saml' }]); + + expect(handleAuthProviderChange).toHaveBeenCalledWith(['']); + }); + it('should call the "handleAttributeSelectorChange" prop when a value is selected', () => { const wrapper = shallow(); const select = wrapper.find('[data-test-subj="ExternalAttributeSelect"]'); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index 0ee093ed934c9b..bb8bf4ab1abf90 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -11,13 +11,8 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFieldText, - EuiFlexGroup, - EuiFlexItem, EuiFormRow, - EuiPanel, EuiSelect, - EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { AttributeName, AttributeExamples } from '../types'; @@ -27,10 +22,6 @@ import { ANY_AUTH_PROVIDER_OPTION_LABEL, AUTH_ANY_PROVIDER_LABEL, AUTH_INDIVIDUAL_PROVIDER_LABEL, - ATTRIBUTE_SELECTOR_TITLE, - AUTH_PROVIDER_LABEL, - EXTERNAL_ATTRIBUTE_LABEL, - ATTRIBUTE_VALUE_LABEL, } from './constants'; interface Props { @@ -100,80 +91,65 @@ export const AttributeSelector: React.FC = ({ handleAuthProviderChange = () => null, }) => { return ( - - -

{ATTRIBUTE_SELECTOR_TITLE}

-
- +
{availableAuthProviders && multipleAuthProvidersConfig && ( - - - - { - handleAuthProviderChange(options.map((o) => (o as ChildOption).value)); - }} - fullWidth - isDisabled={disabled} - /> - - - - + + { + handleAuthProviderChange(options.map((o) => o.value || '')); + }} + fullWidth + isDisabled={disabled} + /> + )} - - - - ({ value: attribute, text: attribute }))} - onChange={(e) => { - handleAttributeSelectorChange(e.target.value, elasticsearchRoles[0]); - }} - fullWidth - disabled={disabled} - /> - - - - - {attributeName === 'role' ? ( - ({ - value: elasticsearchRole, - text: elasticsearchRole, - }))} - onChange={(e) => { - handleAttributeValueChange(e.target.value); - }} - fullWidth - disabled={disabled} - /> - ) : ( - { - handleAttributeValueChange(e.target.value); - }} - fullWidth - disabled={disabled} - /> - )} - - - - + + ({ value: attribute, text: attribute }))} + onChange={(e) => { + handleAttributeSelectorChange(e.target.value, elasticsearchRoles[0]); + }} + fullWidth + disabled={disabled} + /> + + + {attributeName === 'role' ? ( + ({ + value: elasticsearchRole, + text: elasticsearchRole, + }))} + onChange={(e) => { + handleAttributeValueChange(e.target.value); + }} + fullWidth + disabled={disabled} + /> + ) : ( + { + handleAttributeValueChange(e.target.value); + }} + fullWidth + disabled={disabled} + /> + )} + +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index a172fbae18d8fe..7c53e37437e847 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -132,3 +132,62 @@ export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', } ); + +export const ROLE_MAPPING_NOT_FOUND = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.notFoundMessage', + { + defaultMessage: 'No matching Role mapping found.', + } +); + +export const ROLE_MAPPING_FLYOUT_CREATE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.flyoutCreateTitle', + { + defaultMessage: 'Create a role mapping', + } +); + +export const ROLE_MAPPING_FLYOUT_UPDATE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.flyoutUpdateTitle', + { + defaultMessage: 'Update role mapping', + } +); + +export const ROLE_MAPPING_FLYOUT_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.flyoutDescription', + { + defaultMessage: 'Assign roles and permissions based on user attributes', + } +); + +export const ROLE_MAPPING_ADD_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingAddButton', + { + defaultMessage: 'Add mapping', + } +); + +export const ROLE_MAPPING_FLYOUT_CREATE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingFlyoutCreateButton', + { + defaultMessage: 'Create mapping', + } +); + +export const ROLE_MAPPING_FLYOUT_UPDATE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingFlyoutUpdateButton', + { + defaultMessage: 'Update mapping', + } +); + +export const SAVE_ROLE_MAPPING = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel', + { defaultMessage: 'Save role mapping' } +); + +export const UPDATE_ROLE_MAPPING = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel', + { defaultMessage: 'Update role mapping' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx deleted file mode 100644 index c7556ee20e26a3..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiButton, EuiCallOut } from '@elastic/eui'; - -import { DeleteMappingCallout } from './delete_mapping_callout'; - -describe('DeleteMappingCallout', () => { - const handleDeleteMapping = jest.fn(); - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiCallOut)).toHaveLength(1); - expect(wrapper.find(EuiButton).prop('onClick')).toEqual(handleDeleteMapping); - }); - - it('handles button click', () => { - const wrapper = shallow(); - wrapper.find(EuiButton).simulate('click'); - - expect(handleDeleteMapping).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx deleted file mode 100644 index cb3c27038c5665..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/delete_mapping_callout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiButton, EuiCallOut } from '@elastic/eui'; - -import { - DELETE_ROLE_MAPPING_TITLE, - DELETE_ROLE_MAPPING_DESCRIPTION, - DELETE_ROLE_MAPPING_BUTTON, -} from './constants'; - -interface Props { - handleDeleteMapping(): void; -} - -export const DeleteMappingCallout: React.FC = ({ handleDeleteMapping }) => ( - -

{DELETE_ROLE_MAPPING_DESCRIPTION}

- - {DELETE_ROLE_MAPPING_BUTTON} - -
-); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index e6320dbb7feef8..6f67bc682f3336 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -export { AddRoleMappingButton } from './add_role_mapping_button'; export { AttributeSelector } from './attribute_selector'; -export { DeleteMappingCallout } from './delete_mapping_callout'; export { RoleMappingsTable } from './role_mappings_table'; +export { RoleOptionLabel } from './role_option_label'; export { RoleSelector } from './role_selector'; +export { RoleMappingFlyout } from './role_mapping_flyout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx new file mode 100644 index 00000000000000..c0973bb2c95044 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout } from '@elastic/eui'; + +import { + ROLE_MAPPING_FLYOUT_CREATE_TITLE, + ROLE_MAPPING_FLYOUT_UPDATE_TITLE, + ROLE_MAPPING_FLYOUT_CREATE_BUTTON, + ROLE_MAPPING_FLYOUT_UPDATE_BUTTON, +} from './constants'; +import { RoleMappingFlyout } from './role_mapping_flyout'; + +describe('RoleMappingFlyout', () => { + const closeRoleMappingFlyout = jest.fn(); + const handleSaveMapping = jest.fn(); + + const props = { + isNew: true, + disabled: false, + closeRoleMappingFlyout, + handleSaveMapping, + }; + + it('renders for new mapping', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper.find(EuiFlyout)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="FlyoutTitle"]').prop('children')).toEqual( + ROLE_MAPPING_FLYOUT_CREATE_TITLE + ); + expect(wrapper.find('[data-test-subj="FlyoutButton"]').prop('children')).toEqual( + ROLE_MAPPING_FLYOUT_CREATE_BUTTON + ); + }); + + it('renders for existing mapping', () => { + const wrapper = shallow( + +
+ + ); + + expect(wrapper.find(EuiFlyout)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="FlyoutTitle"]').prop('children')).toEqual( + ROLE_MAPPING_FLYOUT_UPDATE_TITLE + ); + expect(wrapper.find('[data-test-subj="FlyoutButton"]').prop('children')).toEqual( + ROLE_MAPPING_FLYOUT_UPDATE_BUTTON + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx new file mode 100644 index 00000000000000..bae991fef36550 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiPortal, + EuiText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +import { CANCEL_BUTTON_LABEL } from '../../shared/constants/actions'; + +import { + ROLE_MAPPING_FLYOUT_CREATE_TITLE, + ROLE_MAPPING_FLYOUT_UPDATE_TITLE, + ROLE_MAPPING_FLYOUT_DESCRIPTION, + ROLE_MAPPING_FLYOUT_CREATE_BUTTON, + ROLE_MAPPING_FLYOUT_UPDATE_BUTTON, +} from './constants'; + +interface Props { + children: React.ReactNode; + isNew: boolean; + disabled: boolean; + closeRoleMappingFlyout(): void; + handleSaveMapping(): void; +} + +export const RoleMappingFlyout: React.FC = ({ + children, + isNew, + disabled, + closeRoleMappingFlyout, + handleSaveMapping, +}) => ( + + + + +

+ {isNew ? ROLE_MAPPING_FLYOUT_CREATE_TITLE : ROLE_MAPPING_FLYOUT_UPDATE_TITLE} +

+
+ +

{ROLE_MAPPING_FLYOUT_DESCRIPTION}

+
+
+ + {children} + + + + + + {CANCEL_BUTTON_LABEL} + + + + {isNew ? ROLE_MAPPING_FLYOUT_CREATE_BUTTON : ROLE_MAPPING_FLYOUT_UPDATE_BUTTON} + + + + +
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index e1c43dca581fe2..5ec84db478bc3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -18,7 +18,8 @@ import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; import { RoleMappingsTable } from './role_mappings_table'; describe('RoleMappingsTable', () => { - const getRoleMappingPath = jest.fn(); + const initializeRoleMapping = jest.fn(); + const handleDeleteMapping = jest.fn(); const roleMappings = [ { ...wsRoleMapping, @@ -36,7 +37,8 @@ describe('RoleMappingsTable', () => { roleMappings, addMappingButton: