diff --git a/package.json b/package.json index a79d41a67f580..c3762c2eabd28 100644 --- a/package.json +++ b/package.json @@ -387,7 +387,7 @@ "babel-jest": "^24.9.0", "babel-plugin-dynamic-import-node": "^2.3.0", "babel-plugin-istanbul": "^5.2.0", - "backport": "4.8.0", + "backport": "4.9.0", "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index a7f1a0e8c6dc9..e168324b950f2 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -1857,180 +1857,39 @@ describe('delete()', () => { }); describe('update()', () => { - test('updates given parameters', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - alertTypeRegistry.get.mockReturnValueOnce({ + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + alertTypeId: '123', + scheduledTaskId: 'task-123', + }, + references: [], + version: '123', + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + alertTypeRegistry.get.mockReturnValue({ id: '123', name: 'Test', actionGroups: ['default'], async executor() {}, }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - alertTypeId: '123', - scheduledTaskId: 'task-123', - }, - references: [], - version: '123', - }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - savedObjectsClient.update.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": true, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "apiKey": null, - "apiKeyOwner": null, - "enabled": true, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "updatedBy": "elastic", - } - `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); }); - it('updates with multiple actions', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: ['default'], - async executor() {}, - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - alertTypeId: '123', - scheduledTaskId: 'task-123', - }, - references: [], - version: '123', - }); + test('updates given parameters', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -2060,7 +1919,6 @@ describe('update()', () => { params: { bar: true, }, - createdAt: new Date().toISOString(), actions: [ { group: 'default', @@ -2088,6 +1946,7 @@ describe('update()', () => { }, ], scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), }, updated_at: new Date().toISOString(), references: [ @@ -2183,37 +2042,85 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ - { - id: '1', - type: 'action', - }, - { - id: '2', - type: 'action', - }, - ]); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + Object { + "actionRef": "action_1", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + Object { + "actionRef": "action_2", + "actionTypeId": "test2", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "apiKey": null, + "apiKeyOwner": null, + "enabled": true, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "updatedBy": "elastic", + } + `); + expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + Object { + "id": "1", + "name": "action_1", + "type": "action", + }, + Object { + "id": "2", + "name": "action_2", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('calls the createApiKey function', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: ['default'], - async executor() {}, - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - alertTypeId: '123', - scheduledTaskId: 'task-123', - }, - references: [], - version: '123', - }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -2357,7 +2264,6 @@ describe('update()', () => { }); it('should validate params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); alertTypeRegistry.get.mockReturnValueOnce({ id: '123', name: 'Test', @@ -2369,14 +2275,6 @@ describe('update()', () => { }, async executor() {}, }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - }, - references: [], - }); await expect( alertsClient.update({ id: '1', @@ -2404,26 +2302,75 @@ describe('update()', () => { }); it('swallows error when invalidate API key throws', async () => { - const alertsClient = new AlertsClient(alertsClientParams); alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: ['default'], - async executor() {}, + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + savedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { enabled: true, - alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], scheduledTaskId: 'task-123', - apiKey: Buffer.from('123:abc').toString('base64'), }, - references: [], - version: '123', + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], }); + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + it('swallows error when getDecryptedAsInternalUser throws', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -2434,6 +2381,14 @@ describe('update()', () => { }, references: [], }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: 'test2', + }, + references: [], + }, ], }); savedObjectsClient.update.mockResolvedValueOnce({ @@ -2454,15 +2409,43 @@ describe('update()', () => { foo: true, }, }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, ], scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), }, + updated_at: new Date().toISOString(), references: [ { name: 'action_0', type: 'action', id: '1', }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, ], }); await alertsClient.update({ @@ -2482,11 +2465,26 @@ describe('update()', () => { foo: true, }, }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, ], }, }); + expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'update(): Failed to load API key to invalidate on alert 1: Fail' ); }); @@ -2575,7 +2573,6 @@ describe('update()', () => { test('updating the alert schedule should rerun the task immediately', async () => { const alertId = uuid.v4(); const taskId = uuid.v4(); - const alertsClient = new AlertsClient(alertsClientParams); mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' }); @@ -2606,7 +2603,6 @@ describe('update()', () => { test('updating the alert without changing the schedule should not rerun the task', async () => { const alertId = uuid.v4(); const taskId = uuid.v4(); - const alertsClient = new AlertsClient(alertsClientParams); mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' }); @@ -2637,7 +2633,6 @@ describe('update()', () => { test('updating the alert should not wait for the rerun the task to complete', async done => { const alertId = uuid.v4(); const taskId = uuid.v4(); - const alertsClient = new AlertsClient(alertsClientParams); mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); @@ -2676,7 +2671,6 @@ describe('update()', () => { test('logs when the rerun of an alerts underlying task fails', async () => { const alertId = uuid.v4(); const taskId = uuid.v4(); - const alertsClient = new AlertsClient(alertsClientParams); mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 97f556be04957..4f4443a9ce655 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -269,22 +269,41 @@ export class AlertsClient { } public async update({ id, data }: UpdateOptions): Promise { - const decryptedAlertSavedObject = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); - const updateResult = await this.updateAlert({ id, data }, decryptedAlertSavedObject); - - if ( - updateResult.scheduledTaskId && - !isEqual(decryptedAlertSavedObject.attributes.schedule, updateResult.schedule) - ) { - this.taskManager.runNow(updateResult.scheduledTaskId).catch((err: Error) => { - this.logger.error( - `Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}` - ); - }); + let alertSavedObject: SavedObject; + + try { + alertSavedObject = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser< + RawAlert + >('alert', id, { namespace: this.namespace }); + } catch (e) { + // We'll skip invalidating the API key since we failed to load the decrypted saved object + this.logger.error( + `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ); + // Still attempt to load the object using SOC + alertSavedObject = await this.savedObjectsClient.get('alert', id); } + const updateResult = await this.updateAlert({ id, data }, alertSavedObject); + + await Promise.all([ + alertSavedObject.attributes.apiKey + ? this.invalidateApiKey({ apiKey: alertSavedObject.attributes.apiKey }) + : null, + (async () => { + if ( + updateResult.scheduledTaskId && + !isEqual(alertSavedObject.attributes.schedule, updateResult.schedule) + ) { + this.taskManager.runNow(updateResult.scheduledTaskId).catch((err: Error) => { + this.logger.error( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}` + ); + }); + } + })(), + ]); + return updateResult; } @@ -319,8 +338,6 @@ export class AlertsClient { } ); - await this.invalidateApiKey({ apiKey: attributes.apiKey }); - return this.getPartialAlertFromRaw( id, updatedObject.attributes, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 441ebfb2d53bf..9636dab406a44 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -153,7 +153,7 @@ export class VectorStyleEditor extends Component { _hasMarkerOrIcon() { const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; - return !iconSize.isDynamic() && iconSize.getOptions().size > 0; + return iconSize.isDynamic() || iconSize.getOptions().size > 0; } _hasLabel() { diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 5735faa9c6f52..dce5e7ad52b09 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -5,8 +5,13 @@ */ import React, { FC, useState } from 'react'; +import { encode } from 'rison-node'; + import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; + +import { useUrlState } from '../../util/url_state'; + import { TabId } from './navigation_menu'; export interface Tab { @@ -65,6 +70,7 @@ const TAB_DATA: Record = { }; export const MainTabs: FC = ({ tabId, disableLinks }) => { + const [globalState] = useUrlState('_g'); const [selectedTabId, setSelectedTabId] = useState(tabId); function onSelectedTabChanged(id: string) { setSelectedTabId(id); @@ -78,10 +84,13 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { const id = tab.id; const testSubject = TAB_DATA[id].testSubject; const defaultPathId = TAB_DATA[id].pathId || id; + // globalState (e.g. selected jobs and time range) should be retained when changing pages. + // appState will not be considered. + const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : ''; return ( diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx index c76967455fa42..a63a07b3ec538 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx @@ -22,6 +22,11 @@ interface Duration { end: string; } +interface RefreshInterval { + pause: boolean; + value: number; +} + function getRecentlyUsedRangesFactory(timeHistory: TimeHistory) { return function(): Duration[] { return ( @@ -44,7 +49,7 @@ export const TopNav: FC = () => { const [globalState, setGlobalState] = useUrlState('_g'); const getRecentlyUsedRanges = getRecentlyUsedRangesFactory(timeHistory); - const [refreshInterval, setRefreshInterval] = useState( + const [refreshInterval, setRefreshInterval] = useState( globalState?.refreshInterval ?? timefilter.getRefreshInterval() ); useEffect(() => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 28f95b3c1ba21..6999f4c591eac 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -5,7 +5,6 @@ */ import React, { Component } from 'react'; -import { timefilter } from 'ui/timefilter'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -35,13 +34,8 @@ import { UpgradeWarning } from '../../../../components/upgrade'; import { RefreshJobsListButton } from '../refresh_jobs_list_button'; import { isEqual } from 'lodash'; -import { - DEFAULT_REFRESH_INTERVAL_MS, - DELETING_JOBS_REFRESH_INTERVAL_MS, - MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../../common/constants/jobs_list'; +import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; -let jobsRefreshInterval = null; let deletingJobsRefreshTimeout = null; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page @@ -67,21 +61,12 @@ export class JobsListView extends Component { this.showDeleteJobModal = () => {}; this.showStartDatafeedModal = () => {}; this.showCreateWatchFlyout = () => {}; - - this.blockRefresh = false; - this.refreshIntervalSubscription = null; } componentDidMount() { - if (this.props.isManagementTable === true) { - this.refreshJobSummaryList(true); - } else { - timefilter.disableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - - this.initAutoRefresh(); - this.initAutoRefreshUpdate(); + this.refreshJobSummaryList(true); + if (this.props.isManagementTable !== true) { // check to see if we need to open the start datafeed modal // after the page has rendered. This will happen if the user // has just created a job in the advanced wizard and selected to @@ -90,59 +75,18 @@ export class JobsListView extends Component { } } - componentWillUnmount() { - if (this.props.isManagementTable === undefined) { - if (this.refreshIntervalSubscription) this.refreshIntervalSubscription.unsubscribe(); - deletingJobsRefreshTimeout = null; - this.clearRefreshInterval(); - } - } - - initAutoRefresh() { - const { value } = timefilter.getRefreshInterval(); - if (value === 0) { - // the auto refresher starts in an off state - // so switch it on and set the interval to 30s - timefilter.setRefreshInterval({ - pause: false, - value: DEFAULT_REFRESH_INTERVAL_MS, - }); - } - - this.setAutoRefresh(); - } - - initAutoRefreshUpdate() { - // update the interval if it changes - this.refreshIntervalSubscription = timefilter.getRefreshIntervalUpdate$().subscribe({ - next: () => this.setAutoRefresh(), - }); - } - - setAutoRefresh() { - const { value, pause } = timefilter.getRefreshInterval(); - if (pause) { - this.clearRefreshInterval(); - } else { - this.setRefreshInterval(value); + componentDidUpdate(prevProps) { + if (prevProps.lastRefresh !== this.props.lastRefresh) { + this.refreshJobSummaryList(); } - // force load the jobs list when the refresh interval changes - this.refreshJobSummaryList(true); } - setRefreshInterval(interval) { - this.clearRefreshInterval(); - if (interval >= MINIMUM_REFRESH_INTERVAL_MS) { - this.blockRefresh = false; - jobsRefreshInterval = setInterval(() => this.refreshJobSummaryList(), interval); + componentWillUnmount() { + if (this.props.isManagementTable === undefined) { + deletingJobsRefreshTimeout = null; } } - clearRefreshInterval() { - this.blockRefresh = true; - clearInterval(jobsRefreshInterval); - } - openAutoStartDatafeedModal() { const job = checkForAutoStartDatafeed(); if (job !== undefined) { @@ -281,7 +225,7 @@ export class JobsListView extends Component { }; async refreshJobSummaryList(forceRefresh = false) { - if (forceRefresh === true || this.blockRefresh === false) { + if (forceRefresh === true || this.props.blockRefresh !== true) { // Set loading to true for jobs_list table for initial job loading if (this.state.loading === null) { this.setState({ loading: true }); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index f820372e20c09..c3c2550f47645 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -11,7 +11,14 @@ import { NavigationMenu } from '../../components/navigation_menu'; // @ts-ignore import { JobsListView } from './components/jobs_list_view'; -export const JobsPage: FC<{ props?: any }> = props => { +interface JobsPageProps { + blockRefresh?: boolean; + isManagementTable?: boolean; + isMlEnabledInSpace?: boolean; + lastRefresh?: number; +} + +export const JobsPage: FC = props => { return (
diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx index e61c24426bde9..3d9a2adedc40d 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useEffect, FC } from 'react'; +import { useObservable } from 'react-use'; import { i18n } from '@kbn/i18n'; +import { timefilter } from 'ui/timefilter'; +import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../common/constants/jobs_list'; +import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; +import { useUrlState } from '../../util/url_state'; import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { basicResolvers } from '../resolvers'; @@ -32,9 +37,31 @@ export const jobListRoute: MlRoute = { const PageWrapper: FC = ({ config, deps }) => { const { context } = useResolver(undefined, undefined, config, basicResolvers(deps)); + const [globalState, setGlobalState] = useUrlState('_g'); + + const mlTimefilterRefresh = useObservable(mlTimefilterRefresh$); + const lastRefresh = mlTimefilterRefresh?.lastRefresh ?? 0; + const refreshValue = globalState?.refreshInterval?.value ?? 0; + const refreshPause = globalState?.refreshInterval?.pause ?? true; + const blockRefresh = refreshValue === 0 || refreshPause === true; + + useEffect(() => { + timefilter.disableTimeRangeSelector(); + timefilter.enableAutoRefreshSelector(); + + // If the refreshInterval defaults to 0s/pause=true, set it to 30s/pause=false, + // otherwise pass on the globalState's settings to the date picker. + const refreshInterval = + refreshValue === 0 && refreshPause === true + ? { pause: false, value: DEFAULT_REFRESH_INTERVAL_MS } + : { pause: refreshPause, value: refreshValue }; + setGlobalState({ refreshInterval }); + timefilter.setRefreshInterval(refreshInterval); + }, []); + return ( - + ); }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 2a7e0b2203824..7a4f73885107c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -108,6 +108,87 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should still be able to update when AAD is broken', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + await supertest + .put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '12s' }, + actions: [], + throttle: '2m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + ...updatedData, + id: createdAlert.id, + alertTypeId: 'test.noop', + consumer: 'bar', + createdBy: 'elastic', + enabled: true, + updatedBy: user.username, + apiKeyOwner: user.username, + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: createdAlert.scheduledTaskId, + createdAt: response.body.createdAt, + updatedAt: response.body.updatedAt, + }); + expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan( + Date.parse(response.body.createdAt) + ); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alert`) diff --git a/yarn.lock b/yarn.lock index 5dc4db12c5db4..e1b39e85d17f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7577,10 +7577,10 @@ backo2@1.0.2: resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= -backport@4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/backport/-/backport-4.8.0.tgz#bbb97fbebc523cfc006fd94c887c4044a37aba08" - integrity sha512-Gk78NWuB+FJN4lSb+NWTE2b5Qs+JWJAV9fRAQ5ncYHSsWeowhuvBNHa3qSQHO2mbXW95suXe8aneycHq2CUveg== +backport@4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/backport/-/backport-4.9.0.tgz#01ca46af57f33f582801e20ef2111b8a2710f8fc" + integrity sha512-PueA741RIv3mK4mrCoTBa0oB4WTJOOkXlSXQojL/jBqZBfHQ8MRsW8qDygVe/Q9Z6na4gqqieMOZA8qHn8GVVw== dependencies: "@types/yargs-parser" "^13.1.0" axios "^0.19.0"