From 4c5bd53612b09393834668fe6b952548997a0e34 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 12 Nov 2020 17:45:58 +0000 Subject: [PATCH 01/16] [ML] Space management UI --- .../plugins/ml/common/types/saved_objects.ts | 15 ++ x-pack/plugins/ml/kibana.json | 3 +- .../components/job_spaces_list/index.ts | 2 +- .../job_spaces_list/job_spaces_list.tsx | 68 ++++-- .../components/job_spaces_repair/index.ts | 7 + .../job_spaces_repair_flyout.tsx | 109 ++++++++++ .../job_spaces_repair/repair_results.tsx | 179 +++++++++++++++ .../cannot_edit_callout.tsx | 29 +++ .../components/job_spaces_selector/index.ts | 7 + .../jobs_spaces_flyout.tsx | 131 +++++++++++ .../job_spaces_selector/spaces_selectors.tsx | 204 ++++++++++++++++++ .../application/contexts/kibana/index.ts | 1 + .../contexts/kibana/use_ml_api_context.ts | 11 + .../application/contexts/spaces/index.ts | 12 ++ .../contexts/spaces/spaces_context.ts | 30 +++ .../analytics_list/analytics_list.tsx | 5 +- .../components/analytics_list/common.ts | 2 +- .../components/analytics_list/use_columns.tsx | 14 +- .../analytics_service/get_analytics.ts | 2 +- .../components/jobs_list/jobs_list.js | 9 +- .../jobs_list_view/jobs_list_view.js | 3 +- .../jobs_list_page/jobs_list_page.tsx | 114 ++++++---- .../services/ml_api_service/saved_objects.ts | 21 +- .../ml/server/saved_objects/service.ts | 6 +- .../shared_services/providers/modules.ts | 3 +- x-pack/plugins/spaces/public/index.ts | 2 + 26 files changed, 912 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts create mode 100644 x-pack/plugins/ml/public/application/contexts/spaces/index.ts create mode 100644 x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index 6fd1b2cc997be..a131c0cebe086 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -6,3 +6,18 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; + +export interface SavedObjectResult { + [jobId: string]: { success: boolean; error?: any }; +} + +export interface RepairSavedObjectResponse { + savedObjectsCreated: SavedObjectResult; + savedObjectsDeleted: SavedObjectResult; + datafeedsAdded: SavedObjectResult; + datafeedsRemoved: SavedObjectResult; +} + +export type JobsSpacesResponse = { + [jobType in JobType]: { [jobId: string]: string[] }; +}; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 1cd52079b4e39..8ec9b8ee976d4 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -34,7 +34,8 @@ "kibanaReact", "dashboard", "savedObjects", - "home" + "home", + "spaces" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts b/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts index d154d82a8ee7f..f8b851e4fee35 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { JobSpacesList } from './job_spaces_list'; +export { JobSpacesList, ALL_SPACES_ID } from './job_spaces_list'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx index b362c87a12210..fa8d65d3e79fd 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx @@ -4,20 +4,64 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { JobSpacesFlyout } from '../job_spaces_selector'; +import { JobType } from '../../../../common/types/saved_objects'; +import { useSpacesContext } from '../../contexts/spaces'; +import { Space, SpaceAvatar } from '../../../../../spaces/public'; + +export const ALL_SPACES_ID = '*'; interface Props { - spaces: string[]; + spaceIds: string[]; + jobId: string; + jobType: JobType; + refresh(): void; +} + +function filterUnknownSpaces(ids: string[]) { + return ids.filter((id) => id !== '?'); } -export const JobSpacesList: FC = ({ spaces }) => ( - - {spaces.map((space) => ( - - {space} - - ))} - -); +export const JobSpacesList: FC = ({ spaceIds, jobId, jobType, refresh }) => { + const { allSpaces } = useSpacesContext(); + + const [showFlyout, setShowFlyout] = useState(false); + const [spaces, setSpaces] = useState([]); + + useEffect(() => { + const tempSpaces = spaceIds.includes(ALL_SPACES_ID) + ? [{ id: ALL_SPACES_ID, name: ALL_SPACES_ID, disabledFeatures: [], color: '#DDD' }] + : allSpaces.filter((s) => spaceIds.includes(s.id)); + setSpaces(tempSpaces); + }, [spaceIds, allSpaces]); + + function onClose() { + setShowFlyout(false); + refresh(); + } + + return ( + <> + setShowFlyout(true)} style={{ height: 'auto' }}> + + {spaces.map((space) => ( + + + + ))} + + + {showFlyout && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts b/x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts new file mode 100644 index 0000000000000..3a9c22c1f3688 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { JobSpacesRepairFlyout } from './job_spaces_repair_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx new file mode 100644 index 0000000000000..9de40fa42408e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -0,0 +1,109 @@ +/* + * 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, { FC, useState, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiText, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { ml } from '../../services/ml_api_service'; +import { RepairSavedObjectResponse } from '../../../../common/types/saved_objects'; +import { RepairList } from './repair_results'; + +interface Props { + onClose: () => void; +} +export const JobSpacesRepairFlyout: FC = ({ onClose }) => { + const [loading, setLoading] = useState(false); + const [repairable, setRepairable] = useState(false); + const [repairResp, setRepairResp] = useState(null); + + async function loadRepairList(simulate: boolean = true) { + setLoading(true); + const resp = await ml.savedObjects.repairSavedObjects(simulate); + setRepairResp(resp); + + const count = Object.values(resp).reduce((acc, cur) => acc + Object.keys(cur).length, 0); + setRepairable(count > 0); + setLoading(false); + } + + useEffect(() => { + loadRepairList(); + }, []); + + async function repair() { + if (repairable) { + await loadRepairList(false); + await loadRepairList(true); + } + } + + return ( + <> + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx new file mode 100644 index 0000000000000..8d6074ba23459 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx @@ -0,0 +1,179 @@ +/* + * 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, { FC } from 'react'; +// import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiText, EuiTitle, EuiAccordion, EuiTextColor, EuiHorizontalRule } from '@elastic/eui'; + +import { RepairSavedObjectResponse } from '../../../../common/types/saved_objects'; + +export const RepairList: FC<{ repairItems: RepairSavedObjectResponse | null }> = ({ + repairItems, +}) => { + if (repairItems === null) { + return null; + } + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; + +const SavedObjectsCreated: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.savedObjectsCreated); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const SavedObjectsDeleted: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.savedObjectsDeleted); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const DatafeedsAdded: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.datafeedsAdded); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const DatafeedsRemoved: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repairItems }) => { + const items = Object.keys(repairItems.datafeedsRemoved); + + const title = ( + <> + +

+ + + +

+
+ +

+ + + +

+
+ + ); + return ; +}; + +const RepairItem: FC<{ id: string; title: JSX.Element; items: string[] }> = ({ + id, + title, + items, +}) => ( + + + {items.map((item) => ( +
{item}
+ ))} +
+
+); diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx new file mode 100644 index 0000000000000..eb02d1ecbbc34 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +export const CannotEditCallout: FC<{ jobId: string }> = ({ jobId }) => ( + <> + + + + + +); diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts b/x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts new file mode 100644 index 0000000000000..fe1537f58531f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { JobSpacesFlyout } from './jobs_spaces_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx new file mode 100644 index 0000000000000..9aa8942bce795 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/jobs_spaces_flyout.tsx @@ -0,0 +1,131 @@ +/* + * 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, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { difference, xor } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, +} from '@elastic/eui'; + +import { JobType, SavedObjectResult } from '../../../../common/types/saved_objects'; +import { ml } from '../../services/ml_api_service'; +import { useToastNotificationService } from '../../services/toast_notification_service'; + +import { SpacesSelector } from './spaces_selectors'; + +interface Props { + jobId: string; + jobType: JobType; + spaceIds: string[]; + onClose: () => void; +} +export const JobSpacesFlyout: FC = ({ jobId, jobType, spaceIds, onClose }) => { + const { displayErrorToast } = useToastNotificationService(); + + const [selectedSpaceIds, setSelectedSpaceIds] = useState(spaceIds); + const [saving, setSaving] = useState(false); + const [savable, setSavable] = useState(false); + const [canEditSpaces, setCanEditSpaces] = useState(false); + + useEffect(() => { + const different = xor(selectedSpaceIds, spaceIds).length !== 0; + setSavable(different === true && selectedSpaceIds.length > 0); + }, [selectedSpaceIds.length]); + + async function applySpaces() { + if (savable) { + setSaving(true); + const addedSpaces = difference(selectedSpaceIds, spaceIds); + const removedSpaces = difference(spaceIds, selectedSpaceIds); + if (addedSpaces.length) { + const resp = await ml.savedObjects.assignJobToSpace(jobType, [jobId], addedSpaces); + handleApplySpaces(resp); + } + if (removedSpaces.length) { + const resp = await ml.savedObjects.removeJobFromSpace(jobType, [jobId], removedSpaces); + handleApplySpaces(resp); + } + onClose(); + } + } + + function handleApplySpaces(resp: SavedObjectResult) { + Object.entries(resp).forEach(([id, { success, error }]) => { + if (success === false) { + const title = i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.updateSpaces.error', + { + defaultMessage: 'Error updating {id}', + values: { id }, + } + ); + displayErrorToast(error, title); + } + }); + } + + return ( + <> + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx new file mode 100644 index 0000000000000..2db01ad1ac2c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx @@ -0,0 +1,204 @@ +/* + * 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, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiSelectable, + EuiSelectableOption, + EuiIconTip, + EuiText, + EuiCheckableCard, + EuiFormFieldset, +} from '@elastic/eui'; + +import { SpaceAvatar } from '../../../../../spaces/public'; +import { useSpacesContext } from '../../contexts/spaces'; +import { ML_SAVED_OBJECT_TYPE } from '../../../../common/types/saved_objects'; +import { ALL_SPACES_ID } from '../job_spaces_list'; +import { CannotEditCallout } from './cannot_edit_callout'; + +type SpaceOption = EuiSelectableOption & { ['data-space-id']: string }; + +interface Props { + jobId: string; + spaceIds: string[]; + setSelectedSpaceIds: (ids: string[]) => void; + selectedSpaceIds: string[]; + canEditSpaces: boolean; + setCanEditSpaces: (canEditSpaces: boolean) => void; +} + +export const SpacesSelector: FC = ({ + jobId, + spaceIds, + setSelectedSpaceIds, + selectedSpaceIds, + canEditSpaces, + setCanEditSpaces, +}) => { + const { spacesManager, allSpaces } = useSpacesContext(); + + const [canShareToAllSpaces, setCanShareToAllSpaces] = useState(false); + + useEffect(() => { + const getPermissions = spacesManager.getShareSavedObjectPermissions(ML_SAVED_OBJECT_TYPE); + Promise.all([getPermissions]).then(([{ shareToAllSpaces }]) => { + setCanShareToAllSpaces(shareToAllSpaces); + setCanEditSpaces(shareToAllSpaces || spaceIds.includes(ALL_SPACES_ID) === false); + }); + }, []); + + function toggleShareOption(isAllSpaces: boolean) { + const updatedSpaceIds = isAllSpaces + ? [ALL_SPACES_ID, ...selectedSpaceIds] + : selectedSpaceIds.filter((id) => id !== ALL_SPACES_ID); + setSelectedSpaceIds(updatedSpaceIds); + } + + function updateSelectedSpaces(selectedOptions: SpaceOption[]) { + const ids = selectedOptions.filter((opt) => opt.checked).map((opt) => opt['data-space-id']); + setSelectedSpaceIds(ids); + } + + const isGlobalControlChecked = selectedSpaceIds.includes(ALL_SPACES_ID); + + const options = allSpaces.map((space) => { + return { + label: space.name, + prepend: , + checked: selectedSpaceIds.includes(space.id) ? 'on' : undefined, + disabled: canEditSpaces === false, + ['data-space-id']: space.id, + ['data-test-subj']: `cts-space-selector-row-${space.id}`, + }; + }); + + const shareToAllSpaces = { + id: 'shareToAllSpaces', + title: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.title', { + defaultMessage: 'All spaces', + }), + text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.text', { + defaultMessage: 'Make job available in all current and future spaces.', + }), + ...(!canShareToAllSpaces && { + tooltip: isGlobalControlChecked + ? i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotUncheckTooltip', + { defaultMessage: 'You need additional privileges to change this option.' } + ) + : i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotCheckTooltip', + { defaultMessage: 'You need additional privileges to use this option.' } + ), + }), + disabled: !canShareToAllSpaces, + }; + + const shareToExplicitSpaces = { + id: 'shareToExplicitSpaces', + title: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.title', { + defaultMessage: 'Select spaces', + }), + text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.text', { + defaultMessage: 'Make job available in selected spaces only.', + }), + disabled: !canShareToAllSpaces && isGlobalControlChecked, + }; + + return ( + <> + {canEditSpaces === false && } + + toggleShareOption(false)} + disabled={shareToExplicitSpaces.disabled} + > + + } + fullWidth + > + updateSelectedSpaces(newOptions as SpaceOption[])} + listProps={{ + bordered: true, + rowHeight: 40, + className: 'spcCopyToSpace__spacesList', + 'data-test-subj': 'cts-form-space-selector', + }} + searchable + > + {(list, search) => { + return ( + <> + {search} + {list} + + ); + }} + + + + + + + toggleShareOption(true)} + disabled={shareToAllSpaces.disabled} + /> + + + ); +}; + +function createLabel({ + title, + text, + disabled, + tooltip, +}: { + title: string; + text: string; + disabled: boolean; + tooltip?: string; +}) { + return ( + <> + + + {title} + + {tooltip && ( + + + + )} + + + + {text} + + + ); +} diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index f08ca3c153961..0f96c8f8282ef 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -10,3 +10,4 @@ export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; export { useMlUrlGenerator, useMlLink } from './use_create_url'; +export { useMlApiContext } from './use_ml_api_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts new file mode 100644 index 0000000000000..4f0d4f9cacf19 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_ml_api_context.ts @@ -0,0 +1,11 @@ +/* + * 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 { useMlKibana } from './kibana_context'; + +export const useMlApiContext = () => { + return useMlKibana().services.mlServices.mlApiServices; +}; diff --git a/x-pack/plugins/ml/public/application/contexts/spaces/index.ts b/x-pack/plugins/ml/public/application/contexts/spaces/index.ts new file mode 100644 index 0000000000000..dc68767052176 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/spaces/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export { + SpacesContext, + SpacesContextValue, + createSpacesContext, + useSpacesContext, +} from './spaces_context'; diff --git a/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts new file mode 100644 index 0000000000000..52ae80a89f375 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts @@ -0,0 +1,30 @@ +/* + * 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 { createContext, useContext } from 'react'; +import { HttpSetup } from 'src/core/public'; +import { SpacesManager, Space } from '../../../../../spaces/public'; + +export interface SpacesContextValue { + spacesManager: SpacesManager; + allSpaces: Space[]; +} + +export const SpacesContext = createContext>({}); + +export function createSpacesContext(http: HttpSetup) { + return { spacesManager: new SpacesManager(http), allSpaces: [] } as SpacesContextValue; +} + +export function useSpacesContext() { + const context = useContext(SpacesContext); + + if (context.spacesManager === undefined) { + throw new Error('required attribute is undefined'); + } + + return context as SpacesContextValue; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 17ef84179ce63..3e77243ee10b8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -167,7 +167,7 @@ export const DataFrameAnalyticsList: FC = ({ const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); // Subscribe to the refresh observable to trigger reloading the analytics list. - useRefreshAnalyticsList( + const { refresh } = useRefreshAnalyticsList( { isLoading: setIsLoading, onRefresh: getAnalyticsCallback, @@ -179,7 +179,8 @@ export const DataFrameAnalyticsList: FC = ({ expandedRowItemIds, setExpandedRowItemIds, isManagementTable, - isMlEnabledInSpace + isMlEnabledInSpace, + refresh ); const { onTableChange, pagination, sorting } = useTableSettings( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 8c7c8b9db8b64..53fca024f5883 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -112,7 +112,7 @@ export interface DataFrameAnalyticsListRow { mode: string; state: DataFrameAnalyticsStats['state']; stats: DataFrameAnalyticsStats; - spaces?: string[]; + spaceIds?: string[]; } // Used to pass on attribute names to table columns diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 2b63b9e780819..8d9671ff1f7dd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -148,7 +148,8 @@ export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, - isMlEnabledInSpace: boolean = true + isMlEnabledInSpace: boolean = true, + refresh: () => void = () => {} ) => { const { actions, modals } = useActions(isManagementTable); function toggleDetails(item: DataFrameAnalyticsListRow) { @@ -280,8 +281,15 @@ export const useColumns = ( defaultMessage: 'Spaces', }), render: (item: DataFrameAnalyticsListRow) => - Array.isArray(item.spaces) ? : null, - width: '75px', + Array.isArray(item.spaceIds) ? ( + + ) : null, + width: '90px', }); // Remove actions if Ml not enabled in current space diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index beb490d025785..2d251d94e9ca7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -155,7 +155,7 @@ export const getAnalyticsFactory = ( mode: DATA_FRAME_MODE.BATCH, state: stats.state, stats, - spaces: spaces[config.id] ?? [], + spaceIds: spaces[config.id] ?? [], }); return reducedtableRows; }, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 8a05cd51e4d65..86b36805fd390 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -247,7 +247,14 @@ export class JobsList extends Component { name: i18n.translate('xpack.ml.jobsList.spacesLabel', { defaultMessage: 'Spaces', }), - render: (item) => , + render: (item) => ( + + ), }); // Remove actions if Ml not enabled in current space if (this.props.isMlEnabledInSpace === false) { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 570172abb28c1..77af9a77e63a1 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -266,7 +266,7 @@ export class JobsListView extends Component { delete job.fullJob; } job.latestTimestampSortValue = job.latestTimestampMs || 0; - job.spaces = + job.spaceIds = this.props.isManagementTable && spaces && spaces[job.id] !== undefined ? spaces[job.id] : []; @@ -381,6 +381,7 @@ export class JobsListView extends Component { isMlEnabledInSpace={this.props.isMlEnabledInSpace} jobsViewState={this.props.jobsViewState} onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} + refreshJobs={() => this.refreshJobSummaryList(true)} /> diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index ad4b9ad78902b..fdd743f0632a0 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -19,8 +19,10 @@ import { EuiTabbedContent, EuiText, EuiTitle, + EuiTabbedContentTab, } from '@elastic/eui'; +import { createSpacesContext, SpacesContext } from '../../../../contexts/spaces'; import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; @@ -40,12 +42,10 @@ import { getDefaultAnomalyDetectionJobsListState, } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; +import { JobSpacesRepairFlyout } from '../../../../components/job_spaces_repair'; -interface Tab { +interface Tab extends EuiTabbedContentTab { 'data-test-subj': string; - id: string; - name: string; - content: any; } function useTabs(isMlEnabledInSpace: boolean): Tab[] { @@ -111,15 +111,18 @@ export const JobsListPage: FC<{ }> = ({ coreStart, share, history }) => { const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); + const [showRepairFlyout, setShowRepairFlyout] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); const tabs = useTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; + const spacesContext = useMemo(() => createSpacesContext(coreStart.http), []); const check = async () => { try { const checkPrivilege = await checkGetManagementMlJobsResolver(); setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); + spacesContext.allSpaces = await spacesContext.spacesManager.getSpaces(); } catch (e) { setAccessDenied(true); } @@ -162,6 +165,10 @@ export const JobsListPage: FC<{ ); } + function onCloseRepairFlyout() { + setShowRepairFlyout(false); + } + if (accessDenied) { return ; } @@ -172,51 +179,64 @@ export const JobsListPage: FC<{ - - - - - -

- {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { - defaultMessage: 'Machine Learning Jobs', + + + + + + +

+ {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', { + defaultMessage: 'Machine Learning Jobs', + })} +

+
+ + + {currentTabId === 'anomaly_detection_jobs' + ? anomalyDetectionDocsLabel + : analyticsDocsLabel} + + +
+
+ + + + {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { + defaultMessage: 'View machine learning analytics and anomaly detection jobs.', + })} + + + + + <> + setShowRepairFlyout(true)}> + {i18n.translate('xpack.ml.management.jobsList.repairFlyoutButton', { + defaultMessage: 'Repair saved objects', })} -

-
- - - {currentTabId === 'anomaly_detection_jobs' - ? anomalyDetectionDocsLabel - : analyticsDocsLabel} - -
-
- - - - {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', { - defaultMessage: 'View machine learning analytics and anomaly detection jobs.', - })} - - - - {renderTabs()} -
-
+ {showRepairFlyout && } + + + {renderTabs()} + + + +
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts index a1323b39b3bcc..b47cf3f62871c 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts @@ -9,18 +9,23 @@ import { HttpService } from '../http_service'; import { basePath } from './index'; -import { JobType } from '../../../../common/types/saved_objects'; +import { + JobType, + RepairSavedObjectResponse, + SavedObjectResult, + JobsSpacesResponse, +} from '../../../../common/types/saved_objects'; export const savedObjectsApiProvider = (httpService: HttpService) => ({ jobsSpaces() { - return httpService.http({ + return httpService.http({ path: `${basePath()}/saved_objects/jobs_spaces`, method: 'GET', }); }, assignJobToSpace(jobType: JobType, jobIds: string[], spaces: string[]) { const body = JSON.stringify({ jobType, jobIds, spaces }); - return httpService.http({ + return httpService.http({ path: `${basePath()}/saved_objects/assign_job_to_space`, method: 'POST', body, @@ -28,10 +33,18 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({ }, removeJobFromSpace(jobType: JobType, jobIds: string[], spaces: string[]) { const body = JSON.stringify({ jobType, jobIds, spaces }); - return httpService.http({ + return httpService.http({ path: `${basePath()}/saved_objects/remove_job_from_space`, method: 'POST', body, }); }, + + repairSavedObjects(simulate: boolean = false) { + return httpService.http({ + path: `${basePath()}/saved_objects/repair`, + method: 'GET', + query: { simulate }, + }); + }, }); diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index a2453b9ab3fa1..9adfdd523c675 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -247,7 +247,8 @@ export function jobSavedObjectServiceFactory( results[id] = { success: true, }; - } catch (error) { + } catch (e) { + const error = e.isBoom && e.output?.payload ? e.output.payload : e; results[id] = { success: false, error, @@ -268,7 +269,8 @@ export function jobSavedObjectServiceFactory( results[job.attributes.job_id] = { success: true, }; - } catch (error) { + } catch (e) { + const error = e.isBoom && e.output?.payload ? e.output.payload : e; results[job.attributes.job_id] = { success: false, error, diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index ede92208902ae..8a8dcb16359fa 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -75,7 +75,8 @@ export function getModulesProvider(getGuards: GetGuards): ModulesProvider { payload.end, payload.jobOverrides, payload.datafeedOverrides, - payload.estimateModelMemory + payload.estimateModelMemory, + payload.applyToAllSpaces ); }); }, diff --git a/x-pack/plugins/spaces/public/index.ts b/x-pack/plugins/spaces/public/index.ts index ecbf1d8b36b7d..5fc56dfb7a295 100644 --- a/x-pack/plugins/spaces/public/index.ts +++ b/x-pack/plugins/spaces/public/index.ts @@ -14,6 +14,8 @@ export { SpaceAvatar, getSpaceColor, getSpaceImageUrl, getSpaceInitials } from ' export { SpacesPluginSetup, SpacesPluginStart } from './plugin'; +export { SpacesManager } from './spaces_manager'; + export const plugin = () => { return new SpacesPlugin(); }; From ed430078917ed704f0dcdd04ec74ae70f5d8e875 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 12 Nov 2020 18:08:32 +0000 Subject: [PATCH 02/16] fixing types --- x-pack/plugins/ml/server/shared_services/providers/modules.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index 8a8dcb16359fa..ede92208902ae 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -75,8 +75,7 @@ export function getModulesProvider(getGuards: GetGuards): ModulesProvider { payload.end, payload.jobOverrides, payload.datafeedOverrides, - payload.estimateModelMemory, - payload.applyToAllSpaces + payload.estimateModelMemory ); }); }, From 105eb3d38b05bf16aa1da863a95b2d178e27b44e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Nov 2020 09:56:51 +0000 Subject: [PATCH 03/16] small react refactor --- .../job_spaces_repair/repair_results.tsx | 1 - .../job_spaces_selector/spaces_selectors.tsx | 101 ++++++++++-------- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx index 8d6074ba23459..add128d4b926c 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx @@ -5,7 +5,6 @@ */ import React, { FC } from 'react'; -// import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiTitle, EuiAccordion, EuiTextColor, EuiHorizontalRule } from '@elastic/eui'; diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx index 2db01ad1ac2c5..a348abbfcc7f0 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -69,51 +69,66 @@ export const SpacesSelector: FC = ({ setSelectedSpaceIds(ids); } - const isGlobalControlChecked = selectedSpaceIds.includes(ALL_SPACES_ID); - - const options = allSpaces.map((space) => { - return { - label: space.name, - prepend: , - checked: selectedSpaceIds.includes(space.id) ? 'on' : undefined, - disabled: canEditSpaces === false, - ['data-space-id']: space.id, - ['data-test-subj']: `cts-space-selector-row-${space.id}`, - }; - }); - - const shareToAllSpaces = { - id: 'shareToAllSpaces', - title: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.title', { - defaultMessage: 'All spaces', - }), - text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.text', { - defaultMessage: 'Make job available in all current and future spaces.', - }), - ...(!canShareToAllSpaces && { - tooltip: isGlobalControlChecked - ? i18n.translate( - 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotUncheckTooltip', - { defaultMessage: 'You need additional privileges to change this option.' } - ) - : i18n.translate( - 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotCheckTooltip', - { defaultMessage: 'You need additional privileges to use this option.' } - ), - }), - disabled: !canShareToAllSpaces, - }; + const isGlobalControlChecked = useMemo(() => selectedSpaceIds.includes(ALL_SPACES_ID), [ + selectedSpaceIds, + ]); + + const options = useMemo( + () => + allSpaces.map((space) => { + return { + label: space.name, + prepend: , + checked: selectedSpaceIds.includes(space.id) ? 'on' : undefined, + disabled: canEditSpaces === false, + ['data-space-id']: space.id, + ['data-test-subj']: `cts-space-selector-row-${space.id}`, + }; + }), + [allSpaces, selectedSpaceIds, canEditSpaces] + ); - const shareToExplicitSpaces = { - id: 'shareToExplicitSpaces', - title: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.title', { - defaultMessage: 'Select spaces', + const shareToAllSpaces = useMemo( + () => ({ + id: 'shareToAllSpaces', + title: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.title', { + defaultMessage: 'All spaces', + }), + text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.text', { + defaultMessage: 'Make job available in all current and future spaces.', + }), + ...(!canShareToAllSpaces && { + tooltip: isGlobalControlChecked + ? i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotUncheckTooltip', + { defaultMessage: 'You need additional privileges to change this option.' } + ) + : i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToAllSpaces.cannotCheckTooltip', + { defaultMessage: 'You need additional privileges to use this option.' } + ), + }), + disabled: !canShareToAllSpaces, }), - text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.text', { - defaultMessage: 'Make job available in selected spaces only.', + [isGlobalControlChecked, canShareToAllSpaces] + ); + + const shareToExplicitSpaces = useMemo( + () => ({ + id: 'shareToExplicitSpaces', + title: i18n.translate( + 'xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.title', + { + defaultMessage: 'Select spaces', + } + ), + text: i18n.translate('xpack.ml.management.spacesSelectorFlyout.shareToExplicitSpaces.text', { + defaultMessage: 'Make job available in selected spaces only.', + }), + disabled: !canShareToAllSpaces && isGlobalControlChecked, }), - disabled: !canShareToAllSpaces && isGlobalControlChecked, - }; + [canShareToAllSpaces, isGlobalControlChecked] + ); return ( <> From 1f74d8c6e7e5d1b30e2c54b116d5ea7f9ef3784c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Nov 2020 13:54:41 +0000 Subject: [PATCH 04/16] adding repair toasts --- .../job_spaces_repair_flyout.tsx | 65 +++++++++++++++++-- .../plugins/ml/server/saved_objects/repair.ts | 19 ++++-- .../ml/server/saved_objects/service.ts | 11 ++-- .../plugins/ml/server/saved_objects/util.ts | 4 ++ 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx index 9de40fa42408e..f7559a35524d6 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -5,6 +5,7 @@ */ import React, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -22,25 +23,38 @@ import { } from '@elastic/eui'; import { ml } from '../../services/ml_api_service'; -import { RepairSavedObjectResponse } from '../../../../common/types/saved_objects'; +import { + RepairSavedObjectResponse, + SavedObjectResult, +} from '../../../../common/types/saved_objects'; import { RepairList } from './repair_results'; +import { useToastNotificationService } from '../../services/toast_notification_service'; interface Props { onClose: () => void; } export const JobSpacesRepairFlyout: FC = ({ onClose }) => { + const { displayErrorToast, displaySuccessToast } = useToastNotificationService(); const [loading, setLoading] = useState(false); const [repairable, setRepairable] = useState(false); const [repairResp, setRepairResp] = useState(null); async function loadRepairList(simulate: boolean = true) { setLoading(true); - const resp = await ml.savedObjects.repairSavedObjects(simulate); - setRepairResp(resp); + try { + const resp = await ml.savedObjects.repairSavedObjects(simulate); + setRepairResp(resp); - const count = Object.values(resp).reduce((acc, cur) => acc + Object.keys(cur).length, 0); - setRepairable(count > 0); - setLoading(false); + const count = Object.values(resp).reduce((acc, cur) => acc + Object.keys(cur).length, 0); + setRepairable(count > 0); + setLoading(false); + return resp; + } catch (error) { + // this shouldn't be hit as errors are returned per-repair task + // as part of the response + displayErrorToast(error); + } + return null; } useEffect(() => { @@ -49,8 +63,30 @@ export const JobSpacesRepairFlyout: FC = ({ onClose }) => { async function repair() { if (repairable) { - await loadRepairList(false); + // perform the repair + const resp = await loadRepairList(false); + // check simulate the repair again to check that all + // items have been repaired. await loadRepairList(true); + + if (resp === null) { + return; + } + const { successCount, errorCount } = getResponseCounts(resp); + if (errorCount > 0) { + const title = i18n.translate('xpack.ml.management.repairSavedObjectsFlyout.repair.error', { + defaultMessage: 'Some jobs could not be repaired', + }); + displayErrorToast(resp as any, title); + return; + } + + displaySuccessToast( + i18n.translate('xpack.ml.management.repairSavedObjectsFlyout.repair.success', { + defaultMessage: '{successCount} {successCount, plural, one {job} other {jobs}} repaired', + values: { successCount }, + }) + ); } } @@ -107,3 +143,18 @@ export const JobSpacesRepairFlyout: FC = ({ onClose }) => { ); }; + +function getResponseCounts(resp: RepairSavedObjectResponse) { + let successCount = 0; + let errorCount = 0; + Object.values(resp).forEach((result: SavedObjectResult) => { + Object.values(result).forEach(({ success, error }) => { + if (success === true) { + successCount++; + } else if (error !== undefined) { + errorCount++; + } + }); + }); + return { successCount, errorCount }; +} diff --git a/x-pack/plugins/ml/server/saved_objects/repair.ts b/x-pack/plugins/ml/server/saved_objects/repair.ts index 9271032f83aec..0b009a128585a 100644 --- a/x-pack/plugins/ml/server/saved_objects/repair.ts +++ b/x-pack/plugins/ml/server/saved_objects/repair.ts @@ -9,6 +9,7 @@ import { IScopedClusterClient } from 'kibana/server'; import type { JobObject, JobSavedObjectService } from './service'; import { JobType } from '../../common/types/saved_objects'; import { checksFactory } from './checks'; +import { getSavedObjectClientError } from './util'; import { Datafeed } from '../../common/types/anomaly_detection_jobs'; @@ -54,7 +55,7 @@ export function repairFactory( } catch (error) { results.savedObjectsCreated[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -75,7 +76,7 @@ export function repairFactory( } catch (error) { results.savedObjectsCreated[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -97,7 +98,7 @@ export function repairFactory( } catch (error) { results.savedObjectsDeleted[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -118,7 +119,7 @@ export function repairFactory( } catch (error) { results.savedObjectsDeleted[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -143,7 +144,10 @@ export function repairFactory( } results.datafeedsAdded[job.jobId] = { success: true }; } catch (error) { - results.datafeedsAdded[job.jobId] = { success: false, error }; + results.datafeedsAdded[job.jobId] = { + success: false, + error: getSavedObjectClientError(error), + }; } }); } @@ -163,7 +167,10 @@ export function repairFactory( await jobSavedObjectService.deleteDatafeed(datafeedId); results.datafeedsRemoved[job.jobId] = { success: true }; } catch (error) { - results.datafeedsRemoved[job.jobId] = { success: false, error: error.body ?? error }; + results.datafeedsRemoved[job.jobId] = { + success: false, + error: getSavedObjectClientError(error), + }; } }); } diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index 9adfdd523c675..e700a9513ebc2 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -8,6 +8,7 @@ import RE2 from 're2'; import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/server'; import { JobType, ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; import { MLJobNotFound } from '../lib/ml_client'; +import { getSavedObjectClientError } from './util'; export interface JobObject { job_id: string; @@ -247,11 +248,10 @@ export function jobSavedObjectServiceFactory( results[id] = { success: true, }; - } catch (e) { - const error = e.isBoom && e.output?.payload ? e.output.payload : e; + } catch (error) { results[id] = { success: false, - error, + error: getSavedObjectClientError(error), }; } } @@ -269,11 +269,10 @@ export function jobSavedObjectServiceFactory( results[job.attributes.job_id] = { success: true, }; - } catch (e) { - const error = e.isBoom && e.output?.payload ? e.output.payload : e; + } catch (error) { results[job.attributes.job_id] = { success: false, - error, + error: getSavedObjectClientError(error), }; } } diff --git a/x-pack/plugins/ml/server/saved_objects/util.ts b/x-pack/plugins/ml/server/saved_objects/util.ts index 72eca6ff5977a..4349c216abffa 100644 --- a/x-pack/plugins/ml/server/saved_objects/util.ts +++ b/x-pack/plugins/ml/server/saved_objects/util.ts @@ -35,3 +35,7 @@ export function savedObjectClientsFactory( }, }; } + +export function getSavedObjectClientError(error: any) { + return error.isBoom && error.output?.payload ? error.output.payload : error.body ?? error; +} From b4300cd9d11de9816e71d18a2a3695d36918d111 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Nov 2020 18:18:04 +0000 Subject: [PATCH 05/16] text and style changes --- .../components/job_spaces_selector/cannot_edit_callout.tsx | 2 +- .../components/job_spaces_selector/spaces_selector.scss | 3 +++ .../components/job_spaces_selector/spaces_selectors.tsx | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx index eb02d1ecbbc34..a56aa756dda1e 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx @@ -21,7 +21,7 @@ export const CannotEditCallout: FC<{ jobId: string }> = ({ jobId }) => ( > diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss new file mode 100644 index 0000000000000..75cdbd972455b --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selector.scss @@ -0,0 +1,3 @@ +.mlCopyToSpace__spacesList { + margin-top: $euiSizeXS; +} diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx index a348abbfcc7f0..ed6e8a4785c20 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import './spaces_selector.scss'; import React, { FC, useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -82,7 +83,7 @@ export const SpacesSelector: FC = ({ checked: selectedSpaceIds.includes(space.id) ? 'on' : undefined, disabled: canEditSpaces === false, ['data-space-id']: space.id, - ['data-test-subj']: `cts-space-selector-row-${space.id}`, + ['data-test-subj']: `mlSpaceSelectorRow_${space.id}`, }; }), [allSpaces, selectedSpaceIds, canEditSpaces] @@ -156,8 +157,8 @@ export const SpacesSelector: FC = ({ listProps={{ bordered: true, rowHeight: 40, - className: 'spcCopyToSpace__spacesList', - 'data-test-subj': 'cts-form-space-selector', + className: 'mlCopyToSpace__spacesList', + 'data-test-subj': 'mlFormSpaceSelector', }} searchable > From 21a176cb19fbf9a71d31ee245964808dc946d331 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 13:35:11 +0000 Subject: [PATCH 06/16] handling spaces being disabled --- .../contexts/spaces/spaces_context.ts | 7 +++- .../analytics_list/analytics_list.tsx | 3 ++ .../components/analytics_list/use_columns.tsx | 36 +++++++++-------- .../components/jobs_list/jobs_list.js | 32 ++++++++------- .../jobs_list_view/jobs_list_view.js | 9 ++++- .../jobs_list_page/jobs_list_page.tsx | 40 ++++++++++++------- .../application/management/jobs_list/index.ts | 18 +++++++-- x-pack/plugins/ml/public/plugin.ts | 2 + 8 files changed, 93 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts index 52ae80a89f375..6219709882ad9 100644 --- a/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts @@ -11,12 +11,17 @@ import { SpacesManager, Space } from '../../../../../spaces/public'; export interface SpacesContextValue { spacesManager: SpacesManager; allSpaces: Space[]; + spacesEnabled: boolean; } export const SpacesContext = createContext>({}); export function createSpacesContext(http: HttpSetup) { - return { spacesManager: new SpacesManager(http), allSpaces: [] } as SpacesContextValue; + return { + spacesManager: new SpacesManager(http), + allSpaces: [], + spacesEnabled: false, + } as SpacesContextValue; } export function useSpacesContext() { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 3e77243ee10b8..da30dcc08f792 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -83,11 +83,13 @@ function getItemIdToExpandedRowMap( interface Props { isManagementTable?: boolean; isMlEnabledInSpace?: boolean; + spacesEnabled?: boolean; blockRefresh?: boolean; } export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, isMlEnabledInSpace = true, + spacesEnabled = false, blockRefresh = false, }) => { const [isInitialized, setIsInitialized] = useState(false); @@ -180,6 +182,7 @@ export const DataFrameAnalyticsList: FC = ({ setExpandedRowItemIds, isManagementTable, isMlEnabledInSpace, + spacesEnabled, refresh ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 8d9671ff1f7dd..ede876dd3dd42 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -149,6 +149,7 @@ export const useColumns = ( setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, isMlEnabledInSpace: boolean = true, + spacesEnabled: boolean = true, refresh: () => void = () => {} ) => { const { actions, modals } = useActions(isManagementTable); @@ -275,23 +276,24 @@ export const useColumns = ( ]; if (isManagementTable === true) { - // insert before last column - columns.splice(columns.length - 1, 0, { - name: i18n.translate('xpack.ml.jobsList.analyticsSpacesLabel', { - defaultMessage: 'Spaces', - }), - render: (item: DataFrameAnalyticsListRow) => - Array.isArray(item.spaceIds) ? ( - - ) : null, - width: '90px', - }); - + if (spacesEnabled === true) { + // insert before last column + columns.splice(columns.length - 1, 0, { + name: i18n.translate('xpack.ml.jobsList.analyticsSpacesLabel', { + defaultMessage: 'Spaces', + }), + render: (item: DataFrameAnalyticsListRow) => + Array.isArray(item.spaceIds) ? ( + + ) : null, + width: '90px', + }); + } // Remove actions if Ml not enabled in current space if (isMlEnabledInSpace === false) { columns.pop(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 86b36805fd390..9c58dc556e535 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -95,7 +95,7 @@ export class JobsList extends Component { } render() { - const { loading, isManagementTable } = this.props; + const { loading, isManagementTable, spacesEnabled } = this.props; const selectionControls = { selectable: (job) => job.deleting !== true, selectableMessage: (selectable, rowItem) => @@ -242,20 +242,22 @@ export class JobsList extends Component { ]; if (isManagementTable === true) { - // insert before last column - columns.splice(columns.length - 1, 0, { - name: i18n.translate('xpack.ml.jobsList.spacesLabel', { - defaultMessage: 'Spaces', - }), - render: (item) => ( - - ), - }); + if (spacesEnabled === true) { + // insert before last column + columns.splice(columns.length - 1, 0, { + name: i18n.translate('xpack.ml.jobsList.spacesLabel', { + defaultMessage: 'Spaces', + }), + render: (item) => ( + + ), + }); + } // Remove actions if Ml not enabled in current space if (this.props.isMlEnabledInSpace === false) { columns.pop(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 77af9a77e63a1..6e3b9031de653 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -57,6 +57,7 @@ export class JobsListView extends Component { deletingJobIds: [], }; + this.spacesEnabled = props.spacesEnabled ?? false; this.updateFunctions = {}; this.showEditJobFlyout = () => {}; @@ -253,7 +254,7 @@ export class JobsListView extends Component { const expandedJobsIds = Object.keys(this.state.itemIdToExpandedRowMap); try { let spaces = {}; - if (this.props.isManagementTable) { + if (this.props.spacesEnabled && this.props.isManagementTable) { const allSpaces = await ml.savedObjects.jobsSpaces(); spaces = allSpaces['anomaly-detector']; } @@ -267,7 +268,10 @@ export class JobsListView extends Component { } job.latestTimestampSortValue = job.latestTimestampMs || 0; job.spaceIds = - this.props.isManagementTable && spaces && spaces[job.id] !== undefined + this.props.spacesEnabled && + this.props.isManagementTable && + spaces && + spaces[job.id] !== undefined ? spaces[job.id] : []; return job; @@ -379,6 +383,7 @@ export class JobsListView extends Component { loading={loading} isManagementTable={true} isMlEnabledInSpace={this.props.isMlEnabledInSpace} + spacesEnabled={this.props.spacesEnabled} jobsViewState={this.props.jobsViewState} onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} refreshJobs={() => this.refreshJobSummaryList(true)} diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index fdd743f0632a0..5d2c8605313af 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -37,6 +37,7 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; +import { SpacesPluginStart } from '../../../../../../../spaces/public'; import { AnomalyDetectionJobsListState, getDefaultAnomalyDetectionJobsListState, @@ -48,7 +49,7 @@ interface Tab extends EuiTabbedContentTab { 'data-test-subj': string; } -function useTabs(isMlEnabledInSpace: boolean): Tab[] { +function useTabs(isMlEnabledInSpace: boolean, spacesEnabled: boolean): Tab[] { const [jobsViewState, setJobsViewState] = useState( getDefaultAnomalyDetectionJobsListState() ); @@ -79,6 +80,7 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { onJobsViewStateUpdate={updateState} isManagementTable={true} isMlEnabledInSpace={isMlEnabledInSpace} + spacesEnabled={spacesEnabled} /> ), @@ -95,6 +97,7 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { ), @@ -108,21 +111,26 @@ export const JobsListPage: FC<{ coreStart: CoreStart; share: SharePluginStart; history: ManagementAppMountParams['history']; -}> = ({ coreStart, share, history }) => { + spaces?: SpacesPluginStart; +}> = ({ coreStart, share, history, spaces }) => { + const spacesEnabled = spaces !== undefined; const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [showRepairFlyout, setShowRepairFlyout] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); - const tabs = useTabs(isMlEnabledInSpace); + const tabs = useTabs(isMlEnabledInSpace, spacesEnabled); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; const spacesContext = useMemo(() => createSpacesContext(coreStart.http), []); const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobsResolver(); - setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); - spacesContext.allSpaces = await spacesContext.spacesManager.getSpaces(); + const { mlFeatureEnabledInSpace } = await checkGetManagementMlJobsResolver(); + setIsMlEnabledInSpace(mlFeatureEnabledInSpace); + spacesContext.spacesEnabled = spacesEnabled; + if (spacesEnabled) { + spacesContext.allSpaces = await spacesContext.spacesManager.getSpaces(); + } } catch (e) { setAccessDenied(true); } @@ -223,15 +231,17 @@ export const JobsListPage: FC<{ - <> - setShowRepairFlyout(true)}> - {i18n.translate('xpack.ml.management.jobsList.repairFlyoutButton', { - defaultMessage: 'Repair saved objects', - })} - - {showRepairFlyout && } - - + {spacesEnabled && ( + <> + setShowRepairFlyout(true)}> + {i18n.translate('xpack.ml.management.jobsList.repairFlyoutButton', { + defaultMessage: 'Repair saved objects', + })} + + {showRepairFlyout && } + + + )} {renderTabs()} diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts index 422121e1845b2..284220e4e3caf 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts +++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts @@ -14,14 +14,19 @@ import { getJobsListBreadcrumbs } from '../breadcrumbs'; import { setDependencyCache, clearCache } from '../../util/dependency_cache'; import './_index.scss'; import { SharePluginStart } from '../../../../../../../src/plugins/share/public'; +import { SpacesPluginStart } from '../../../../../spaces/public'; const renderApp = ( element: HTMLElement, history: ManagementAppMountParams['history'], coreStart: CoreStart, - share: SharePluginStart + share: SharePluginStart, + spaces?: SpacesPluginStart ) => { - ReactDOM.render(React.createElement(JobsListPage, { coreStart, history, share }), element); + ReactDOM.render( + React.createElement(JobsListPage, { coreStart, history, share, spaces }), + element + ); return () => { unmountComponentAtNode(element); clearCache(); @@ -42,6 +47,11 @@ export async function mountApp( }); params.setBreadcrumbs(getJobsListBreadcrumbs()); - - return renderApp(params.element, params.history, coreStart, pluginsStart.share); + return renderApp( + params.element, + params.history, + coreStart, + pluginsStart.share, + pluginsStart.spaces + ); } diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 8a25c1c49e255..1cc69ac2239ab 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -26,6 +26,7 @@ import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import type { EmbeddableSetup } from 'src/plugins/embeddable/public'; +import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; @@ -50,6 +51,7 @@ export interface MlStartDependencies { share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; uiActions: UiActionsStart; + spaces?: SpacesPluginStart; } export interface MlSetupDependencies { security?: SecurityPluginSetup; From 8952cf378a1a1803562d03abd934ba046294d0f6 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 14:24:35 +0000 Subject: [PATCH 07/16] correcting initalizing endpoint response --- x-pack/plugins/ml/common/types/saved_objects.ts | 7 ++++++- x-pack/plugins/ml/server/saved_objects/repair.ts | 14 ++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index 2dad9177dd0f7..9f4d402ec1759 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -7,7 +7,6 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; - export interface SavedObjectResult { [jobId: string]: { success: boolean; error?: any }; } @@ -22,3 +21,9 @@ export interface RepairSavedObjectResponse { export type JobsSpacesResponse = { [jobType in JobType]: { [jobId: string]: string[] }; }; + +export interface InitializeSavedObjectResponse { + jobs: Array<{ id: string; type: string }>; + success: boolean; + error?: any; +} diff --git a/x-pack/plugins/ml/server/saved_objects/repair.ts b/x-pack/plugins/ml/server/saved_objects/repair.ts index 810a3172c8ca6..692217e5fac36 100644 --- a/x-pack/plugins/ml/server/saved_objects/repair.ts +++ b/x-pack/plugins/ml/server/saved_objects/repair.ts @@ -7,7 +7,11 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import type { JobObject, JobSavedObjectService } from './service'; -import { JobType, RepairSavedObjectResponse } from '../../common/types/saved_objects'; +import { + JobType, + RepairSavedObjectResponse, + InitializeSavedObjectResponse, +} from '../../common/types/saved_objects'; import { checksFactory } from './checks'; import { getSavedObjectClientError } from './util'; @@ -180,8 +184,11 @@ export function repairFactory( return results; } - async function initSavedObjects(simulate: boolean = false, spaceOverrides?: JobSpaceOverrides) { - const results: { jobs: Array<{ id: string; type: string }>; success: boolean; error?: any } = { + async function initSavedObjects( + simulate: boolean = false, + spaceOverrides?: JobSpaceOverrides + ): Promise { + const results: InitializeSavedObjectResponse = { jobs: [], success: true, }; @@ -218,7 +225,6 @@ export function repairFactory( type: attributes.type, }); }); - return { jobs: jobs.map((j) => j.job.job_id) }; } catch (error) { results.success = false; results.error = Boom.boomify(error).output; From 29c6175bd0c1efa92d388f57dfb34a8fcb23e47e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 14:54:37 +0000 Subject: [PATCH 08/16] text updates --- .../job_spaces_repair_flyout.tsx | 4 ++-- .../job_spaces_repair/repair_results.tsx | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx index f7559a35524d6..a68d56e749f14 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -98,7 +98,7 @@ export const JobSpacesRepairFlyout: FC = ({ onClose }) => {

@@ -108,7 +108,7 @@ export const JobSpacesRepairFlyout: FC = ({ onClose }) => { diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx index add128d4b926c..14d5c38d75ea3 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx @@ -49,7 +49,7 @@ const SavedObjectsCreated: FC<{ repairItems: RepairSavedObjectResponse }> = ({ r @@ -60,7 +60,7 @@ const SavedObjectsCreated: FC<{ repairItems: RepairSavedObjectResponse }> = ({ r

@@ -80,7 +80,7 @@ const SavedObjectsDeleted: FC<{ repairItems: RepairSavedObjectResponse }> = ({ r @@ -91,7 +91,7 @@ const SavedObjectsDeleted: FC<{ repairItems: RepairSavedObjectResponse }> = ({ r

@@ -111,7 +111,7 @@ const DatafeedsAdded: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repair @@ -122,7 +122,7 @@ const DatafeedsAdded: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repair

@@ -142,7 +142,7 @@ const DatafeedsRemoved: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repa @@ -153,7 +153,7 @@ const DatafeedsRemoved: FC<{ repairItems: RepairSavedObjectResponse }> = ({ repa

From e90c0917cf0e1297beafdebc3ad7488c7ada46a6 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 14:57:21 +0000 Subject: [PATCH 09/16] text updates --- .../components/job_spaces_selector/cannot_edit_callout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx index a56aa756dda1e..98473cf6a7f59 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/cannot_edit_callout.tsx @@ -21,7 +21,7 @@ export const CannotEditCallout: FC<{ jobId: string }> = ({ jobId }) => ( > From e10663d216ba34ec4f56e141e3c22890014c9e9d Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 15:49:33 +0000 Subject: [PATCH 10/16] fixing spaces manager use when spaces is disabled --- .../job_spaces_selector/spaces_selectors.tsx | 12 +++++++----- .../application/contexts/spaces/spaces_context.ts | 8 ++++---- .../components/jobs_list_page/jobs_list_page.tsx | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx index ed6e8a4785c20..233b64dc1432e 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx @@ -51,11 +51,13 @@ export const SpacesSelector: FC = ({ const [canShareToAllSpaces, setCanShareToAllSpaces] = useState(false); useEffect(() => { - const getPermissions = spacesManager.getShareSavedObjectPermissions(ML_SAVED_OBJECT_TYPE); - Promise.all([getPermissions]).then(([{ shareToAllSpaces }]) => { - setCanShareToAllSpaces(shareToAllSpaces); - setCanEditSpaces(shareToAllSpaces || spaceIds.includes(ALL_SPACES_ID) === false); - }); + if (spacesManager !== null) { + const getPermissions = spacesManager.getShareSavedObjectPermissions(ML_SAVED_OBJECT_TYPE); + Promise.all([getPermissions]).then(([{ shareToAllSpaces }]) => { + setCanShareToAllSpaces(shareToAllSpaces); + setCanEditSpaces(shareToAllSpaces || spaceIds.includes(ALL_SPACES_ID) === false); + }); + } }, []); function toggleShareOption(isAllSpaces: boolean) { diff --git a/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts index 6219709882ad9..d83273c6a9c89 100644 --- a/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts @@ -9,18 +9,18 @@ import { HttpSetup } from 'src/core/public'; import { SpacesManager, Space } from '../../../../../spaces/public'; export interface SpacesContextValue { - spacesManager: SpacesManager; + spacesManager: SpacesManager | null; allSpaces: Space[]; spacesEnabled: boolean; } export const SpacesContext = createContext>({}); -export function createSpacesContext(http: HttpSetup) { +export function createSpacesContext(http: HttpSetup, spacesEnabled: boolean) { return { - spacesManager: new SpacesManager(http), + spacesManager: spacesEnabled ? new SpacesManager(http) : null, allSpaces: [], - spacesEnabled: false, + spacesEnabled, } as SpacesContextValue; } diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 5d2c8605313af..481f1d734e94a 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -121,14 +121,14 @@ export const JobsListPage: FC<{ const tabs = useTabs(isMlEnabledInSpace, spacesEnabled); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; - const spacesContext = useMemo(() => createSpacesContext(coreStart.http), []); + const spacesContext = useMemo(() => createSpacesContext(coreStart.http, spacesEnabled), []); const check = async () => { try { const { mlFeatureEnabledInSpace } = await checkGetManagementMlJobsResolver(); setIsMlEnabledInSpace(mlFeatureEnabledInSpace); spacesContext.spacesEnabled = spacesEnabled; - if (spacesEnabled) { + if (spacesEnabled && spacesContext.spacesManager !== null) { spacesContext.allSpaces = await spacesContext.spacesManager.getSpaces(); } } catch (e) { From 50cf87e7307cc3c0a525c0b328d0eb302ee1506c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 16:46:09 +0000 Subject: [PATCH 11/16] more text updates --- .../components/job_spaces_repair/job_spaces_repair_flyout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx index a68d56e749f14..521ee2db736ba 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -75,7 +75,7 @@ export const JobSpacesRepairFlyout: FC = ({ onClose }) => { const { successCount, errorCount } = getResponseCounts(resp); if (errorCount > 0) { const title = i18n.translate('xpack.ml.management.repairSavedObjectsFlyout.repair.error', { - defaultMessage: 'Some jobs could not be repaired', + defaultMessage: 'Some jobs cannot be repaired.', }); displayErrorToast(resp as any, title); return; From a6fd8ac7b54d9be6a3bb054e3f2afb3224aacca9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 16:46:45 +0000 Subject: [PATCH 12/16] switching to delete saved object first rather than overwrite --- x-pack/plugins/ml/server/saved_objects/service.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index 6720e6a2f024b..ecaf0869d196c 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -62,14 +62,24 @@ export function jobSavedObjectServiceFactory( async function _createJob(jobType: JobType, jobId: string, datafeedId?: string) { await isMlReady(); + const job: JobObject = { job_id: jobId, datafeed_id: datafeedId ?? null, type: jobType, }; + + const id = savedObjectId(job); + + try { + await savedObjectsClient.delete(ML_SAVED_OBJECT_TYPE, id, { force: true }); + } catch (error) { + // the saved object may exist if a previous job with the same ID has been deleted. + // if not, this error will be throw which we ignore. + } + await savedObjectsClient.create(ML_SAVED_OBJECT_TYPE, job, { - id: savedObjectId(job), - overwrite: true, + id, }); } From d42c2d1317c9eb2a59fd579bd804b05f1e49b415 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 18:19:59 +0000 Subject: [PATCH 13/16] filtering non ml spaces --- .../jobs_list/components/jobs_list_page/jobs_list_page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 481f1d734e94a..68edbb691891a 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -22,6 +22,7 @@ import { EuiTabbedContentTab, } from '@elastic/eui'; +import { PLUGIN_ID } from '../../../../../../common/constants/app'; import { createSpacesContext, SpacesContext } from '../../../../contexts/spaces'; import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; @@ -129,7 +130,9 @@ export const JobsListPage: FC<{ setIsMlEnabledInSpace(mlFeatureEnabledInSpace); spacesContext.spacesEnabled = spacesEnabled; if (spacesEnabled && spacesContext.spacesManager !== null) { - spacesContext.allSpaces = await spacesContext.spacesManager.getSpaces(); + spacesContext.allSpaces = (await spacesContext.spacesManager.getSpaces()).filter( + (space) => space.disabledFeatures.includes(PLUGIN_ID) === false + ); } } catch (e) { setAccessDenied(true); From b217713f58a6c1d4ef5189e7daf51592bd56693a Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 17 Nov 2020 18:27:30 +0000 Subject: [PATCH 14/16] renaming file --- .../components/job_spaces_repair/job_spaces_repair_flyout.tsx | 3 ++- .../job_spaces_repair/{repair_results.tsx => repair_list.tsx} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename x-pack/plugins/ml/public/application/components/job_spaces_repair/{repair_results.tsx => repair_list.tsx} (100%) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx index 521ee2db736ba..47d3fe065dd66 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -27,7 +27,7 @@ import { RepairSavedObjectResponse, SavedObjectResult, } from '../../../../common/types/saved_objects'; -import { RepairList } from './repair_results'; +import { RepairList } from './repair_list'; import { useToastNotificationService } from '../../services/toast_notification_service'; interface Props { @@ -53,6 +53,7 @@ export const JobSpacesRepairFlyout: FC = ({ onClose }) => { // this shouldn't be hit as errors are returned per-repair task // as part of the response displayErrorToast(error); + setLoading(false); } return null; } diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_results.tsx rename to x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx From 9ed6cf6e0122cdf007fd6cd428d665b146c6a516 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 18 Nov 2020 09:44:46 +0000 Subject: [PATCH 15/16] fixing types --- .../jobs_list/components/jobs_list_page/jobs_list_page.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 8b1907045baa8..8ad18e2b821b6 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -39,11 +39,6 @@ import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/a import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; import { SpacesPluginStart } from '../../../../../../../spaces/public'; -import { - AnomalyDetectionJobsListState, - getDefaultAnomalyDetectionJobsListState, -} from '../../../../jobs/jobs_list/jobs'; -import { getMlGlobalServices } from '../../../../app'; import { JobSpacesRepairFlyout } from '../../../../components/job_spaces_repair'; import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; From 37f041d387e5bc3f7b61e528aca9993aeefd94cb Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 18 Nov 2020 12:53:58 +0000 Subject: [PATCH 16/16] updating list style --- .../components/job_spaces_repair/repair_list.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx index 14d5c38d75ea3..3eab255ba34e6 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx @@ -170,9 +170,13 @@ const RepairItem: FC<{ id: string; title: JSX.Element; items: string[] }> = ({ }) => ( - {items.map((item) => ( -
{item}
- ))} + {items.length && ( +
    + {items.map((item) => ( +
  • {item}
  • + ))} +
+ )}
);