diff --git a/bitmovin-analytics-datasource/src/components/FilterInput.tsx b/bitmovin-analytics-datasource/src/components/FilterInput.tsx index c7eec8d..dc34f7b 100644 --- a/bitmovin-analytics-datasource/src/components/FilterInput.tsx +++ b/bitmovin-analytics-datasource/src/components/FilterInput.tsx @@ -6,7 +6,7 @@ import { isEmpty } from 'lodash'; import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes'; import { QueryAdAttribute, SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; import { QueryFilterOperator, SELECTABLE_QUERY_FILTER_OPERATORS } from '../types/queryFilter'; -import type { FilterRowData } from './QueryEditor'; +import { FilterRowData } from './FilterRow'; const mapAttributeToSelectableValue = ( attribute: QueryAttribute | QueryAdAttribute, @@ -31,22 +31,25 @@ type Props = { readonly onOperatorChange: (newValue: SelectableValue) => void; readonly onValueChange: (newValue: string) => void; readonly onDelete: () => void; - readonly addFilterDisabled: boolean; - readonly onAddFilter: () => void; + readonly onSaveFilter: () => void; }; export function FilterInput(props: Props) { return ( props.onOperatorChange(value)} + value={props.filter.operator ? mapOperatorToSelectableOperator(props.filter.operator) : undefined} + onChange={(selectableValue) => props.onOperatorChange(selectableValue)} options={SELECTABLE_QUERY_FILTER_OPERATORS} width={15} /> @@ -59,17 +62,17 @@ export function FilterInput(props: Props) { value={props.filter.rawFilterValue} invalid={!isEmpty(props.filter.parsingValueError)} type="text" - onChange={(value) => props.onValueChange(value.currentTarget.value)} + onChange={(input) => props.onValueChange(input.currentTarget.value)} width={30} /> diff --git a/bitmovin-analytics-datasource/src/components/FilterRow.tsx b/bitmovin-analytics-datasource/src/components/FilterRow.tsx index da6cfa5..fc337cf 100644 --- a/bitmovin-analytics-datasource/src/components/FilterRow.tsx +++ b/bitmovin-analytics-datasource/src/components/FilterRow.tsx @@ -1,14 +1,21 @@ -import React from 'react'; -import { differenceWith, isEmpty } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { differenceWith } from 'lodash'; import { SelectableValue } from '@grafana/data'; import { Box, HorizontalGroup, IconButton, InlineLabel, VerticalGroup } from '@grafana/ui'; -import { QueryFilter, QueryFilterOperator } from '../types/queryFilter'; +import { QueryFilter, QueryFilterOperator, QueryFilterValue } from '../types/queryFilter'; import { QueryAdAttribute, SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes'; import { FilterInput } from './FilterInput'; -import { convertFilterValueToProperType } from '../utils/filterUtils'; -import type { FilterRowData } from './QueryEditor'; +import { convertFilterValueToProperType, mapQueryFilterValueToRawFilterValue } from '../utils/filterUtils'; + +export type FilterRowData = { + attribute: QueryAdAttribute | QueryAttribute | undefined; + operator: QueryFilterOperator | undefined; + rawFilterValue: string; + convertedFilterValue: QueryFilterValue; + parsingValueError: string; +}; const mapFilterAttributesToSelectableValue = ( filters: FilterRowData[], @@ -32,104 +39,119 @@ const mapFilterAttributesToSelectableValue = ( const mapFilterRowsToQueryFilters = (filters: FilterRowData[]): QueryFilter[] => { return filters.map((filter) => { return { - name: filter.attribute, - operator: filter.operator, + name: filter.attribute!, + operator: filter.operator!, value: filter.convertedFilterValue, - } as QueryFilter; + }; }); }; type Props = { readonly isAdAnalytics: boolean; readonly onQueryFilterChange: (newFilters: QueryFilter[]) => void; - readonly onFilterRowChange: (newFilters: FilterRowData[]) => void; - readonly filters: FilterRowData[]; + readonly filters: QueryFilter[]; }; export function FilterRow(props: Props) { + const [filterInputs, setFilterInputs] = useState([]); + + /** Map QueryFilters to FilterRowData */ + useEffect(() => { + const filterRows = props.filters.map((filter) => { + return { + attribute: filter.name, + operator: filter.operator, + rawFilterValue: mapQueryFilterValueToRawFilterValue(filter.value), + convertedFilterValue: filter.value, + parsingValueError: '', + }; + }); + setFilterInputs(filterRows); + }, [props.filters]); + const addFilterInput = () => { - const newFilters = [...props.filters]; - newFilters.push({ - attribute: {}, - operator: {}, + const newFilterInputs = [...filterInputs]; + newFilterInputs.push({ + attribute: undefined, + operator: undefined, rawFilterValue: '', convertedFilterValue: '', parsingValueError: '', - } as FilterRowData); - props.onFilterRowChange(newFilters); + }); + setFilterInputs(newFilterInputs); }; - const onAddFilter = (index: number) => { - const filter = props.filters[index]; + const onSaveFilter = (index: number) => { + const filter = filterInputs[index]; try { const convertedValue = convertFilterValueToProperType( filter.rawFilterValue, - filter.attribute, - filter.attribute, - filter.operator, + filter.attribute!, + filter.attribute!, + filter.operator!, props.isAdAnalytics ); - const newFilter = { ...filter, convertedFilterValue: convertedValue, parsingValueError: '' } as FilterRowData; + const newFilter: FilterRowData = { ...filter, convertedFilterValue: convertedValue, parsingValueError: '' }; - const newFilters = [...props.filters]; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - props.onFilterRowChange(newFilters); + setFilterInputs(filterInputs); props.onQueryFilterChange(mapFilterRowsToQueryFilters(newFilters)); } catch (e: unknown) { if (e instanceof Error) { const errorMessage = e.message; - const newFilter = { ...filter, parsingValueError: errorMessage } as FilterRowData; + const newFilter: FilterRowData = { ...filter, parsingValueError: errorMessage }; - const newFilters = [...props.filters]; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - props.onFilterRowChange(newFilters); + setFilterInputs(newFilters); } } }; const deleteFilterInput = (index: number) => { - const newFilters = [...props.filters]; + const newFilters = [...filterInputs]; newFilters.splice(index, 1); - props.onFilterRowChange(newFilters); + setFilterInputs(newFilters); props.onQueryFilterChange(mapFilterRowsToQueryFilters(newFilters)); }; const onAttributesChange = (index: number, newAttribute: SelectableValue) => { - const filter = props.filters[index]; - const newFilter = { ...filter, attribute: newAttribute.value } as FilterRowData; - const newFilters = [...props.filters]; + const filter = filterInputs[index]; + const newFilter: FilterRowData = { ...filter, attribute: newAttribute.value! }; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - props.onFilterRowChange(newFilters); + setFilterInputs(newFilters); }; const onOperatorsChange = (index: number, newOperator: SelectableValue) => { - const filter = props.filters[index]; - const newFilter = { ...filter, operator: newOperator.value } as FilterRowData; - const newFilters = [...props.filters]; + const filter = filterInputs[index]; + const newFilter: FilterRowData = { ...filter, operator: newOperator.value! }; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - props.onFilterRowChange(newFilters); + setFilterInputs(newFilters); }; const onValuesChange = (index: number, newValue: string) => { - const filter = props.filters[index]; - const newFilter = { ...filter, rawFilterValue: newValue }; - const newFilters = [...props.filters]; + const filter = filterInputs[index]; + const newFilter: FilterRowData = { ...filter, rawFilterValue: newValue }; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - props.onFilterRowChange(newFilters); + setFilterInputs(newFilters); }; return ( - {props.filters.length !== 0 && ( + {filterInputs.length > 0 && ( Dimension @@ -142,7 +164,7 @@ export function FilterRow(props: Props) { )} - {props.filters.map((filter, index, filtersArray) => ( + {filterInputs.map((filter, index, filtersArray) => ( ) => onOperatorsChange(index, newValue)} onValueChange={(newValue: string) => onValuesChange(index, newValue)} onDelete={() => deleteFilterInput(index)} - addFilterDisabled={isEmpty(filter.attribute) || isEmpty(filter.operator)} - onAddFilter={() => onAddFilter(index)} + onSaveFilter={() => onSaveFilter(index)} /> ))} - + addFilterInput()} size="xl" /> diff --git a/bitmovin-analytics-datasource/src/components/GroupByInput.tsx b/bitmovin-analytics-datasource/src/components/GroupByInput.tsx index 8d2670d..be3e2ef 100644 --- a/bitmovin-analytics-datasource/src/components/GroupByInput.tsx +++ b/bitmovin-analytics-datasource/src/components/GroupByInput.tsx @@ -26,7 +26,7 @@ export function GroupByInput(props: Props) { props.onAttributeChange(value)} + onChange={(selectableValue) => props.onAttributeChange(selectableValue)} options={props.selectableOrderByAttributes} width={30} /> diff --git a/bitmovin-analytics-datasource/src/components/OrderByRow.tsx b/bitmovin-analytics-datasource/src/components/OrderByRow.tsx index 9708168..6e6404d 100644 --- a/bitmovin-analytics-datasource/src/components/OrderByRow.tsx +++ b/bitmovin-analytics-datasource/src/components/OrderByRow.tsx @@ -55,7 +55,7 @@ export function OrderByRow(props: Props) { const onAttributesChange = (index: number, newAttribute: SelectableValue) => { const newOrderBys = [...props.orderBys]; - const newOrderBy = { name: newAttribute.value, order: newOrderBys[index].order } as QueryOrderBy; + const newOrderBy: QueryOrderBy = { name: newAttribute.value!, order: newOrderBys[index].order }; newOrderBys.splice(index, 1, newOrderBy); @@ -64,7 +64,7 @@ export function OrderByRow(props: Props) { const onSortOrdersChange = (index: number, newSortOrder: QuerySortOrder) => { const newOrderBys = [...props.orderBys]; - const newOrderBy = { name: newOrderBys[index].name, order: newSortOrder } as QueryOrderBy; + const newOrderBy: QueryOrderBy = { name: newOrderBys[index].name, order: newSortOrder }; newOrderBys.splice(index, 1, newOrderBy); diff --git a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx index 5702e8f..a208f84 100644 --- a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx +++ b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx @@ -14,9 +14,8 @@ import { isMetric, SELECTABLE_METRICS } from '../types/metric'; import { GroupByRow } from './GroupByRow'; import { OrderByRow } from './OrderByRow'; import type { QueryOrderBy } from '../types/queryOrderBy'; -import type { QueryFilter, QueryFilterOperator, QueryFilterValue } from '../types/queryFilter'; +import type { QueryFilter } from '../types/queryFilter'; import { FilterRow } from './FilterRow'; -import { mapFilterValueToRawFilterValue } from '../utils/filterUtils'; enum LoadingState { Default = 'DEFAULT', @@ -25,14 +24,6 @@ enum LoadingState { Error = 'ERROR', } -export type FilterRowData = { - attribute: QueryAdAttribute | QueryAttribute; - operator: QueryFilterOperator; - rawFilterValue: string; - convertedFilterValue: QueryFilterValue; - parsingValueError: string; -}; - type Props = QueryEditorProps; export function QueryEditor(props: Props) { @@ -40,11 +31,11 @@ export function QueryEditor(props: Props) { const [licenseLoadingState, setLicenseLoadingState] = useState(LoadingState.Default); const [licenseErrorMessage, setLicenseErrorMessage] = useState(''); const [isTimeSeries, setIsTimeSeries] = useState(!!props.query.interval); - const [filterRows, setFilterRows] = useState([]); const isDimensionMetricSelected = useMemo(() => { return props.query.metric !== undefined; }, [props.query.metric]); + /** Fetch Licenses */ useEffect(() => { setLicenseLoadingState(LoadingState.Loading); fetchLicenses(props.datasource.apiKey, props.datasource.baseUrl) @@ -58,19 +49,6 @@ export function QueryEditor(props: Props) { }); }, [props.datasource.apiKey, props.datasource.baseUrl]); - useEffect(() => { - const filterRows = props.query.filters.map((filter) => { - return { - attribute: filter.name, - operator: filter.operator, - rawFilterValue: mapFilterValueToRawFilterValue(filter.value), - convertedFilterValue: filter.value, - parsingValueError: '', - } as FilterRowData; - }); - setFilterRows(filterRows); - }, [props.query.filters]); - const query = defaults(props.query, DEFAULT_QUERY); const handleLicenseChange = (item: SelectableValue) => { @@ -102,10 +80,6 @@ export function QueryEditor(props: Props) { props.onRunQuery(); }; - const handleFilterRowChange = (newFilters: FilterRowData[]) => { - setFilterRows(newFilters); - }; - const handleQueryFilterChange = (newFilters: QueryFilter[]) => { props.onChange({ ...query, filters: newFilters }); props.onRunQuery(); @@ -199,8 +173,7 @@ export function QueryEditor(props: Props) { diff --git a/bitmovin-analytics-datasource/src/types/aggregations.ts b/bitmovin-analytics-datasource/src/types/aggregations.ts index 8c26f9f..4f98f5e 100644 --- a/bitmovin-analytics-datasource/src/types/aggregations.ts +++ b/bitmovin-analytics-datasource/src/types/aggregations.ts @@ -1,4 +1,4 @@ -import { SelectableValue } from '@grafana/data'; +import type { SelectableValue } from '@grafana/data'; export type Aggregation = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'stddev' | 'percentile' | 'variance' | 'median'; diff --git a/bitmovin-analytics-datasource/src/utils/filterUtils.test.ts b/bitmovin-analytics-datasource/src/utils/filterUtils.test.ts index bafe87c..cfaaad7 100644 --- a/bitmovin-analytics-datasource/src/utils/filterUtils.test.ts +++ b/bitmovin-analytics-datasource/src/utils/filterUtils.test.ts @@ -1,4 +1,4 @@ -import { convertFilterValueToProperType } from './filterUtils'; +import { convertFilterValueToProperType, mapQueryFilterValueToRawFilterValue } from './filterUtils'; import { QUERY_ATTRIBUTES } from '../types/queryAttributes'; import { QUERY_FILTER_OPERATORS } from '../types/queryFilter'; import { QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; @@ -177,3 +177,53 @@ describe('convertFilterValueToProperType', () => { ).toThrow(new Error(`Couldn't parse filter for Error Percentage, please provide data as a number`)); }); }); + +describe('mapQueryFilterValueToRawFilterValue', () => { + it('should return empty string for null filter value', () => { + //arrange && act + const result = mapQueryFilterValueToRawFilterValue(null); + + //assert + expect(result).toEqual(''); + }); + + it('should return boolean as string', () => { + //arrange && act + const result = mapQueryFilterValueToRawFilterValue(true); + + //assert + expect(result).toEqual('true'); + }); + + it('should return integer as string', () => { + //arrange && act + const result = mapQueryFilterValueToRawFilterValue(23); + + //assert + expect(result).toEqual('23'); + }); + + it('should return float as string', () => { + //arrange && act + const result = mapQueryFilterValueToRawFilterValue(23.5); + + //assert + expect(result).toEqual('23.5'); + }); + + it('should return string as string', () => { + //arrange && act + const result = mapQueryFilterValueToRawFilterValue('de'); + + //assert + expect(result).toEqual('de'); + }); + + it('should return string array as string', () => { + //arrange && act + const result = mapQueryFilterValueToRawFilterValue(['Firefox', 'Opera']); + + //assert + expect(result).toEqual('["Firefox","Opera"]'); + }); +}); diff --git a/bitmovin-analytics-datasource/src/utils/filterUtils.ts b/bitmovin-analytics-datasource/src/utils/filterUtils.ts index ca3b9ed..3c20fc0 100644 --- a/bitmovin-analytics-datasource/src/utils/filterUtils.ts +++ b/bitmovin-analytics-datasource/src/utils/filterUtils.ts @@ -4,7 +4,12 @@ import { QUERY_AD_ATTRIBUTES, QueryAdAttribute } from '../types/queryAdAttribute import { QUERY_FILTER_OPERATORS, QueryFilterOperator, QueryFilterValue } from '../types/queryFilter'; import { QUERY_ATTRIBUTES, QueryAttribute } from '../types/queryAttributes'; -export const mapFilterValueToRawFilterValue = (filterValue: QueryFilterValue) => { +/** + * Convert QueryFilter value to string to correctly display the value in the Input Element. + * @param {QueryFilterValue} filterValue the filter value with the from our API expected correct type + * @return {string} the rawFilterValue as a string + * */ +export const mapQueryFilterValueToRawFilterValue = (filterValue: QueryFilterValue): string => { if (filterValue == null) { return ''; } else if (Array.isArray(filterValue)) {