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

Address detector list feedback and add feature required state #48

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
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;
};