Skip to content

Commit

Permalink
makes filter, dimension, metric and limit compatible with old JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
MGJamJam committed Jun 25, 2024
1 parent e945c6a commit e6926f2
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 230 deletions.
10 changes: 5 additions & 5 deletions bitmovin-analytics-datasource/src/components/FilterRow.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand All @@ -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);
Expand Down
121 changes: 22 additions & 99 deletions bitmovin-analytics-datasource/src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -30,21 +29,25 @@ enum LoadingState {
Error = 'ERROR',
}

type Props = QueryEditorProps<DataSource, BitmovinAnalyticsDataQuery, BitmovinDataSourceOptions>;
type Props = QueryEditorProps<
DataSource,
BitmovinAnalyticsDataQuery | OldBitmovinAnalyticsDataQuery,

Check warning on line 34 in bitmovin-analytics-datasource/src/components/QueryEditor.tsx

View workflow job for this annotation

GitHub Actions / build

'OldBitmovinAnalyticsDataQuery' is deprecated. These are the options query options of the old Angular based plugin
BitmovinDataSourceOptions
>;

export function QueryEditor(props: Props) {
const query = defaults(props.query, DEFAULT_QUERY);
const [selectableLicenses, setSelectableLicenses] = useState<SelectableValue[]>([]);
const [licenseLoadingState, setLicenseLoadingState] = useState<LoadingState>(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(() => {
Expand All @@ -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();
};

Expand All @@ -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();
};
Expand All @@ -179,9 +102,9 @@ export function QueryEditor(props: Props) {
const handleFormatAsTimeSeriesChange = (event: ChangeEvent<HTMLInputElement>) => {
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();
};
Expand Down Expand Up @@ -251,7 +174,7 @@ export function QueryEditor(props: Props) {
{!isMetricSelected && (
<InlineField label="Metric" labelWidth={20} required>
<Select
value={query.queryAggregationMethod}
value={query.metric}
onChange={(item) => handleAggregationChange(item)}
width={30}
options={SELECTABLE_AGGREGATION_METHODS}
Expand Down Expand Up @@ -285,7 +208,7 @@ export function QueryEditor(props: Props) {
<FilterRow
isAdAnalytics={props.datasource.adAnalytics ? true : false}
onQueryFilterChange={handleQueryFilterChange}
filters={props.query.filter}
filters={query.filter}
/>
</InlineField>
<InlineField label="Group By" labelWidth={20}>
Expand Down
32 changes: 15 additions & 17 deletions bitmovin-analytics-datasource/src/components/QueryFilterInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React, { useEffect, useMemo, useState } from 'react';
import { HorizontalGroup, IconButton, Input, Select, Tooltip } from '@grafana/ui';

import { QueryFilter, QueryFilterOperator, SELECTABLE_QUERY_FILTER_OPERATORS } from '../types/queryFilter';
import { InputQueryFilter, QueryFilterOperator, SELECTABLE_QUERY_FILTER_OPERATORS } from '../types/queryFilter';
import type { SelectableValue } from '@grafana/data';
import { QueryAttribute, SELECTABLE_QUERY_ATTRIBUTES } from '../types/queryAttributes';
import { QueryAdAttribute, SELECTABLE_QUERY_AD_ATTRIBUTES } from '../types/queryAdAttributes';
import { differenceWith } from 'lodash';
import { convertFilterValueToProperType, mapQueryFilterValueToRawFilterValue } from 'utils/filterUtils';
import { convertFilterValueToProperType } from 'utils/filterUtils';

interface QueryFilterInputProps {
/** `undefined` when component is used to create new filter (no values yet) */
value: undefined | QueryFilter;
onChange(queryFilter: QueryFilter): void;
value: undefined | InputQueryFilter;
onChange(queryFilter: InputQueryFilter): void;
onDelete(): void;
isAdAnalytics: boolean;
/** Selected query filters are used to filter out used values from attribute select options */
selectedQueryFilters: QueryFilter[];
selectedQueryFilters: InputQueryFilter[];
}

export function QueryFilterInput(props: Readonly<QueryFilterInputProps>) {
Expand Down Expand Up @@ -64,7 +64,7 @@ export function QueryFilterInput(props: Readonly<QueryFilterInputProps>) {
setDerivedQueryFilterState((prevState) => ({
...prevState,
dirty: true,
inputValue: value,
value: value,
inputValueError: undefined,
}));
}
Expand All @@ -91,8 +91,8 @@ export function QueryFilterInput(props: Readonly<QueryFilterInputProps>) {
}

try {
const validQueryFilterValue = convertFilterValueToProperType(
derivedQueryFilterState.inputValue,
convertFilterValueToProperType(
derivedQueryFilterState.value!,
derivedQueryFilterState.attribute!,
derivedQueryFilterState.operator!,
props.isAdAnalytics
Expand All @@ -101,7 +101,7 @@ export function QueryFilterInput(props: Readonly<QueryFilterInputProps>) {
props.onChange({
name: derivedQueryFilterState.attribute!,
operator: derivedQueryFilterState.operator!,
value: validQueryFilterValue,
value: derivedQueryFilterState.value!,
});
} catch (e: unknown) {
setDerivedQueryFilterState((prevState) => ({
Expand Down Expand Up @@ -151,7 +151,7 @@ export function QueryFilterInput(props: Readonly<QueryFilterInputProps>) {
theme="error"
>
<Input
value={derivedQueryFilterState.inputValue}
value={derivedQueryFilterState.value}
onChange={(e) => handleInputValueChange(e.currentTarget.value)}
invalid={derivedQueryFilterState.inputValueError != null}
type="text"
Expand Down Expand Up @@ -185,33 +185,31 @@ 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,
operator: queryFilter?.operator,
operatorError: undefined,
value: queryFilter?.value,
dirty: false,
inputValue: mapQueryFilterValueToRawFilterValue(queryFilter?.value ?? null),
inputValueError: undefined,
};
}

function buildAttributeSelectableValues(
usedQueryFilters: QueryFilter[],
usedQueryFilters: InputQueryFilter[],
isAdAnalytics: boolean
): Array<SelectableValue<QueryAttribute | QueryAdAttribute>> {
const ALL_ATTRIBUTES: Array<SelectableValue<QueryAttribute | QueryAdAttribute>> = isAdAnalytics
Expand Down
Loading

0 comments on commit e6926f2

Please sign in to comment.