diff --git a/bitmovin-analytics-datasource/src/components/FilterRow.tsx b/bitmovin-analytics-datasource/src/components/FilterRow.tsx index 2816fee..ad226b2 100644 --- a/bitmovin-analytics-datasource/src/components/FilterRow.tsx +++ b/bitmovin-analytics-datasource/src/components/FilterRow.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Box, HorizontalGroup, IconButton, InlineLabel, VerticalGroup } from '@grafana/ui'; -import { QueryFilter } from '../types/queryFilter'; +import { InputQueryFilter } from '../types/queryFilter'; import { ATTRIBUTE_COMPONENT_WIDTH, OPERATOR_COMPONENT_WIDTH, @@ -11,8 +11,8 @@ import { type Props = { readonly isAdAnalytics: boolean; - readonly onQueryFilterChange: (newFilters: QueryFilter[]) => void; - readonly filters: QueryFilter[]; + readonly onQueryFilterChange: (newFilters: InputQueryFilter[]) => void; + readonly filters: InputQueryFilter[]; }; export function FilterRow(props: Props) { @@ -24,13 +24,13 @@ export function FilterRow(props: Props) { props.onQueryFilterChange(newQueryFilters); } - function handleQueryFilterChange(queryFilterIndex: number, changedQueryFilter: QueryFilter) { + function handleQueryFilterChange(queryFilterIndex: number, changedQueryFilter: InputQueryFilter) { const newQueryFilters = [...props.filters]; newQueryFilters.splice(queryFilterIndex, 1, changedQueryFilter); props.onQueryFilterChange(newQueryFilters); } - function handleNewQueryFilterChange(newQueryFilter: QueryFilter) { + function handleNewQueryFilterChange(newQueryFilter: InputQueryFilter) { const newQueryFilters = [...props.filters, newQueryFilter]; props.onQueryFilterChange(newQueryFilters); setHasNewQueryFilter(false); diff --git a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx index e563a8b..639ebb4 100644 --- a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx +++ b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx @@ -8,20 +8,19 @@ import { BitmovinDataSourceOptions, BitmovinAnalyticsDataQuery, DEFAULT_QUERY, - isOldBitmovinAnalyticsDataQuery, + OldBitmovinAnalyticsDataQuery, } from '../types/grafanaTypes'; import { fetchLicenses } from '../utils/licenses'; import { DEFAULT_SELECTABLE_QUERY_INTERVAL, SELECTABLE_QUERY_INTERVALS } from '../utils/intervalUtils'; import { SELECTABLE_AGGREGATION_METHODS } from '../types/aggregationMethod'; import { QueryAdAttribute, SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes'; -import { isMetric, Metric, SELECTABLE_METRICS } from '../types/metric'; +import { isMetric, SELECTABLE_METRICS } from '../types/metric'; import { GroupByRow } from './GroupByRow'; import { OrderByRow } from './OrderByRow'; import type { QueryOrderBy } from '../types/queryOrderBy'; -import type { QueryFilter } from '../types/queryFilter'; +import type { InputQueryFilter } from '../types/queryFilter'; import { FilterRow } from './FilterRow'; -import { convertFilterValueToProperType } from '../utils/filterUtils'; enum LoadingState { Default = 'DEFAULT', @@ -30,21 +29,25 @@ enum LoadingState { Error = 'ERROR', } -type Props = QueryEditorProps; +type Props = QueryEditorProps< + DataSource, + BitmovinAnalyticsDataQuery | OldBitmovinAnalyticsDataQuery, + BitmovinDataSourceOptions +>; export function QueryEditor(props: Props) { + const query = defaults(props.query, DEFAULT_QUERY); const [selectableLicenses, setSelectableLicenses] = useState([]); const [licenseLoadingState, setLicenseLoadingState] = useState(LoadingState.Default); const [licenseErrorMessage, setLicenseErrorMessage] = useState(''); - const [isTimeSeries, setIsTimeSeries] = useState(!!props.query.interval); - const [percentileValue, setPercentileValue] = useState(props.query.percentileValue); + const [isTimeSeries, setIsTimeSeries] = useState(query.resultFormat === 'time_series'); + const [percentileValue, setPercentileValue] = useState(query.percentileValue); const isMetricSelected = useMemo(() => { - return props.query.metric != null; - }, [props.query.metric]); + return query.dimension ? isMetric(query.dimension) : false; + }, [query.dimension]); const isPercentileSelected = useMemo(() => { - return props.query.queryAggregationMethod === 'percentile'; - }, [props.query.queryAggregationMethod]); - const query = defaults(props.query, DEFAULT_QUERY); + return query.metric === 'percentile'; + }, [query.metric]); /** Fetch Licenses */ useEffect(() => { @@ -60,98 +63,18 @@ export function QueryEditor(props: Props) { }); }, [props.datasource.apiKey, props.datasource.baseUrl]); - /** - * Ensures that dashboard JSON Models from the old Angular plugin are mapped correctly to the - * current model used by the application. Uses the {@link BitmovinAnalyticsDataQuery.resultFormat} - * as an indicator of whether an old JSON model was loaded. - */ - useEffect(() => { - if (!isOldBitmovinAnalyticsDataQuery(props.query)) { - return; - } - //TODOMY why is it not working for more than one queries in a dashboard? Why do I need to first reset to the ewnewst graph - - // The old Angular plugin did the filter value conversion in the query method before - // sending the request, so the filter values saved in the old JSON model are the "raw" - // input values as strings. The new react plugin, however, saves the converted API conform filter - // values in the JSON model, as it converts the filter values in the `QueryInputFilter` component. - // This allows the new plugin to provide error feedback directly to the user via a tooltip before - // sending the request. - const convertedFilters = props.query.filter.map((filter) => { - return { - name: filter.name, - operator: filter.operator, - value: convertFilterValueToProperType( - filter.value as string, - filter.name, - filter.operator, - !!props.datasource.adAnalytics - ), - } as QueryFilter; - }); - - // interval was always set in the old plugin's logic even for table data - // the new plugin only sets the interval for timeseries so for table data we need to reset the interval - let interval = props.query.interval; - if (props.query.resultFormat === 'table') { - setIsTimeSeries(false); - interval = undefined; - } - - // percentileValue was always set in the old plugin's logic, - // but it should only be set with the 'percentile' metric selected - let percentile = props.query.percentileValue; - if (props.query.metric !== 'percentile') { - percentile = undefined; - } - - // mapping is needed because old plugin used - // - the metric field to save the query aggregations - // - the dimension field to save metric and query attributes - // new plugin separates metric, query aggregations and query attributes in own fields to make data model more future-proof - // - the metric field saves Metrics (e.g. avg_concurrent_viewers) - // - the aggregation field saves query Aggregations (e.g. count) - // - the dimension field saves the query Attributes (e.g. Impression_id) - const aggregation = props.query.metric; - let metric = undefined; - let dimension = props.query.dimension; - if (props.query.dimension && isMetric(props.query.dimension)) { - metric = props.query.dimension as Metric; - dimension = undefined; - } - - const oldQuery = { ...props.query }; - delete oldQuery['resultFormat']; - const newQuery: BitmovinAnalyticsDataQuery = { - ...oldQuery, - filter: convertedFilters, - interval: interval, - percentileValue: percentile, - metric: metric, - dimension: dimension, - queryAggregationMethod: aggregation, - }; - - props.onChange(newQuery); - props.onRunQuery(); - }, []); - const handleLicenseChange = (item: SelectableValue) => { props.onChange({ ...query, license: item.value }); props.onRunQuery(); }; const handleAggregationChange = (item: SelectableValue) => { - props.onChange({ ...query, queryAggregationMethod: item.value, metric: undefined }); + props.onChange({ ...query, metric: item.value }); props.onRunQuery(); }; const handleDimensionChange = (item: SelectableValue) => { - if (isMetric(item.value)) { - props.onChange({ ...query, queryAggregationMethod: undefined, dimension: undefined, metric: item.value }); - } else { - props.onChange({ ...query, dimension: item.value, metric: undefined }); - } + props.onChange({ ...query, dimension: item.value }); props.onRunQuery(); }; @@ -165,7 +88,7 @@ export function QueryEditor(props: Props) { props.onRunQuery(); }; - const handleQueryFilterChange = (newFilters: QueryFilter[]) => { + const handleQueryFilterChange = (newFilters: InputQueryFilter[]) => { props.onChange({ ...query, filter: newFilters }); props.onRunQuery(); }; @@ -179,9 +102,9 @@ export function QueryEditor(props: Props) { const handleFormatAsTimeSeriesChange = (event: ChangeEvent) => { setIsTimeSeries(event.currentTarget.checked); if (event.currentTarget.checked) { - props.onChange({ ...query, interval: 'AUTO' }); + props.onChange({ ...query, interval: 'AUTO', resultFormat: 'time_series' }); } else { - props.onChange({ ...query, interval: undefined }); + props.onChange({ ...query, interval: undefined, resultFormat: 'table' }); } props.onRunQuery(); }; @@ -251,7 +174,7 @@ export function QueryEditor(props: Props) { {!isMetricSelected && ( handleInputValueChange(e.currentTarget.value)} invalid={derivedQueryFilterState.inputValueError != null} type="text" @@ -185,19 +185,18 @@ export const OPERATOR_COMPONENT_WIDTH = 15; export const VALUE_COMPONENT_WIDTH = 30; type DerivedQueryFilterState = { - attribute: undefined | QueryFilter['name']; + attribute: undefined | InputQueryFilter['name']; attributeError: undefined | string; - operator: undefined | QueryFilter['operator']; + operator: undefined | InputQueryFilter['operator']; operatorError: undefined | string; - value: undefined | QueryFilter['value']; + value: undefined | InputQueryFilter['value']; /** `true` if some values have been changed by inputs */ dirty: boolean; - inputValue: string; /** `undefined` when input value is valid */ inputValueError: undefined | string; }; -function buildInitialDerivedQueryFilterState(queryFilter: undefined | QueryFilter): DerivedQueryFilterState { +function buildInitialDerivedQueryFilterState(queryFilter: undefined | InputQueryFilter): DerivedQueryFilterState { return { attribute: queryFilter?.name, attributeError: undefined, @@ -205,13 +204,12 @@ function buildInitialDerivedQueryFilterState(queryFilter: undefined | QueryFilte operatorError: undefined, value: queryFilter?.value, dirty: false, - inputValue: mapQueryFilterValueToRawFilterValue(queryFilter?.value ?? null), inputValueError: undefined, }; } function buildAttributeSelectableValues( - usedQueryFilters: QueryFilter[], + usedQueryFilters: InputQueryFilter[], isAdAnalytics: boolean ): Array> { const ALL_ATTRIBUTES: Array> = isAdAnalytics diff --git a/bitmovin-analytics-datasource/src/datasource.ts b/bitmovin-analytics-datasource/src/datasource.ts index 847ae15..c48b309 100644 --- a/bitmovin-analytics-datasource/src/datasource.ts +++ b/bitmovin-analytics-datasource/src/datasource.ts @@ -12,7 +12,12 @@ import { getBackendSrv } from '@grafana/runtime'; import { filter } from 'lodash'; import { catchError, lastValueFrom, map, Observable, of } from 'rxjs'; -import { BitmovinDataSourceOptions, BitmovinAnalyticsDataQuery, DEFAULT_QUERY } from './types/grafanaTypes'; +import { + BitmovinDataSourceOptions, + BitmovinAnalyticsDataQuery, + DEFAULT_QUERY, + OldBitmovinAnalyticsDataQuery, +} from './types/grafanaTypes'; import { MixedDataRowList, NumberDataRowList, @@ -21,18 +26,19 @@ import { transformTableData, } from './utils/dataUtils'; import { calculateQueryInterval, QueryInterval } from './utils/intervalUtils'; -import { Metric } from './types/metric'; +import { isMetric, Metric } from './types/metric'; import { AggregationMethod } from './types/aggregationMethod'; -import { QueryFilter } from './types/queryFilter'; +import { OutputQueryFilter } from './types/queryFilter'; import { QueryAttribute } from './types/queryAttributes'; import { QueryAdAttribute } from './types/queryAdAttributes'; import { QueryOrderBy } from './types/queryOrderBy'; +import { convertFilterValueToProperType } from './utils/filterUtils'; type BitmovinAnalyticsRequestQuery = { licenseKey: string; start: Date; end: Date; - filters: QueryFilter[]; + filters: OutputQueryFilter[]; groupBy: Array; orderBy: QueryOrderBy[]; dimension?: QueryAttribute | QueryAdAttribute; @@ -42,7 +48,10 @@ type BitmovinAnalyticsRequestQuery = { percentile?: number; }; -export class DataSource extends DataSourceApi { +export class DataSource extends DataSourceApi< + BitmovinAnalyticsDataQuery | OldBitmovinAnalyticsDataQuery, + BitmovinDataSourceOptions +> { baseUrl: string; apiKey: string; tenantOrgId?: string; @@ -79,27 +88,47 @@ export class DataSource extends DataSourceApi !t.hide)); const promises = enabledQueries.map(async (target) => { - const interval = target.interval - ? calculateQueryInterval(target.interval!, from.getTime(), to.getTime()) - : undefined; + const interval = + target.resultFormat === 'time_series' && target.interval + ? calculateQueryInterval(target.interval, from.getTime(), to.getTime()) + : undefined; + + let aggregationMethod: AggregationMethod | undefined = target.metric; + const percentileValue = aggregationMethod === 'percentile' ? target.percentileValue : undefined; + + let metric: Metric | undefined = undefined; + let dimension: QueryAttribute | QueryAdAttribute | undefined = undefined; + if (target.dimension) { + if (isMetric(target.dimension)) { + metric = target.dimension as Metric; + } else { + dimension = target.dimension as QueryAttribute | QueryAdAttribute; + } + } + + const filters: OutputQueryFilter[] = target.filter.map((filter) => { + return { + name: filter.name, + operator: filter.operator, + value: convertFilterValueToProperType(filter.value, filter.name, filter.operator, !!this.adAnalytics), + }; + }); const query: BitmovinAnalyticsRequestQuery = { - filters: target.filter, + filters: filters, groupBy: target.groupBy, orderBy: target.orderBy, - dimension: target.dimension, - metric: target.metric, + dimension: dimension, + metric: metric, start: from, end: to, licenseKey: target.license, interval: interval, - limit: target.limit, - percentile: target.percentileValue, + limit: this.parseLimit(target.limit), + percentile: percentileValue, }; - const response = await lastValueFrom( - this.request(this.getRequestUrl(target.metric, target.queryAggregationMethod), 'POST', query) - ); + const response = await lastValueFrom(this.request(this.getRequestUrl(metric, aggregationMethod), 'POST', query)); const dataRows: MixedDataRowList = response.data.data.result.rows; const dataRowCount: number = response.data.data.result.rowCount; @@ -149,6 +178,19 @@ export class DataSource extends DataSourceApi ({ data })); } + /** needed because of old plugin logic where limit was saved as string and not as number */ + parseLimit(limit: number | string | undefined): undefined | number { + if (limit == null) { + return undefined; + } + + if (Number.isInteger(limit)) { + return limit as number; + } else { + return parseInt(limit as string, 10); + } + } + getRequestUrl(metric?: Metric, aggregation?: AggregationMethod): string { let url = '/analytics'; if (this.adAnalytics === true) { diff --git a/bitmovin-analytics-datasource/src/module.ts b/bitmovin-analytics-datasource/src/module.ts index 917913c..c9ae72d 100644 --- a/bitmovin-analytics-datasource/src/module.ts +++ b/bitmovin-analytics-datasource/src/module.ts @@ -2,10 +2,16 @@ import { DataSourcePlugin } from '@grafana/data'; import { DataSource } from './datasource'; import { ConfigEditor } from './components/ConfigEditor'; import { QueryEditor } from './components/QueryEditor'; -import { BitmovinAnalyticsDataQuery, BitmovinDataSourceOptions } from './types/grafanaTypes'; +import { + BitmovinAnalyticsDataQuery, + BitmovinDataSourceOptions, + OldBitmovinAnalyticsDataQuery, +} from './types/grafanaTypes'; -export const plugin = new DataSourcePlugin( - DataSource -) +export const plugin = new DataSourcePlugin< + DataSource, + BitmovinAnalyticsDataQuery | OldBitmovinAnalyticsDataQuery, + BitmovinDataSourceOptions +>(DataSource) .setConfigEditor(ConfigEditor) .setQueryEditor(QueryEditor); diff --git a/bitmovin-analytics-datasource/src/types/grafanaTypes.ts b/bitmovin-analytics-datasource/src/types/grafanaTypes.ts index a23ccc9..a4cece1 100644 --- a/bitmovin-analytics-datasource/src/types/grafanaTypes.ts +++ b/bitmovin-analytics-datasource/src/types/grafanaTypes.ts @@ -5,24 +5,26 @@ import { QueryAttribute } from './queryAttributes'; import { QueryAdAttribute } from './queryAdAttributes'; import { Metric } from './metric'; import { QueryOrderBy } from './queryOrderBy'; -import { QueryFilter } from './queryFilter'; +import { InputQueryFilter } from './queryFilter'; import { AggregationMethod } from './aggregationMethod'; +type ResultFormat = 'table' | 'time_series'; + /** * These are the options configurable via the QueryEditor * */ export interface BitmovinAnalyticsDataQuery extends DataQuery { license: string; interval?: QueryInterval | 'AUTO'; - queryAggregationMethod?: AggregationMethod; - metric?: Metric; - dimension?: QueryAttribute | QueryAdAttribute; + metric?: AggregationMethod; + dimension?: QueryAttribute | QueryAdAttribute | Metric; groupBy: Array; orderBy: QueryOrderBy[]; limit?: number; - filter: QueryFilter[]; + filter: InputQueryFilter[]; alias?: string; percentileValue?: number; + resultFormat: ResultFormat; } /** @@ -36,18 +38,11 @@ export interface OldBitmovinAnalyticsDataQuery extends DataQuery { dimension?: QueryAttribute | QueryAdAttribute | Metric; groupBy: Array; orderBy: QueryOrderBy[]; - limit?: number; - filter: QueryFilter[]; + limit?: string; + filter: InputQueryFilter[]; alias?: string; - percentileValue?: number; - resultFormat?: 'table' | 'time_series'; -} - -export function isOldBitmovinAnalyticsDataQuery( - query: OldBitmovinAnalyticsDataQuery | BitmovinAnalyticsDataQuery -): query is OldBitmovinAnalyticsDataQuery { - // resultFormat is always set through the old plugin's logic - return (query as OldBitmovinAnalyticsDataQuery).resultFormat != null; + percentileValue: number; + resultFormat: ResultFormat; } export const DEFAULT_QUERY: Partial = { @@ -55,6 +50,8 @@ export const DEFAULT_QUERY: Partial = { orderBy: [], groupBy: [], filter: [], + resultFormat: 'time_series', + interval: 'AUTO', }; /** diff --git a/bitmovin-analytics-datasource/src/types/metric.ts b/bitmovin-analytics-datasource/src/types/metric.ts index 9ef61f1..a3a1532 100644 --- a/bitmovin-analytics-datasource/src/types/metric.ts +++ b/bitmovin-analytics-datasource/src/types/metric.ts @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; -const METRICS = ['avg-concurrentviewers', 'max-concurrentviewers', 'avg-dropped-frames'] as const; +const METRICS = ['AVG_CONCURRENTVIEWERS', 'MAX_CONCURRENTVIEWERS', 'AVG-DROPPED-FRAMES'] as const; export type Metric = (typeof METRICS)[number]; diff --git a/bitmovin-analytics-datasource/src/types/queryFilter.ts b/bitmovin-analytics-datasource/src/types/queryFilter.ts index 727be73..55da901 100644 --- a/bitmovin-analytics-datasource/src/types/queryFilter.ts +++ b/bitmovin-analytics-datasource/src/types/queryFilter.ts @@ -9,10 +9,20 @@ export type QueryFilterOperator = (typeof QUERY_FILTER_OPERATORS)[number]; export const SELECTABLE_QUERY_FILTER_OPERATORS: Array> = QUERY_FILTER_OPERATORS.map((o) => ({ value: o, label: o })); -export type QueryFilter = { +/** This type is needed because of legacy reasons. + * In the angular plugin the value was saved as a string in a dashboard JSON file. */ +export type InputQueryFilter = { name: QueryAdAttribute | QueryAttribute; operator: QueryFilterOperator; - value: QueryFilterValue; + value: string; }; -export type QueryFilterValue = boolean | number | string | string[] | null; +/** QueryFilter type with the correct value type that is accepted by the Bitmovin API */ +export type OutputQueryFilter = { + name: QueryAdAttribute | QueryAttribute; + operator: QueryFilterOperator; + value: OutputQueryFilterValue; +}; + +/** Correct Filter value type that is accepted by the Bitmovin API */ +export type OutputQueryFilterValue = boolean | number | string | string[] | null; diff --git a/bitmovin-analytics-datasource/src/utils/filterUtils.test.ts b/bitmovin-analytics-datasource/src/utils/filterUtils.test.ts index 7315396..f20cb65 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, mapQueryFilterValueToRawFilterValue } from './filterUtils'; +import { convertFilterValueToProperType } from './filterUtils'; describe('convertFilterValueToProperType', () => { it('should return null if rawValue is empty and attribute a NullFilter', () => { @@ -100,53 +100,3 @@ describe('convertFilterValueToProperType', () => { ); }); }); - -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 572c436..f27f7ef 100644 --- a/bitmovin-analytics-datasource/src/utils/filterUtils.ts +++ b/bitmovin-analytics-datasource/src/utils/filterUtils.ts @@ -1,24 +1,9 @@ import { isEmpty } from 'lodash'; import { QueryAdAttribute } from '../types/queryAdAttributes'; -import { QueryFilterOperator, QueryFilterValue } from '../types/queryFilter'; +import { QueryFilterOperator, OutputQueryFilterValue } from '../types/queryFilter'; import { 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 'CDN_PROVIDER': @@ -182,14 +167,14 @@ const convertFilter = (rawValue: string, filterAttribute: QueryAttribute) => { * @param {QueryAttribute | QueryAdAttribute} filterAttribute The filter attribute. * @param {QueryFilterOperator} filterOperator The filter operator. * @param {boolean} isAdAnalytics If Ad Analytics are queried. - * @returns {QueryFilterValue} The correctly converted Filter Value. + * @returns {OutputQueryFilterValue} The correctly converted Filter Value. * */ export const convertFilterValueToProperType = ( rawValue: string, filterAttribute: QueryAttribute | QueryAdAttribute, filterOperator: QueryFilterOperator, isAdAnalytics: boolean -): QueryFilterValue => { +): OutputQueryFilterValue => { if (isEmpty(rawValue) && isNullFilter(filterAttribute)) { return null; }