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

Commit

Permalink
Add feature required state and address detector list feedback (#48)
Browse files Browse the repository at this point in the history
* Address feedback and add feature required state
  • Loading branch information
ohltyler authored Apr 28, 2020
1 parent 76b5459 commit 59a8951
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const ListControls = (props: ListControlsProps) => (
? props.selectedDetectorStates.map(index => ({ label: index }))
: []
}
fullWidth={true}
/>
</EuiFlexItem>
<EuiFlexItem>
Expand All @@ -78,6 +79,7 @@ export const ListControls = (props: ListControlsProps) => (
? props.selectedIndices.map(index => ({ label: index }))
: []
}
fullWidth={true}
/>
</EuiFlexItem>
{props.pageCount > 1 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ exports[`<ListControls /> spec Empty results renders component with empty messag
<div
aria-expanded="false"
aria-haspopup="listbox"
class="euiComboBox"
class="euiComboBox euiComboBox--fullWidth"
role="combobox"
>
<div
class="euiFormControlLayout"
class="euiFormControlLayout euiFormControlLayout--fullWidth"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<div
class="euiComboBox__inputWrap euiComboBox__inputWrap-isClearable"
class="euiComboBox__inputWrap euiComboBox__inputWrap--fullWidth euiComboBox__inputWrap-isClearable"
data-test-subj="comboBoxInput"
tabindex="-1"
>
Expand Down Expand Up @@ -112,17 +112,17 @@ exports[`<ListControls /> spec Empty results renders component with empty messag
<div
aria-expanded="false"
aria-haspopup="listbox"
class="euiComboBox"
class="euiComboBox euiComboBox--fullWidth"
role="combobox"
>
<div
class="euiFormControlLayout"
class="euiFormControlLayout euiFormControlLayout--fullWidth"
>
<div
class="euiFormControlLayout__childrenWrapper"
>
<div
class="euiComboBox__inputWrap euiComboBox__inputWrap-isClearable"
class="euiComboBox__inputWrap euiComboBox__inputWrap--fullWidth euiComboBox__inputWrap-isClearable"
data-test-subj="comboBoxInput"
tabindex="-1"
>
Expand Down
39 changes: 18 additions & 21 deletions public/pages/DetectorsList/List/__tests__/List.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('<ListControls /> spec', () => {
name: `detector_name_${index}`,
indices: [`index_${index}`],
curState: DETECTOR_STATE.DISABLED,
featureAttributes: [`feature_${index}`],
totalAnomalies: index,
lastActiveAnomaly: moment('2020-04-15T09:00:00')
.add(index, 'minutes')
Expand All @@ -160,12 +161,7 @@ describe('<ListControls /> spec', () => {
},
});

const {
getByText,
getAllByTestId,
queryByText,
getAllByText,
} = renderWithRouter({
const { getByText, getAllByTestId, queryByText } = renderWithRouter({
...initialDetectorsState,
requesting: true,
});
Expand All @@ -191,13 +187,14 @@ describe('<ListControls /> spec', () => {
expect(queryByText('index_0')).toBeNull();

// Sort by detector state (enum sorting)
// NOTE: this is assuming DETECTOR_STATE.RUNNING is higher alphabetically ('running')
// than DETECTOR_STATE.DISABLED ('stopped')
userEvent.click(getAllByTestId('tableHeaderSortButton')[2]);
await wait();
getAllByText(DETECTOR_STATE.DISABLED);
expect(queryByText(DETECTOR_STATE.RUNNING)).toBeNull();
expect(queryByText(DETECTOR_STATE.RUNNING)).not.toBeNull();
userEvent.click(getAllByTestId('tableHeaderSortButton')[2]);
await wait();
expect(queryByText(DETECTOR_STATE.RUNNING)).not.toBeNull();
expect(queryByText(DETECTOR_STATE.RUNNING)).toBeNull();

// Sort by totalAnomalies (numeric sorting)
userEvent.click(getAllByTestId('tableHeaderSortButton')[3]);
Expand All @@ -216,22 +213,22 @@ describe('<ListControls /> spec', () => {
// Sort by last anomaly occurrence (date sorting)
userEvent.click(getAllByTestId('tableHeaderSortButton')[4]);
await wait();
getByText('04/15/2020 9:00 am');
expect(queryByText('04/15/2020 9:30 am')).toBeNull();
getByText('04/15/2020 9:00 AM');
expect(queryByText('04/15/2020 9:30 AM')).toBeNull();
userEvent.click(getAllByTestId('tableHeaderSortButton')[4]);
await wait();
getByText('04/15/2020 9:30 am');
expect(queryByText('04/15/2020 9:00 am')).toBeNull();
getByText('04/15/2020 9:30 AM');
expect(queryByText('04/15/2020 9:00 AM')).toBeNull();

// Sort by last updated (date sorting)
userEvent.click(getAllByTestId('tableHeaderSortButton')[5]);
await wait();
getByText('04/15/2020 7:00 am');
expect(queryByText('04/15/2020 7:30 am')).toBeNull();
getByText('04/15/2020 7:00 AM');
expect(queryByText('04/15/2020 7:30 AM')).toBeNull();
userEvent.click(getAllByTestId('tableHeaderSortButton')[5]);
await wait();
getByText('04/15/2020 7:30 am');
expect(queryByText('04/15/2020 7:00 am')).toBeNull();
getByText('04/15/2020 7:30 AM');
expect(queryByText('04/15/2020 7:00 AM')).toBeNull();
});
test('should be able to search', async () => {
const randomDetectors = new Array(40).fill(null).map((_, index) => {
Expand Down Expand Up @@ -328,15 +325,15 @@ describe('<ListControls /> spec', () => {
getByText(randomDetectors[0].indices[0]);
getByText(randomDetectors[0].curState);
getByText(randomDetectors[0].totalAnomalies.toString());
getByText('10/19/2019 9:00 am');
getByText('10/19/2019 7:00 am');
getByText('10/19/2019 9:00 AM');
getByText('10/19/2019 7:00 AM');
//Test3 Detector
getByText(randomDetectors[2].name);
getByText(randomDetectors[2].indices[0]);
getByText(randomDetectors[2].curState);
getByText(randomDetectors[2].totalAnomalies.toString());
getByText('10/19/2019 9:30 am');
getByText('10/19/2019 7:30 am');
getByText('10/19/2019 9:30 AM');
getByText('10/19/2019 7:30 AM');
});
});
});
8 changes: 6 additions & 2 deletions public/pages/DetectorsList/utils/tableUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { EuiIcon, EuiLink, EuiToolTip, EuiHealth } from '@elastic/eui';
import moment from 'moment';
import get from 'lodash/get';
import React from 'react';
import { Detector } from '../../../../server/models/types';
import { Detector } from '../../../models/interfaces';
import { PLUGIN_NAME, DETECTOR_STATE } from '../../../utils/constants';
import { stateToColorMap } from '../../utils/constants';
import { darkModeEnabled } from '../../../utils/kibanaUtils';
Expand All @@ -29,7 +29,7 @@ const hintColor = darkModeEnabled() ? '#98A2B3' : '#535966';
const renderTime = (time: number) => {
const momentTime = moment(time);
if (time && momentTime.isValid())
return momentTime.format('MM/DD/YYYY h:mm a');
return momentTime.format('MM/DD/YYYY h:mm A');
return DEFAULT_EMPTY_DATA;
};

Expand Down Expand Up @@ -64,6 +64,7 @@ export const staticColumn = [
truncateText: true,
textOnly: true,
align: 'left',
width: '15%',
render: (name: string, detector: Detector) => (
<EuiLink href={`${PLUGIN_NAME}#/detectors/${detector.id}`}>
{name}
Expand All @@ -89,6 +90,7 @@ export const staticColumn = [
truncateText: true,
textOnly: true,
align: 'left',
width: '15%',
render: renderIndices,
},
{
Expand Down Expand Up @@ -153,6 +155,7 @@ export const staticColumn = [
dataType: 'date',
truncateText: false,
align: 'left',
width: '16%',
render: renderTime,
},
{
Expand All @@ -174,6 +177,7 @@ export const staticColumn = [
dataType: 'date',
truncateText: false,
align: 'left',
width: '16%',
render: renderTime,
},
];
4 changes: 3 additions & 1 deletion public/pages/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import { DETECTOR_STATE } from '../../utils/constants';

export enum DETECTOR_STATE_COLOR {
DISABLED = 'subdued',
INIT = '#0000cc',
INIT = 'primary',
RUNNING = 'success',
FEATURE_REQUIRED = 'subdued',
INIT_FAILURE = 'danger',
UNEXPECTED_FAILURE = 'danger',
}
Expand All @@ -28,6 +29,7 @@ export const stateToColorMap = new Map<DETECTOR_STATE, DETECTOR_STATE_COLOR>()
.set(DETECTOR_STATE.DISABLED, DETECTOR_STATE_COLOR.DISABLED)
.set(DETECTOR_STATE.INIT, DETECTOR_STATE_COLOR.INIT)
.set(DETECTOR_STATE.RUNNING, DETECTOR_STATE_COLOR.RUNNING)
.set(DETECTOR_STATE.FEATURE_REQUIRED, DETECTOR_STATE_COLOR.FEATURE_REQUIRED)
.set(DETECTOR_STATE.INIT_FAILURE, DETECTOR_STATE_COLOR.INIT_FAILURE)
.set(
DETECTOR_STATE.UNEXPECTED_FAILURE,
Expand Down
1 change: 1 addition & 0 deletions public/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export enum DETECTOR_STATE {
DISABLED = 'Stopped',
INIT = 'Initializing',
RUNNING = 'Running',
FEATURE_REQUIRED = 'Feature required',
INIT_FAILURE = 'Initialization failure',
UNEXPECTED_FAILURE = 'Unexpected failure',
}
34 changes: 7 additions & 27 deletions server/routes/ad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import {
convertDetectorKeysToCamelCase,
convertDetectorKeysToSnakeCase,
getResultAggregationQuery,
getFinalDetectorStates,
} from './utils/adHelpers';
import { DETECTOR_STATE } from '../../public/utils/constants';

type PutDetectorParams = {
detectorId: string;
Expand Down Expand Up @@ -356,7 +356,6 @@ const getDetectors = async (
description: get(detector, '_source.description', ''),
indices: get(detector, '_source.indices', []),
lastUpdateTime: get(detector, '_source.last_update_time', 0),
// TODO: get the state of the detector once possible (enabled/disabled for now)
...convertDetectorKeysToCamelCase(get(detector, '_source', {})),
},
}),
Expand Down Expand Up @@ -439,33 +438,14 @@ const getDetectors = async (
);
}
});
const detectorStates = await Promise.all(detectorStatePromises);
detectorStates.forEach(detectorState => {
//@ts-ignore
detectorState.state = DETECTOR_STATE[detectorState.state];
});

// check if there was any failures
detectorStates.forEach(detectorState => {
/*
If the error starts with 'Stopped detector', then an EndRunException was thrown.
All EndRunExceptions are related to initialization failures except for the
unknown prediction error which contains the message "We might have bugs".
*/
if (
detectorState.state === DETECTOR_STATE.DISABLED &&
detectorState.error !== undefined &&
detectorState.error.includes('Stopped detector')
) {
detectorState.state = detectorState.error.includes('We might have bugs')
? DETECTOR_STATE.UNEXPECTED_FAILURE
: DETECTOR_STATE.INIT_FAILURE;
}
});

const detectorStateResponses = await Promise.all(detectorStatePromises);
const finalDetectorStates = getFinalDetectorStates(
detectorStateResponses,
finalDetectors
);
// update the final detectors to include the detector state
finalDetectors.forEach((detector, i) => {
detector.curState = detectorStates[i].state;
detector.curState = finalDetectorStates[i].state;
});

return {
Expand Down
45 changes: 44 additions & 1 deletion server/routes/utils/adHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
* permissions and limitations under the License.
*/

import { get, omit } from 'lodash';
import { get, omit, cloneDeep } from 'lodash';
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';

export const convertDetectorKeysToSnakeCase = (payload: any) => {
return {
Expand Down Expand Up @@ -153,3 +154,45 @@ export const anomalyResultMapper = (anomalyResults: any[]): AnomalyResults => {
});
return resultData;
};

export const getFinalDetectorStates = (
detectorStateResponses: any[],
finalDetectors: any[]
) => {
let finalDetectorStates = cloneDeep(detectorStateResponses);
finalDetectorStates.forEach(detectorState => {
//@ts-ignore
detectorState.state = DETECTOR_STATE[detectorState.state];
});

// check if there was any failures / detectors that are unable to start
finalDetectorStates.forEach((detectorState, i) => {
/*
If the error starts with 'Stopped detector', then an EndRunException was thrown.
All EndRunExceptions are related to initialization failures except for the
unknown prediction error which contains the message "We might have bugs".
*/
if (
detectorState.state === DETECTOR_STATE.DISABLED &&
detectorState.error !== undefined &&
detectorState.error.includes('Stopped detector')
) {
detectorState.state = detectorState.error.includes('We might have bugs')
? DETECTOR_STATE.UNEXPECTED_FAILURE
: DETECTOR_STATE.INIT_FAILURE;
}

/*
If a detector is disabled and has no features, set to
a feature required state
*/
if (
detectorState.state === DETECTOR_STATE.DISABLED &&
finalDetectors[i].featureAttributes.length === 0
) {
detectorState.state = DETECTOR_STATE.FEATURE_REQUIRED;
}
});

return finalDetectorStates;
};

0 comments on commit 59a8951

Please sign in to comment.