Chore: Reduce Elasticsearch data source plugin strict errors (#37399)

This commit is contained in:
Giordano Ricci 2021-08-05 16:03:38 +01:00 committed by GitHub
parent e0010860bd
commit 2f21bf5cfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 528 additions and 834 deletions

View File

@ -7,7 +7,6 @@ import { segmentStyles } from '../styles';
import { BucketAggregation, BucketAggregationType, isBucketAggregationWithField } from './aggregations';
import { SettingsEditor } from './SettingsEditor';
import { changeBucketAggregationField, changeBucketAggregationType } from './state/actions';
import { BucketAggregationAction } from './state/types';
import { bucketAggregationConfig } from './utils';
const bucketAggOptions: Array<SelectableValue<BucketAggregationType>> = Object.entries(bucketAggregationConfig).map(
@ -27,7 +26,7 @@ interface QueryMetricEditorProps {
}
export const BucketAggregationEditor = ({ value }: QueryMetricEditorProps) => {
const dispatch = useDispatch<BucketAggregationAction>();
const dispatch = useDispatch();
const getFields = useFields(value.type);
return (
@ -36,7 +35,7 @@ export const BucketAggregationEditor = ({ value }: QueryMetricEditorProps) => {
<Segment
className={segmentStyles}
options={bucketAggOptions}
onChange={(e) => dispatch(changeBucketAggregationType(value.id, e.value!))}
onChange={(e) => dispatch(changeBucketAggregationType({ id: value.id, newType: e.value! }))}
value={toOption(value)}
/>
@ -44,7 +43,7 @@ export const BucketAggregationEditor = ({ value }: QueryMetricEditorProps) => {
<SegmentAsync
className={segmentStyles}
loadOptions={getFields}
onChange={(e) => dispatch(changeBucketAggregationField(value.id, e.value))}
onChange={(e) => dispatch(changeBucketAggregationField({ id: value.id, newField: e.value }))}
placeholder="Select Field"
value={value.field}
/>

View File

@ -46,7 +46,8 @@ interface Props {
export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
const dispatch = useDispatch();
const handleIntervalChange = (v: string) => dispatch(changeBucketAggregationSetting(bucketAgg, 'interval', v));
const handleIntervalChange = (newValue: string) =>
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'interval', newValue }));
return (
<>
@ -66,7 +67,11 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Min Doc Count" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
onBlur={(e) =>
dispatch(
changeBucketAggregationSetting({ bucketAgg, settingName: 'min_doc_count', newValue: e.target.value })
)
}
defaultValue={
bucketAgg.settings?.min_doc_count || bucketAggregationConfig.date_histogram.defaultSettings?.min_doc_count
}
@ -75,7 +80,9 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Trim Edges" {...inlineFieldProps} tooltip="Trim the edges on the timeseries datapoints">
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'trimEdges', e.target.value!))}
onBlur={(e) =>
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'trimEdges', newValue: e.target.value }))
}
defaultValue={
bucketAgg.settings?.trimEdges || bucketAggregationConfig.date_histogram.defaultSettings?.trimEdges
}
@ -88,7 +95,9 @@ export const DateHistogramSettingsEditor = ({ bucketAgg }: Props) => {
tooltip="Change the start value of each bucket by the specified positive (+) or negative offset (-) duration, such as 1h for an hour, or 1d for a day"
>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'offset', e.target.value!))}
onBlur={(e) =>
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'offset', newValue: e.target.value }))
}
defaultValue={bucketAgg.settings?.offset || bucketAggregationConfig.date_histogram.defaultSettings?.offset}
/>
</InlineField>

View File

@ -5,7 +5,6 @@ import { AddRemove } from '../../../../AddRemove';
import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer';
import { Filters } from '../../aggregations';
import { changeBucketAggregationSetting } from '../../state/actions';
import { BucketAggregationAction } from '../../state/types';
import { addFilter, changeFilter, removeFilter } from './state/actions';
import { reducer as filtersReducer } from './state/reducer';
@ -14,10 +13,10 @@ interface Props {
}
export const FiltersSettingsEditor = ({ bucketAgg }: Props) => {
const upperStateDispatch = useDispatch<BucketAggregationAction<Filters>>();
const upperStateDispatch = useDispatch();
const dispatch = useStatelessReducer(
(newState) => upperStateDispatch(changeBucketAggregationSetting(bucketAgg, 'filters', newState)),
(newValue) => upperStateDispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'filters', newValue })),
bucketAgg.settings?.filters,
filtersReducer
);
@ -55,7 +54,7 @@ export const FiltersSettingsEditor = ({ bucketAgg }: Props) => {
placeholder="Lucene Query"
portalOrigin="elasticsearch"
onBlur={() => {}}
onChange={(query) => dispatch(changeFilter(index, { ...filter, query }))}
onChange={(query) => dispatch(changeFilter({ index, filter: { ...filter, query } }))}
query={filter.query}
/>
</InlineField>
@ -63,7 +62,7 @@ export const FiltersSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Label" labelWidth={10}>
<Input
placeholder="Label"
onBlur={(e) => dispatch(changeFilter(index, { ...filter, label: e.target.value }))}
onBlur={(e) => dispatch(changeFilter({ index, filter: { ...filter, label: e.target.value } }))}
defaultValue={filter.label}
/>
</InlineField>

View File

@ -1,16 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import { Filter } from '../../../aggregations';
import { FilterAction, ADD_FILTER, REMOVE_FILTER, CHANGE_FILTER } from './types';
export const addFilter = (): FilterAction => ({
type: ADD_FILTER,
});
export const removeFilter = (index: number): FilterAction => ({
type: REMOVE_FILTER,
payload: { index },
});
export const changeFilter = (index: number, filter: Filter): FilterAction => ({
type: CHANGE_FILTER,
payload: { index, filter },
});
export const addFilter = createAction('@bucketAggregations/filter/add');
export const removeFilter = createAction<number>('@bucketAggregations/filter/remove');
export const changeFilter = createAction<{ index: number; filter: Filter }>('@bucketAggregations/filter/change');

View File

@ -46,7 +46,7 @@ describe('Filters Bucket Aggregation Settings Reducer', () => {
reducerTester<Filter[]>()
.givenReducer(reducer, [firstFilter, secondFilter])
.whenActionIsDispatched(changeFilter(1, expectedSecondFilter))
.whenActionIsDispatched(changeFilter({ index: 1, filter: expectedSecondFilter }))
.thenStateShouldEqual([firstFilter, expectedSecondFilter]);
});
});

View File

@ -1,21 +1,26 @@
import { Action } from 'redux';
import { Filter } from '../../../aggregations';
import { defaultFilter } from '../utils';
import { ADD_FILTER, CHANGE_FILTER, FilterAction, REMOVE_FILTER } from './types';
import { addFilter, changeFilter, removeFilter } from './actions';
export const reducer = (state: Filter[] = [], action: FilterAction) => {
switch (action.type) {
case ADD_FILTER:
return [...state, defaultFilter()];
case REMOVE_FILTER:
return state.slice(0, action.payload.index).concat(state.slice(action.payload.index + 1));
case CHANGE_FILTER:
return state.map((filter, index) => {
if (index !== action.payload.index) {
return filter;
}
return action.payload.filter;
});
export const reducer = (state: Filter[] = [], action: Action) => {
if (addFilter.match(action)) {
return [...state, defaultFilter()];
}
if (removeFilter.match(action)) {
return state.slice(0, action.payload).concat(state.slice(action.payload + 1));
}
if (changeFilter.match(action)) {
return state.map((filter, index) => {
if (index !== action.payload.index) {
return filter;
}
return action.payload.filter;
});
}
return state;
};

View File

@ -1,22 +0,0 @@
import { Action } from '../../../../../../hooks/useStatelessReducer';
import { Filter } from '../../../aggregations';
export const ADD_FILTER = '@bucketAggregations/filter/add';
export const REMOVE_FILTER = '@bucketAggregations/filter/remove';
export const CHANGE_FILTER = '@bucketAggregations/filter/change';
export type AddFilterAction = Action<typeof ADD_FILTER>;
export interface RemoveFilterAction extends Action<typeof REMOVE_FILTER> {
payload: {
index: number;
};
}
export interface ChangeFilterAction extends Action<typeof CHANGE_FILTER> {
payload: {
index: number;
filter: Filter;
};
}
export type FilterAction = AddFilterAction | RemoveFilterAction | ChangeFilterAction;

View File

@ -23,7 +23,9 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Order" {...inlineFieldProps}>
<Select
menuShouldPortal
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'order', e.value!))}
onChange={(e) =>
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'order', newValue: e.value }))
}
options={orderOptions}
value={bucketAgg.settings?.order || bucketAggregationConfig.terms.defaultSettings?.order}
/>
@ -36,8 +38,8 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
{...useCreatableSelectPersistedBehaviour({
options: sizeOptions,
value: bucketAgg.settings?.size || bucketAggregationConfig.terms.defaultSettings?.size,
onChange(value) {
dispatch(changeBucketAggregationSetting(bucketAgg, 'size', value));
onChange(newValue) {
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'size', newValue }));
},
})}
/>
@ -45,7 +47,11 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Min Doc Count" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
onBlur={(e) =>
dispatch(
changeBucketAggregationSetting({ bucketAgg, settingName: 'min_doc_count', newValue: e.target.value })
)
}
defaultValue={
bucketAgg.settings?.min_doc_count || bucketAggregationConfig.terms.defaultSettings?.min_doc_count
}
@ -55,7 +61,9 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Order By" {...inlineFieldProps}>
<Select
menuShouldPortal
onChange={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'orderBy', e.value!))}
onChange={(e) =>
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'orderBy', newValue: e.value }))
}
options={orderBy}
value={bucketAgg.settings?.orderBy || bucketAggregationConfig.terms.defaultSettings?.orderBy}
/>
@ -63,7 +71,9 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Missing" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'missing', e.target.value!))}
onBlur={(e) =>
dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'missing', newValue: e.target.value }))
}
defaultValue={bucketAgg.settings?.missing || bucketAggregationConfig.terms.defaultSettings?.missing}
/>
</InlineField>

View File

@ -32,7 +32,11 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
{bucketAgg.type === 'geohash_grid' && (
<InlineField label="Precision" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'precision', e.target.value!))}
onBlur={(e) =>
dispatch(
changeBucketAggregationSetting({ bucketAgg, settingName: 'precision', newValue: e.target.value })
)
}
defaultValue={
bucketAgg.settings?.precision || bucketAggregationConfig[bucketAgg.type].defaultSettings?.precision
}
@ -44,7 +48,11 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
<>
<InlineField label="Interval" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'interval', e.target.value!))}
onBlur={(e) =>
dispatch(
changeBucketAggregationSetting({ bucketAgg, settingName: 'interval', newValue: e.target.value })
)
}
defaultValue={
bucketAgg.settings?.interval || bucketAggregationConfig[bucketAgg.type].defaultSettings?.interval
}
@ -53,7 +61,11 @@ export const SettingsEditor = ({ bucketAgg }: Props) => {
<InlineField label="Min Doc Count" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeBucketAggregationSetting(bucketAgg, 'min_doc_count', e.target.value!))}
onBlur={(e) =>
dispatch(
changeBucketAggregationSetting({ bucketAgg, settingName: 'min_doc_count', newValue: e.target.value })
)
}
defaultValue={
bucketAgg.settings?.min_doc_count ||
bucketAggregationConfig[bucketAgg.type].defaultSettings?.min_doc_count

View File

@ -2,7 +2,6 @@ import React from 'react';
import { BucketAggregationEditor } from './BucketAggregationEditor';
import { useDispatch } from '../../../hooks/useStatelessReducer';
import { addBucketAggregation, removeBucketAggregation } from './state/actions';
import { BucketAggregationAction } from './state/types';
import { BucketAggregation } from './aggregations';
import { useQuery } from '../ElasticsearchQueryContext';
import { QueryEditorRow } from '../QueryEditorRow';
@ -13,7 +12,7 @@ interface Props {
}
export const BucketAggregationsEditor = ({ nextId }: Props) => {
const dispatch = useDispatch<BucketAggregationAction>();
const dispatch = useDispatch();
const { bucketAggs } = useQuery();
const totalBucketAggs = bucketAggs?.length || 0;

View File

@ -1,61 +1,18 @@
import { SettingKeyOf } from '../../../types';
import { BucketAggregation, BucketAggregationWithField } from '../aggregations';
import {
ADD_BUCKET_AGG,
BucketAggregationAction,
REMOVE_BUCKET_AGG,
CHANGE_BUCKET_AGG_TYPE,
CHANGE_BUCKET_AGG_FIELD,
CHANGE_BUCKET_AGG_SETTING,
ChangeBucketAggregationSettingAction,
} from './types';
import { createAction } from '@reduxjs/toolkit';
import { BucketAggregation, BucketAggregationType, BucketAggregationWithField } from '../aggregations';
export const addBucketAggregation = (id: string): BucketAggregationAction => ({
type: ADD_BUCKET_AGG,
payload: {
id,
},
});
export const removeBucketAggregation = (id: BucketAggregation['id']): BucketAggregationAction => ({
type: REMOVE_BUCKET_AGG,
payload: {
id,
},
});
export const changeBucketAggregationType = (
id: BucketAggregation['id'],
newType: BucketAggregation['type']
): BucketAggregationAction => ({
type: CHANGE_BUCKET_AGG_TYPE,
payload: {
id,
newType,
},
});
export const changeBucketAggregationField = (
id: BucketAggregationWithField['id'],
newField: BucketAggregationWithField['field']
): BucketAggregationAction => ({
type: CHANGE_BUCKET_AGG_FIELD,
payload: {
id,
newField,
},
});
export const changeBucketAggregationSetting = <T extends BucketAggregation, K extends SettingKeyOf<T>>(
bucketAgg: T,
settingName: K,
// This could be inferred from T, but it's causing some troubles
newValue: string | string[] | any
): ChangeBucketAggregationSettingAction<T> => ({
type: CHANGE_BUCKET_AGG_SETTING,
payload: {
bucketAgg,
settingName,
newValue,
},
});
export const addBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/add');
export const removeBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/remove');
export const changeBucketAggregationType = createAction<{
id: BucketAggregation['id'];
newType: BucketAggregationType;
}>('@bucketAggs/change_type');
export const changeBucketAggregationField = createAction<{
id: BucketAggregation['id'];
newField: BucketAggregationWithField['field'];
}>('@bucketAggs/change_field');
export const changeBucketAggregationSetting = createAction<{
bucketAgg: BucketAggregation;
settingName: string;
newValue: any;
}>('@bucketAggs/change_setting');

View File

@ -71,7 +71,9 @@ describe('Bucket Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['bucketAggs']>()
.givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeBucketAggregationType(secondAggregation.id, expectedSecondAggregation.type))
.whenActionIsDispatched(
changeBucketAggregationType({ id: secondAggregation.id, newType: expectedSecondAggregation.type })
)
.thenStateShouldEqual([firstAggregation, expectedSecondAggregation]);
});
@ -92,7 +94,9 @@ describe('Bucket Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['bucketAggs']>()
.givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeBucketAggregationField(secondAggregation.id, expectedSecondAggregation.field))
.whenActionIsDispatched(
changeBucketAggregationField({ id: secondAggregation.id, newField: expectedSecondAggregation.field })
)
.thenStateShouldEqual([firstAggregation, expectedSecondAggregation]);
});
@ -108,13 +112,13 @@ describe('Bucket Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['bucketAggs']>()
.givenReducer(createReducer('@timestamp'), initialState)
// If the new metric aggregation is `isSingleMetric` we should remove all bucket aggregations.
.whenActionIsDispatched(changeMetricType('Some id', 'raw_data'))
.whenActionIsDispatched(changeMetricType({ id: 'Some id', type: 'raw_data' }))
.thenStatePredicateShouldEqual((newState) => newState?.length === 0)
// Switching back to another aggregation that is NOT `isSingleMetric` should bring back a bucket aggregation
.whenActionIsDispatched(changeMetricType('Some id', 'max'))
.whenActionIsDispatched(changeMetricType({ id: 'Some id', type: 'max' }))
.thenStatePredicateShouldEqual((newState) => newState?.length === 1)
// When none of the above is true state shouldn't change.
.whenActionIsDispatched(changeMetricType('Some id', 'min'))
.whenActionIsDispatched(changeMetricType({ id: 'Some id', type: 'min' }))
.thenStatePredicateShouldEqual((newState) => newState?.length === 1);
});
});
@ -139,7 +143,11 @@ describe('Bucket Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['bucketAggs']>()
.givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation])
.whenActionIsDispatched(
changeBucketAggregationSetting(firstAggregation, 'min_doc_count', expectedSettings.min_doc_count!)
changeBucketAggregationSetting({
bucketAgg: firstAggregation,
settingName: 'min_doc_count',
newValue: expectedSettings.min_doc_count!,
})
)
.thenStateShouldEqual([{ ...firstAggregation, settings: expectedSettings }, secondAggregation]);
});

View File

@ -1,114 +1,118 @@
import { defaultBucketAgg } from '../../../../query_def';
import { ElasticsearchQuery } from '../../../../types';
import { ChangeMetricTypeAction, CHANGE_METRIC_TYPE } from '../../MetricAggregationsEditor/state/types';
import { metricAggregationConfig } from '../../MetricAggregationsEditor/utils';
import { BucketAggregation, Terms } from '../aggregations';
import { INIT, InitAction } from '../../state';
import {
ADD_BUCKET_AGG,
REMOVE_BUCKET_AGG,
CHANGE_BUCKET_AGG_TYPE,
CHANGE_BUCKET_AGG_FIELD,
CHANGE_BUCKET_AGG_SETTING,
BucketAggregationAction,
} from './types';
import { initQuery } from '../../state';
import { bucketAggregationConfig } from '../utils';
import { removeEmpty } from '../../../../utils';
import { Action } from '@reduxjs/toolkit';
import {
addBucketAggregation,
changeBucketAggregationField,
changeBucketAggregationSetting,
changeBucketAggregationType,
removeBucketAggregation,
} from './actions';
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
export const createReducer = (defaultTimeField: string) => (
state: ElasticsearchQuery['bucketAggs'],
action: BucketAggregationAction | ChangeMetricTypeAction | InitAction
action: Action
): ElasticsearchQuery['bucketAggs'] => {
switch (action.type) {
case ADD_BUCKET_AGG:
const newAgg: Terms = {
id: action.payload.id,
type: 'terms',
settings: bucketAggregationConfig['terms'].defaultSettings,
};
if (addBucketAggregation.match(action)) {
const newAgg: Terms = {
id: action.payload,
type: 'terms',
settings: bucketAggregationConfig['terms'].defaultSettings,
};
// If the last bucket aggregation is a `date_histogram` we add the new one before it.
const lastAgg = state![state!.length - 1];
if (lastAgg?.type === 'date_histogram') {
return [...state!.slice(0, state!.length - 1), newAgg, lastAgg];
}
// If the last bucket aggregation is a `date_histogram` we add the new one before it.
const lastAgg = state![state!.length - 1];
if (lastAgg?.type === 'date_histogram') {
return [...state!.slice(0, state!.length - 1), newAgg, lastAgg];
}
return [...state!, newAgg];
case REMOVE_BUCKET_AGG:
return state!.filter((bucketAgg) => bucketAgg.id !== action.payload.id);
case CHANGE_BUCKET_AGG_TYPE:
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.id) {
return bucketAgg;
}
/*
TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations
in the new selected one (such as field or some settings).
It the future would be nice to have the same behavior but it's hard without a proper definition,
as Elasticsearch will error sometimes if some settings are not compatible.
*/
return {
id: bucketAgg.id,
type: action.payload.newType,
settings: bucketAggregationConfig[action.payload.newType].defaultSettings,
} as BucketAggregation;
});
case CHANGE_BUCKET_AGG_FIELD:
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.id) {
return bucketAgg;
}
return {
...bucketAgg,
field: action.payload.newField,
};
});
case CHANGE_METRIC_TYPE:
// If we are switching to a metric which requires the absence of bucket aggregations
// we remove all of them.
if (metricAggregationConfig[action.payload.type].isSingleMetric) {
return [];
} else if (state!.length === 0) {
// Else, if there are no bucket aggregations we restore a default one.
// This happens when switching from a metric that requires the absence of bucket aggregations to
// one that requires it.
return [{ ...defaultBucketAgg('2'), field: defaultTimeField }];
}
return state;
case CHANGE_BUCKET_AGG_SETTING:
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.bucketAgg.id) {
return bucketAgg;
}
const newSettings = removeEmpty({
...bucketAgg.settings,
[action.payload.settingName]: action.payload.newValue,
});
return {
...bucketAgg,
settings: {
...newSettings,
},
};
});
case INIT:
if (state?.length || 0 > 0) {
return state;
}
return [{ ...defaultBucketAgg('2'), field: defaultTimeField }];
default:
return state;
return [...state!, newAgg];
}
if (removeBucketAggregation.match(action)) {
return state!.filter((bucketAgg) => bucketAgg.id !== action.payload);
}
if (changeBucketAggregationType.match(action)) {
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.id) {
return bucketAgg;
}
/*
TODO: The previous version of the query editor was keeping some of the old bucket aggregation's configurations
in the new selected one (such as field or some settings).
It the future would be nice to have the same behavior but it's hard without a proper definition,
as Elasticsearch will error sometimes if some settings are not compatible.
*/
return {
id: bucketAgg.id,
type: action.payload.newType,
settings: bucketAggregationConfig[action.payload.newType].defaultSettings,
} as BucketAggregation;
});
}
if (changeBucketAggregationField.match(action)) {
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.id) {
return bucketAgg;
}
return {
...bucketAgg,
field: action.payload.newField,
};
});
}
if (changeMetricType.match(action)) {
// If we are switching to a metric which requires the absence of bucket aggregations
// we remove all of them.
if (metricAggregationConfig[action.payload.type].isSingleMetric) {
return [];
} else if (state!.length === 0) {
// Else, if there are no bucket aggregations we restore a default one.
// This happens when switching from a metric that requires the absence of bucket aggregations to
// one that requires it.
return [{ ...defaultBucketAgg('2'), field: defaultTimeField }];
}
return state;
}
if (changeBucketAggregationSetting.match(action)) {
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.bucketAgg.id) {
return bucketAgg;
}
const newSettings = removeEmpty({
...bucketAgg.settings,
[action.payload.settingName]: action.payload.newValue,
});
return {
...bucketAgg,
settings: {
...newSettings,
},
};
});
}
if (initQuery.match(action)) {
if (state?.length || 0 > 0) {
return state;
}
return [{ ...defaultBucketAgg('2'), field: defaultTimeField }];
}
return state;
};

View File

@ -1,51 +0,0 @@
import { Action } from '../../../../hooks/useStatelessReducer';
import { SettingKeyOf } from '../../../types';
import { BucketAggregation, BucketAggregationWithField } from '../aggregations';
export const ADD_BUCKET_AGG = '@bucketAggs/add';
export const REMOVE_BUCKET_AGG = '@bucketAggs/remove';
export const CHANGE_BUCKET_AGG_TYPE = '@bucketAggs/change_type';
export const CHANGE_BUCKET_AGG_FIELD = '@bucketAggs/change_field';
export const CHANGE_BUCKET_AGG_SETTING = '@bucketAggs/change_setting';
export interface AddBucketAggregationAction extends Action<typeof ADD_BUCKET_AGG> {
payload: {
id: BucketAggregation['id'];
};
}
export interface RemoveBucketAggregationAction extends Action<typeof REMOVE_BUCKET_AGG> {
payload: {
id: BucketAggregation['id'];
};
}
export interface ChangeBucketAggregationTypeAction extends Action<typeof CHANGE_BUCKET_AGG_TYPE> {
payload: {
id: BucketAggregation['id'];
newType: BucketAggregation['type'];
};
}
export interface ChangeBucketAggregationFieldAction extends Action<typeof CHANGE_BUCKET_AGG_FIELD> {
payload: {
id: BucketAggregation['id'];
newField: BucketAggregationWithField['field'];
};
}
export interface ChangeBucketAggregationSettingAction<T extends BucketAggregation>
extends Action<typeof CHANGE_BUCKET_AGG_SETTING> {
payload: {
bucketAgg: T;
settingName: SettingKeyOf<T>;
newValue: unknown;
};
}
export type BucketAggregationAction<T extends BucketAggregation = BucketAggregation> =
| AddBucketAggregationAction
| RemoveBucketAggregationAction
| ChangeBucketAggregationTypeAction
| ChangeBucketAggregationFieldAction
| ChangeBucketAggregationSettingAction<T>;

View File

@ -79,7 +79,7 @@ export const orderByOptions = [
/**
* This returns the valid options for each of the enabled extended stat
*/
function createOrderByOptionsForExtendedStats(metric: ExtendedStats): OrderByOption[] {
function createOrderByOptionsForExtendedStats(metric: ExtendedStats): SelectableValue<string> {
if (!metric.meta) {
return [];
}

View File

@ -6,7 +6,6 @@ import { useDatasource, useQuery } from '../ElasticsearchQueryContext';
import { useDispatch } from '../../../hooks/useStatelessReducer';
import { getStyles } from './styles';
import { SettingsEditor } from './SettingsEditor';
import { MetricAggregationAction } from './state/types';
import { metricAggregationConfig } from './utils';
import { changeMetricField, changeMetricType } from './state/actions';
import { MetricPicker } from '../../MetricPicker';
@ -65,7 +64,7 @@ export const MetricEditor = ({ value }: Props) => {
const styles = getStyles(useTheme2(), !!value.hide);
const datasource = useDatasource();
const query = useQuery();
const dispatch = useDispatch<MetricAggregationAction>();
const dispatch = useDispatch();
const getFields = useFields(value.type);
const loadOptions = useCallback(async () => {
@ -90,7 +89,7 @@ export const MetricEditor = ({ value }: Props) => {
<Segment
className={cx(styles.color, segmentStyles)}
options={getTypeOptions(previousMetrics, datasource.esVersion, datasource.xpack)}
onChange={(e) => dispatch(changeMetricType(value.id, e.value!))}
onChange={(e) => dispatch(changeMetricType({ id: value.id, type: e.value! }))}
value={toOption(value)}
/>
@ -98,7 +97,7 @@ export const MetricEditor = ({ value }: Props) => {
<SegmentAsync
className={cx(styles.color, segmentStyles)}
loadOptions={loadOptions}
onChange={(e) => dispatch(changeMetricField(value.id, e.value!))}
onChange={(e) => dispatch(changeMetricField({ id: value.id, field: e.value! }))}
placeholder="Select Field"
value={value.field}
/>
@ -107,7 +106,7 @@ export const MetricEditor = ({ value }: Props) => {
{isPipelineAggregation(value) && !isPipelineAggregationWithMultipleBucketPaths(value) && (
<MetricPicker
className={cx(styles.color, segmentStyles)}
onChange={(e) => dispatch(changeMetricField(value.id, e.value?.id!))}
onChange={(e) => dispatch(changeMetricField({ id: value.id, field: e.value?.id! }))}
options={previousMetrics}
value={value.field}
/>

View File

@ -1,6 +1,5 @@
import React, { Fragment, useEffect } from 'react';
import { Input, InlineLabel } from '@grafana/ui';
import { MetricAggregationAction } from '../../state/types';
import { changeMetricAttribute } from '../../state/actions';
import { css } from '@emotion/css';
import { AddRemove } from '../../../../AddRemove';
@ -23,10 +22,11 @@ interface Props {
}
export const BucketScriptSettingsEditor = ({ value, previousMetrics }: Props) => {
const upperStateDispatch = useDispatch<MetricAggregationAction<BucketScript>>();
const upperStateDispatch = useDispatch();
const dispatch = useStatelessReducer(
(newState) => upperStateDispatch(changeMetricAttribute(value, 'pipelineVariables', newState)),
(newValue) =>
upperStateDispatch(changeMetricAttribute({ metric: value, attribute: 'pipelineVariables', newValue })),
value.pipelineVariables,
reducer
);
@ -74,10 +74,10 @@ export const BucketScriptSettingsEditor = ({ value, previousMetrics }: Props) =>
<Input
defaultValue={pipelineVar.name}
placeholder="Variable Name"
onBlur={(e) => dispatch(renamePipelineVariable(e.target.value, index))}
onBlur={(e) => dispatch(renamePipelineVariable({ newName: e.target.value, index }))}
/>
<MetricPicker
onChange={(e) => dispatch(changePipelineVariableMetric(e.value!.id, index))}
onChange={(e) => dispatch(changePipelineVariableMetric({ newMetric: e.value!.id, index }))}
options={previousMetrics}
value={pipelineVar.pipelineAgg}
/>

View File

@ -1,34 +1,10 @@
import {
ADD_PIPELINE_VARIABLE,
REMOVE_PIPELINE_VARIABLE,
PipelineVariablesAction,
RENAME_PIPELINE_VARIABLE,
CHANGE_PIPELINE_VARIABLE_METRIC,
} from './types';
import { createAction } from '@reduxjs/toolkit';
export const addPipelineVariable = (): PipelineVariablesAction => ({
type: ADD_PIPELINE_VARIABLE,
});
export const addPipelineVariable = createAction('@pipelineVariables/add');
export const removePipelineVariable = createAction<number>('@pipelineVariables/remove');
export const removePipelineVariable = (index: number): PipelineVariablesAction => ({
type: REMOVE_PIPELINE_VARIABLE,
payload: {
index,
},
});
export const renamePipelineVariable = createAction<{ index: number; newName: string }>('@pipelineVariables/rename');
export const renamePipelineVariable = (newName: string, index: number): PipelineVariablesAction => ({
type: RENAME_PIPELINE_VARIABLE,
payload: {
index,
newName,
},
});
export const changePipelineVariableMetric = (newMetric: string, index: number): PipelineVariablesAction => ({
type: CHANGE_PIPELINE_VARIABLE_METRIC,
payload: {
index,
newMetric,
},
});
export const changePipelineVariableMetric = createAction<{ index: number; newMetric: string }>(
'@pipelineVariables/change_metric'
);

View File

@ -72,7 +72,7 @@ describe('BucketScript Settings Reducer', () => {
reducerTester<PipelineVariable[]>()
.givenReducer(reducer, [firstVar, secondVar])
.whenActionIsDispatched(renamePipelineVariable(expectedSecondVar.name, 1))
.whenActionIsDispatched(renamePipelineVariable({ newName: expectedSecondVar.name, index: 1 }))
.thenStateShouldEqual([firstVar, expectedSecondVar]);
});
@ -94,7 +94,7 @@ describe('BucketScript Settings Reducer', () => {
reducerTester<PipelineVariable[]>()
.givenReducer(reducer, [firstVar, secondVar])
.whenActionIsDispatched(changePipelineVariableMetric(expectedSecondVar.pipelineAgg, 1))
.whenActionIsDispatched(changePipelineVariableMetric({ newMetric: expectedSecondVar.pipelineAgg, index: 1 }))
.thenStateShouldEqual([firstVar, expectedSecondVar]);
});

View File

@ -1,46 +1,47 @@
import { Action } from '@reduxjs/toolkit';
import { PipelineVariable } from '../../../aggregations';
import { defaultPipelineVariable, generatePipelineVariableName } from '../utils';
import {
PipelineVariablesAction,
REMOVE_PIPELINE_VARIABLE,
ADD_PIPELINE_VARIABLE,
RENAME_PIPELINE_VARIABLE,
CHANGE_PIPELINE_VARIABLE_METRIC,
} from './types';
addPipelineVariable,
changePipelineVariableMetric,
removePipelineVariable,
renamePipelineVariable,
} from './actions';
export const reducer = (state: PipelineVariable[] = [], action: PipelineVariablesAction) => {
switch (action.type) {
case ADD_PIPELINE_VARIABLE:
return [...state, defaultPipelineVariable(generatePipelineVariableName(state))];
case REMOVE_PIPELINE_VARIABLE:
return state.slice(0, action.payload.index).concat(state.slice(action.payload.index + 1));
case RENAME_PIPELINE_VARIABLE:
return state.map((pipelineVariable, index) => {
if (index !== action.payload.index) {
return pipelineVariable;
}
return {
...pipelineVariable,
name: action.payload.newName,
};
});
case CHANGE_PIPELINE_VARIABLE_METRIC:
return state.map((pipelineVariable, index) => {
if (index !== action.payload.index) {
return pipelineVariable;
}
return {
...pipelineVariable,
pipelineAgg: action.payload.newMetric,
};
});
default:
return state;
export const reducer = (state: PipelineVariable[] = [], action: Action) => {
if (addPipelineVariable.match(action)) {
return [...state, defaultPipelineVariable(generatePipelineVariableName(state))];
}
if (removePipelineVariable.match(action)) {
return state.slice(0, action.payload).concat(state.slice(action.payload + 1));
}
if (renamePipelineVariable.match(action)) {
return state.map((pipelineVariable, index) => {
if (index !== action.payload.index) {
return pipelineVariable;
}
return {
...pipelineVariable,
name: action.payload.newName,
};
});
}
if (changePipelineVariableMetric.match(action)) {
return state.map((pipelineVariable, index) => {
if (index !== action.payload.index) {
return pipelineVariable;
}
return {
...pipelineVariable,
pipelineAgg: action.payload.newMetric,
};
});
}
return state;
};

View File

@ -1,34 +0,0 @@
import { Action } from '../../../../../../hooks/useStatelessReducer';
export const ADD_PIPELINE_VARIABLE = '@pipelineVariables/add';
export const REMOVE_PIPELINE_VARIABLE = '@pipelineVariables/remove';
export const RENAME_PIPELINE_VARIABLE = '@pipelineVariables/rename';
export const CHANGE_PIPELINE_VARIABLE_METRIC = '@pipelineVariables/change_metric';
export type AddPipelineVariableAction = Action<typeof ADD_PIPELINE_VARIABLE>;
export interface RemovePipelineVariableAction extends Action<typeof REMOVE_PIPELINE_VARIABLE> {
payload: {
index: number;
};
}
export interface RenamePipelineVariableAction extends Action<typeof RENAME_PIPELINE_VARIABLE> {
payload: {
index: number;
newName: string;
};
}
export interface ChangePipelineVariableMetricAction extends Action<typeof CHANGE_PIPELINE_VARIABLE_METRIC> {
payload: {
index: number;
newMetric: string;
};
}
export type PipelineVariablesAction =
| AddPipelineVariableAction
| RemovePipelineVariableAction
| RenamePipelineVariableAction
| ChangePipelineVariableMetricAction;

View File

@ -21,7 +21,7 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<InlineField label="Model" labelWidth={16}>
<Select
menuShouldPortal
onChange={(value) => dispatch(changeMetricSetting(metric, 'model', value.value!))}
onChange={(value) => dispatch(changeMetricSetting({ metric, settingName: 'model', newValue: value.value }))}
options={movingAvgModelOptions}
value={metric.settings?.model}
/>
@ -36,9 +36,13 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<Input
onBlur={(e) =>
dispatch(
changeMetricSetting(metric, 'settings', {
...metric.settings?.settings,
alpha: e.target.value,
changeMetricSetting({
metric,
settingName: 'settings',
newValue: {
...metric.settings?.settings,
alpha: e.target.value,
},
})
)
}
@ -52,9 +56,13 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<Input
onBlur={(e) =>
dispatch(
changeMetricSetting(metric, 'settings', {
...metric.settings?.settings,
beta: e.target.value,
changeMetricSetting({
metric,
settingName: 'settings',
newValue: {
...metric.settings?.settings,
beta: e.target.value,
},
})
)
}
@ -69,9 +77,13 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<Input
onBlur={(e) =>
dispatch(
changeMetricSetting(metric, 'settings', {
...metric.settings?.settings,
gamma: e.target.value,
changeMetricSetting({
metric,
settingName: 'settings',
newValue: {
...metric.settings?.settings,
gamma: e.target.value,
},
})
)
}
@ -82,9 +94,13 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<Input
onBlur={(e) =>
dispatch(
changeMetricSetting(metric, 'settings', {
...metric.settings?.settings,
period: e.target.value!,
changeMetricSetting({
metric,
settingName: 'settings',
newValue: {
...metric.settings?.settings,
period: e.target.value!,
},
})
)
}
@ -96,7 +112,11 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<InlineSwitch
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(
changeMetricSetting(metric, 'settings', { ...metric.settings?.settings, pad: e.target.checked })
changeMetricSetting({
metric,
settingName: 'settings',
newValue: { ...metric.settings?.settings, pad: e.target.checked },
})
)
}
checked={!!metric.settings?.settings?.pad}
@ -109,7 +129,7 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => {
<InlineField label="Minimize" labelWidth={16}>
<InlineSwitch
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(changeMetricSetting(metric, 'minimize', e.target.checked))
dispatch(changeMetricSetting({ metric, settingName: 'minimize', newValue: e.target.checked }))
}
checked={!!metric.settings?.minimize}
/>

View File

@ -2,7 +2,6 @@ import React, { ComponentProps, useState } from 'react';
import { InlineField, Input } from '@grafana/ui';
import { useDispatch } from '../../../../hooks/useStatelessReducer';
import { changeMetricSetting } from '../state/actions';
import { ChangeMetricSettingAction } from '../state/types';
import { SettingKeyOf } from '../../../types';
import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../aggregations';
import { uniqueId } from 'lodash';
@ -23,7 +22,7 @@ export function SettingField<T extends MetricAggregationWithSettings, K extends
placeholder,
tooltip,
}: Props<T, K>) {
const dispatch = useDispatch<ChangeMetricSettingAction<T>>();
const dispatch = useDispatch();
const [id] = useState(uniqueId(`es-field-id-`));
const settings = metric.settings;
@ -38,7 +37,7 @@ export function SettingField<T extends MetricAggregationWithSettings, K extends
<Input
id={id}
placeholder={placeholder}
onBlur={(e) => dispatch(changeMetricSetting(metric, settingName, e.target.value as any))}
onBlur={(e) => dispatch(changeMetricSetting({ metric, settingName, newValue: e.target.value }))}
defaultValue={defaultValue}
/>
</InlineField>

View File

@ -26,11 +26,11 @@ export const TopMetricsSettingsEditor: FunctionComponent<Props> = ({ metric }) =
menuShouldPortal
onChange={(e) =>
dispatch(
changeMetricSetting(
changeMetricSetting({
metric,
'metrics',
e.map((v) => v.value!)
)
settingName: 'metrics',
newValue: e.map((v) => v.value!),
})
)
}
loadOptions={getMetricsOptions}
@ -42,7 +42,7 @@ export const TopMetricsSettingsEditor: FunctionComponent<Props> = ({ metric }) =
<InlineField label="Order" labelWidth={16}>
<Select
menuShouldPortal
onChange={(e) => dispatch(changeMetricSetting(metric, 'order', e.value))}
onChange={(e) => dispatch(changeMetricSetting({ metric, settingName: 'order', newValue: e.value }))}
options={orderOptions}
value={metric.settings?.order}
/>
@ -61,7 +61,7 @@ export const TopMetricsSettingsEditor: FunctionComponent<Props> = ({ metric }) =
margin-right: 0;
`}
loadOptions={getOrderByOptions}
onChange={(e) => dispatch(changeMetricSetting(metric, 'orderBy', e.value))}
onChange={(e) => dispatch(changeMetricSetting({ metric, settingName: 'orderBy', newValue: e.value }))}
placeholder="Select Field"
value={metric.settings?.orderBy}
/>

View File

@ -78,7 +78,7 @@ export const SettingsEditor = ({ metric, previousMetrics }: Props) => {
<InlineField label="Size" {...inlineFieldProps}>
<Input
id={`ES-query-${query.refId}_metric-${metric.id}-size`}
onBlur={(e) => dispatch(changeMetricSetting(metric, 'size', e.target.value))}
onBlur={(e) => dispatch(changeMetricSetting({ metric, settingName: 'size', newValue: e.target.value }))}
defaultValue={metric.settings?.size ?? metricAggregationConfig['raw_data'].defaults.settings?.size}
/>
</InlineField>
@ -96,7 +96,7 @@ export const SettingsEditor = ({ metric, previousMetrics }: Props) => {
<ExtendedStatSetting
key={stat.value}
stat={stat}
onChange={(checked) => dispatch(changeMetricMeta(metric, stat.value, checked))}
onChange={(newValue) => dispatch(changeMetricMeta({ metric, meta: stat.value, newValue }))}
value={
metric.meta?.[stat.value] !== undefined
? !!metric.meta?.[stat.value]
@ -112,7 +112,15 @@ export const SettingsEditor = ({ metric, previousMetrics }: Props) => {
{metric.type === 'percentiles' && (
<InlineField label="Percentiles" {...inlineFieldProps}>
<Input
onBlur={(e) => dispatch(changeMetricSetting(metric, 'percents', e.target.value.split(',').filter(Boolean)))}
onBlur={(e) =>
dispatch(
changeMetricSetting({
metric,
settingName: 'percents',
newValue: e.target.value.split(',').filter(Boolean),
})
)
}
defaultValue={
metric.settings?.percents || metricAggregationConfig['percentiles'].defaults.settings?.percents
}
@ -127,7 +135,7 @@ export const SettingsEditor = ({ metric, previousMetrics }: Props) => {
<Select
menuShouldPortal
id={`ES-query-${query.refId}_metric-${metric.id}-unit`}
onChange={(e) => dispatch(changeMetricSetting(metric, 'unit', e.value))}
onChange={(e) => dispatch(changeMetricSetting({ metric, settingName: 'unit', newValue: e.value }))}
options={rateAggUnitOptions}
value={metric.settings?.unit}
/>
@ -137,7 +145,7 @@ export const SettingsEditor = ({ metric, previousMetrics }: Props) => {
<Select
menuShouldPortal
id={`ES-query-${query.refId}_metric-${metric.id}-mode`}
onChange={(e) => dispatch(changeMetricSetting(metric, 'mode', e.value))}
onChange={(e) => dispatch(changeMetricSetting({ metric, settingName: 'mode', newValue: e.value }))}
options={rateAggModeOptions}
value={metric.settings?.unit}
/>

View File

@ -1,7 +1,6 @@
import React from 'react';
import { MetricEditor } from './MetricEditor';
import { useDispatch } from '../../../hooks/useStatelessReducer';
import { MetricAggregationAction } from './state/types';
import { metricAggregationConfig } from './utils';
import { addMetric, removeMetric, toggleMetricVisibility } from './state/actions';
import { MetricAggregation } from './aggregations';
@ -14,7 +13,7 @@ interface Props {
}
export const MetricAggregationsEditor = ({ nextId }: Props) => {
const dispatch = useDispatch<MetricAggregationAction>();
const dispatch = useDispatch();
const { metrics } = useQuery();
const totalMetrics = metrics?.length || 0;

View File

@ -1,96 +1,23 @@
import { SettingKeyOf } from '../../../types';
import { createAction } from '@reduxjs/toolkit';
import { MetricAggregation, MetricAggregationWithMeta, MetricAggregationWithSettings } from '../aggregations';
import {
ADD_METRIC,
CHANGE_METRIC_FIELD,
CHANGE_METRIC_TYPE,
REMOVE_METRIC,
TOGGLE_METRIC_VISIBILITY,
CHANGE_METRIC_SETTING,
CHANGE_METRIC_META,
CHANGE_METRIC_ATTRIBUTE,
MetricAggregationAction,
ChangeMetricAttributeAction,
ChangeMetricSettingAction,
ChangeMetricMetaAction,
} from './types';
export const addMetric = (id: MetricAggregation['id']): MetricAggregationAction => ({
type: ADD_METRIC,
payload: {
id,
},
});
export const removeMetric = (id: MetricAggregation['id']): MetricAggregationAction => ({
type: REMOVE_METRIC,
payload: {
id,
},
});
export const changeMetricType = (
id: MetricAggregation['id'],
type: MetricAggregation['type']
): MetricAggregationAction => ({
type: CHANGE_METRIC_TYPE,
payload: {
id,
type,
},
});
export const changeMetricField = (id: MetricAggregation['id'], field: string): MetricAggregationAction => ({
type: CHANGE_METRIC_FIELD,
payload: {
id,
field,
},
});
export const toggleMetricVisibility = (id: MetricAggregation['id']): MetricAggregationAction => ({
type: TOGGLE_METRIC_VISIBILITY,
payload: {
id,
},
});
export const changeMetricAttribute = <T extends MetricAggregation, K extends Extract<keyof T, string>>(
metric: T,
attribute: K,
newValue: T[K]
): ChangeMetricAttributeAction<T> => ({
type: CHANGE_METRIC_ATTRIBUTE,
payload: {
metric,
attribute,
newValue,
},
});
export const changeMetricSetting = <T extends MetricAggregationWithSettings, K extends SettingKeyOf<T>>(
metric: T,
settingName: K,
// Maybe this could have been NonNullable<T['settings']>[K], but it doesn't seem to work really well
newValue: NonNullable<T['settings']>[K]
): ChangeMetricSettingAction<T> => ({
type: CHANGE_METRIC_SETTING,
payload: {
metric,
settingName,
newValue,
},
});
export const changeMetricMeta = <T extends MetricAggregationWithMeta>(
metric: T,
meta: Extract<keyof Required<T>['meta'], string>,
newValue: string | number | boolean
): ChangeMetricMetaAction<T> => ({
type: CHANGE_METRIC_META,
payload: {
metric,
meta,
newValue,
},
});
export const addMetric = createAction<MetricAggregation['id']>('@metrics/add');
export const removeMetric = createAction<MetricAggregation['id']>('@metrics/remove');
export const toggleMetricVisibility = createAction<MetricAggregation['id']>('@metrics/toggle_visibility');
export const changeMetricField = createAction<{ id: MetricAggregation['id']; field: string }>('@metrics/change_field');
export const changeMetricType = createAction<{ id: MetricAggregation['id']; type: MetricAggregation['type'] }>(
'@metrics/change_type'
);
export const changeMetricAttribute = createAction<{ metric: MetricAggregation; attribute: string; newValue: any }>(
'@metrics/change_attr'
);
export const changeMetricSetting = createAction<{
metric: MetricAggregationWithSettings;
settingName: string;
newValue: any;
}>('@metrics/change_setting');
export const changeMetricMeta = createAction<{
metric: MetricAggregationWithMeta;
meta: string;
newValue: any;
}>('@metrics/change_meta');

View File

@ -77,7 +77,7 @@ describe('Metric Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['metrics']>()
.givenReducer(reducer, [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeMetricType(secondAggregation.id, expectedSecondAggregation.type))
.whenActionIsDispatched(changeMetricType({ id: secondAggregation.id, type: expectedSecondAggregation.type }))
.thenStateShouldEqual([firstAggregation, { ...secondAggregation, type: expectedSecondAggregation.type }]);
});
@ -99,7 +99,7 @@ describe('Metric Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['metrics']>()
.givenReducer(reducer, [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeMetricType(secondAggregation.id, expectedAggregation.type))
.whenActionIsDispatched(changeMetricType({ id: secondAggregation.id, type: expectedAggregation.type }))
.thenStateShouldEqual([expectedAggregation]);
});
});
@ -128,10 +128,10 @@ describe('Metric Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['metrics']>()
.givenReducer(reducer, [firstAggregation, secondAggregation])
// When changing a a pipelineAggregation field we set both pipelineAgg and field
.whenActionIsDispatched(changeMetricField(secondAggregation.id, expectedSecondAggregation.field))
.whenActionIsDispatched(changeMetricField({ id: secondAggregation.id, field: expectedSecondAggregation.field }))
.thenStateShouldEqual([firstAggregation, expectedSecondAggregation])
// otherwhise only field
.whenActionIsDispatched(changeMetricField(firstAggregation.id, expectedFirstAggregation.field))
.whenActionIsDispatched(changeMetricField({ id: firstAggregation.id, field: expectedFirstAggregation.field }))
.thenStateShouldEqual([expectedFirstAggregation, expectedSecondAggregation]);
});
@ -173,7 +173,9 @@ describe('Metric Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['metrics']>()
.givenReducer(reducer, [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeMetricSetting(firstAggregation, 'unit', expectedSettings.unit!))
.whenActionIsDispatched(
changeMetricSetting({ metric: firstAggregation, settingName: 'unit', newValue: expectedSettings.unit! })
)
.thenStateShouldEqual([{ ...firstAggregation, settings: expectedSettings }, secondAggregation]);
});
@ -196,7 +198,7 @@ describe('Metric Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['metrics']>()
.givenReducer(reducer, [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeMetricMeta(firstAggregation, 'avg', expectedMeta.avg!))
.whenActionIsDispatched(changeMetricMeta({ metric: firstAggregation, meta: 'avg', newValue: expectedMeta.avg! }))
.thenStateShouldEqual([{ ...firstAggregation, meta: expectedMeta }, secondAggregation]);
});
@ -214,7 +216,9 @@ describe('Metric Aggregations Reducer', () => {
reducerTester<ElasticsearchQuery['metrics']>()
.givenReducer(reducer, [firstAggregation, secondAggregation])
.whenActionIsDispatched(changeMetricAttribute(firstAggregation, 'hide', expectedHide))
.whenActionIsDispatched(
changeMetricAttribute({ metric: firstAggregation, attribute: 'hide', newValue: expectedHide })
)
.thenStateShouldEqual([{ ...firstAggregation, hide: expectedHide }, secondAggregation]);
});

View File

@ -1,7 +1,8 @@
import { Action } from '@reduxjs/toolkit';
import { defaultMetricAgg } from '../../../../query_def';
import { ElasticsearchQuery } from '../../../../types';
import { removeEmpty } from '../../../../utils';
import { INIT, InitAction } from '../../state';
import { initQuery } from '../../state';
import {
isMetricAggregationWithMeta,
isMetricAggregationWithSettings,
@ -10,156 +11,156 @@ import {
} from '../aggregations';
import { getChildren, metricAggregationConfig } from '../utils';
import {
ADD_METRIC,
CHANGE_METRIC_TYPE,
REMOVE_METRIC,
TOGGLE_METRIC_VISIBILITY,
MetricAggregationAction,
CHANGE_METRIC_FIELD,
CHANGE_METRIC_SETTING,
CHANGE_METRIC_META,
CHANGE_METRIC_ATTRIBUTE,
} from './types';
addMetric,
changeMetricAttribute,
changeMetricField,
changeMetricMeta,
changeMetricSetting,
changeMetricType,
removeMetric,
toggleMetricVisibility,
} from './actions';
export const reducer = (
state: ElasticsearchQuery['metrics'],
action: MetricAggregationAction | InitAction
): ElasticsearchQuery['metrics'] => {
switch (action.type) {
case ADD_METRIC:
return [...state!, defaultMetricAgg(action.payload.id)];
export const reducer = (state: ElasticsearchQuery['metrics'], action: Action): ElasticsearchQuery['metrics'] => {
if (addMetric.match(action)) {
return [...state!, defaultMetricAgg(action.payload)];
}
case REMOVE_METRIC:
const metricToRemove = state!.find((m) => m.id === action.payload.id)!;
const metricsToRemove = [metricToRemove, ...getChildren(metricToRemove, state!)];
const resultingMetrics = state!.filter(
(metric) => !metricsToRemove.some((toRemove) => toRemove.id === metric.id)
);
if (resultingMetrics.length === 0) {
return [defaultMetricAgg('1')];
if (removeMetric.match(action)) {
const metricToRemove = state!.find((m) => m.id === action.payload)!;
const metricsToRemove = [metricToRemove, ...getChildren(metricToRemove, state!)];
const resultingMetrics = state!.filter((metric) => !metricsToRemove.some((toRemove) => toRemove.id === metric.id));
if (resultingMetrics.length === 0) {
return [defaultMetricAgg('1')];
}
return resultingMetrics;
}
if (changeMetricType.match(action)) {
return state!
.filter((metric) =>
// When the new metric type is `isSingleMetric` we remove all other metrics from the query
// leaving only the current one.
!!metricAggregationConfig[action.payload.type].isSingleMetric ? metric.id === action.payload.id : true
)
.map((metric) => {
if (metric.id !== action.payload.id) {
return metric;
}
/*
TODO: The previous version of the query editor was keeping some of the old metric's configurations
in the new selected one (such as field or some settings).
It the future would be nice to have the same behavior but it's hard without a proper definition,
as Elasticsearch will error sometimes if some settings are not compatible.
*/
return {
id: metric.id,
type: action.payload.type,
...metricAggregationConfig[action.payload.type].defaults,
} as MetricAggregation;
});
}
if (changeMetricField.match(action)) {
return state!.map((metric) => {
if (metric.id !== action.payload.id) {
return metric;
}
return resultingMetrics;
case CHANGE_METRIC_TYPE:
return state!
.filter((metric) =>
// When the new metric type is `isSingleMetric` we remove all other metrics from the query
// leaving only the current one.
!!metricAggregationConfig[action.payload.type].isSingleMetric ? metric.id === action.payload.id : true
)
.map((metric) => {
if (metric.id !== action.payload.id) {
return metric;
}
const newMetric = {
...metric,
field: action.payload.field,
};
/*
TODO: The previous version of the query editor was keeping some of the old metric's configurations
in the new selected one (such as field or some settings).
It the future would be nice to have the same behavior but it's hard without a proper definition,
as Elasticsearch will error sometimes if some settings are not compatible.
*/
return {
id: metric.id,
type: action.payload.type,
...metricAggregationConfig[action.payload.type].defaults,
} as MetricAggregation;
if (isPipelineAggregation(metric)) {
return { ...newMetric, pipelineAgg: action.payload.field };
}
return newMetric;
});
}
if (toggleMetricVisibility.match(action)) {
return state!.map((metric) => {
if (metric.id !== action.payload) {
return metric;
}
return {
...metric,
hide: !metric.hide,
};
});
}
if (changeMetricSetting.match(action)) {
return state!.map((metric) => {
if (metric.id !== action.payload.metric.id) {
return metric;
}
// TODO: Here, instead of this if statement, we should assert that metric is MetricAggregationWithSettings
if (isMetricAggregationWithSettings(metric)) {
const newSettings = removeEmpty({
...metric.settings,
[action.payload.settingName]: action.payload.newValue,
});
case CHANGE_METRIC_FIELD:
return state!.map((metric) => {
if (metric.id !== action.payload.id) {
return metric;
}
const newMetric = {
...metric,
field: action.payload.field,
};
if (isPipelineAggregation(metric)) {
return { ...newMetric, pipelineAgg: action.payload.field };
}
return newMetric;
});
case TOGGLE_METRIC_VISIBILITY:
return state!.map((metric) => {
if (metric.id !== action.payload.id) {
return metric;
}
return {
...metric,
hide: !metric.hide,
settings: {
...newSettings,
},
};
});
case CHANGE_METRIC_SETTING:
return state!.map((metric) => {
if (metric.id !== action.payload.metric.id) {
return metric;
}
// TODO: Here, instead of this if statement, we should assert that metric is MetricAggregationWithSettings
if (isMetricAggregationWithSettings(metric)) {
const newSettings = removeEmpty({
...metric.settings,
[action.payload.settingName]: action.payload.newValue,
});
return {
...metric,
settings: {
...newSettings,
},
};
}
// This should never happen.
return metric;
});
case CHANGE_METRIC_META:
return state!.map((metric) => {
if (metric.id !== action.payload.metric.id) {
return metric;
}
// TODO: Here, instead of this if statement, we should assert that metric is MetricAggregationWithMeta
if (isMetricAggregationWithMeta(metric)) {
return {
...metric,
meta: {
...metric.meta,
[action.payload.meta]: action.payload.newValue,
},
};
}
// This should never happen.
return metric;
});
case CHANGE_METRIC_ATTRIBUTE:
return state!.map((metric) => {
if (metric.id !== action.payload.metric.id) {
return metric;
}
return {
...metric,
[action.payload.attribute]: action.payload.newValue,
};
});
case INIT:
if (state?.length || 0 > 0) {
return state;
}
return [defaultMetricAgg('1')];
default:
return state;
// This should never happen.
return metric;
});
}
if (changeMetricMeta.match(action)) {
return state!.map((metric) => {
if (metric.id !== action.payload.metric.id) {
return metric;
}
// TODO: Here, instead of this if statement, we should assert that metric is MetricAggregationWithMeta
if (isMetricAggregationWithMeta(metric)) {
return {
...metric,
meta: {
...metric.meta,
[action.payload.meta]: action.payload.newValue,
},
};
}
// This should never happen.
return metric;
});
}
if (changeMetricAttribute.match(action)) {
return state!.map((metric) => {
if (metric.id !== action.payload.metric.id) {
return metric;
}
return {
...metric,
[action.payload.attribute]: action.payload.newValue,
};
});
}
if (initQuery.match(action)) {
if (state?.length || 0 > 0) {
return state;
}
return [defaultMetricAgg('1')];
}
return state;
};

View File

@ -1,89 +0,0 @@
import { Action } from '../../../../hooks/useStatelessReducer';
import { SettingKeyOf } from '../../../types';
import {
MetricAggregation,
MetricAggregationWithMeta,
MetricAggregationWithSettings,
MetricAggregationWithField,
} from '../aggregations';
export const ADD_METRIC = '@metrics/add';
export const REMOVE_METRIC = '@metrics/remove';
export const CHANGE_METRIC_TYPE = '@metrics/change_type';
export const CHANGE_METRIC_FIELD = '@metrics/change_field';
export const CHANGE_METRIC_SETTING = '@metrics/change_setting';
export const CHANGE_METRIC_META = '@metrics/change_meta';
export const CHANGE_METRIC_ATTRIBUTE = '@metrics/change_attr';
export const TOGGLE_METRIC_VISIBILITY = '@metrics/toggle_visibility';
export interface AddMetricAction extends Action<typeof ADD_METRIC> {
payload: {
id: MetricAggregation['id'];
};
}
export interface RemoveMetricAction extends Action<typeof REMOVE_METRIC> {
payload: {
id: MetricAggregation['id'];
};
}
export interface ChangeMetricTypeAction extends Action<typeof CHANGE_METRIC_TYPE> {
payload: {
id: MetricAggregation['id'];
type: MetricAggregation['type'];
};
}
export interface ChangeMetricFieldAction extends Action<typeof CHANGE_METRIC_FIELD> {
payload: {
id: MetricAggregation['id'];
field: MetricAggregationWithField['field'];
};
}
export interface ToggleMetricVisibilityAction extends Action<typeof TOGGLE_METRIC_VISIBILITY> {
payload: {
id: MetricAggregation['id'];
};
}
export interface ChangeMetricSettingAction<T extends MetricAggregationWithSettings>
extends Action<typeof CHANGE_METRIC_SETTING> {
payload: {
metric: T;
settingName: SettingKeyOf<T>;
newValue: unknown;
};
}
export interface ChangeMetricMetaAction<T extends MetricAggregationWithMeta> extends Action<typeof CHANGE_METRIC_META> {
payload: {
metric: T;
meta: Extract<keyof Required<T>['meta'], string>;
newValue: string | number | boolean;
};
}
export interface ChangeMetricAttributeAction<
T extends MetricAggregation,
K extends Extract<keyof T, string> = Extract<keyof T, string>
> extends Action<typeof CHANGE_METRIC_ATTRIBUTE> {
payload: {
metric: T;
attribute: K;
newValue: T[K];
};
}
type CommonActions =
| AddMetricAction
| RemoveMetricAction
| ChangeMetricTypeAction
| ChangeMetricFieldAction
| ToggleMetricVisibilityAction;
export type MetricAggregationAction<T extends MetricAggregation = MetricAggregation> =
| (T extends MetricAggregationWithSettings ? ChangeMetricSettingAction<T> : never)
| (T extends MetricAggregationWithMeta ? ChangeMetricMetaAction<T> : never)
| ChangeMetricAttributeAction<T>
| CommonActions;

View File

@ -1,69 +1,36 @@
import { Action } from '../../hooks/useStatelessReducer';
import { Action, createAction } from '@reduxjs/toolkit';
import { ElasticsearchQuery } from '../../types';
export const INIT = 'init';
const CHANGE_QUERY = 'change_query';
const CHANGE_ALIAS_PATTERN = 'change_alias_pattern';
export interface InitAction extends Action<typeof INIT> {}
interface ChangeQueryAction extends Action<typeof CHANGE_QUERY> {
payload: {
query: string;
};
}
interface ChangeAliasPatternAction extends Action<typeof CHANGE_ALIAS_PATTERN> {
payload: {
aliasPattern: string;
};
}
/**
* When the `initQuery` Action is dispatched, the query gets populated with default values where values are not present.
* This means it won't override any existing value in place, but just ensure the query is in a "runnable" state.
*/
export const initQuery = (): InitAction => ({ type: INIT });
export const initQuery = createAction('init');
export const changeQuery = (query: string): ChangeQueryAction => ({
type: CHANGE_QUERY,
payload: {
query,
},
});
export const changeQuery = createAction<ElasticsearchQuery['query']>('change_query');
export const changeAliasPattern = (aliasPattern: string): ChangeAliasPatternAction => ({
type: CHANGE_ALIAS_PATTERN,
payload: {
aliasPattern,
},
});
export const changeAliasPattern = createAction<ElasticsearchQuery['alias']>('change_alias_pattern');
export const queryReducer = (prevQuery: ElasticsearchQuery['query'], action: ChangeQueryAction | InitAction) => {
switch (action.type) {
case CHANGE_QUERY:
return action.payload.query;
case INIT:
return prevQuery || '';
default:
return prevQuery;
export const queryReducer = (prevQuery: ElasticsearchQuery['query'], action: Action) => {
if (changeQuery.match(action)) {
return action.payload;
}
if (initQuery.match(action)) {
return prevQuery || '';
}
return prevQuery;
};
export const aliasPatternReducer = (
prevAliasPattern: ElasticsearchQuery['alias'],
action: ChangeAliasPatternAction | InitAction
) => {
switch (action.type) {
case CHANGE_ALIAS_PATTERN:
return action.payload.aliasPattern;
case INIT:
return prevAliasPattern || '';
default:
return prevAliasPattern;
export const aliasPatternReducer = (prevAliasPattern: ElasticsearchQuery['alias'], action: Action) => {
if (changeAliasPattern.match(action)) {
return action.payload;
}
if (initQuery.match(action)) {
return prevAliasPattern || '';
}
return prevAliasPattern;
};

View File

@ -1,10 +1,7 @@
import { createContext, useCallback, useContext } from 'react';
import { Action } from '@reduxjs/toolkit';
export interface Action<T extends string = string> {
type: T;
}
export type Reducer<S, A extends Action = Action> = (state: S, action: A) => S;
export type Reducer<S, A extends Action> = (state: S, action: A) => S;
export const combineReducers = <S, A extends Action = Action>(reducers: { [P in keyof S]: Reducer<S[P], A> }) => (
state: S,

View File

@ -3,7 +3,7 @@ set -e
echo -e "Collecting code stats (typescript errors & more)"
ERROR_COUNT_LIMIT=73
ERROR_COUNT_LIMIT=61
ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --strict true | grep -oP 'Found \K(\d+)')"
if [ "$ERROR_COUNT" -gt $ERROR_COUNT_LIMIT ]; then