From ed1af68af4a94dfaff99b0af5ee5cc35ebb3a577 Mon Sep 17 00:00:00 2001 From: MGJamJam Date: Wed, 8 May 2024 14:50:37 -0300 Subject: [PATCH] implements filter selection --- .../src/components/FilterInput.tsx | 74 +++++++ .../src/components/FilterRow.tsx | 177 +++++++++++++++++ .../src/components/QueryEditor.tsx | 10 + .../src/utils/filterUtils.ts | 184 ++++++++++++++++++ 4 files changed, 445 insertions(+) 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/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..dce3cf9 --- /dev/null +++ b/bitmovin-analytics-datasource/src/components/FilterInput.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { SelectableValue } from '@grafana/data'; +import { HorizontalGroup, IconButton, Input, Select, Tooltip } from '@grafana/ui'; + +import { QueryAttribute } from '../types/queryAttributes'; +import { QueryAdAttribute } from '../types/queryAdAttributes'; +import { REORDER_DIRECTION } from './GroupByInput'; +import { QueryFilterOperator, SELECTABLE_QUERY_FILTER_OPERATORS } from '../types/queryFilter'; + +type Props = { + readonly isAdAnalytics: boolean; + readonly selectableFilterAttributes: Array>; + readonly onAttributeChange: (newValue: SelectableValue) => void; + readonly onOperatorChange: (newValue: SelectableValue) => void; + readonly onValueChange: (newValue: string) => void; + readonly onDelete: () => void; + readonly isFirst: boolean; + readonly isLast: boolean; + readonly onReorderFilter: (direction: REORDER_DIRECTION) => void; + readonly parsingValueError?: string; +}; + +export function FilterInput(props: Props) { + //TODOMY implement headers of the 'table' + return ( + + props.onOperatorChange(value)} + options={SELECTABLE_QUERY_FILTER_OPERATORS} + width={15} + /> + + { + //TODOMY debounce the text editing a little bit... + } + + props.onValueChange(value.currentTarget.value)} + width={30} + /> + + props.onReorderFilter(REORDER_DIRECTION.DOWN)} + name="arrow-down" + disabled={props.isLast} + /> + props.onReorderFilter(REORDER_DIRECTION.UP)} + name="arrow-up" + disabled={props.isFirst} + /> + props.onDelete()} + size="lg" + 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..af40dee --- /dev/null +++ b/bitmovin-analytics-datasource/src/components/FilterRow.tsx @@ -0,0 +1,177 @@ +import React, { useState } from 'react'; +import { difference } from 'lodash'; +import { SelectableValue } from '@grafana/data'; +import { Box, IconButton, VerticalGroup } from '@grafana/ui'; + +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 { REORDER_DIRECTION } from './GroupByInput'; +import { FilterInput } from './FilterInput'; +import { convertFilterValueToProperType } from '../utils/filterUtils'; + +type Props = { + readonly isAdAnalytics: boolean; + readonly onChange: (newFilters: QueryFilter[]) => void; +}; + +export function FilterRow(props: Props) { + const [selectedAttributes, setSelectedAttributes] = useState< + Array> + >([]); + const [selectedOperators, setSelectedOperators] = useState>>([]); + const [values, setValues] = useState([]); + const [parsingValueErrors, setParsingValueErrors] = useState([]); + + const addFilterInput = () => { + setSelectedAttributes((prevState) => [...prevState, {}]); + setSelectedOperators((prevState) => [...prevState, {}]); + setValues((prevState) => [...prevState, '']); + setParsingValueErrors((prevState) => [...prevState, '']); + }; + + const deleteFilterInput = (index: number) => { + const newSelectedAttributes = [...selectedAttributes]; + newSelectedAttributes.splice(index, 1); + + const newSelectedOperators = [...selectedOperators]; + newSelectedOperators.splice(index, 1); + + const newValues = [...values]; + newValues.splice(index, 1); + + const newParsingValueErrors = [...parsingValueErrors]; + newParsingValueErrors.splice(index, 1); + + setSelectedAttributes(newSelectedAttributes); + setSelectedOperators(newSelectedOperators); + setValues(newValues); + setParsingValueErrors(newParsingValueErrors); + //TODOMY before calling this I need to make sure that the filter is actually complete and all the values are set... + props.onChange(mapFiltersToQueryFilters(newSelectedAttributes, newSelectedOperators, newValues)); + }; + + const onAttributesChange = (index: number, newAttribute: SelectableValue) => { + const newSelectedAttributes = [...selectedAttributes]; + newSelectedAttributes.splice(index, 1, newAttribute); + setSelectedAttributes(newSelectedAttributes); + + props.onChange(mapFiltersToQueryFilters(newSelectedAttributes, selectedOperators, values)); + }; + + const onOperatorsChange = (index: number, newOperator: SelectableValue) => { + const newSelectedOperators = [...selectedOperators]; + newSelectedOperators.splice(index, 1, newOperator); + setSelectedOperators(newSelectedOperators); + + //TODOMY here I also need to check the value and ,aybe throw an error + + //TODOMY difference between checking if value is valid and actually parsing it ? + + props.onChange(mapFiltersToQueryFilters(selectedAttributes, newSelectedOperators, values)); + }; + + const onValuesChange = (index: number, newValue: string) => { + try { + const convertedValue = convertFilterValueToProperType( + newValue, + selectedAttributes[index].value!, + selectedOperators[index].value!, + props.isAdAnalytics + ); + console.log(convertedValue, convertedValue); + + const newValues = [...values]; + newValues.splice(index, 1, convertedValue); + console.log(newValues); + setValues(newValues); + + const newParsingValueErrors = [...parsingValueErrors]; + newParsingValueErrors.splice(index, 1, ''); + setParsingValueErrors(newParsingValueErrors); + + props.onChange(mapFiltersToQueryFilters(selectedAttributes, selectedOperators, newValues)); + } catch (e: any) { + const errorMessage = e.message; + const newParsingValueErrors = [...parsingValueErrors]; + newParsingValueErrors.splice(index, 1, errorMessage); + setParsingValueErrors(newParsingValueErrors); + } + }; + + const mapFilterAttributesToSelectableValue = (): Array> => { + if (props.isAdAnalytics) { + return difference(SELECTABLE_QUERY_AD_ATTRIBUTES, selectedAttributes); + } else { + return difference(SELECTABLE_QUERY_ATTRIBUTES, selectedAttributes); + } + }; + + const mapFiltersToQueryFilters = ( + selectedAttributes: Array>, + selectedOperators: Array>, + values: QueryFilterValue[] + ): QueryFilter[] => { + const queryFilters: QueryFilter[] = []; + for (let i = 0; i < selectedAttributes.length; i++) { + queryFilters.push({ + name: selectedAttributes[i].value!, + operator: selectedOperators[i].value!, + value: values[i], + }); + } + console.log(queryFilters); + return queryFilters; + }; + + const reorderFilter = (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 newSelectedOperators = [...selectedOperators]; + const operatorToMove = newSelectedOperators[index]; + newSelectedOperators.splice(index, 1); + newSelectedOperators.splice(newIndex, 0, operatorToMove); + + const newValues = [...values]; + const valueToMove = newValues[index]; + newValues.splice(index, 1); + newValues.splice(newIndex, 0, valueToMove); + + setSelectedAttributes(newSelectedAttributes); + setSelectedOperators(newSelectedOperators); + setValues(newValues); + + props.onChange(mapFiltersToQueryFilters(newSelectedAttributes, newSelectedOperators, newValues)); + }; + + return ( + + {selectedAttributes.map((attribute, index, array) => ( + ) => + onAttributesChange(index, newValue) + } + onOperatorChange={(newValue: SelectableValue) => onOperatorsChange(index, newValue)} + onValueChange={(newValue: string) => onValuesChange(index, newValue)} + onDelete={() => deleteFilterInput(index)} + isFirst={index === 0} + isLast={index === array.length - 1} + onReorderFilter={(direction: REORDER_DIRECTION) => reorderFilter(direction, index)} + parsingValueError={parsingValueErrors[index] === '' ? undefined : parsingValueErrors[index]} + /> + ))} + + + addFilterInput()} size="xl" /> + + + ); +} diff --git a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx index cdd2cab..7f5655e 100644 --- a/bitmovin-analytics-datasource/src/components/QueryEditor.tsx +++ b/bitmovin-analytics-datasource/src/components/QueryEditor.tsx @@ -13,6 +13,8 @@ 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 { FilterRow } from './FilterRow'; enum LoadingState { Default = 'DEFAULT', @@ -74,6 +76,11 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) onRunQuery(); }; + const onFilterChange = (newFilters: QueryFilter[]) => { + onChange({ ...query, filters: newFilters }); + onRunQuery(); + }; + const onFormatAsTimeSeriesChange = (event: ChangeEvent) => { setIsTimeSeries(event.currentTarget.checked); if (event.currentTarget.checked) { @@ -150,6 +157,9 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) + + + diff --git a/bitmovin-analytics-datasource/src/utils/filterUtils.ts b/bitmovin-analytics-datasource/src/utils/filterUtils.ts new file mode 100644 index 0000000..fee7883 --- /dev/null +++ b/bitmovin-analytics-datasource/src/utils/filterUtils.ts @@ -0,0 +1,184 @@ +import { QUERY_AD_ATTRIBUTES, QueryAdAttribute } from '../types/queryAdAttributes'; +import { QUERY_FILTER_OPERATORS, QueryFilterOperator, QueryFilterValue } from '../types/queryFilter'; +import { QUERY_ATTRIBUTES, QueryAttribute } from '../types/queryAttributes'; + +export const isNullFilter = (filterAttribute: QueryAttribute | QueryAdAttribute): boolean => { + switch (filterAttribute) { + case QUERY_ATTRIBUTES.CDN_PROVIDER: + case QUERY_ATTRIBUTES.CUSTOM_DATA_1: + case QUERY_ATTRIBUTES.CUSTOM_DATA_2: + case QUERY_ATTRIBUTES.CUSTOM_DATA_3: + case QUERY_ATTRIBUTES.CUSTOM_DATA_4: + case QUERY_ATTRIBUTES.CUSTOM_DATA_5: + case QUERY_ATTRIBUTES.CUSTOM_DATA_6: + case QUERY_ATTRIBUTES.CUSTOM_DATA_7: + case QUERY_ATTRIBUTES.CUSTOM_DATA_8: + case QUERY_ATTRIBUTES.CUSTOM_DATA_9: + case QUERY_ATTRIBUTES.CUSTOM_DATA_10: + case QUERY_ATTRIBUTES.CUSTOM_DATA_11: + case QUERY_ATTRIBUTES.CUSTOM_DATA_12: + case QUERY_ATTRIBUTES.CUSTOM_DATA_13: + case QUERY_ATTRIBUTES.CUSTOM_DATA_14: + case QUERY_ATTRIBUTES.CUSTOM_DATA_15: + case QUERY_ATTRIBUTES.CUSTOM_DATA_16: + case QUERY_ATTRIBUTES.CUSTOM_DATA_17: + case QUERY_ATTRIBUTES.CUSTOM_DATA_18: + case QUERY_ATTRIBUTES.CUSTOM_DATA_19: + case QUERY_ATTRIBUTES.CUSTOM_DATA_20: + case QUERY_ATTRIBUTES.CUSTOM_DATA_21: + case QUERY_ATTRIBUTES.CUSTOM_DATA_22: + case QUERY_ATTRIBUTES.CUSTOM_DATA_23: + case QUERY_ATTRIBUTES.CUSTOM_DATA_24: + case QUERY_ATTRIBUTES.CUSTOM_DATA_25: + case QUERY_ATTRIBUTES.CUSTOM_DATA_26: + case QUERY_ATTRIBUTES.CUSTOM_DATA_27: + case QUERY_ATTRIBUTES.CUSTOM_DATA_28: + case QUERY_ATTRIBUTES.CUSTOM_DATA_29: + case QUERY_ATTRIBUTES.CUSTOM_DATA_30: + case QUERY_ATTRIBUTES.CUSTOM_USER_ID: + case QUERY_ATTRIBUTES.EXPERIMENT_NAME: + case QUERY_ATTRIBUTES.ISP: + case QUERY_ATTRIBUTES.PLAYER_TECH: + case QUERY_ATTRIBUTES.PLAYER_VERSION: + case QUERY_ATTRIBUTES.VIDEO_ID: + return true; + default: + return false; + } +}; + +const parseValueForInFilter = (rawValue: string) => { + const value: Array = JSON.parse(rawValue); + if (!Array.isArray(value)) { + throw Error('Couldn\'t parse IN filter, please provide data in JSON array form (e.g.: ["Firefox", "Chrome"]).'); + } + return value; +}; + +const convertFilterForAds = (rawValue: string, filterAttribute: QueryAdAttribute) => { + switch (filterAttribute) { + 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: { + const parsedValue = parseInt(rawValue, 10); + if (isNaN(parsedValue)) { + throw Error(`Couldn't parse filter for ${filterAttribute}, please provide data as a number`); + } + return parsedValue; + } + + 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: { + const parsedValue = parseFloat(rawValue); + if (isNaN(parsedValue)) { + throw Error(`Couldn't parse filter for ${filterAttribute}, please provide data as a number`); + } + return parsedValue; + } + + default: + return rawValue; + } +}; + +const convertFilter = (rawValue: string, filterAttribute: QueryAttribute) => { + switch (filterAttribute) { + 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: { + const parsedValue = parseInt(rawValue, 10); + if (isNaN(parsedValue)) { + throw Error(`Couldn't parse filter for ${filterAttribute}, please provide data as a number`); + } + return parsedValue; + } + + case QUERY_ATTRIBUTES.ERROR_PERCENTAGE: + case QUERY_ATTRIBUTES.REBUFFER_PERCENTAGE: { + const parsedValue = parseFloat(rawValue); + if (isNaN(parsedValue)) { + //TODOMY formatting of filterAttribute is not working, it is taking the value and not the label + throw Error(`Couldn't parse filter for ${filterAttribute}, please provide data as a number`); + } + return parsedValue; + } + + default: + return rawValue; + } +}; + +export const convertFilterValueToProperType = ( + rawValue: string, + filterAttribute: QueryAttribute | QueryAdAttribute, + filterOperator: QueryFilterOperator, + isAdAnalytics: boolean +): QueryFilterValue => { + //TODOMY check if the filters are actually being parsed correctly or if it is retruning NaN + //TODOMY check if empty the attributes + //TODOMY tests? + //TODOMY difference between throw new and throw + if (rawValue === '' && isNullFilter(filterAttribute)) { + return null; + } + + if (filterOperator === QUERY_FILTER_OPERATORS.IN) { + return parseValueForInFilter(rawValue); + } + + if (isAdAnalytics) { + return convertFilterForAds(rawValue, filterAttribute as QueryAdAttribute); + } + return convertFilter(rawValue, filterAttribute as QueryAttribute); +};