Skip to content

Commit

Permalink
Feat/AN-4128 make new data query compatible with old example dashboar…
Browse files Browse the repository at this point in the history
…d json (#78)

* adapt license, metric, dimension, filter and alias to work with old query type

* add migration for filters and table/timeseries options

* lint

* lint

* add type predicates for OldBitmovinAnalyticsDataQuery

* implement percentile input

* renames percentile to percentileValue

* separates metric, query aggregations and query attributes

* change metric values to lower case

* makes filter, dimension, metric and limit compatible with old JSON

* rename QueryFilter types
  • Loading branch information
MGJamJam authored Jun 26, 2024
1 parent e612f20 commit 2e33db7
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 222 deletions.
119 changes: 53 additions & 66 deletions bitmovin-analytics-datasource/example_dashboard.json

Large diffs are not rendered by default.

103 changes: 69 additions & 34 deletions bitmovin-analytics-datasource/src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { FieldSet, InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
import { FieldSet, HorizontalGroup, InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
import type { QueryEditorProps, SelectableValue } from '@grafana/data';
import { defaults } from 'lodash';

import { DataSource } from '../datasource';
import { BitmovinDataSourceOptions, BitmovinAnalyticsDataQuery, DEFAULT_QUERY } from '../types/grafanaTypes';
import {
BitmovinDataSourceOptions,
BitmovinAnalyticsDataQuery,
DEFAULT_QUERY,
OldBitmovinAnalyticsDataQuery,
} from '../types/grafanaTypes';
import { fetchLicenses } from '../utils/licenses';
import { DEFAULT_SELECTABLE_QUERY_INTERVAL, SELECTABLE_QUERY_INTERVALS } from '../utils/intervalUtils';
import { SELECTABLE_AGGREGATIONS } from '../types/aggregations';
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, SELECTABLE_METRICS } from '../types/metric';
Expand All @@ -24,16 +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 isDimensionMetricSelected = useMemo(() => {
return props.query.metric !== undefined;
}, [props.query.metric]);
const [isTimeSeries, setIsTimeSeries] = useState(query.resultFormat === 'time_series');
const [percentileValue, setPercentileValue] = useState(query.percentileValue);
const isMetricSelected = useMemo(() => {
return query.dimension ? isMetric(query.dimension) : false;
}, [query.dimension]);
const isPercentileSelected = useMemo(() => {
return query.metric === 'percentile';
}, [query.metric]);

/** Fetch Licenses */
useEffect(() => {
Expand All @@ -49,24 +63,18 @@ export function QueryEditor(props: Props) {
});
}, [props.datasource.apiKey, props.datasource.baseUrl]);

const query = defaults(props.query, DEFAULT_QUERY);

const handleLicenseChange = (item: SelectableValue) => {
props.onChange({ ...query, licenseKey: item.value });
props.onChange({ ...query, license: item.value });
props.onRunQuery();
};

const handleAggregationChange = (item: SelectableValue) => {
props.onChange({ ...query, aggregation: item.value, metric: undefined });
props.onChange({ ...query, metric: item.value });
props.onRunQuery();
};

const handleDimensionChange = (item: SelectableValue) => {
if (isMetric(item.value)) {
props.onChange({ ...query, aggregation: 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 @@ -81,7 +89,7 @@ export function QueryEditor(props: Props) {
};

const handleQueryFilterChange = (newFilters: QueryFilter[]) => {
props.onChange({ ...query, filters: newFilters });
props.onChange({ ...query, filter: newFilters });
props.onRunQuery();
};

Expand All @@ -94,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 All @@ -107,7 +115,22 @@ export function QueryEditor(props: Props) {
};

const handleAliasByBlur = (event: ChangeEvent<HTMLInputElement>) => {
props.onChange({ ...query, aliasBy: event.target.value });
props.onChange({ ...query, alias: event.target.value });
props.onRunQuery();
};

const handlePercentileValueChange = (event: ChangeEvent<HTMLInputElement>) => {
let percentile = parseInt(event.target.value, 10);
if (percentile < 0) {
percentile = 0;
} else if (percentile > 99) {
percentile = 99;
}
setPercentileValue(percentile);
};

const handlePercentileBlur = () => {
props.onChange({ ...query, percentileValue: percentileValue });
props.onRunQuery();
};

Expand Down Expand Up @@ -138,7 +161,7 @@ export function QueryEditor(props: Props) {
required
>
<Select
value={query.licenseKey}
value={query.license}
onChange={handleLicenseChange}
width={30}
options={selectableLicenses}
Expand All @@ -147,19 +170,31 @@ export function QueryEditor(props: Props) {
placeholder={licenseLoadingState === LoadingState.Loading ? 'Loading Licenses' : 'Choose License'}
/>
</InlineField>
{!isDimensionMetricSelected && (
<InlineField label="Metric" labelWidth={20} required>
<Select
value={query.aggregation}
onChange={(item) => handleAggregationChange(item)}
width={30}
options={SELECTABLE_AGGREGATIONS}
<HorizontalGroup spacing="xs">
{!isMetricSelected && (
<InlineField label="Metric" labelWidth={20} required>
<Select
value={query.metric}
onChange={(item) => handleAggregationChange(item)}
width={30}
options={SELECTABLE_AGGREGATION_METHODS}
/>
</InlineField>
)}
{isPercentileSelected && (
<Input
value={percentileValue}
onChange={handlePercentileValueChange}
onBlur={handlePercentileBlur}
type="number"
placeholder="value"
width={10}
/>
</InlineField>
)}
)}
</HorizontalGroup>
<InlineField label="Dimension" labelWidth={20} required>
<Select
value={query.dimension || query.metric}
value={query.dimension}
onChange={handleDimensionChange}
width={30}
options={
Expand All @@ -173,7 +208,7 @@ export function QueryEditor(props: Props) {
<FilterRow
isAdAnalytics={props.datasource.adAnalytics ? true : false}
onQueryFilterChange={handleQueryFilterChange}
filters={props.query.filters}
filters={query.filter}
/>
</InlineField>
<InlineField label="Group By" labelWidth={20}>
Expand All @@ -198,7 +233,7 @@ export function QueryEditor(props: Props) {
</InlineField>
{isTimeSeries && renderTimeSeriesOption()}
<InlineField label="Alias By" labelWidth={20}>
<Input defaultValue={query.aliasBy} placeholder="Naming pattern" onBlur={handleAliasByBlur} />
<Input defaultValue={query.alias} placeholder="Naming pattern" onBlur={handleAliasByBlur} />
</InlineField>
</FieldSet>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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) */
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 @@ -192,7 +192,6 @@ type DerivedQueryFilterState = {
value: undefined | QueryFilter['value'];
/** `true` if some values have been changed by inputs */
dirty: boolean;
inputValue: string;
/** `undefined` when input value is valid */
inputValueError: undefined | string;
};
Expand All @@ -205,7 +204,6 @@ function buildInitialDerivedQueryFilterState(queryFilter: undefined | QueryFilte
operatorError: undefined,
value: queryFilter?.value,
dirty: false,
inputValue: mapQueryFilterValueToRawFilterValue(queryFilter?.value ?? null),
inputValueError: undefined,
};
}
Expand Down
82 changes: 63 additions & 19 deletions bitmovin-analytics-datasource/src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,27 +26,32 @@ import {
transformTableData,
} from './utils/dataUtils';
import { calculateQueryInterval, QueryInterval } from './utils/intervalUtils';
import { Metric } from './types/metric';
import { Aggregation } from './types/aggregations';
import { QueryFilter } from './types/queryFilter';
import { isMetric, Metric } from './types/metric';
import { AggregationMethod } from './types/aggregationMethod';
import { ProperTypedQueryFilter } 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: ProperTypedQueryFilter[];
groupBy: Array<QueryAttribute | QueryAdAttribute>;
orderBy: QueryOrderBy[];
dimension?: QueryAttribute | QueryAdAttribute;
metric?: Metric;
interval?: QueryInterval;
limit?: number;
percentile?: number;
};

export class DataSource extends DataSourceApi<BitmovinAnalyticsDataQuery, BitmovinDataSourceOptions> {
export class DataSource extends DataSourceApi<
BitmovinAnalyticsDataQuery | OldBitmovinAnalyticsDataQuery,

Check warning on line 52 in bitmovin-analytics-datasource/src/datasource.ts

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
> {
baseUrl: string;
apiKey: string;
tenantOrgId?: string;
Expand Down Expand Up @@ -78,26 +88,47 @@ export class DataSource extends DataSourceApi<BitmovinAnalyticsDataQuery, Bitmov
const enabledQueries = (options.targets = filter(options.targets, (t) => !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: ProperTypedQueryFilter[] = 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.filters,
filters: filters,
groupBy: target.groupBy,
orderBy: target.orderBy,
dimension: target.dimension,
metric: target.metric,
dimension: dimension,
metric: metric,
start: from,
end: to,
licenseKey: target.licenseKey,
licenseKey: target.license,
interval: interval,
limit: target.limit,
limit: this.parseLimit(target.limit),
percentile: percentileValue,
};

const response = await lastValueFrom(
this.request(this.getRequestUrl(target.metric, target.aggregation), '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;
Expand Down Expand Up @@ -138,7 +169,7 @@ export class DataSource extends DataSourceApi<BitmovinAnalyticsDataQuery, Bitmov
}

return createDataFrame({
name: target.aliasBy,
name: target.alias,
fields: fields,
meta: { notices: metaNotices },
});
Expand All @@ -147,7 +178,20 @@ export class DataSource extends DataSourceApi<BitmovinAnalyticsDataQuery, Bitmov
return Promise.all(promises).then((data) => ({ data }));
}

getRequestUrl(metric?: Metric, aggregation?: Aggregation): string {
/** 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) {
url += '/ads';
Expand Down
Loading

0 comments on commit 2e33db7

Please sign in to comment.