Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/AN-4135 pass query parameters to components #74

Merged
51 changes: 37 additions & 14 deletions bitmovin-analytics-datasource/src/components/FilterInput.tsx
Original file line number Diff line number Diff line change
@@ -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<QueryAttribute | QueryAdAttribute> => {
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<QueryFilterOperator> => {
return SELECTABLE_QUERY_FILTER_OPERATORS.filter((selectableOperator) => selectableOperator.value === operator);
};

type Props = {
readonly isAdAnalytics: boolean;
readonly filter: FilterRowData;
readonly selectableFilterAttributes: Array<SelectableValue<QueryAttribute | QueryAdAttribute>>;
readonly onAttributeChange: (newValue: SelectableValue<QueryAdAttribute | QueryAttribute>) => void;
readonly onOperatorChange: (newValue: SelectableValue<QueryFilterOperator>) => 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 (
<HorizontalGroup spacing="xs">
<Select
onChange={(value) => props.onAttributeChange(value)}
value={
props.filter.attribute
? mapAttributeToSelectableValue(props.filter.attribute, props.isAdAnalytics)
: undefined
}
onChange={(selectableValue) => props.onAttributeChange(selectableValue)}
options={props.selectableFilterAttributes}
width={30}
/>
<Select
onChange={(value) => props.onOperatorChange(value)}
value={props.filter.operator ? mapOperatorToSelectableOperator(props.filter.operator) : undefined}
onChange={(selectableValue) => props.onOperatorChange(selectableValue)}
options={SELECTABLE_QUERY_FILTER_OPERATORS}
width={15}
/>
<Tooltip
content={props.parsingValueError ? props.parsingValueError : ''}
show={!!props.parsingValueError}
content={isEmpty(props.filter.parsingValueError) ? '' : props.filter.parsingValueError}
show={!isEmpty(props.filter.parsingValueError)}
theme="error"
>
<Input
invalid={!!props.parsingValueError}
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}
/>
</Tooltip>
<IconButton
tooltip="Add Filter"
onClick={props.onAddFilter}
onClick={props.onSaveFilter}
name="check"
size="xl"
variant="primary"
disabled={props.addFilterDisabled}
disabled={isEmpty(props.filter.attribute) || isEmpty(props.filter.operator)}
/>
<IconButton tooltip="Delete Filter" name="trash-alt" onClick={props.onDelete} size="lg" variant="destructive" />
</HorizontalGroup>
Expand Down
138 changes: 79 additions & 59 deletions bitmovin-analytics-datasource/src/components/FilterRow.tsx
Original file line number Diff line number Diff line change
@@ -1,136 +1,157 @@
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';

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 { convertFilterValueToProperType, mapQueryFilterValueToRawFilterValue } from '../utils/filterUtils';

type Filter = {
selectedAttribute: SelectableValue<QueryAdAttribute | QueryAttribute>;
selectedOperator: SelectableValue<QueryFilterOperator>;
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<SelectableValue<QueryAttribute | QueryAdAttribute>> => {
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<Filter[]>([]);
const [filterInputs, setFilterInputs] = useState<FilterRowData[]>([]);

/** 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<QueryAttribute | QueryAdAttribute>) => {
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);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe lodash has something more human readable instead of .splice(index, 1, newFilter);

setFilters(newFilters);
setFilterInputs(newFilters);
};

const onOperatorsChange = (index: number, newOperator: SelectableValue<QueryFilterOperator>) => {
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 (
<VerticalGroup>
{filters.length !== 0 && (
{filterInputs.length > 0 && (
<HorizontalGroup spacing={'none'}>
<InlineLabel width={30} tooltip="">
Dimension
Expand All @@ -143,24 +164,23 @@ export function FilterRow(props: Props) {
</InlineLabel>
</HorizontalGroup>
)}
{filters.map((filter, index, filtersArray) => (
{filterInputs.map((filter, index, filtersArray) => (
<FilterInput
key={index}
isAdAnalytics={props.isAdAnalytics}
filter={filter}
selectableFilterAttributes={mapFilterAttributesToSelectableValue(filtersArray, props.isAdAnalytics)}
onAttributeChange={(newValue: SelectableValue<QueryAdAttribute | QueryAttribute>) =>
onAttributesChange(index, newValue)
}
onOperatorChange={(newValue: SelectableValue<QueryFilterOperator>) => 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)}
/>
))}

<Box paddingTop={filters.length === 0 ? 0.5 : 0}>
<Box paddingTop={filterInputs.length === 0 ? 0.5 : 0}>
<IconButton name="plus-square" tooltip="Add Filter" onClick={() => addFilterInput()} size="xl" />
</Box>
</VerticalGroup>
Expand Down
4 changes: 2 additions & 2 deletions bitmovin-analytics-datasource/src/components/GroupByInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Props = {
readonly groupBy: SelectableValue<QueryAttribute | QueryAdAttribute>;
readonly selectableGroupBys: Array<SelectableValue<QueryAttribute | QueryAdAttribute>>;
readonly onDelete: () => void;
readonly onChange: (newValue: SelectableValue<QueryAdAttribute | QueryAttribute>) => void;
readonly onChange: (newValue: QueryAdAttribute | QueryAttribute) => void;
readonly isFirst: boolean;
readonly isLast: boolean;
readonly onReorderGroupBy: (direction: REORDER_DIRECTION) => void;
Expand All @@ -26,7 +26,7 @@ export function GroupByInput(props: Props) {
<HorizontalGroup>
<Select
value={isEmpty(props.groupBy) ? undefined : props.groupBy}
onChange={props.onChange}
onChange={(selectableValue) => props.onChange(selectableValue.value!)}
options={props.selectableGroupBys}
width={30}
/>
Expand Down
Loading
Loading