Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Add progress bar for initialization #253

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions public/models/interfaces.ts
Original file line number Diff line number Diff line change
@@ -86,6 +86,11 @@ export type UiMetaData = {
};
};

export type InitProgress = {
percentageStr: string;
estimatedMinutesLeft: number;
neededShingles: number;
};
export type Detector = {
primaryTerm: number;
seqNo: number;
@@ -105,6 +110,7 @@ export type Detector = {
disabledTime?: number;
curState: DETECTOR_STATE;
stateError: string;
initProgress?: InitProgress;
};

export type DetectorListItem = {
13 changes: 8 additions & 5 deletions public/pages/DetectorDetail/containers/DetectorDetail.tsx
Original file line number Diff line number Diff line change
@@ -290,7 +290,10 @@ export const DetectorDetail = (props: DetectorDetailProps) => {
) : detector.enabled &&
detector.curState === DETECTOR_STATE.INIT ? (
<EuiHealth color={DETECTOR_STATE_COLOR.INIT}>
Initializing
{detector.initProgress
? //@ts-ignore
`Initializing (${detector.initProgress.percentageStr} complete)`
: 'Initializing'}
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
</EuiHealth>
) : detector.curState === DETECTOR_STATE.INIT_FAILURE ||
detector.curState === DETECTOR_STATE.UNEXPECTED_FAILURE ? (
@@ -343,7 +346,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => {
<EuiFlexGroup>
<EuiFlexItem>
<EuiTabs>
{tabs.map(tab => (
{tabs.map((tab) => (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

automatically changed by linter I guess. same for the other similar changes.

<EuiTab
onClick={() => {
handleTabChange(tab.route);
@@ -388,7 +391,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => {
<EuiFieldText
fullWidth={true}
placeholder="delete"
onChange={e => {
onChange={(e) => {
if (e.target.value === 'delete') {
setDetectorDetailModel({
...detectorDetailModel,
@@ -472,7 +475,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => {
<Route
exact
path="/detectors/:detectorId/results"
render={props => (
render={(props) => (
<AnomalyResults
{...props}
detectorId={detectorId}
@@ -484,7 +487,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => {
<Route
exact
path="/detectors/:detectorId/configurations"
render={props => (
render={(props) => (
<DetectorConfig
{...props}
detectorId={detectorId}
2 changes: 1 addition & 1 deletion public/pages/DetectorDetail/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import { DETECTOR_INIT_FAILURES } from './constants';

export const getInitFailureMessageAndActionItem = (error: string): object => {
const failureDetails = Object.values(DETECTOR_INIT_FAILURES);
const failureDetail = failureDetails.find(failure =>
const failureDetail = failureDetails.find((failure) =>
error.includes(failure.keyword)
);
if (!failureDetail) {
45 changes: 44 additions & 1 deletion public/pages/DetectorResults/containers/AnomalyResults.tsx
Original file line number Diff line number Diff line change
@@ -21,6 +21,10 @@ import {
EuiSpacer,
EuiCallOut,
EuiButton,
EuiProgress,
EuiFlexGroup,
EuiFlexItem,
EuiText,
} from '@elastic/eui';
import { get } from 'lodash';
import React, { useEffect, Fragment, useState } from 'react';
@@ -128,7 +132,6 @@ export function AnomalyResults(props: AnomalyResultsProps) {
detector && detector.curState === DETECTOR_STATE.INIT;

const initializationInfo = getDetectorInitializationInfo(detector);

const isInitOvertime = get(initializationInfo, IS_INIT_OVERTIME_FIELD, false);
const initDetails = get(initializationInfo, INIT_DETAILS_FIELD, {});
const initErrorMessage = get(initDetails, INIT_ERROR_MESSAGE_FIELD, '');
@@ -292,6 +295,17 @@ export function AnomalyResults(props: AnomalyResultsProps) {
</p>
) : isInitializingNormally ? (
<p>
{detector.initProgress
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to show the message besides isInitializingNormally? When it is over time, I can see a need for customer to ask when it is gonna finish.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, will add it for overtime case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will also add that message to missing data case as well.

? `The detector needs to capture approximately
${
//@ts-ignore
detector.initProgress.neededShingles
} data points for initializing. If your data stream is continuous, this process will take around
${
//@ts-ignore
detector.initProgress.estimatedMinutesLeft
} minutes; if not, it may take even longer. `
: ''}
After the initialization is complete, you will see the anomaly results
based on your latest configuration changes.
</p>
@@ -332,6 +346,35 @@ export function AnomalyResults(props: AnomalyResultsProps) {
style={{ marginBottom: '20px' }}
>
{getCalloutContent()}
{isDetectorInitializing && detector.initProgress ? (
<div>
<EuiFlexGroup alignItems="center">
<EuiFlexItem
style={{ maxWidth: '20px', marginRight: '5px' }}
>
<EuiText>
{
//@ts-ignore
detector.initProgress.percentageStr
}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiProgress
//@ts-ignore
value={detector.initProgress.percentageStr.replace(
'%',
''
)}
max={100}
color="primary"
size="xs"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</div>
) : null}
<EuiButton
onClick={props.onSwitchToConfiguration}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a switch to configuration button when the configuration tab is only a finger click away?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this was just part of the existing approved UI, but agreed that it doesn't really seem necessary with the tab right above. Guess it was for quickly navigating to config page since a config change must have occurred for initialization process to begin again / for callout to appear again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this already exists in UX design since long time ago. This configuration button is shown in callout because all the messages in callout mentions detector configuration, and such button can direct user quickly to detector config page .

color={
1 change: 1 addition & 0 deletions public/redux/reducers/ad.ts
Original file line number Diff line number Diff line change
@@ -139,6 +139,7 @@ const reducer = handleActions<Detectors>(
disabledTime: moment().valueOf(),
curState: DETECTOR_STATE.DISABLED,
stateError: '',
initProgress: undefined,
},
},
}),
2 changes: 1 addition & 1 deletion server/cluster/ad/adPlugin.ts
Original file line number Diff line number Diff line change
@@ -129,7 +129,7 @@ export default function adPlugin(Client: any, config: any, components: any) {

ad.detectorProfile = ca({
url: {
fmt: `${API.DETECTOR_BASE}/<%=detectorId%>/_profile`,
fmt: `${API.DETECTOR_BASE}/<%=detectorId%>/_profile/init_progress,state,error`,
req: {
detectorId: {
type: 'string',
20 changes: 9 additions & 11 deletions server/routes/ad.ts
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ import {
getResultAggregationQuery,
getFinalDetectorStates,
getDetectorsWithJob,
getDetectorInitProgress,
} from './utils/adHelpers';
import { set } from 'lodash';

@@ -55,7 +56,7 @@ type PutDetectorParams = {
body: string;
};

export default function(apiRouter: Router) {
export default function (apiRouter: Router) {
apiRouter.post('/detectors', putDetector);
apiRouter.put('/detectors/{detectorId}', putDetector);
apiRouter.post('/detectors/_search', searchDetector);
@@ -181,7 +182,6 @@ const getDetector = async (
detectorId: detectorId,
}
);

const detectorStates = getFinalDetectorStates(
[detectorStateResp],
[convertDetectorKeysToCamelCase(response.anomaly_detector)]
@@ -202,6 +202,10 @@ const getDetector = async (
? //@ts-ignore
{ stateError: detectorState.error }
: {}),
...(detectorState !== undefined
? //@ts-ignore
{ initProgress: getDetectorInitProgress(detectorState) }
: {}),
ohltyler marked this conversation as resolved.
Show resolved Hide resolved
};
return {
ok: true,
@@ -333,10 +337,7 @@ const getDetectors = async (
query_string: {
fields: ['name', 'description'],
default_operator: 'AND',
query: `*${search
.trim()
.split(' ')
.join('* *')}*`,
query: `*${search.trim().split(' ').join('* *')}*`,
},
});
}
@@ -345,10 +346,7 @@ const getDetectors = async (
query_string: {
fields: ['indices'],
default_operator: 'OR',
query: `*${indices
.trim()
.split(' ')
.join('* *')}*`,
query: `*${indices.trim().split(' ').join('* *')}*`,
},
});
}
@@ -452,7 +450,7 @@ const getDetectors = async (
}

// Get detector state as well: loop through the ids to get each detector's state using profile api
const allIds = finalDetectors.map(detector => detector.id);
const allIds = finalDetectors.map((detector) => detector.id);

const detectorStatePromises = allIds.map(async (id: string) => {
try {
19 changes: 17 additions & 2 deletions server/routes/utils/adHelpers.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import { AnomalyResults } from 'server/models/interfaces';
import { GetDetectorsQueryParams } from '../../models/types';
import { mapKeysDeep, toCamel, toSnake } from '../../utils/helpers';
import { DETECTOR_STATE } from '../../../public/utils/constants';
import { InitProgress } from 'public/models/interfaces';

export const convertDetectorKeysToSnakeCase = (payload: any) => {
return {
@@ -151,12 +152,26 @@ export const anomalyResultMapper = (anomalyResults: any[]): AnomalyResults => {
return resultData;
};

export const getDetectorInitProgress = (
detectorStateResponse: any
): InitProgress | undefined => {
if (detectorStateResponse.init_progress) {
return {
percentageStr: detectorStateResponse.init_progress.percentage,
estimatedMinutesLeft:
detectorStateResponse.init_progress.estimated_minutes_left,
neededShingles: detectorStateResponse.init_progress.needed_shingles,
};
}
return undefined;
};

export const getFinalDetectorStates = (
detectorStateResponses: any[],
finalDetectors: any[]
) => {
let finalDetectorStates = cloneDeep(detectorStateResponses);
finalDetectorStates.forEach(detectorState => {
finalDetectorStates.forEach((detectorState) => {
//@ts-ignore
detectorState.state = DETECTOR_STATE[detectorState.state];
});
@@ -198,7 +213,7 @@ export const getDetectorsWithJob = (
): any[] => {
const finalDetectorsWithJobResponses = cloneDeep(detectorsWithJobResponses);
const resultDetectorWithJobs = [] as any[];
finalDetectorsWithJobResponses.forEach(detectorWithJobResponse => {
finalDetectorsWithJobResponses.forEach((detectorWithJobResponse) => {
const resp = {
...detectorWithJobResponse.anomaly_detector,
id: detectorWithJobResponse._id,