From e2ddf8bc2d829e357e7f9ea06f8e3d291a86619e Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 2 Jun 2020 11:28:25 +0200 Subject: [PATCH 1/5] [SIEM] Fix draft timeline can be attached to a case (#67844) --- .../components/flyout/header/index.tsx | 4 ++ .../timeline/properties/helpers.tsx | 63 ++++++++-------- .../timeline/properties/index.test.tsx | 71 +++++++++++++++++++ .../components/timeline/properties/index.tsx | 4 ++ .../timeline/properties/properties_right.tsx | 4 ++ .../containers/one/index.gql_query.ts | 1 + 6 files changed, 118 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx index b332260597f22..ab8a24889e9bf 100644 --- a/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx @@ -39,6 +39,7 @@ const StatefulFlyoutHeader = React.memo( title, noteIds, notesById, + status, timelineId, toggleLock, updateDescription, @@ -62,6 +63,7 @@ const StatefulFlyoutHeader = React.memo( isFavorite={isFavorite} title={title} noteIds={noteIds} + status={status} timelineId={timelineId} toggleLock={toggleLock} updateDescription={updateDescription} @@ -94,6 +96,7 @@ const makeMapStateToProps = () => { kqlQuery, title = '', noteIds = emptyNotesId, + status, } = timeline; const history = emptyHistory; // TODO: get history from store via selector @@ -107,6 +110,7 @@ const makeMapStateToProps = () => { isFavorite, isDatepickerLocked: globalInput.linkTo.includes('timeline'), noteIds, + status, title, }; }; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx index 3b1d324f3444d..3ef10d394bc7b 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx @@ -23,6 +23,7 @@ import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { TimelineStatus } from '../../../../../common/types/timeline'; import { Note } from '../../../../common/lib/note'; import { Notes } from '../../notes'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; @@ -119,40 +120,44 @@ Name.displayName = 'Name'; interface NewCaseProps { onClosePopover: () => void; timelineId: string; + timelineStatus: TimelineStatus; timelineTitle: string; } -export const NewCase = React.memo(({ onClosePopover, timelineId, timelineTitle }) => { - const history = useHistory(); - const { savedObjectId } = useSelector((state: State) => - timelineSelectors.selectTimeline(state, timelineId) - ); - const handleClick = useCallback(() => { - onClosePopover(); - history.push({ - pathname: `/${SiemPageName.case}/create`, - state: { - insertTimeline: { - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, +export const NewCase = React.memo( + ({ onClosePopover, timelineId, timelineStatus, timelineTitle }) => { + const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + const handleClick = useCallback(() => { + onClosePopover(); + history.push({ + pathname: `/${SiemPageName.case}/create`, + state: { + insertTimeline: { + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }, }, - }, - }); - }, [onClosePopover, history, timelineId, timelineTitle]); + }); + }, [onClosePopover, history, timelineId, timelineTitle]); - return ( - - {i18n.ATTACH_TIMELINE_TO_NEW_CASE} - - ); -}); + return ( + + {i18n.ATTACH_TIMELINE_TO_NEW_CASE} + + ); + } +); NewCase.displayName = 'NewCase'; interface NewTimelineProps { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx index bfa32fecac89b..8bdec78ec8da2 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx @@ -8,6 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; +import { TimelineStatus } from '../../../../../common/types/timeline'; import { mockGlobalState, apolloClientObservable, @@ -25,6 +26,24 @@ jest.mock('../../../../common/components/utils'); width: mockedWidth, })); +jest.mock('react-redux', () => { + const originalModule = jest.requireActual('react-redux'); + + return { + ...originalModule, + useSelector: jest.fn().mockReturnValue({ savedObjectId: '1' }), + }; +}); + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + describe('Properties', () => { const usersViewing = ['elastic']; @@ -50,6 +69,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -60,7 +80,45 @@ describe('Properties', () => { /> ); + + wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); + expect(wrapper.find('[data-test-subj="timeline-properties"]').exists()).toEqual(true); + expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( + false + ); + }); + + test('renders correctly draft timeline', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); + + expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( + true + ); }); test('it renders an empty star icon when it is NOT a favorite', () => { @@ -76,6 +134,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -103,6 +162,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -132,6 +192,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -159,6 +220,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -191,6 +253,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -222,6 +285,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -256,6 +320,7 @@ describe('Properties', () => { description={description} getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -292,6 +357,7 @@ describe('Properties', () => { description={description} getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -326,6 +392,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -360,6 +427,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -392,6 +460,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -421,6 +490,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} @@ -448,6 +518,7 @@ describe('Properties', () => { description="" getNotesByIds={jest.fn()} noteIds={[]} + status={TimelineStatus.active} timelineId="abc" toggleLock={jest.fn()} updateDescription={jest.fn()} diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx index 502cc85ce907a..d8966a58748ed 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx @@ -6,6 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react'; +import { TimelineStatus } from '../../../../../common/types/timeline'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Note } from '../../../../common/lib/note'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -31,6 +32,7 @@ interface Props { isFavorite: boolean; noteIds: string[]; timelineId: string; + status: TimelineStatus; title: string; toggleLock: ToggleLock; updateDescription: UpdateDescription; @@ -62,6 +64,7 @@ export const Properties = React.memo( isDatepickerLocked, isFavorite, noteIds, + status, timelineId, title, toggleLock, @@ -140,6 +143,7 @@ export const Properties = React.memo( showNotesFromWidth={width < showNotesThreshold} showTimelineModal={showTimelineModal} showUsersView={title.length > 0} + status={status} timelineId={timelineId} title={title} updateDescription={updateDescription} diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx index 4149a958e889d..963b67838e811 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx @@ -22,6 +22,7 @@ import { InspectButton, InspectButtonContainer } from '../../../../common/compon import * as i18n from './translations'; import { AssociateNote } from '../../notes/helpers'; import { Note } from '../../../../common/lib/note'; +import { TimelineStatus } from '../../../../../common/types/timeline'; export const PropertiesRightStyle = styled(EuiFlexGroup)` margin-right: 5px; @@ -79,6 +80,7 @@ interface Props { onCloseTimelineModal: () => void; onOpenTimelineModal: () => void; showTimelineModal: boolean; + status: TimelineStatus; title: string; updateNote: UpdateNote; } @@ -106,6 +108,7 @@ const PropertiesRightComponent: React.FC = ({ onCloseTimelineModal, onOpenTimelineModal, title, + status, }) => ( @@ -142,6 +145,7 @@ const PropertiesRightComponent: React.FC = ({ onClosePopover={onClosePopover} timelineId={timelineId} timelineTitle={title} + timelineStatus={status} /> diff --git a/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts index d70a419b99a3b..47e80b005fb99 100644 --- a/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts @@ -128,6 +128,7 @@ export const oneTimelineQuery = gql` updatedBy version } + status title timelineType templateTimelineId From 8373eb8680935a95c0f61e731b1f1e6332a968de Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 2 Jun 2020 12:47:13 +0300 Subject: [PATCH 2/5] Fix bug in tsvb metric add color rules (#67763) Co-authored-by: Elastic Machine --- .../public/application/components/color_rules.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.js index a0cd206822223..46b2886daf36d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.js +++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.js @@ -54,7 +54,7 @@ class ColorRulesUI extends Component { renderRow(row, i, items) { const defaults = { value: 0 }; const model = { ...defaults, ...row }; - const handleAdd = collectionActions.handleAdd.bind(null, this.props); + const handleAdd = () => collectionActions.handleAdd(this.props); const handleDelete = collectionActions.handleDelete.bind(null, this.props, model); const { intl } = this.props; const operatorOptions = [ From 7da774ff5a72dec56c70ece69cb83172e2f9f3cd Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 2 Jun 2020 07:26:22 -0400 Subject: [PATCH 3/5] [Endpoint] add new policy fields (#67323) --- .../policy/store/policy_list/middleware.ts | 8 +- .../policy_list/mock_policy_result_list.ts | 40 ++++ .../pages/policy/view/policy_list.test.tsx | 65 ++++++ .../pages/policy/view/policy_list.tsx | 202 ++++++++++++++---- 4 files changed, 265 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/siem/public/management/pages/policy/store/policy_list/mock_policy_result_list.ts create mode 100644 x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx diff --git a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts index 6054ec34b2d01..7259c0fd4d21c 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts @@ -9,6 +9,7 @@ import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; import { Immutable } from '../../../../../../common/endpoint/types'; +import { initialPolicyListState } from './reducer'; export const policyListMiddlewareFactory: ImmutableMiddlewareFactory> = ( coreStart @@ -19,6 +20,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory GetPolicyListResponse = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const policies = []; + for (let index = 0; index < actualCountToReturn; index++) { + const generator = new EndpointDocGenerator('seed'); + policies.push(generator.generatePolicyDatasource()); + } + const mock: GetPolicyListResponse = { + items: policies, + total, + page: requestPageIndex, + perPage: requestPageSize, + success: true, + }; + return mock; +}; diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx new file mode 100644 index 0000000000000..a2901ab6bfe5c --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import * as reactTestingLibrary from '@testing-library/react'; + +import { PolicyList } from './index'; +import { mockPolicyResultList } from '../store/policy_list/mock_policy_result_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { AppAction } from '../../../../common/store/actions'; + +describe('when on the policies page', () => { + let render: () => ReturnType; + let history: AppContextTestRender['history']; + let store: AppContextTestRender['store']; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + ({ history, store } = mockedContext); + render = () => mockedContext.render(); + }); + + it('should show a table', async () => { + const renderResult = render(); + const table = await renderResult.findByTestId('policyTable'); + expect(table).not.toBeNull(); + }); + + describe('when list data loads', () => { + let firstPolicyID: string; + beforeEach(() => { + reactTestingLibrary.act(() => { + history.push('/management/policy'); + reactTestingLibrary.act(() => { + const policyListData = mockPolicyResultList({ total: 3 }); + firstPolicyID = policyListData.items[0].id; + const action: AppAction = { + type: 'serverReturnedPolicyListData', + payload: { + policyItems: policyListData.items, + total: policyListData.total, + pageSize: policyListData.perPage, + pageIndex: policyListData.page, + }, + }; + store.dispatch(action); + }); + }); + }); + it('should display rows in the table', async () => { + const renderResult = render(); + const rows = await renderResult.findAllByRole('row'); + expect(rows).toHaveLength(4); + }); + it('should display policy name value as a link', async () => { + const renderResult = render(); + const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; + expect(policyNameLink).not.toBeNull(); + expect(policyNameLink.getAttribute('href')).toContain(`policy/${firstPolicyID}`); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx index 3a8004aa2ec6d..2826289dab8e2 100644 --- a/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx @@ -4,20 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useMemo } from 'react'; -import { EuiBasicTable, EuiText, EuiTableFieldDataColumnType, EuiLink } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from 'react'; +import { + EuiBasicTable, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiTableFieldDataColumnType, + EuiLink, + EuiPopover, + EuiContextMenuPanelProps, + EuiContextMenuItem, + EuiButtonIcon, + EuiContextMenuPanel, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { useLocation, useHistory } from 'react-router-dom'; -import { - selectApiError, - selectIsLoading, - selectPageIndex, - selectPageSize, - selectPolicyItems, - selectTotal, -} from '../store/policy_list/selectors'; +import { createStructuredSelector } from 'reselect'; +import { CreateStructuredSelector } from '../../../../common/store'; +import * as selectors from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -27,11 +34,53 @@ import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; import { ManagementPageView } from '../../../components/management_page_view'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { getManagementUrl } from '../../../common/routing'; +import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time'; interface TableChangeCallbackArguments { page: { index: number; size: number }; } +interface PackageData { + name: string; + title: string; + version: string; +} + +const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); + +// eslint-disable-next-line react/display-name +export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['items'] }>( + ({ items }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + ); + } +); + const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ name, route, @@ -40,24 +89,32 @@ const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ const clickHandler = useNavigateByRouterEventHandler(route); return ( // eslint-disable-next-line @elastic/eui/href-or-on-click - + {name} ); }; +const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const PolicyList = React.memo(() => { const { services, notifications } = useKibana(); const history = useHistory(); const location = useLocation(); const dispatch = useDispatch<(action: PolicyListAction) => void>(); - const policyItems = usePolicyListSelector(selectPolicyItems); - const pageIndex = usePolicyListSelector(selectPageIndex); - const pageSize = usePolicyListSelector(selectPageSize); - const totalItemCount = usePolicyListSelector(selectTotal); - const loading = usePolicyListSelector(selectIsLoading); - const apiError = usePolicyListSelector(selectApiError); + const { + selectPolicyItems: policyItems, + selectPageIndex: pageIndex, + selectPageSize: pageSize, + selectTotal: totalItemCount, + selectIsLoading: loading, + selectApiError: apiError, + } = usePolicyListSelector(selector); useEffect(() => { if (apiError) { @@ -94,58 +151,110 @@ export const PolicyList = React.memo(() => { defaultMessage: 'Policy Name', }), // eslint-disable-next-line react/display-name - render: (value: string, item: Immutable) => { + render: (name: string, item: Immutable) => { const routePath = getManagementUrl({ name: 'policyDetails', policyId: item.id, excludePrefix: true, }); const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id }); - return ; + return ( + + + + + + + + + + + ); }, - truncateText: true, }, { - field: 'revision', - name: i18n.translate('xpack.siem.endpoint.policyList.revisionField', { - defaultMessage: 'Revision', + field: 'created_by', + name: i18n.translate('xpack.siem.endpoint.policyList.createdBy', { + defaultMessage: 'Created By', }), - dataType: 'number', + truncateText: true, }, { - field: 'package', - name: i18n.translate('xpack.siem.endpoint.policyList.versionField', { - defaultMessage: 'Version', + field: 'created_at', + name: i18n.translate('xpack.siem.endpoint.policyList.createdAt', { + defaultMessage: 'Created Date', }), - render(pkg) { - return `${pkg.title} v${pkg.version}`; + render(createdAt: string) { + return ; }, }, { - field: 'description', - name: i18n.translate('xpack.siem.endpoint.policyList.descriptionField', { - defaultMessage: 'Description', + field: 'updated_by', + name: i18n.translate('xpack.siem.endpoint.policyList.updatedBy', { + defaultMessage: 'Last Updated By', }), truncateText: true, }, { - field: 'config_id', - name: i18n.translate('xpack.siem.endpoint.policyList.agentConfigField', { - defaultMessage: 'Agent Configuration', + field: 'updated_at', + name: i18n.translate('xpack.siem.endpoint.policyList.updatedAt', { + defaultMessage: 'Last Updated', }), - render(version: string) { - return ( - - {version} - - ); + render(updatedAt: string) { + return ; }, }, + { + field: 'package', + name: i18n.translate('xpack.siem.endpoint.policyList.versionFieldLabel', { + defaultMessage: 'Version', + }), + render(pkg: Immutable) { + return i18n.translate('xpack.siem.endpoint.policyList.versionField', { + defaultMessage: '{title} v{version}', + values: { + title: pkg.title, + version: pkg.version, + }, + }); + }, + }, + { + field: '', + name: 'Actions', + actions: [ + { + // eslint-disable-next-line react/display-name + render: (item: Immutable) => { + return ( + + + + + , + ]} + /> + ); + }, + }, + ], + }, ], [services.application] ); @@ -174,6 +283,7 @@ export const PolicyList = React.memo(() => { pagination={paginationSetup} onChange={handleTableChange} data-test-subj="policyTable" + hasActions={false} /> From 7d0ffb53bcb9d302e9f1f675b70d36b235f78e8d Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 2 Jun 2020 08:32:24 -0400 Subject: [PATCH 4/5] install default packages in parallel (#67893) --- .../server/services/epm/packages/install.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 7c0d5d571f6a5..736711f9152e9 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -52,22 +52,22 @@ export async function ensureInstalledDefaultPackages( const installations = []; for (const pkgName in DefaultPackages) { if (!DefaultPackages.hasOwnProperty(pkgName)) continue; - const installation = await ensureInstalledPackage({ + const installation = ensureInstalledPackage({ savedObjectsClient, pkgName, callCluster, }); - if (installation) installations.push(installation); + installations.push(installation); } - return installations; + return Promise.all(installations); } export async function ensureInstalledPackage(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; callCluster: CallESAsCurrentUser; -}): Promise { +}): Promise { const { savedObjectsClient, pkgName, callCluster } = options; const installedPackage = await getInstallation({ savedObjectsClient, pkgName }); if (installedPackage) { @@ -79,7 +79,9 @@ export async function ensureInstalledPackage(options: { pkgName, callCluster, }); - return await getInstallation({ savedObjectsClient, pkgName }); + const installation = await getInstallation({ savedObjectsClient, pkgName }); + if (!installation) throw new Error(`could not get installation ${pkgName}`); + return installation; } export async function installPackage(options: { From a091124fab89573e6744582a09d4eea19e4c28f7 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 2 Jun 2020 15:15:21 +0200 Subject: [PATCH 5/5] Move application.applications$ to public contract (#67463) * expose applications$ on public contract * review comments --- ...e-public.applicationstart.applications_.md | 18 ++++++ ...ana-plugin-core-public.applicationstart.md | 1 + ...ana-plugin-core-public.legacyapp.appurl.md | 11 ++++ ...-public.legacyapp.disablesuburltracking.md | 11 ++++ ...-core-public.legacyapp.linktolastsuburl.md | 11 ++++ .../kibana-plugin-core-public.legacyapp.md | 22 +++++++ ...plugin-core-public.legacyapp.suburlbase.md | 11 ++++ .../core/public/kibana-plugin-core-public.md | 3 + ...kibana-plugin-core-public.publicappinfo.md | 15 +++++ ...-plugin-core-public.publiclegacyappinfo.md | 15 +++++ .../application/application_service.mock.ts | 7 ++- .../application/application_service.test.ts | 15 ++++- .../application/application_service.tsx | 7 ++- src/core/public/application/index.ts | 4 +- src/core/public/application/types.ts | 42 +++++++++----- src/core/public/application/utils.test.ts | 57 ++++++++++++++++++- src/core/public/application/utils.ts | 18 +++++- src/core/public/chrome/chrome_service.test.ts | 7 ++- .../chrome/nav_links/to_nav_link.test.ts | 9 +-- .../public/chrome/nav_links/to_nav_link.ts | 9 ++- src/core/public/index.ts | 3 + src/core/public/legacy/legacy_service.ts | 1 + src/core/public/plugins/plugin_context.ts | 1 + src/core/public/public.api.md | 23 ++++++++ 24 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappinfo.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md new file mode 100644 index 0000000000000..d428faa500faf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) > [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) + +## ApplicationStart.applications$ property + +Observable emitting the list of currently registered apps and their associated status. + +Signature: + +```typescript +applications$: Observable>; +``` + +## Remarks + +Applications disabled by [Capabilities](./kibana-plugin-core-public.capabilities.md) will not be present in the map. Applications manually disabled from the client-side using an [application updater](./kibana-plugin-core-public.appupdater.md) are present, with their status properly set as `inaccessible`. + diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md index 6f45bab3ebd2d..896de2de32dd5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md @@ -15,6 +15,7 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | +| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | Observable<ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo>> | Observable emitting the list of currently registered apps and their associated status. | | [capabilities](./kibana-plugin-core-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | | [currentAppId$](./kibana-plugin-core-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md new file mode 100644 index 0000000000000..292bf29962839 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) + +## LegacyApp.appUrl property + +Signature: + +```typescript +appUrl: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md new file mode 100644 index 0000000000000..af4d0eb7969d3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) + +## LegacyApp.disableSubUrlTracking property + +Signature: + +```typescript +disableSubUrlTracking?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md new file mode 100644 index 0000000000000..fa1314b74fd83 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) + +## LegacyApp.linkToLastSubUrl property + +Signature: + +```typescript +linkToLastSubUrl?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.md new file mode 100644 index 0000000000000..06533aaa99170 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) + +## LegacyApp interface + + +Signature: + +```typescript +export interface LegacyApp extends AppBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) | string | | +| [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) | boolean | | +| [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) | boolean | | +| [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md new file mode 100644 index 0000000000000..44a1e52ccd244 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) + +## LegacyApp.subUrlBase property + +Signature: + +```typescript +subUrlBase?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b2524ec48c757..9e4afe0f5133c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -90,6 +90,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [ImageValidation](./kibana-plugin-core-public.imagevalidation.md) | | | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | +| [LegacyApp](./kibana-plugin-core-public.legacyapp.md) | | | [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) | | @@ -162,6 +163,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | +| [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | +| [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) | Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md new file mode 100644 index 0000000000000..c70f3a97a8882 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) + +## PublicAppInfo type + +Public information about a registered [application](./kibana-plugin-core-public.app.md) + +Signature: + +```typescript +export declare type PublicAppInfo = Omit & { + legacy: false; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md new file mode 100644 index 0000000000000..cc3e9de3193cb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) + +## PublicLegacyAppInfo type + +Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) + +Signature: + +```typescript +export declare type PublicLegacyAppInfo = Omit & { + legacy: true; +}; +``` diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 24c0e66359afa..300b09e17d15d 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -25,8 +25,8 @@ import { InternalApplicationStart, ApplicationStart, InternalApplicationSetup, - App, - LegacyApp, + PublicAppInfo, + PublicLegacyAppInfo, } from './types'; import { ApplicationServiceContract } from './test_types'; @@ -47,6 +47,7 @@ const createStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); return { + applications$: new BehaviorSubject>(new Map()), currentAppId$: currentAppId$.asObservable(), capabilities: capabilitiesServiceMock.createStartContract().capabilities, navigateToApp: jest.fn(), @@ -60,7 +61,7 @@ const createInternalStartContractMock = (): jest.Mocked(); return { - applications$: new BehaviorSubject>(new Map()), + applications$: new BehaviorSubject>(new Map()), capabilities: capabilitiesServiceMock.createStartContract().capabilities, currentAppId$: currentAppId$.asObservable(), getComponent: jest.fn(), diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index b65a8581e5b58..400d1881a5af8 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -34,7 +34,15 @@ import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; -import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types'; +import { + App, + PublicAppInfo, + AppNavLinkStatus, + AppStatus, + AppUpdater, + LegacyApp, + PublicLegacyAppInfo, +} from './types'; import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { @@ -366,7 +374,10 @@ describe('#setup()', () => { setup.registerAppUpdater(statusUpdater); const start = await service.start(startDeps); - let latestValue: ReadonlyMap = new Map(); + let latestValue: ReadonlyMap = new Map< + string, + PublicAppInfo | PublicLegacyAppInfo + >(); start.applications$.subscribe((apps) => { latestValue = apps; }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index b52b4984fb5e1..2224f72e2bd91 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -46,7 +46,7 @@ import { Mounter, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; -import { appendAppPath, parseAppUrl, relativeToAbsolute } from './utils'; +import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils'; interface SetupDeps { context: ContextSetup; @@ -291,7 +291,10 @@ export class ApplicationService { }; return { - applications$, + applications$: applications$.pipe( + map((apps) => new Map([...apps.entries()].map(([id, app]) => [id, getAppInfo(app)]))), + shareReplay(1) + ), capabilities, currentAppId$: this.currentAppId$.pipe( filter((appId) => appId !== undefined), diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index ec10d2bc22871..d51a4c0d69d42 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -39,7 +39,9 @@ export { AppLeaveAction, AppLeaveDefaultAction, AppLeaveConfirmAction, + LegacyApp, + PublicAppInfo, + PublicLegacyAppInfo, // Internal types InternalApplicationStart, - LegacyApp, } from './types'; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index c07d929fc5cea..2269fd0a4ca48 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -235,7 +235,7 @@ export interface App extends AppBase { appRoute?: string; } -/** @internal */ +/** @public */ export interface LegacyApp extends AppBase { appUrl: string; subUrlBase?: string; @@ -243,6 +243,24 @@ export interface LegacyApp extends AppBase { disableSubUrlTracking?: boolean; } +/** + * Public information about a registered {@link App | application} + * + * @public + */ +export type PublicAppInfo = Omit & { + legacy: false; +}; + +/** + * Information about a registered {@link LegacyApp | legacy application} + * + * @public + */ +export type PublicLegacyAppInfo = Omit & { + legacy: true; +}; + /** * A mount function called when the user navigates to this app's route. * @@ -649,6 +667,15 @@ export interface ApplicationStart { */ capabilities: RecursiveReadonly; + /** + * Observable emitting the list of currently registered apps and their associated status. + * + * @remarks + * Applications disabled by {@link Capabilities} will not be present in the map. Applications manually disabled from + * the client-side using an {@link AppUpdater | application updater} are present, with their status properly set as `inaccessible`. + */ + applications$: Observable>; + /** * Navigate to a given app * @@ -721,18 +748,7 @@ export interface ApplicationStart { } /** @internal */ -export interface InternalApplicationStart - extends Pick< - ApplicationStart, - 'capabilities' | 'navigateToApp' | 'navigateToUrl' | 'getUrlForApp' | 'currentAppId$' - > { - /** - * Apps available based on the current capabilities. - * Should be used to show navigation links and make routing decisions. - * Applications manually disabled from the client-side using {@link AppUpdater} - */ - applications$: Observable>; - +export interface InternalApplicationStart extends Omit { /** * Register a context provider for application mounting. Will only be available to applications that depend on the * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts index a86a1206fc983..b41945aa43682 100644 --- a/src/core/public/application/utils.test.ts +++ b/src/core/public/application/utils.test.ts @@ -17,7 +17,8 @@ * under the License. */ -import { LegacyApp, App } from './types'; +import { of } from 'rxjs'; +import { LegacyApp, App, AppStatus, AppNavLinkStatus } from './types'; import { BasePath } from '../http/base_path'; import { removeSlashes, @@ -25,6 +26,7 @@ import { isLegacyApp, relativeToAbsolute, parseAppUrl, + getAppInfo, } from './utils'; describe('removeSlashes', () => { @@ -459,3 +461,56 @@ describe('parseAppUrl', () => { }); }); }); + +describe('getAppInfo', () => { + const createApp = (props: Partial = {}): App => ({ + mount: () => () => undefined, + updater$: of(() => undefined), + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + appRoute: `/app/some-id`, + legacy: false, + ...props, + }); + + const createLegacyApp = (props: Partial = {}): LegacyApp => ({ + appUrl: '/my-app-url', + updater$: of(() => undefined), + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + legacy: true, + ...props, + }); + + it('converts an application and remove sensitive properties', () => { + const app = createApp(); + const info = getAppInfo(app); + + expect(info).toEqual({ + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + appRoute: `/app/some-id`, + legacy: false, + }); + }); + + it('converts a legacy application and remove sensitive properties', () => { + const app = createLegacyApp(); + const info = getAppInfo(app); + + expect(info).toEqual({ + appUrl: '/my-app-url', + id: 'some-id', + title: 'some-title', + status: AppStatus.accessible, + navLinkStatus: AppNavLinkStatus.default, + legacy: true, + }); + }); +}); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts index 8987a9402f2db..1abd710548745 100644 --- a/src/core/public/application/utils.ts +++ b/src/core/public/application/utils.ts @@ -18,7 +18,7 @@ */ import { IBasePath } from '../http'; -import { App, LegacyApp } from './types'; +import { App, LegacyApp, PublicAppInfo, PublicLegacyAppInfo } from './types'; export interface AppUrlInfo { app: string; @@ -119,3 +119,19 @@ const removeBasePath = (url: string, basePath: IBasePath, origin: string): strin } return basePath.remove(url); }; + +export function getAppInfo(app: App | LegacyApp): PublicAppInfo | PublicLegacyAppInfo { + if (isLegacyApp(app)) { + const { updater$, ...infos } = app; + return { + ...infos, + legacy: true, + }; + } else { + const { updater$, mount, ...infos } = app; + return { + ...infos, + legacy: false, + }; + } +} diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0bc305ed9e28c..e39733cc10de7 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import * as Rx from 'rxjs'; import { take, toArray } from 'rxjs/operators'; -import { App } from '../application'; +import { App, PublicAppInfo } from '../application'; import { applicationServiceMock } from '../application/application_service.mock'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -29,6 +29,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { ChromeService } from './chrome_service'; +import { getAppInfo } from '../application/utils'; class FakeApp implements App { public title = `${this.id} App`; @@ -55,8 +56,8 @@ function defaultStartDeps(availableApps?: App[]) { }; if (availableApps) { - deps.application.applications$ = new Rx.BehaviorSubject>( - new Map(availableApps.map((app) => [app.id, app])) + deps.application.applications$ = new Rx.BehaviorSubject>( + new Map(availableApps.map((app) => [app.id, getAppInfo(app) as PublicAppInfo])) ); } diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 4c319873af804..ba04dbed49cd4 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -17,15 +17,12 @@ * under the License. */ -import { App, AppMount, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; +import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application'; import { toNavLink } from './to_nav_link'; import { httpServiceMock } from '../../mocks'; -function mount() {} - -const app = (props: Partial = {}): App => ({ - mount: (mount as unknown) as AppMount, +const app = (props: Partial = {}): PublicAppInfo => ({ id: 'some-id', title: 'some-title', status: AppStatus.accessible, @@ -35,7 +32,7 @@ const app = (props: Partial = {}): App => ({ ...props, }); -const legacyApp = (props: Partial = {}): LegacyApp => ({ +const legacyApp = (props: Partial = {}): PublicLegacyAppInfo => ({ appUrl: '/my-app-url', id: 'some-id', title: 'some-title', diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 24744fe53c82c..b8f97f9ddc005 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -17,12 +17,15 @@ * under the License. */ -import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; +import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; import { appendAppPath } from '../../application/utils'; -export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { +export function toNavLink( + app: PublicAppInfo | PublicLegacyAppInfo, + basePath: IBasePath +): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; const relativeBaseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) @@ -63,6 +66,6 @@ export function relativeToAbsolute(url: string) { return a.href; } -function isLegacyApp(app: App | LegacyApp): app is LegacyApp { +function isLegacyApp(app: PublicAppInfo | PublicLegacyAppInfo): app is PublicLegacyAppInfo { return app.legacy === true; } diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 3698fdcfe9512..aa037329b1b6e 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -104,6 +104,7 @@ export { ApplicationSetup, ApplicationStart, App, + PublicAppInfo, AppBase, AppMount, AppMountDeprecated, @@ -120,6 +121,8 @@ export { AppUpdatableFields, AppUpdater, ScopedHistory, + LegacyApp, + PublicLegacyAppInfo, } from './application'; export { diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 810416cdbfe16..d77676b350f93 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -131,6 +131,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreStart = { ...core, application: { + applications$: core.application.applications$, currentAppId$: core.application.currentAppId$, capabilities: core.application.capabilities, getUrlForApp: core.application.getUrlForApp, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index c688373630a07..65c6b6ce4edba 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -135,6 +135,7 @@ export function createPluginStartContext< ): CoreStart { return { application: { + applications$: deps.application.applications$, currentAppId$: deps.application.currentAppId$, capabilities: deps.application.capabilities, navigateToApp: deps.application.navigateToApp, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 90c5dbb5f6558..bae0f9a2281cf 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -106,6 +106,7 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { + applications$: Observable>; capabilities: RecursiveReadonly; currentAppId$: Observable; getUrlForApp(appId: string, options?: { @@ -857,6 +858,18 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } +// @public (undocumented) +export interface LegacyApp extends AppBase { + // (undocumented) + appUrl: string; + // (undocumented) + disableSubUrlTracking?: boolean; + // (undocumented) + linkToLastSubUrl?: boolean; + // (undocumented) + subUrlBase?: string; +} + // @public @deprecated export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts @@ -993,6 +1006,16 @@ export interface PluginInitializerContext // @public (undocumented) export type PluginOpaqueId = symbol; +// @public +export type PublicAppInfo = Omit & { + legacy: false; +}; + +// @public +export type PublicLegacyAppInfo = Omit & { + legacy: true; +}; + // @public export type PublicUiSettingsParams = Omit;