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}
+ ))}
+
+ )}
);