From 441b473f8e7f71821815a92695179f690ee8c6aa Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Thu, 19 Nov 2020 09:21:40 -0600 Subject: [PATCH 1/3] test just part of the message to avoid updates (#83703) --- .../apps/telemetry/_telemetry.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js index 09698675f0678..5cfc88ec9bce1 100644 --- a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js +++ b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js @@ -19,13 +19,10 @@ export default ({ getService, getPageObjects }) => { await appsMenu.clickLink('Stack Monitoring'); }); - it('should show banner Help us improve Kibana and Elasticsearch', async () => { - const expectedMessage = `Help us improve the Elastic Stack -To learn about how usage data helps us manage and improve our products and services, see our Privacy Statement. To stop collection, disable usage data here. -Dismiss`; + it('should show banner Help us improve the Elastic Stack', async () => { const actualMessage = await PageObjects.monitoring.getWelcome(); log.debug(`X-Pack message = ${actualMessage}`); - expect(actualMessage).to.be(expectedMessage); + expect(actualMessage).to.contain('Help us improve the Elastic Stack'); }); }); }; From 78d7bfdf9765dd8256657c8c6c9da44d8963fea9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 19 Nov 2020 15:26:01 +0000 Subject: [PATCH 2/3] [ML] Space management UI (#83320) * [ML] Space management UI * fixing types * small react refactor * adding repair toasts * text and style changes * handling spaces being disabled * correcting initalizing endpoint response * text updates * text updates * fixing spaces manager use when spaces is disabled * more text updates * switching to delete saved object first rather than overwrite * filtering non ml spaces * renaming file * fixing types * updating list style Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/ml/common/types/saved_objects.ts | 22 +- 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 | 161 +++++++++++++ .../job_spaces_repair/repair_list.tsx | 182 ++++++++++++++ .../cannot_edit_callout.tsx | 29 +++ .../components/job_spaces_selector/index.ts | 7 + .../jobs_spaces_flyout.tsx | 131 +++++++++++ .../job_spaces_selector/spaces_selector.scss | 3 + .../job_spaces_selector/spaces_selectors.tsx | 222 ++++++++++++++++++ .../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 | 35 +++ .../analytics_list/analytics_list.tsx | 8 +- .../components/analytics_list/common.ts | 2 +- .../components/analytics_list/use_columns.tsx | 32 ++- .../analytics_service/get_analytics.ts | 2 +- .../components/jobs_list/jobs_list.js | 25 +- .../jobs_list_view/jobs_list_view.js | 12 +- .../jobs_list_page/jobs_list_page.tsx | 141 ++++++----- .../application/management/jobs_list/index.ts | 18 +- .../services/ml_api_service/saved_objects.ts | 21 +- x-pack/plugins/ml/public/plugin.ts | 2 + .../plugins/ml/server/saved_objects/repair.ts | 33 ++- .../ml/server/saved_objects/service.ts | 19 +- .../plugins/ml/server/saved_objects/util.ts | 4 + x-pack/plugins/spaces/public/index.ts | 2 + 30 files changed, 1096 insertions(+), 121 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_list.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_selector.scss 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 dde235476f1f9..9f4d402ec1759 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -7,11 +7,23 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; -type Result = Record; +export interface SavedObjectResult { + [jobId: string]: { success: boolean; error?: any }; +} export interface RepairSavedObjectResponse { - savedObjectsCreated: Result; - savedObjectsDeleted: Result; - datafeedsAdded: Result; - datafeedsRemoved: Result; + savedObjectsCreated: SavedObjectResult; + savedObjectsDeleted: SavedObjectResult; + datafeedsAdded: SavedObjectResult; + datafeedsRemoved: SavedObjectResult; +} + +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/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..47d3fe065dd66 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/job_spaces_repair_flyout.tsx @@ -0,0 +1,161 @@ +/* + * 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 { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiText, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { ml } from '../../services/ml_api_service'; +import { + RepairSavedObjectResponse, + SavedObjectResult, +} from '../../../../common/types/saved_objects'; +import { RepairList } from './repair_list'; +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); + 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); + return resp; + } catch (error) { + // this shouldn't be hit as errors are returned per-repair task + // as part of the response + displayErrorToast(error); + setLoading(false); + } + return null; + } + + useEffect(() => { + loadRepairList(); + }, []); + + async function repair() { + if (repairable) { + // 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 cannot 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 }, + }) + ); + } + } + + return ( + <> + + + +

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ); +}; + +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/public/application/components/job_spaces_repair/repair_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx new file mode 100644 index 0000000000000..3eab255ba34e6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_repair/repair_list.tsx @@ -0,0 +1,182 @@ +/* + * 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 { 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.length && ( +
    + {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..98473cf6a7f59 --- /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_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 new file mode 100644 index 0000000000000..233b64dc1432e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_spaces_selector/spaces_selectors.tsx @@ -0,0 +1,222 @@ +/* + * 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 './spaces_selector.scss'; +import React, { FC, useState, useEffect, useMemo } 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(() => { + 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) { + 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 = 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']: `mlSpaceSelectorRow_${space.id}`, + }; + }), + [allSpaces, selectedSpaceIds, canEditSpaces] + ); + + 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, + }), + [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, + }), + [canShareToAllSpaces, isGlobalControlChecked] + ); + + return ( + <> + {canEditSpaces === false && } + + toggleShareOption(false)} + disabled={shareToExplicitSpaces.disabled} + > + + } + fullWidth + > + updateSelectedSpaces(newOptions as SpaceOption[])} + listProps={{ + bordered: true, + rowHeight: 40, + className: 'mlCopyToSpace__spacesList', + 'data-test-subj': 'mlFormSpaceSelector', + }} + 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..d83273c6a9c89 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/spaces/spaces_context.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 | null; + allSpaces: Space[]; + spacesEnabled: boolean; +} + +export const SpacesContext = createContext>({}); + +export function createSpacesContext(http: HttpSetup, spacesEnabled: boolean) { + return { + spacesManager: spacesEnabled ? new SpacesManager(http) : null, + allSpaces: [], + spacesEnabled, + } 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 63b7074ec3aaa..f4cd64aa8c497 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 @@ -82,6 +82,7 @@ function getItemIdToExpandedRowMap( interface Props { isManagementTable?: boolean; isMlEnabledInSpace?: boolean; + spacesEnabled?: boolean; blockRefresh?: boolean; pageState: ListingPageUrlState; updatePageState: (update: Partial) => void; @@ -89,6 +90,7 @@ interface Props { export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, isMlEnabledInSpace = true, + spacesEnabled = false, blockRefresh = false, pageState, updatePageState, @@ -159,7 +161,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, @@ -171,7 +173,9 @@ export const DataFrameAnalyticsList: FC = ({ expandedRowItemIds, setExpandedRowItemIds, isManagementTable, - isMlEnabledInSpace + isMlEnabledInSpace, + spacesEnabled, + 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 84c37ac8b816b..bf13471c0d18b 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 93868ce0c17e6..69335b55f4c78 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,9 @@ export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, - isMlEnabledInSpace: boolean = true + isMlEnabledInSpace: boolean = true, + spacesEnabled: boolean = true, + refresh: () => void = () => {} ) => { const { actions, modals } = useActions(isManagementTable); function toggleDetails(item: DataFrameAnalyticsListRow) { @@ -278,16 +280,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.spaces) ? : null, - width: '75px', - }); - + 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/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..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,13 +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 570172abb28c1..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']; } @@ -266,8 +267,11 @@ export class JobsListView extends Component { delete job.fullJob; } job.latestTimestampSortValue = job.latestTimestampMs || 0; - job.spaces = - this.props.isManagementTable && spaces && spaces[job.id] !== undefined + job.spaceIds = + this.props.spacesEnabled && + this.props.isManagementTable && + spaces && + spaces[job.id] !== undefined ? spaces[job.id] : []; return job; @@ -379,8 +383,10 @@ 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 1089484449bab..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 @@ -19,8 +19,11 @@ import { EuiTabbedContent, EuiText, EuiTitle, + EuiTabbedContentTab, } from '@elastic/eui'; +import { PLUGIN_ID } from '../../../../../../common/constants/app'; +import { createSpacesContext, SpacesContext } from '../../../../contexts/spaces'; import { ManagementAppMountParams } from '../../../../../../../../../src/plugins/management/public/'; import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; @@ -35,16 +38,15 @@ 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 { JobSpacesRepairFlyout } from '../../../../components/job_spaces_repair'; import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; import { ListingPageUrlState } from '../../../../../../common/types/common'; import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; -interface Tab { +interface Tab extends EuiTabbedContentTab { 'data-test-subj': string; - id: string; - name: string; - content: any; } function usePageState( @@ -65,7 +67,7 @@ function usePageState( return [pageState, updateState]; } -function useTabs(isMlEnabledInSpace: boolean): Tab[] { +function useTabs(isMlEnabledInSpace: boolean, spacesEnabled: boolean): Tab[] { const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState()); const [dfaPageState, updateDfaPageState] = usePageState(getDefaultDFAListState()); @@ -85,6 +87,7 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { onJobsViewStateUpdate={updateAdPageState} isManagementTable={true} isMlEnabledInSpace={isMlEnabledInSpace} + spacesEnabled={spacesEnabled} /> ), @@ -101,6 +104,7 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { @@ -116,18 +120,28 @@ 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, spacesEnabled), []); const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobsResolver(); - setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); + const { mlFeatureEnabledInSpace } = await checkGetManagementMlJobsResolver(); + setIsMlEnabledInSpace(mlFeatureEnabledInSpace); + spacesContext.spacesEnabled = spacesEnabled; + if (spacesEnabled && spacesContext.spacesManager !== null) { + spacesContext.allSpaces = (await spacesContext.spacesManager.getSpaces()).filter( + (space) => space.disabledFeatures.includes(PLUGIN_ID) === false + ); + } } catch (e) { setAccessDenied(true); } @@ -170,6 +184,10 @@ export const JobsListPage: FC<{ ); } + function onCloseRepairFlyout() { + setShowRepairFlyout(false); + } + if (accessDenied) { return ; } @@ -180,51 +198,66 @@ export const JobsListPage: FC<{ - - - - - -

- {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.', - })} - - - - {renderTabs()} -
-
+ + + + + + +

+ {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.', + })} + + + + + {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/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/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; diff --git a/x-pack/plugins/ml/server/saved_objects/repair.ts b/x-pack/plugins/ml/server/saved_objects/repair.ts index 1b0b4b2609a91..692217e5fac36 100644 --- a/x-pack/plugins/ml/server/saved_objects/repair.ts +++ b/x-pack/plugins/ml/server/saved_objects/repair.ts @@ -7,8 +7,13 @@ 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'; import { Datafeed } from '../../common/types/anomaly_detection_jobs'; @@ -54,7 +59,7 @@ export function repairFactory( } catch (error) { results.savedObjectsCreated[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -75,7 +80,7 @@ export function repairFactory( } catch (error) { results.savedObjectsCreated[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -97,7 +102,7 @@ export function repairFactory( } catch (error) { results.savedObjectsDeleted[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -118,7 +123,7 @@ export function repairFactory( } catch (error) { results.savedObjectsDeleted[job.jobId] = { success: false, - error: error.body ?? error, + error: getSavedObjectClientError(error), }; } }); @@ -143,7 +148,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 +171,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), + }; } }); } @@ -173,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, }; @@ -211,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; diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index 1193dfde85f1c..ecaf0869d196c 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -9,6 +9,7 @@ import { KibanaRequest, SavedObjectsClientContract, SavedObjectsFindOptions } fr import type { SecurityPluginSetup } from '../../../security/server'; import { JobType, ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; import { MLJobNotFound } from '../lib/ml_client'; +import { getSavedObjectClientError } from './util'; import { authorizationProvider } from './authorization'; export interface JobObject { @@ -61,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, }); } @@ -257,7 +268,7 @@ export function jobSavedObjectServiceFactory( } catch (error) { results[id] = { success: false, - error, + error: getSavedObjectClientError(error), }; } } @@ -278,7 +289,7 @@ export function jobSavedObjectServiceFactory( } 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; +} 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 02cda96229afa28f189753a84fa88c26169f9f3a Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Thu, 19 Nov 2020 09:44:17 -0600 Subject: [PATCH 3/3] [DOCS] Consolidates plugins (#83712) --- docs/plugins/known-plugins.asciidoc | 74 ------------------------ docs/user/plugins.asciidoc | 89 ++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 83 deletions(-) delete mode 100644 docs/plugins/known-plugins.asciidoc diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc deleted file mode 100644 index 7b24de42d8e1c..0000000000000 --- a/docs/plugins/known-plugins.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -[[known-plugins]] -== Known Plugins - -[IMPORTANT] -.Plugin compatibility -============================================== -The Kibana plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. Kibana enforces that the installed plugins match the version of Kibana itself. Plugin developers will have to release a new version of their plugin for each new Kibana release as a result. -============================================== - -This list of plugins is not guaranteed to work on your version of Kibana. Instead, these are plugins that were known to work at some point with Kibana *5.x*. The Kibana installer will reject any plugins that haven't been published for your specific version of Kibana. These plugins are not evaluated or maintained by Elastic, so care should be taken before installing them into your environment. - -[float] -=== Apps -* https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface -* https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy -* https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation -* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. -* https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. -* https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API -* https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. -* https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules -* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights - -[float] -=== Timelion Extensions -* https://github.com/fermiumlabs/mathlion[mathlion] (fermiumlabs) - enables equation parsing and advanced math under Timelion - -[float] -=== Visualizations -* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu) -* https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) -* https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization -* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) -* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) -* https://github.com/elo7/cohort[Cohort analysis] (elo7) -* https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) -* https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) -* https://github.com/dlumbrer/kbn_dotplot[Dotplot] (dlumbrer) -* https://github.com/AnnaGerber/kibana_dropdown[Dropdown] (AnnaGerber) -* https://github.com/fbaligand/kibana-enhanced-table[Enhanced Table] (fbaligand) -* https://github.com/nreese/enhanced_tilemap[Enhanced Tilemap] (nreese) -* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions) -* https://github.com/flexmonster/pivot-kibana[Flexmonster Pivot Table & Charts] - a customizable pivot table component for advanced data analysis and reporting. -* https://github.com/outbrain/ob-kb-funnel[Funnel Visualization] (roybass) -* https://github.com/sbeyn/kibana-plugin-gauge-sg[Gauge] (sbeyn) -* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque) -* https://github.com/tshoeb/Insight[Insight] (tshoeb) - Multidimensional data exploration -* https://github.com/sbeyn/kibana-plugin-line-sg[Line] (sbeyn) -* https://github.com/walterra/kibana-milestones-vis[Milestones] (walterra) -* https://github.com/varundbest/navigation[Navigation] (varundbest) -* https://github.com/dlumbrer/kbn_network[Network Plugin] (dlumbrer) -* https://github.com/amannocci/kibana-plugin-metric-percent[Percent] (amannocci) -* https://github.com/dlumbrer/kbn_polar[Polar] (dlumbrer) -* https://github.com/dlumbrer/kbn_radar[Radar] (dlumbrer) -* https://github.com/dlumbrer/kbn_searchtables[Search-Tables] (dlumbrer) -* https://github.com/Smeds/status_light_visualization[Status Light] (smeds) -* https://github.com/prelert/kibana-swimlane-vis[Swimlanes] (prelert) -* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) -* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) -* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. -* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) - -[float] -=== Other -* https://github.com/nreese/kibana-time-plugin[Time filter as a dashboard panel] Widget to view and edit the time range from within dashboards. - -* https://github.com/Webiks/kibana-API.git[Kibana-API] (webiks) Exposes an API with Kibana functionality. -Use it to create, edit and embed visualizations, and also to search inside an embedded dashboard. - -* https://github.com/sw-jung/kibana_markdown_doc_view[Markdown Doc View] (sw-jung) - A plugin for custom doc view using markdown+handlebars template. -* https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization. -* https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format - -NOTE: If you want your plugin to be added to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index a96fe811dc84f..fa9e7d0c513b5 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -1,20 +1,90 @@ +[chapter] [[kibana-plugins]] -= Kibana plugins += {kib} plugins -[partintro] --- -Add-on functionality for {kib} is implemented with plug-in modules. You use the `bin/kibana-plugin` -command to manage these modules. +Implement add-on functionality for {kib} with plug-in modules. [IMPORTANT] .Plugin compatibility ============================================== -The {kib} plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. {kib} enforces that the installed plugins match the version of {kib} itself. Plugin developers will have to release a new version of their plugin for each new {kib} release as a result. +The {kib} plugin interfaces are in a state of constant development. We cannot provide backwards compatibility for plugins due to the high rate of change. {kib} enforces that the installed plugins match the version of {kib}. +Plugin developers must release a new version of their plugin for each new {kib} release. ============================================== --- +[float] +[[known-plugins]] +== Known plugins + +The known plugins were tested for {kib} *5.x*, so we are unable to guarantee compatibility with your version of {kib}. The {kib} installer rejects any plugins that haven't been published for your specific version of {kib}. +We are unable to evaluate or maintain the known plugins, so care should be taken before installation. + +[float] +=== Apps +* https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface +* https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy +* https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation +* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. +* https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. +* https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API +* https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. +* https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules +* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights + +[float] +=== Timelion Extensions +* https://github.com/fermiumlabs/mathlion[mathlion] (fermiumlabs) - enables equation parsing and advanced math under Timelion + +[float] +=== Visualizations +* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu) +* https://github.com/JuanCarniglia/area3d_vis[3D Graph] (JuanCarniglia) +* https://github.com/TrumanDu/bmap[Bmap](TrumanDu) - integrated echarts for map visualization +* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano) +* https://github.com/aaronoah/kibana_calendar_vis[Calendar Visualization] (aaronoah) +* https://github.com/elo7/cohort[Cohort analysis] (elo7) +* https://github.com/DeanF/health_metric_vis[Colored Metric Visualization] (deanf) +* https://github.com/JuanCarniglia/dendrogram_vis[Dendrogram] (JuanCarniglia) +* https://github.com/dlumbrer/kbn_dotplot[Dotplot] (dlumbrer) +* https://github.com/AnnaGerber/kibana_dropdown[Dropdown] (AnnaGerber) +* https://github.com/fbaligand/kibana-enhanced-table[Enhanced Table] (fbaligand) +* https://github.com/nreese/enhanced_tilemap[Enhanced Tilemap] (nreese) +* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions) +* https://github.com/flexmonster/pivot-kibana[Flexmonster Pivot Table & Charts] - a customizable pivot table component for advanced data analysis and reporting. +* https://github.com/outbrain/ob-kb-funnel[Funnel Visualization] (roybass) +* https://github.com/sbeyn/kibana-plugin-gauge-sg[Gauge] (sbeyn) +* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque) +* https://github.com/tshoeb/Insight[Insight] (tshoeb) - Multidimensional data exploration +* https://github.com/sbeyn/kibana-plugin-line-sg[Line] (sbeyn) +* https://github.com/walterra/kibana-milestones-vis[Milestones] (walterra) +* https://github.com/varundbest/navigation[Navigation] (varundbest) +* https://github.com/dlumbrer/kbn_network[Network Plugin] (dlumbrer) +* https://github.com/amannocci/kibana-plugin-metric-percent[Percent] (amannocci) +* https://github.com/dlumbrer/kbn_polar[Polar] (dlumbrer) +* https://github.com/dlumbrer/kbn_radar[Radar] (dlumbrer) +* https://github.com/dlumbrer/kbn_searchtables[Search-Tables] (dlumbrer) +* https://github.com/Smeds/status_light_visualization[Status Light] (smeds) +* https://github.com/prelert/kibana-swimlane-vis[Swimlanes] (prelert) +* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn) +* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek) +* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity. +* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan) + +[float] +=== Other +* https://github.com/nreese/kibana-time-plugin[Time filter as a dashboard panel] Widget to view and edit the time range from within dashboards. + +* https://github.com/Webiks/kibana-API.git[Kibana-API] (webiks) Exposes an API with Kibana functionality. +Use it to create, edit and embed visualizations, and also to search inside an embedded dashboard. + +* https://github.com/sw-jung/kibana_markdown_doc_view[Markdown Doc View] (sw-jung) - A plugin for custom doc view using markdown+handlebars template. +* https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization. +* https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format + +NOTE: To add your plugin to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. + +[float] [[install-plugin]] == Install plugins @@ -60,6 +130,7 @@ You can specify the environment variable directly when installing plugins: [source,shell] $ http_proxy="http://proxy.local:4242" bin/kibana-plugin install +[float] [[update-remove-plugin]] == Update and remove plugins @@ -74,6 +145,7 @@ You can also remove a plugin manually by deleting the plugin's subdirectory unde NOTE: Removing a plugin will result in an "optimize" run which will delay the next start of {kib}. +[float] [[disable-plugin]] == Disable plugins @@ -88,6 +160,7 @@ NOTE: Disabling or enabling a plugin will result in an "optimize" run which will <1> You can find a plugin's plugin ID as the value of the `name` property in the plugin's `package.json` file. +[float] [[configure-plugin-manager]] == Configure the plugin manager @@ -125,5 +198,3 @@ you must specify the path to that configuration file each time you use the `bin/ 64:: Unknown command or incorrect option parameter 74:: I/O error 70:: Other error - -include::{kib-repo-dir}/plugins/known-plugins.asciidoc[]