Skip to content

Commit 97120f0

Browse files
janheisekmerzdennisoelkers
authored
Fix: Alerts aggregations - show only compatible fields in black, gray otherwise (#23778)
* show only compatible fields in black, gray otherwise - just like in the search aggregation wizard * fix icon, test * remove console.log * adding changelog * fixing linter hint * Fixing field select options. --------- Co-authored-by: Konrad Merz <konrad@graylog.com> Co-authored-by: Dennis Oelkers <dennis@graylog.com>
1 parent 3ca316a commit 97120f0

File tree

6 files changed

+110
-17
lines changed

6 files changed

+110
-17
lines changed

changelog/unreleased/pr-23778.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type = "a"
2+
message = "When creating an Alert definition, show the fields compatible to an aggregation like we do in the Search aggregation wizard (grey for incompatible)."
3+
4+
pulls = ["23778"]

graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpression.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ import GroupExpression from './AggregationConditionExpressions/GroupExpression';
3636

3737
import styles from './AggregationConditionExpression.css';
3838
import type { EventDefinition } from 'components/event-definitions/event-definitions-types';
39+
import type FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping';
3940

4041
type AggregationConditionExpressionProps = {
4142
eventDefinition: EventDefinition;
4243
validation?: any;
43-
formattedFields: any[];
44+
formattedFields: FieldTypeMapping[];
4445
aggregationFunctions: any[];
4546
onChange: (...args: any[]) => void;
4647
expression: any;

graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpressions/NumberRefExpression.test.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import * as React from 'react';
1818
import { render, screen } from 'wrappedTestingLibrary';
1919
import userEvent from '@testing-library/user-event';
2020

21+
import FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping';
22+
import { FieldTypes } from 'views/logic/fieldtypes/FieldType';
23+
2124
import NumberRefExpression from './NumberRefExpression';
2225

2326
describe('NumberRefExpression', () => {
@@ -28,9 +31,9 @@ describe('NumberRefExpression', () => {
2831
});
2932

3033
const aggregationFunctions = ['avg', 'card'];
31-
const formattedFields = [
32-
{ label: 'source - string', value: 'source' },
33-
{ label: 'took_ms - long', value: 'took_ms' },
34+
const formattedFields: FieldTypeMapping[] = [
35+
new FieldTypeMapping('source', FieldTypes.STRING()),
36+
new FieldTypeMapping('took_ms', FieldTypes.LONG()),
3437
];
3538

3639
it('should have no selected function and field with an undefined ref', async () => {
@@ -75,7 +78,7 @@ describe('NumberRefExpression', () => {
7578
);
7679

7780
await screen.findByText(/avg\(\)/i);
78-
await screen.findByText(/took_ms - long/i);
81+
await screen.findByText(/took_ms/i);
7982
});
8083

8184
it('should update ref and add series when function changes', async () => {

graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionExpressions/NumberRefExpression.tsx

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@
1515
* <http://www.mongodb.com/licensing/server-side-public-license>.
1616
*/
1717
import * as React from 'react';
18-
import { useCallback } from 'react';
18+
import { useCallback, useMemo } from 'react';
1919
import cloneDeep from 'lodash/cloneDeep';
20+
import * as Immutable from 'immutable';
21+
import styled, { css } from 'styled-components';
2022

21-
import { defaultCompare as naturalSort } from 'logic/DefaultCompare';
23+
import { defaultCompare as naturalSort, defaultCompare } from 'logic/DefaultCompare';
2224
import { Select } from 'components/common';
2325
import { Col, ControlLabel, FormGroup, HelpBlock, Row } from 'components/bootstrap';
2426
import { percentileOptions, percentageStrategyOptions } from 'views/Constants';
27+
import type FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping';
28+
import type FieldType from 'views/logic/fieldtypes/FieldType';
29+
import { Properties, type Property } from 'views/logic/fieldtypes/FieldType';
30+
import FieldTypeIcon from 'views/components/sidebar/fields/FieldTypeIcon';
2531

2632
const formatFunctions = (functions) =>
2733
functions.sort(naturalSort).map((fn) => ({ label: `${fn.toLowerCase()}()`, value: fn }));
@@ -32,12 +38,50 @@ type NumberRefExpressionProps = {
3238
aggregationFunctions: any[];
3339
eventDefinition: any;
3440
expression: any;
35-
formattedFields: any[];
41+
formattedFields: FieldTypeMapping[];
3642
onChange: (...args: any[]) => void;
3743
renderLabel: boolean;
3844
validation?: any;
3945
};
4046

47+
// start - copied from FieldSelectBase.tsx
48+
const FieldName = styled.span`
49+
display: inline-flex;
50+
gap: 2px;
51+
align-items: center;
52+
`;
53+
54+
const UnqualifiedOption = styled.span(
55+
({ theme }) => css`
56+
color: ${theme.colors.gray[70]};
57+
`,
58+
);
59+
60+
type OptionRendererProps = {
61+
label: string;
62+
qualified: boolean;
63+
type?: FieldType;
64+
};
65+
66+
const optionRenderer = ({ label, qualified, type = undefined }: OptionRendererProps) => {
67+
const children = (
68+
<FieldName>
69+
{type && (
70+
<>
71+
<FieldTypeIcon type={type} />{' '}
72+
</>
73+
)}
74+
{label}
75+
</FieldName>
76+
);
77+
78+
return qualified ? <span>{children}</span> : <UnqualifiedOption>{children}</UnqualifiedOption>;
79+
};
80+
81+
const sortByLabel = ({ label: label1 }: { label: string }, { label: label2 }: { label: string }) =>
82+
defaultCompare(label1, label2);
83+
// end - copied from FieldSelectBase.tsx
84+
4185
const NumberRefExpression = ({
4286
aggregationFunctions,
4387
formattedFields,
@@ -116,6 +160,45 @@ const NumberRefExpression = ({
116160

117161
const elements = ['percentage', 'percentile'].includes(series.type) ? 3 : 2;
118162

163+
// start - copied from MetricConfiguration.tsx
164+
const hasProperty = (fieldType: FieldTypeMapping, properties: Array<Property>) => {
165+
const fieldProperties = fieldType?.type?.properties ?? Immutable.Set();
166+
167+
return (
168+
properties.map((property) => fieldProperties.contains(property)).find((result) => result === false) === undefined
169+
);
170+
};
171+
172+
const currentFunction = series.type;
173+
const isPercentage = currentFunction === 'percentage';
174+
const requiresNumericField =
175+
(isPercentage && series.strategy === 'SUM') || !['card', 'count', 'latest', 'percentage'].includes(currentFunction);
176+
177+
const isFieldQualified = useCallback(
178+
(field: FieldTypeMapping) => {
179+
if (!requiresNumericField) {
180+
return true;
181+
}
182+
183+
return hasProperty(field, [Properties.Numeric]);
184+
},
185+
[requiresNumericField],
186+
);
187+
188+
const fieldOptions = useMemo(
189+
() =>
190+
formattedFields
191+
.map((field) => ({
192+
label: `${field.name}`,
193+
value: field.name,
194+
type: field.value.type,
195+
qualified: isFieldQualified(field),
196+
}))
197+
.sort(sortByLabel),
198+
[isFieldQualified, formattedFields],
199+
);
200+
// end - copied from MetricConfiguration.tsx
201+
119202
return (
120203
<Col md={6}>
121204
<FormGroup controlId="aggregation-function" validationState={validation.message ? 'error' : null}>
@@ -148,7 +231,8 @@ const NumberRefExpression = ({
148231
ignoreAccents={false}
149232
placeholder="Select Field (Optional)"
150233
onChange={handleAggregationFieldChange}
151-
options={formattedFields}
234+
options={fieldOptions}
235+
optionRenderer={optionRenderer}
152236
value={series.field}
153237
allowCreate
154238
/>

graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationConditionsForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
*/
1717
import React from 'react';
1818
import styled from 'styled-components';
19-
import get from 'lodash/get';
2019

2120
import { Alert, Row } from 'components/bootstrap';
2221
import { emptyComparisonExpressionConfig } from 'logic/alerts/AggregationExpressionConfig';
2322
import validateExpression from 'logic/alerts/AggregationExpressionValidation';
23+
import type FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping';
2424

2525
import AggregationConditionExpression from './AggregationConditionExpression';
2626
import AggregationConditionsFormSummary from './AggregationConditionsFormSummary';
@@ -52,7 +52,7 @@ const StyledAlert = styled(Alert)`
5252
type AggregationConditionsFormProps = {
5353
eventDefinition: any;
5454
validation: any;
55-
formattedFields: any[];
55+
formattedFields: FieldTypeMapping[];
5656
aggregationFunctions: any[];
5757
onChange: (...args: any[]) => void;
5858
};
@@ -118,7 +118,7 @@ class AggregationConditionsForm extends React.Component<
118118
<h3 className={commonStyles.title}>Create Events for Definition</h3>
119119
{validation.errors.conditions && (
120120
<StyledAlert bsStyle="danger" title="Errors found">
121-
<p>{get(validation, 'errors.conditions[0]')}</p>
121+
<p>{validation?.errors?.conditions[0]}</p>
122122
</StyledAlert>
123123
)}
124124

graylog2-web-interface/src/components/event-definitions/event-definition-types/AggregationForm.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import { useCallback, useMemo } from 'react';
2020
import { MultiSelect } from 'components/common';
2121
import { Col, ControlLabel, FormGroup, HelpBlock, Row } from 'components/bootstrap';
2222
// TODO: This should be moved to a general place outside of `views`
23-
import { defaultCompare } from 'logic/DefaultCompare';
2423
import useFieldTypes from 'views/logic/fieldtypes/useFieldTypes';
2524
import { ALL_MESSAGES_TIMERANGE } from 'views/Constants';
2625
import { getPathnameWithoutId } from 'util/URLUtils';
2726
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
2827
import useLocation from 'routing/useLocation';
2928
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
29+
import { defaultCompare } from 'logic/DefaultCompare';
3030

3131
import AggregationConditionsForm from './AggregationConditionsForm';
3232

@@ -43,15 +43,16 @@ type Props = {
4343
const AggregationForm = ({ aggregationFunctions, eventDefinition, validation, onChange }: Props) => {
4444
const { data: allFieldTypes } = useFieldTypes(eventDefinition?.config?.streams ?? [], ALL_MESSAGES_TIMERANGE);
4545
// Memoize function to only format fields when they change. Use joined fieldNames as cache key.
46-
const formattedFields = useMemo(
46+
const formattedFields = useMemo(() => allFieldTypes ?? [], [allFieldTypes]);
47+
const formattedFieldOptions = useMemo(
4748
() =>
48-
(allFieldTypes ?? [])
49+
formattedFields
4950
.sort((ftA, ftB) => defaultCompare(ftA.name, ftB.name))
5051
.map((fieldType) => ({
5152
label: `${fieldType.name}${fieldType.value.type.type}`,
5253
value: fieldType.name,
5354
})),
54-
[allFieldTypes],
55+
[formattedFields],
5556
);
5657

5758
const { pathname } = useLocation();
@@ -98,7 +99,7 @@ const AggregationForm = ({ aggregationFunctions, eventDefinition, validation, on
9899
<MultiSelect
99100
id="group-by"
100101
onChange={handleGroupByChange}
101-
options={formattedFields}
102+
options={formattedFieldOptions}
102103
ignoreAccents={false}
103104
value={(eventDefinition.config.group_by ?? []).join(',')}
104105
allowCreate

0 commit comments

Comments
 (0)