Skip to content

Commit

Permalink
[ML] Space management UI (#83320)
Browse files Browse the repository at this point in the history
* [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 <[email protected]>
  • Loading branch information
jgowdyelastic and kibanamachine authored Nov 19, 2020
1 parent 441b473 commit 78d7bfd
Show file tree
Hide file tree
Showing 30 changed files with 1,096 additions and 121 deletions.
22 changes: 17 additions & 5 deletions x-pack/plugins/ml/common/types/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@
export type JobType = 'anomaly-detector' | 'data-frame-analytics';
export const ML_SAVED_OBJECT_TYPE = 'ml-job';

type Result = Record<string, { success: boolean; error?: any }>;
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;
}
3 changes: 2 additions & 1 deletion x-pack/plugins/ml/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"kibanaReact",
"dashboard",
"savedObjects",
"home"
"home",
"spaces"
],
"extraPublicDirs": [
"common"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ spaces }) => (
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
{spaces.map((space) => (
<EuiFlexItem grow={false} key={space}>
<EuiBadge color={'hollow'}>{space}</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
export const JobSpacesList: FC<Props> = ({ spaceIds, jobId, jobType, refresh }) => {
const { allSpaces } = useSpacesContext();

const [showFlyout, setShowFlyout] = useState(false);
const [spaces, setSpaces] = useState<Space[]>([]);

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 (
<>
<EuiButtonEmpty onClick={() => setShowFlyout(true)} style={{ height: 'auto' }}>
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
{spaces.map((space) => (
<EuiFlexItem grow={false} key={space.id}>
<SpaceAvatar space={space} size={'s'} />
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiButtonEmpty>
{showFlyout && (
<JobSpacesFlyout
jobId={jobId}
spaceIds={filterUnknownSpaces(spaceIds)}
jobType={jobType}
onClose={onClose}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ onClose }) => {
const { displayErrorToast, displaySuccessToast } = useToastNotificationService();
const [loading, setLoading] = useState(false);
const [repairable, setRepairable] = useState(false);
const [repairResp, setRepairResp] = useState<RepairSavedObjectResponse | null>(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 (
<>
<EuiFlyout maxWidth={600} onClose={onClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.ml.management.repairSavedObjectsFlyout.headerLabel"
defaultMessage="Repair saved objects"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiCallOut color="primary">
<EuiText size="s">
<FormattedMessage
id="xpack.ml.management.repairSavedObjectsFlyout.description"
defaultMessage="Repair the saved objects if they are out of sync with the machine learning jobs in Elasticsearch."
/>
</EuiText>
</EuiCallOut>
<EuiSpacer />
<RepairList repairItems={repairResp} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<FormattedMessage
id="xpack.ml.management.repairSavedObjectsFlyout.closeButton"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={repair}
fill
isDisabled={repairable === false || loading === true}
>
<FormattedMessage
id="xpack.ml.management.repairSavedObjectsFlyout.repairButton"
defaultMessage="Repair"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
</>
);
};

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 };
}
Loading

0 comments on commit 78d7bfd

Please sign in to comment.