[ML] For categorization anomalies, display the category regex/terms in the expanded row (#28376) (#28484)

* Fetch terms/regex when row expanded

* Adds error handling for definition fetch

* update anomalyDetails tests

* Handle definition regex/terms not returned

* Adds tooltip to regex header
This commit is contained in:
Melissa Alvarez 2019-01-10 11:21:02 -05:00 committed by GitHub
parent 0377081d8d
commit 683ab12ec5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 4 deletions

View file

@ -75,6 +75,10 @@
min-width: 150px;
}
.mlAnomalyCategoryExamples__header {
display: inline;
}
.mlAnomalyCategoryExamples__link {
width: 100%;
}

View file

@ -29,9 +29,11 @@ import { AnomalyDetails } from './anomaly_details';
import { mlTableService } from '../../services/table_service';
import { RuleEditorFlyout } from '../../components/rule_editor';
import { ml } from '../../services/ml_api_service';
import {
INFLUENCERS_LIMIT,
ANOMALIES_TABLE_TABS
ANOMALIES_TABLE_TABS,
MAX_CHARS
} from './anomalies_table_constants';
class AnomaliesTable extends Component {
@ -71,18 +73,36 @@ class AnomaliesTable extends Component {
return null;
}
toggleRow = (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
toggleRow = async (item, tab = ANOMALIES_TABLE_TABS.DETAILS) => {
const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
if (itemIdToExpandedRowMap[item.rowId]) {
delete itemIdToExpandedRowMap[item.rowId];
} else {
const examples = (item.entityName === 'mlcategory') ?
_.get(this.props.tableData, ['examplesByJobId', item.jobId, item.entityValue]) : undefined;
let definition = undefined;
if (examples !== undefined) {
try {
definition = await ml.results.getCategoryDefinition(item.jobId, item.source.mlcategory[0]);
if (definition.terms && definition.terms.length > MAX_CHARS) {
definition.terms = `${definition.terms.substring(0, MAX_CHARS)}...`;
}
if (definition.regex && definition.regex.length > MAX_CHARS) {
definition.terms = `${definition.regex.substring(0, MAX_CHARS)}...`;
}
} catch(error) {
console.log('Error fetching category definition for row item.', error);
}
}
itemIdToExpandedRowMap[item.rowId] = (
<AnomalyDetails
tabIndex={tab}
anomaly={item}
examples={examples}
definition={definition}
isAggregatedData={this.isShowingAggregatedData()}
filter={this.props.filter}
influencersLimit={INFLUENCERS_LIMIT}

View file

@ -13,3 +13,5 @@ export const ANOMALIES_TABLE_TABS = {
DETAILS: 0,
CATEGORY_EXAMPLES: 1
};
export const MAX_CHARS = 500;

View file

@ -19,6 +19,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiIconTip,
EuiLink,
EuiSpacer,
EuiTabbedContent,
@ -35,6 +36,7 @@ import {
} from '../../../common/util/anomaly_utils';
import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact';
import { formatValue } from '../../formatters/format_value';
import { MAX_CHARS } from './anomalies_table_constants';
const TIME_FIELD_NAME = 'timestamp';
@ -218,6 +220,8 @@ export class AnomalyDetails extends Component {
}
renderCategoryExamples() {
const { examples, definition } = this.props;
return (
<EuiFlexGroup
direction="column"
@ -225,9 +229,54 @@ export class AnomalyDetails extends Component {
gutterSize="m"
className="mlAnomalyCategoryExamples"
>
{this.props.examples.map((example, i) => {
{(definition !== undefined && definition.terms) &&
<Fragment>
<EuiFlexItem key={`example-terms`}>
<EuiText size="xs">
<h4 className="mlAnomalyCategoryExamples__header">Terms</h4>&nbsp;
<EuiIconTip
aria-label="Description"
type="questionInCircle"
color="subdued"
size="s"
content={`A space separated list of the common tokens that are matched in values of the category
(may have been truncated to a max character limit of ${MAX_CHARS})`}
/>
</EuiText>
<EuiText size="xs">
{definition.terms}
</EuiText>
</EuiFlexItem>
<EuiSpacer size="m" />
</Fragment> }
{(definition !== undefined && definition.regex) &&
<Fragment>
<EuiFlexItem key={`example-regex`}>
<EuiText size="xs">
<h4 className="mlAnomalyCategoryExamples__header">Regex</h4>&nbsp;
<EuiIconTip
aria-label="Description"
type="questionInCircle"
color="subdued"
size="s"
content={`The regular expression that is used to search for values that match the category
(may have been truncated to a max character limit of ${MAX_CHARS})`}
/>
</EuiText>
<EuiText size="xs">
{definition.regex}
</EuiText>
</EuiFlexItem>
<EuiSpacer size="l" />
</Fragment>}
{examples.map((example, i) => {
return (
<EuiFlexItem key={`example${i}`}>
{(i === 0 && definition !== undefined) &&
<EuiText size="s">
<h4>Examples</h4>
</EuiText>}
<span className="mlAnomalyCategoryExamples__item">{example}</span>
</EuiFlexItem>
);
@ -384,6 +433,7 @@ export class AnomalyDetails extends Component {
AnomalyDetails.propTypes = {
anomaly: PropTypes.object.isRequired,
examples: PropTypes.array,
definition: PropTypes.object,
isAggregatedData: PropTypes.bool,
filter: PropTypes.func,
influencersLimit: PropTypes.number,

View file

@ -6,7 +6,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';
import { AnomalyDetails } from './anomaly_details';
const props = {
@ -86,4 +86,75 @@ describe('AnomalyDetails', () => {
);
expect(wrapper.prop('initialSelectedTab').id).toBe('Category examples');
});
test('Renders with terms and regex when definition prop is not undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: {
terms: 'example terms for test',
regex: '.*?DBMS.+?ERROR.+?svc_prod.+?Err.+?Microsoft.+?ODBC.+?SQL.+?Server.+?Driver'
}
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(true);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(true);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(true);
});
test('Renders only with examples when definition prop is undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: undefined
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(false);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(false);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(false);
});
test('Renders only with terms when definition.regex is undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: {
terms: 'example terms for test',
}
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(false);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(true);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(true);
});
test('Renders only with regex when definition.terms is undefined', () => {
const categoryTabProps = {
...props,
tabIndex: 1,
definition: {
regex: '.*?DBMS.+?ERROR.+?svc_prod.+?Err.+?Microsoft.+?ODBC.+?SQL.+?Server.+?Driver'
}
};
const wrapper = mount(
<AnomalyDetails {...categoryTabProps} />
);
expect(wrapper.containsMatchingElement(<h4>Regex</h4>)).toBe(true);
expect(wrapper.containsMatchingElement(<h4>Terms</h4>)).toBe(false);
expect(wrapper.contains(<h4>Examples</h4>)).toBe(true);
});
});