From a928c8754f53f9f08017866d6992d38229ab4c3a Mon Sep 17 00:00:00 2001 From: MGJamJam Date: Thu, 25 Apr 2024 17:00:55 -0300 Subject: [PATCH] add filter selection --- .../src/components/FilterInput.tsx | 66 ++++++++++ .../src/components/FilterRow.tsx | 84 +++++++++++++ .../src/components/QueryEditor.tsx | 11 ++ .../src/datasource.ts | 14 +-- bitmovin-analytics-datasource/src/types.ts | 4 +- .../src/types/queryAdAttributes.ts | 2 +- .../src/types/queryAttributes.ts | 2 +- .../src/types/queryFilter.ts | 43 +++++++ .../src/utils/filterUtils.ts | 115 ++++++++++++++++++ 9 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 bitmovin-analytics-datasource/src/components/FilterInput.tsx create mode 100644 bitmovin-analytics-datasource/src/components/FilterRow.tsx create mode 100644 bitmovin-analytics-datasource/src/types/queryFilter.ts create mode 100644 bitmovin-analytics-datasource/src/utils/filterUtils.ts diff --git a/bitmovin-analytics-datasource/src/components/FilterInput.tsx b/bitmovin-analytics-datasource/src/components/FilterInput.tsx new file mode 100644 index 0000000..3f28beb --- /dev/null +++ b/bitmovin-analytics-datasource/src/components/FilterInput.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { QueryFilterOperator, SELECTABLE_QUERY_FILTER_OPERATORS, SelectableQueryFilter } from '../types/queryFilter'; +import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes'; +import { QueryAdAttribute, SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; +import { Field, HorizontalGroup, IconButton, Input, Select } from '@grafana/ui'; +import { Space } from '@grafana/plugin-ui'; +import { convertFilterValueToProperType } from '../utils/filterUtils'; +import { useState } from 'react'; + +type Props = { + readonly isAdAnalytics: boolean; + readonly filter: SelectableQueryFilter; + readonly onAttributeChange: (newValue: QueryAttribute | QueryAdAttribute) => void; + readonly onOperatorChange: (newValue: QueryFilterOperator) => void; + readonly onFilterValueChange: (newValue: boolean | number | string | Array) => void; + readonly onDelete: () => void; +}; +export const FilterInput = (props: Props) => { + const [filterValueError, setFilterValueError] = useState(null); + + //TODOMY delay check here, only on enter? Small timeout or check button? + const onFilterValueChange = (value: string, filter: SelectableQueryFilter) => { + try { + const convertedValue = convertFilterValueToProperType(value, filter, props.isAdAnalytics); + setFilterValueError(null); + props.onFilterValueChange(convertedValue); + } catch (e) { + setFilterValueError(e.message); + } + }; + + return ( + + { + //TODOMY fix autospaching ui issue + } + + + props.onOperatorChange(value.value as QueryFilterOperator)} + options={SELECTABLE_QUERY_FILTER_OPERATORS} + /> + + + onFilterValueChange(value.currentTarget.value, props.filter)} /> + + + props.onDelete()} + variant="destructive" + /> + + ); +}; diff --git a/bitmovin-analytics-datasource/src/components/FilterRow.tsx b/bitmovin-analytics-datasource/src/components/FilterRow.tsx new file mode 100644 index 0000000..03fc526 --- /dev/null +++ b/bitmovin-analytics-datasource/src/components/FilterRow.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { QueryAttribute } from '../types/queryAttributes'; +import { QueryAdAttribute } from '../types/queryAdAttributes'; +import { FieldSet, IconButton } from '@grafana/ui'; +import { QueryFilter, QueryFilterOperator, SelectableQueryFilter } from '../types/queryFilter'; +import { FilterInput } from './FilterInput'; + +type Props = { + readonly isAdAnalytics: boolean; + readonly onChange: (newFilters: QueryFilter[]) => void; +}; +export const FilterRow = (props: Props) => { + const [filters, setFilters] = useState([]); + + const deleteFilter = (index: number) => { + const newFilter = [...filters]; + newFilter.splice(index, 1); + + setFilters(newFilter); + props.onChange(newFilter as QueryFilter[]); + }; + + const onAttributesChange = (index: number, newAttribute: QueryAttribute | QueryAdAttribute) => { + const newFilters = [...filters]; + const newValue = { + name: newAttribute, + operator: newFilters[index].operator, + value: newFilters[index].value, + } as QueryFilter; + newFilters.splice(index, 1, newValue); + setFilters(newFilters); + props.onChange(newFilters as QueryFilter[]); + }; + + const onOperatorChange = (index: number, newOperator: QueryFilterOperator) => { + const newFilters = [...filters]; + const newValue = { + name: newFilters[index].name, + operator: newOperator, + value: newFilters[index].value, + } as QueryFilter; + newFilters.splice(index, 1, newValue); + setFilters(newFilters); + props.onChange(newFilters as QueryFilter[]); + }; + + const onFilterValueChange = (index: number, newFilterValue: string | number) => { + const newFilters = [...filters]; + const newValue = { + name: newFilters[index].name, + operator: newFilters[index].operator, + value: newFilterValue, + } as QueryFilter; + newFilters.splice(index, 1, newValue); + setFilters(newFilters); + props.onChange(newFilters as QueryFilter[]); + }; + + const addFilterInput = () => { + //TODOMY what to use as the default value + setFilters((prevState) => [...prevState, { name: '', operator: '', value: '' }]); + }; + + return ( +
+ {filters.map((item, index) => ( + onAttributesChange(index, newValue)} + onOperatorChange={(newValue: QueryFilterOperator) => onOperatorChange(index, newValue)} + onFilterValueChange={(newValue: string | number) => onFilterValueChange(index, newValue)} + onDelete={() => deleteFilter(index)} + /> + ))} + + { + //TODOMY fix position of the IconButton + } + addFilterInput()} /> +
+ ); +}; diff --git a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx index 094f6cc..816cf54 100644 --- a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx +++ b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx @@ -10,6 +10,8 @@ import { SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes'; import { SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; import { OrderByRow } from './OrderByRow'; import { QueryOrderBy } from '../types/queryOrderBy'; +import { FilterRow } from './FilterRow'; +import { QueryFilter } from '../types/queryFilter'; type Props = QueryEditorProps; @@ -54,6 +56,12 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) onRunQuery(); }; + const onFiltersChange = (newFilters: QueryFilter[]) => { + console.log('filters in QueryEditor', newFilters); + onChange({ ...query, filters: newFilters }); + onRunQuery(); + }; + const onFormatAsTimeSeriesChange = (event: ChangeEvent) => { setIsTimeSeries(event.currentTarget.checked); if (event.currentTarget.checked) { @@ -117,6 +125,9 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) + + + diff --git a/bitmovin-analytics-datasource/src/datasource.ts b/bitmovin-analytics-datasource/src/datasource.ts index 4df32c2..02097f7 100644 --- a/bitmovin-analytics-datasource/src/datasource.ts +++ b/bitmovin-analytics-datasource/src/datasource.ts @@ -12,11 +12,13 @@ import { catchError, lastValueFrom, map, Observable, of } from 'rxjs'; import { MixedDataRowList, MyDataSourceOptions, MyQuery, NumberDataRowList } from './types'; import { transformGroupedTimeSeriesData, transformSimpleTimeSeries, transformTableData } from './utils/dataUtils'; import { calculateQueryInterval, QueryInterval } from './utils/intervalUtils'; +import { QueryFilter } from './types/queryFilter'; +import { QueryOrderBy } from './types/queryOrderBy'; type AnalyticsQuery = { - filters: Array<{ name: string; operator: string; value: number }>; + filters: QueryFilter[]; groupBy: string[]; - orderBy: Array<{ name: string; order: string }>; + orderBy: QueryOrderBy[]; dimension: string; start: Date; end: Date; @@ -61,13 +63,7 @@ export class DataSource extends DataSourceApi { : undefined; const query: AnalyticsQuery = { - filters: [ - { - name: 'VIDEO_STARTUPTIME', - operator: 'GT', - value: 0, - }, - ], + filters: target.filters, groupBy: target.groupBy, orderBy: target.orderBy, dimension: target.dimension, diff --git a/bitmovin-analytics-datasource/src/types.ts b/bitmovin-analytics-datasource/src/types.ts index 9350f1f..c9da47c 100644 --- a/bitmovin-analytics-datasource/src/types.ts +++ b/bitmovin-analytics-datasource/src/types.ts @@ -5,16 +5,17 @@ import { Aggregation } from './types/aggregations'; import { QueryAttribute } from './types/queryAttributes'; import { QueryAdAttribute } from './types/queryAdAttributes'; import { QueryOrderBy } from './types/queryOrderBy'; +import { QueryFilter } from './types/queryFilter'; export interface MyQuery extends DataQuery { interval?: QueryInterval | 'AUTO'; - timeSeries: boolean; limit: number; aggregation: Aggregation; licenseKey: string; dimension: QueryAttribute | QueryAdAttribute; groupBy: QueryAttribute[] | QueryAdAttribute[]; orderBy: QueryOrderBy[]; + filters: QueryFilter[]; } export const DEFAULT_QUERY: Partial = { @@ -25,6 +26,7 @@ export const DEFAULT_QUERY: Partial = { dimension: 'IMPRESSION_ID', groupBy: [], orderBy: [], + filters: [], }; /** diff --git a/bitmovin-analytics-datasource/src/types/queryAdAttributes.ts b/bitmovin-analytics-datasource/src/types/queryAdAttributes.ts index 37572d7..ac7de81 100644 --- a/bitmovin-analytics-datasource/src/types/queryAdAttributes.ts +++ b/bitmovin-analytics-datasource/src/types/queryAdAttributes.ts @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; -enum QUERY_AD_ATTRIBUTES { +export enum QUERY_AD_ATTRIBUTES { ADVERTISER_NAME = 'ADVERTISER_NAME', AD_CLICKTHROUGH_URL = 'AD_CLICKTHROUGH_URL', AD_DESCRIPTION = 'AD_DESCRIPTION', diff --git a/bitmovin-analytics-datasource/src/types/queryAttributes.ts b/bitmovin-analytics-datasource/src/types/queryAttributes.ts index 9a00f44..18cb92c 100644 --- a/bitmovin-analytics-datasource/src/types/queryAttributes.ts +++ b/bitmovin-analytics-datasource/src/types/queryAttributes.ts @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; -enum QUERY_ATTRIBUTES { +export enum QUERY_ATTRIBUTES { AD = 'AD', ANALYTICS_VERSION = 'ANALYTICS_VERSION', AUDIO_BITRATE = 'AUDIO_BITRATE', diff --git a/bitmovin-analytics-datasource/src/types/queryFilter.ts b/bitmovin-analytics-datasource/src/types/queryFilter.ts new file mode 100644 index 0000000..9a6d817 --- /dev/null +++ b/bitmovin-analytics-datasource/src/types/queryFilter.ts @@ -0,0 +1,43 @@ +import { QueryAdAttribute } from './queryAdAttributes'; +import { SelectableValue } from '@grafana/data'; +import { QueryAttribute } from './queryAttributes'; + +enum QUERY_FILTER_OPERATORS { + GT = 'GT', + GTE = 'GTE', + LT = 'LT', + LTE = 'LTE', + EQ = 'EQ', + NE = 'NE', + CONTAINS = 'CONTAINS', + NOTCONTAINS = 'NOTCONTAINS', + IN = 'IN', +} + +export type QueryFilterOperator = keyof typeof QUERY_FILTER_OPERATORS; + +export const SELECTABLE_QUERY_FILTER_OPERATORS: SelectableValue[] = [ + { value: QUERY_FILTER_OPERATORS.GT, label: QUERY_FILTER_OPERATORS.GT }, + { value: QUERY_FILTER_OPERATORS.GTE, label: QUERY_FILTER_OPERATORS.GTE }, + { value: QUERY_FILTER_OPERATORS.LT, label: QUERY_FILTER_OPERATORS.LT }, + { value: QUERY_FILTER_OPERATORS.LTE, label: QUERY_FILTER_OPERATORS.LTE }, + { value: QUERY_FILTER_OPERATORS.EQ, label: QUERY_FILTER_OPERATORS.EQ }, + { value: QUERY_FILTER_OPERATORS.NE, label: QUERY_FILTER_OPERATORS.NE }, + { value: QUERY_FILTER_OPERATORS.CONTAINS, label: QUERY_FILTER_OPERATORS.CONTAINS }, + { value: QUERY_FILTER_OPERATORS.NOTCONTAINS, label: QUERY_FILTER_OPERATORS.NOTCONTAINS }, + { value: QUERY_FILTER_OPERATORS.IN, label: QUERY_FILTER_OPERATORS.IN }, +]; + +export type QueryFilter = { + name: QueryAdAttribute | QueryAttribute; + operator: QueryFilterOperator; + value: boolean | number | string | Array; +}; + +export type SelectableQueryFilter = { + name: QueryAdAttribute | QueryAttribute | ''; + operator: QueryFilterOperator | ''; + value: boolean | number | string | Array; +}; + +//TODOMY add specific filter attributes diff --git a/bitmovin-analytics-datasource/src/utils/filterUtils.ts b/bitmovin-analytics-datasource/src/utils/filterUtils.ts new file mode 100644 index 0000000..cbb3e31 --- /dev/null +++ b/bitmovin-analytics-datasource/src/utils/filterUtils.ts @@ -0,0 +1,115 @@ +import { SelectableQueryFilter } from '../types/queryFilter'; +import { QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes'; +import { QUERY_ATTRIBUTES } from '../types/queryAttributes'; + +const parseValueForInFilter = (rawValue: string) => { + const value: Array = JSON.parse(rawValue); + if (!Array.isArray(value)) { + throw Error(); + } + return value; +}; + +const convertFilterForAds = (rawValue: string, filter: SelectableQueryFilter) => { + switch (filter.name) { + case QUERY_AD_ATTRIBUTES.IS_LINEAR: + return rawValue === 'true'; + + case QUERY_AD_ATTRIBUTES.AD_STARTUP_TIME: + case QUERY_AD_ATTRIBUTES.AD_WRAPPER_ADS_COUNT: + case QUERY_AD_ATTRIBUTES.AUDIO_BITRATE: + case QUERY_AD_ATTRIBUTES.CLICK_POSITION: + case QUERY_AD_ATTRIBUTES.CLOSE_POSITION: + case QUERY_AD_ATTRIBUTES.ERROR_CODE: + case QUERY_AD_ATTRIBUTES.MANIFEST_DOWNLOAD_TIME: + case QUERY_AD_ATTRIBUTES.MIN_SUGGESTED_DURATION: + case QUERY_AD_ATTRIBUTES.PAGE_LOAD_TIME: + case QUERY_AD_ATTRIBUTES.PLAYER_STARTUPTIME: + case QUERY_AD_ATTRIBUTES.SCREEN_HEIGHT: + case QUERY_AD_ATTRIBUTES.SCREEN_WIDTH: + case QUERY_AD_ATTRIBUTES.SKIP_POSITION: + case QUERY_AD_ATTRIBUTES.TIME_HOVERED: + case QUERY_AD_ATTRIBUTES.TIME_IN_VIEWPORT: + case QUERY_AD_ATTRIBUTES.TIME_PLAYED: + case QUERY_AD_ATTRIBUTES.TIME_UNTIL_HOVER: + case QUERY_AD_ATTRIBUTES.VIDEO_BITRATE: + case QUERY_AD_ATTRIBUTES.VIDEO_WINDOW_HEIGHT: + case QUERY_AD_ATTRIBUTES.VIDEO_WINDOW_WIDTH: + return parseInt(rawValue, 10); + + case QUERY_AD_ATTRIBUTES.CLICK_PERCENTAGE: + case QUERY_AD_ATTRIBUTES.CLOSE_PERCENTAGE: + case QUERY_AD_ATTRIBUTES.PERCENTAGE_IN_VIEWPORT: + case QUERY_AD_ATTRIBUTES.SKIP_PERCENTAGE: + return parseFloat(rawValue); + + default: + return rawValue; + } +}; + +const convertFilter = (rawValue: string, filter: SelectableQueryFilter) => { + switch (filter.name) { + case QUERY_ATTRIBUTES.IS_CASTING: + case QUERY_ATTRIBUTES.IS_LIVE: + case QUERY_ATTRIBUTES.IS_MUTED: + return rawValue === 'true'; + + case QUERY_ATTRIBUTES.AUDIO_BITRATE: + case QUERY_ATTRIBUTES.BUFFERED: + case QUERY_ATTRIBUTES.CLIENT_TIME: + case QUERY_ATTRIBUTES.DOWNLOAD_SPEED: + case QUERY_ATTRIBUTES.DRM_LOAD_TIME: + case QUERY_ATTRIBUTES.DROPPED_FRAMES: + case QUERY_ATTRIBUTES.DURATION: + case QUERY_ATTRIBUTES.ERROR_CODE: + case QUERY_ATTRIBUTES.PAGE_LOAD_TIME: + case QUERY_ATTRIBUTES.PAGE_LOAD_TYPE: + case QUERY_ATTRIBUTES.PAUSED: + case QUERY_ATTRIBUTES.PLAYED: + case QUERY_ATTRIBUTES.PLAYER_STARTUPTIME: + case QUERY_ATTRIBUTES.SCREEN_HEIGHT: + case QUERY_ATTRIBUTES.SCREEN_WIDTH: + case QUERY_ATTRIBUTES.SEEKED: + case QUERY_ATTRIBUTES.STARTUPTIME: + case QUERY_ATTRIBUTES.VIDEO_BITRATE: + case QUERY_ATTRIBUTES.VIDEO_DURATION: + case QUERY_ATTRIBUTES.VIDEO_PLAYBACK_HEIGHT: + case QUERY_ATTRIBUTES.VIDEO_PLAYBACK_WIDTH: + case QUERY_ATTRIBUTES.VIDEO_STARTUPTIME: + case QUERY_ATTRIBUTES.VIDEO_WINDOW_HEIGHT: + case QUERY_ATTRIBUTES.VIDEO_WINDOW_WIDTH: + case QUERY_ATTRIBUTES.VIDEOTIME_END: + case QUERY_ATTRIBUTES.VIDEOTIME_START: + case QUERY_ATTRIBUTES.VIEWTIME: + return parseInt(rawValue, 10); + + case QUERY_ATTRIBUTES.ERROR_PERCENTAGE: + case QUERY_ATTRIBUTES.REBUFFER_PERCENTAGE: + return parseFloat(rawValue); + + default: + return rawValue; + } +}; +export const convertFilterValueToProperType = ( + rawValue: string, + filter: SelectableQueryFilter, + isAdAnalytics: boolean +): boolean | number | string | Array => { + //TODOMY check null filter parsing + //TODOMY check if empty + //TODOMY tests? + if (filter.operator === 'IN') { + try { + return parseValueForInFilter(rawValue); + } catch (e) { + throw Error('Couldn\'t parse IN filter, please provide data in JSON array form (e.g.: ["Firefox", "Chrome"]).'); + } + } + + if (isAdAnalytics) { + return convertFilterForAds(rawValue, filter); + } + return convertFilter(rawValue, filter); +};