From e63d6dccb4489020b9cf5051336f6b29bf21c0a0 Mon Sep 17 00:00:00 2001 From: Myriam Gantner <48029745+MGJamJam@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:53:56 +0200 Subject: [PATCH] Feat/AN-4135 pass query parameters to components (#74) * make GroupByRow controlled * make OrderByRow controlled * make FilterByRow controlled * make simple selection controlled * fix groupBy types * fix interval default value bug * rename filterRow prop * delete TODOs * split useEffect and useMemo for isMetricSelected * lint * refactor FilterRow and QueryEditor to hold UI related Row data as local state * implement PR feedback * add unit tests for mapQueryFilterValueToRawFilterValue --- .../src/components/FilterInput.tsx | 51 +++++-- .../src/components/FilterRow.tsx | 138 ++++++++++-------- .../src/components/GroupByInput.tsx | 4 +- .../src/components/GroupByRow.tsx | 77 +++++----- .../src/components/OrderByInput.tsx | 2 +- .../src/components/OrderByRow.tsx | 122 +++++++--------- .../src/components/QueryEditor.tsx | 54 ++++--- .../src/datasource.ts | 2 +- .../src/types/aggregations.ts | 4 +- .../src/types/grafanaTypes.ts | 3 +- .../src/utils/filterUtils.test.ts | 52 ++++++- .../src/utils/filterUtils.ts | 15 ++ 12 files changed, 324 insertions(+), 200 deletions(-) diff --git a/bitmovin-analytics-datasource/src/components/FilterInput.tsx b/bitmovin-analytics-datasource/src/components/FilterInput.tsx index ba4a747..dc34f7b 100644 --- a/bitmovin-analytics-datasource/src/components/FilterInput.tsx +++ b/bitmovin-analytics-datasource/src/components/FilterInput.tsx @@ -1,55 +1,78 @@ import React from 'react'; -import { SelectableValue } from '@grafana/data'; +import type { SelectableValue } from '@grafana/data'; import { HorizontalGroup, IconButton, Input, Select, Tooltip } from '@grafana/ui'; +import { isEmpty } from 'lodash'; -import { QueryAttribute } from '../types/queryAttributes'; -import { QueryAdAttribute } from '../types/queryAdAttributes'; +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 { FilterRowData } from './FilterRow'; + +const mapAttributeToSelectableValue = ( + attribute: QueryAttribute | QueryAdAttribute, + isAdAnalytics: boolean +): SelectableValue => { + if (isAdAnalytics) { + return SELECTABLE_QUERY_AD_ATTRIBUTES.filter((selectableValue) => selectableValue.value === attribute); + } else { + return SELECTABLE_QUERY_ATTRIBUTES.filter((selectableValue) => selectableValue.value === attribute); + } +}; + +const mapOperatorToSelectableOperator = (operator: QueryFilterOperator): SelectableValue => { + return SELECTABLE_QUERY_FILTER_OPERATORS.filter((selectableOperator) => selectableOperator.value === operator); +}; type Props = { readonly isAdAnalytics: boolean; + readonly filter: FilterRowData; readonly selectableFilterAttributes: Array>; readonly onAttributeChange: (newValue: SelectableValue) => void; readonly onOperatorChange: (newValue: SelectableValue) => void; readonly onValueChange: (newValue: string) => void; readonly onDelete: () => void; - readonly addFilterDisabled: boolean; - readonly onAddFilter: () => void; - readonly parsingValueError: string | undefined; + 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} /> 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 0344fec..fc337cf 100644 --- a/bitmovin-analytics-datasource/src/components/FilterRow.tsx +++ b/bitmovin-analytics-datasource/src/components/FilterRow.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { difference, 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'; @@ -7,130 +7,151 @@ import { QueryFilter, QueryFilterOperator, QueryFilterValue } from '../types/que 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 { convertFilterValueToProperType, mapQueryFilterValueToRawFilterValue } from '../utils/filterUtils'; -type Filter = { - selectedAttribute: SelectableValue; - selectedOperator: SelectableValue; +export type FilterRowData = { + attribute: QueryAdAttribute | QueryAttribute | undefined; + operator: QueryFilterOperator | undefined; rawFilterValue: string; convertedFilterValue: QueryFilterValue; parsingValueError: string; }; const mapFilterAttributesToSelectableValue = ( - filters: Filter[], + filters: FilterRowData[], isAdAnalytics: boolean ): Array> => { - const selectedAttributes = filters.map((filter) => filter.selectedAttribute); if (isAdAnalytics) { - return difference(SELECTABLE_QUERY_AD_ATTRIBUTES, selectedAttributes); + return differenceWith( + SELECTABLE_QUERY_AD_ATTRIBUTES, + filters, + (selectableValue, selectedValue) => selectableValue.value === selectedValue.attribute + ); } else { - return difference(SELECTABLE_QUERY_ATTRIBUTES, selectedAttributes); + return differenceWith( + SELECTABLE_QUERY_ATTRIBUTES, + filters, + (selectableValue, selectedValue) => selectableValue.value === selectedValue.attribute + ); } }; -const mapFiltersToQueryFilters = (filters: Filter[]): QueryFilter[] => { +const mapFilterRowsToQueryFilters = (filters: FilterRowData[]): QueryFilter[] => { return filters.map((filter) => { return { - name: filter.selectedAttribute.value!, - operator: filter.selectedOperator.value!, + name: filter.attribute!, + operator: filter.operator!, value: filter.convertedFilterValue, - } as QueryFilter; + }; }); }; type Props = { readonly isAdAnalytics: boolean; - readonly onChange: (newFilters: QueryFilter[]) => void; + readonly onQueryFilterChange: (newFilters: QueryFilter[]) => void; + readonly filters: QueryFilter[]; }; export function FilterRow(props: Props) { - const [filters, setFilters] = useState([]); + 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 = () => { - setFilters((prevState) => [ - ...prevState, - { - selectedAttribute: {}, - selectedOperator: {}, - rawFilterValue: '', - convertedFilterValue: '', - parsingValueError: '', - } as Filter, - ]); + const newFilterInputs = [...filterInputs]; + newFilterInputs.push({ + attribute: undefined, + operator: undefined, + rawFilterValue: '', + convertedFilterValue: '', + parsingValueError: '', + }); + setFilterInputs(newFilterInputs); }; - const onAddFilter = (index: number) => { - const filter = filters[index]; + const onSaveFilter = (index: number) => { + const filter = filterInputs[index]; try { const convertedValue = convertFilterValueToProperType( filter.rawFilterValue, - filter.selectedAttribute.value!, - filter.selectedAttribute.label!, - filter.selectedOperator.value!, + filter.attribute!, + filter.attribute!, + filter.operator!, props.isAdAnalytics ); - const newFilter = { ...filter, convertedFilterValue: convertedValue, parsingValueError: '' } as Filter; + const newFilter: FilterRowData = { ...filter, convertedFilterValue: convertedValue, parsingValueError: '' }; - const newFilters = [...filters]; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - setFilters(newFilters); + setFilterInputs(filterInputs); - props.onChange(mapFiltersToQueryFilters(newFilters)); + props.onQueryFilterChange(mapFilterRowsToQueryFilters(newFilters)); } catch (e: unknown) { if (e instanceof Error) { const errorMessage = e.message; - const newFilter = { ...filter, parsingValueError: errorMessage } as Filter; + const newFilter: FilterRowData = { ...filter, parsingValueError: errorMessage }; - const newFilters = [...filters]; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - setFilters(newFilters); + setFilterInputs(newFilters); } } }; const deleteFilterInput = (index: number) => { - const newFilters = [...filters]; + const newFilters = [...filterInputs]; newFilters.splice(index, 1); - setFilters(newFilters); + setFilterInputs(newFilters); - props.onChange(mapFiltersToQueryFilters(newFilters)); + props.onQueryFilterChange(mapFilterRowsToQueryFilters(newFilters)); }; const onAttributesChange = (index: number, newAttribute: SelectableValue) => { - const filter = filters[index]; - const newFilter = { ...filter, selectedAttribute: newAttribute } as Filter; - const newFilters = [...filters]; + const filter = filterInputs[index]; + const newFilter: FilterRowData = { ...filter, attribute: newAttribute.value! }; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - setFilters(newFilters); + setFilterInputs(newFilters); }; const onOperatorsChange = (index: number, newOperator: SelectableValue) => { - const filter = filters[index]; - const newFilter = { ...filter, selectedOperator: newOperator } as Filter; - const newFilters = [...filters]; + const filter = filterInputs[index]; + const newFilter: FilterRowData = { ...filter, operator: newOperator.value! }; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - setFilters(newFilters); + setFilterInputs(newFilters); }; const onValuesChange = (index: number, newValue: string) => { - const filter = filters[index]; - const newFilter = { ...filter, rawFilterValue: newValue }; - const newFilters = [...filters]; + const filter = filterInputs[index]; + const newFilter: FilterRowData = { ...filter, rawFilterValue: newValue }; + const newFilters = [...filterInputs]; newFilters.splice(index, 1, newFilter); - setFilters(newFilters); + setFilterInputs(newFilters); }; return ( - {filters.length !== 0 && ( + {filterInputs.length > 0 && ( Dimension @@ -143,10 +164,11 @@ export function FilterRow(props: Props) { )} - {filters.map((filter, index, filtersArray) => ( + {filterInputs.map((filter, index, filtersArray) => ( ) => onAttributesChange(index, newValue) @@ -154,13 +176,11 @@ export function FilterRow(props: Props) { onOperatorChange={(newValue: SelectableValue) => onOperatorsChange(index, newValue)} onValueChange={(newValue: string) => onValuesChange(index, newValue)} onDelete={() => deleteFilterInput(index)} - addFilterDisabled={isEmpty(filter.selectedAttribute) || isEmpty(filter.selectedOperator)} - onAddFilter={() => onAddFilter(index)} - parsingValueError={isEmpty(filter.parsingValueError) ? undefined : filter.parsingValueError} + 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 2a0e2f7..be3e2ef 100644 --- a/bitmovin-analytics-datasource/src/components/GroupByInput.tsx +++ b/bitmovin-analytics-datasource/src/components/GroupByInput.tsx @@ -15,7 +15,7 @@ type Props = { readonly groupBy: SelectableValue; readonly selectableGroupBys: Array>; readonly onDelete: () => void; - readonly onChange: (newValue: SelectableValue) => void; + readonly onChange: (newValue: QueryAdAttribute | QueryAttribute) => void; readonly isFirst: boolean; readonly isLast: boolean; readonly onReorderGroupBy: (direction: REORDER_DIRECTION) => void; @@ -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 e4b19be..6e6404d 100644 --- a/bitmovin-analytics-datasource/src/components/OrderByRow.tsx +++ b/bitmovin-analytics-datasource/src/components/OrderByRow.tsx @@ -1,122 +1,112 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Box, IconButton, VerticalGroup } from '@grafana/ui'; -import { SelectableValue } from '@grafana/data'; -import { difference } from 'lodash'; +import type { SelectableValue } from '@grafana/data'; +import { differenceWith } from 'lodash'; import { QueryAdAttribute, SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes'; -import { QueryOrderBy, QuerySortOrder } from '../types/queryOrderBy'; +import type { QueryOrderBy, QuerySortOrder } from '../types/queryOrderBy'; import { OrderByInput } from './OrderByInput'; import { REORDER_DIRECTION } from './GroupByInput'; -const mapOrderBysToSelectableValue = ( - selectedAttributes: Array>, +const getSelectableOrderByOptions = ( + selectedOrderBys: QueryOrderBy[], isAdAnalytics: boolean -): Array> => { +): Array> => { if (isAdAnalytics) { - return difference(SELECTABLE_QUERY_AD_ATTRIBUTES, selectedAttributes); + return differenceWith( + SELECTABLE_QUERY_AD_ATTRIBUTES, + selectedOrderBys, + (selectableValue, selectedValue) => selectableValue.value === selectedValue.name + ); } else { - return difference(SELECTABLE_QUERY_ATTRIBUTES, selectedAttributes); + return differenceWith( + SELECTABLE_QUERY_ATTRIBUTES, + selectedOrderBys, + (selectableValue, selectedValue) => selectableValue.value === selectedValue.name + ); + } +}; + +const mapOrderByAttributeToSelectableValue = ( + selectedOrderBy: QueryAttribute | QueryAdAttribute, + isAdAnalytics: boolean +): SelectableValue => { + if (isAdAnalytics) { + return SELECTABLE_QUERY_AD_ATTRIBUTES.filter((selectableValue) => selectableValue.value === selectedOrderBy); + } else { + return SELECTABLE_QUERY_ATTRIBUTES.filter((selectableValue) => selectableValue.value === selectedOrderBy); } }; type Props = { readonly isAdAnalytics: boolean; readonly onChange: (newOrderBy: QueryOrderBy[]) => void; + readonly orderBys: QueryOrderBy[]; }; export function OrderByRow(props: Props) { - const [selectedAttributes, setSelectedAttributes] = useState< - Array> - >([]); - const [selectedSortOrders, setSelectedSortOrders] = useState([]); - - const mapSelectedValuesToQueryOrderBy = ( - selectedAttributes: Array>, - selectedSortOrders: QuerySortOrder[] - ): QueryOrderBy[] => { - const queryOrderBys: QueryOrderBy[] = []; - for (let i = 0; i < selectedAttributes.length; i++) { - queryOrderBys.push({ - name: selectedAttributes[i].value!, - order: selectedSortOrders[i], - }); - } - return queryOrderBys; - }; - const deleteOrderByInput = (index: number) => { - const newSelectedAttributes = [...selectedAttributes]; - newSelectedAttributes.splice(index, 1); + const newOrderBys = [...props.orderBys]; + newOrderBys.splice(index, 1); - const newSelectedSortOrders = [...selectedSortOrders]; - newSelectedSortOrders.splice(index, 1); - - setSelectedAttributes(newSelectedAttributes); - setSelectedSortOrders(newSelectedSortOrders); - props.onChange(mapSelectedValuesToQueryOrderBy(newSelectedAttributes, newSelectedSortOrders)); + props.onChange(newOrderBys); }; const onAttributesChange = (index: number, newAttribute: SelectableValue) => { - const newSelectedAttributes = [...selectedAttributes]; - newSelectedAttributes.splice(index, 1, newAttribute); - setSelectedAttributes(newSelectedAttributes); + const newOrderBys = [...props.orderBys]; + const newOrderBy: QueryOrderBy = { name: newAttribute.value!, order: newOrderBys[index].order }; + + newOrderBys.splice(index, 1, newOrderBy); - props.onChange(mapSelectedValuesToQueryOrderBy(newSelectedAttributes, selectedSortOrders)); + props.onChange(newOrderBys); }; const onSortOrdersChange = (index: number, newSortOrder: QuerySortOrder) => { - const newSelectedSortOrders = [...selectedSortOrders]; - newSelectedSortOrders.splice(index, 1, newSortOrder); - setSelectedSortOrders(newSelectedSortOrders); + const newOrderBys = [...props.orderBys]; + const newOrderBy: QueryOrderBy = { name: newOrderBys[index].name, order: newSortOrder }; - props.onChange(mapSelectedValuesToQueryOrderBy(selectedAttributes, newSelectedSortOrders)); - }; + newOrderBys.splice(index, 1, newOrderBy); + props.onChange(newOrderBys); + }; const reorderOrderBy = (direction: REORDER_DIRECTION, index: number) => { const newIndex = direction === REORDER_DIRECTION.UP ? index - 1 : index + 1; - const newSelectedAttributes = [...selectedAttributes]; - const attributeToMove = newSelectedAttributes[index]; - newSelectedAttributes.splice(index, 1); - newSelectedAttributes.splice(newIndex, 0, attributeToMove); + const newOrderBys = [...props.orderBys]; + const orderByToMove = newOrderBys[index]; + newOrderBys.splice(index, 1); + newOrderBys.splice(newIndex, 0, orderByToMove); - const newSelectedSortOrders = [...selectedSortOrders]; - const sortOrderToMove = newSelectedSortOrders[index]; - newSelectedSortOrders.splice(index, 1); - newSelectedSortOrders.splice(newIndex, 0, sortOrderToMove); - - setSelectedAttributes(newSelectedAttributes); - setSelectedSortOrders(newSelectedSortOrders); - - props.onChange(mapSelectedValuesToQueryOrderBy(newSelectedAttributes, newSelectedSortOrders)); + props.onChange(newOrderBys); }; + const addOrderByInput = () => { - setSelectedAttributes((prevState) => [...prevState, {}]); - setSelectedSortOrders((prevState) => [...prevState, 'ASC']); + const newDefaultSelectedValue = getSelectableOrderByOptions(props.orderBys, props.isAdAnalytics)[0].value!; + props.onChange([...props.orderBys, { name: newDefaultSelectedValue, order: 'ASC' }]); }; return ( - {selectedAttributes.map((attribute, index, selectedAttributesArray) => ( + {props.orderBys.map((orderBy, index, selectedOrderBys) => ( ) => onAttributesChange(index, newValue) } - sortOrder={selectedSortOrders[index]} + sortOrder={orderBy.order} onSortOrderChange={(newValue: QuerySortOrder) => onSortOrdersChange(index, newValue)} onDelete={() => deleteOrderByInput(index)} isFirst={index === 0} - isLast={index === selectedAttributesArray.length - 1} + isLast={index === selectedOrderBys.length - 1} onReorderOrderBy={(direction: REORDER_DIRECTION) => reorderOrderBy(direction, index)} /> ))} - + addOrderByInput()} size="xl" /> diff --git a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx index 5630f73..a208f84 100644 --- a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx +++ b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx @@ -1,6 +1,6 @@ -import React, { ChangeEvent, useEffect, useState } from 'react'; +import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'; import { FieldSet, InlineField, InlineSwitch, Input, Select } from '@grafana/ui'; -import { QueryEditorProps, SelectableValue } from '@grafana/data'; +import type { QueryEditorProps, SelectableValue } from '@grafana/data'; import { defaults } from 'lodash'; import { DataSource } from '../datasource'; @@ -13,8 +13,8 @@ import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttri import { isMetric, SELECTABLE_METRICS } from '../types/metric'; import { GroupByRow } from './GroupByRow'; import { OrderByRow } from './OrderByRow'; -import { QueryOrderBy } from '../types/queryOrderBy'; -import { QueryFilter } from '../types/queryFilter'; +import type { QueryOrderBy } from '../types/queryOrderBy'; +import type { QueryFilter } from '../types/queryFilter'; import { FilterRow } from './FilterRow'; enum LoadingState { @@ -30,9 +30,12 @@ export function QueryEditor(props: Props) { const [selectableLicenses, setSelectableLicenses] = useState([]); const [licenseLoadingState, setLicenseLoadingState] = useState(LoadingState.Default); const [licenseErrorMessage, setLicenseErrorMessage] = useState(''); - const [isTimeSeries, setIsTimeSeries] = useState(true); - const [isDimensionMetricSelected, setIsDimensionMetricSelected] = useState(false); + const [isTimeSeries, setIsTimeSeries] = useState(!!props.query.interval); + const isDimensionMetricSelected = useMemo(() => { + return props.query.metric !== undefined; + }, [props.query.metric]); + /** Fetch Licenses */ useEffect(() => { setLicenseLoadingState(LoadingState.Loading); fetchLicenses(props.datasource.apiKey, props.datasource.baseUrl) @@ -60,16 +63,14 @@ export function QueryEditor(props: Props) { const handleDimensionChange = (item: SelectableValue) => { if (isMetric(item.value)) { - setIsDimensionMetricSelected(true); props.onChange({ ...query, aggregation: undefined, dimension: undefined, metric: item.value }); } else { - setIsDimensionMetricSelected(false); - props.onChange({ ...query, dimension: item.value }); + props.onChange({ ...query, dimension: item.value, metric: undefined }); } props.onRunQuery(); }; - const handleGroupByChange = (newGroupBys: QueryAdAttribute[] | QueryAttribute[]) => { + const handleGroupByChange = (newGroupBys: Array) => { props.onChange({ ...query, groupBy: newGroupBys }); props.onRunQuery(); }; @@ -79,7 +80,7 @@ export function QueryEditor(props: Props) { props.onRunQuery(); }; - const handleFilterChange = (newFilters: QueryFilter[]) => { + const handleQueryFilterChange = (newFilters: QueryFilter[]) => { props.onChange({ ...query, filters: newFilters }); props.onRunQuery(); }; @@ -137,6 +138,7 @@ export function QueryEditor(props: Props) { required > handleAggregationChange(item)} width={30} options={SELECTABLE_AGGREGATIONS} /> + - + - + - + - + {isTimeSeries && renderTimeSeriesOption()} - + diff --git a/bitmovin-analytics-datasource/src/datasource.ts b/bitmovin-analytics-datasource/src/datasource.ts index 0d384b2..6a3caac 100644 --- a/bitmovin-analytics-datasource/src/datasource.ts +++ b/bitmovin-analytics-datasource/src/datasource.ts @@ -32,7 +32,7 @@ type BitmovinAnalyticsRequestQuery = { start: Date; end: Date; filters: QueryFilter[]; - groupBy: QueryAttribute[] | QueryAdAttribute[]; + groupBy: Array; orderBy: QueryOrderBy[]; dimension?: QueryAttribute | QueryAdAttribute; metric?: Metric; diff --git a/bitmovin-analytics-datasource/src/types/aggregations.ts b/bitmovin-analytics-datasource/src/types/aggregations.ts index 72072ad..4f98f5e 100644 --- a/bitmovin-analytics-datasource/src/types/aggregations.ts +++ b/bitmovin-analytics-datasource/src/types/aggregations.ts @@ -1,6 +1,8 @@ +import type { SelectableValue } from '@grafana/data'; + export type Aggregation = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'stddev' | 'percentile' | 'variance' | 'median'; -export const SELECTABLE_AGGREGATIONS: Array<{ value: Aggregation; label: string }> = [ +export const SELECTABLE_AGGREGATIONS: Array> = [ { value: 'count', label: 'Count' }, { value: 'sum', label: 'Sum' }, { value: 'avg', label: 'Avg' }, diff --git a/bitmovin-analytics-datasource/src/types/grafanaTypes.ts b/bitmovin-analytics-datasource/src/types/grafanaTypes.ts index e5d3f2b..045ec5d 100644 --- a/bitmovin-analytics-datasource/src/types/grafanaTypes.ts +++ b/bitmovin-analytics-datasource/src/types/grafanaTypes.ts @@ -17,7 +17,7 @@ export interface BitmovinAnalyticsDataQuery extends DataQuery { aggregation?: Aggregation; metric?: Metric; dimension?: QueryAttribute | QueryAdAttribute; - groupBy: QueryAttribute[] | QueryAdAttribute[]; + groupBy: Array; orderBy: QueryOrderBy[]; limit?: number; filters: QueryFilter[]; @@ -26,7 +26,6 @@ export interface BitmovinAnalyticsDataQuery extends DataQuery { export const DEFAULT_QUERY: Partial = { licenseKey: '', - interval: 'AUTO', orderBy: [], groupBy: [], filters: [], 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 082d490..3c20fc0 100644 --- a/bitmovin-analytics-datasource/src/utils/filterUtils.ts +++ b/bitmovin-analytics-datasource/src/utils/filterUtils.ts @@ -4,6 +4,21 @@ import { QUERY_AD_ATTRIBUTES, QueryAdAttribute } from '../types/queryAdAttribute import { QUERY_FILTER_OPERATORS, QueryFilterOperator, QueryFilterValue } from '../types/queryFilter'; import { QUERY_ATTRIBUTES, QueryAttribute } from '../types/queryAttributes'; +/** + * 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)) { + return JSON.stringify(filterValue); + } else { + return filterValue.toString(); + } +}; + const isNullFilter = (filterAttribute: QueryAttribute | QueryAdAttribute): boolean => { switch (filterAttribute) { case QUERY_ATTRIBUTES.CDN_PROVIDER: