Skip to content

Commit

Permalink
Feat/AN-4041 implement license and interval selection (#64)
Browse files Browse the repository at this point in the history
* implements license fetching and interval selection

* add tests for intervalUtils

* fix ceiling of timestamp for DAY interval

* add tests

* fix linting

* delete unnecessary export of enum

* rename MyQuery to BitmovinAnalyticsDataQuery

* add AnalyticsLicenseType
  • Loading branch information
MGJamJam authored May 6, 2024
1 parent 8edd6fc commit 182e924
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 62 deletions.
103 changes: 85 additions & 18 deletions bitmovin-analytics-datasource/src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,99 @@
import React, { ChangeEvent } from 'react';
import { InlineField, Input } from '@grafana/ui';
import { QueryEditorProps } from '@grafana/data';
import React, { ChangeEvent, useEffect, useState } from 'react';
import { FieldSet, InlineField, InlineSwitch, Select } from '@grafana/ui';
import { QueryEditorProps, SelectableValue } from '@grafana/data';

import { DataSource } from '../datasource';
import { MyDataSourceOptions, MyQuery } from '../types';
import { MyDataSourceOptions, BitmovinAnalyticsDataQuery } from '../types';
import { fetchLicenses } from '../utils/licenses';
import { DEFAULT_SELECTABLE_QUERY_INTERVAL, SELECTABLE_QUERY_INTERVALS } from '../utils/intervalUtils';

enum LoadingState {
Default = 'DEFAULT',
Loading = 'LOADING',
Success = 'SUCCESS',
Error = 'ERROR',
}

type Props = QueryEditorProps<DataSource, BitmovinAnalyticsDataQuery, MyDataSourceOptions>;

export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) {
const [selectableLicenses, setSelectableLicenses] = useState<SelectableValue[]>([]);
const [licenseLoadingState, setLicenseLoadingState] = useState<LoadingState>(LoadingState.Default);
const [licenseErrorMessage, setLicenseErrorMessage] = useState('');
const [isTimeSeries, setIsTimeSeries] = useState(true);

type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
useEffect(() => {
setLicenseLoadingState(LoadingState.Loading);
fetchLicenses(datasource.apiKey, datasource.baseUrl)
.then((licenses) => {
setSelectableLicenses(licenses);
setLicenseLoadingState(LoadingState.Success);
})
.catch((e) => {
setLicenseLoadingState(LoadingState.Error);
setLicenseErrorMessage(e.status + ' ' + e.statusText);
});
}, [datasource.apiKey, datasource.baseUrl]);

export function QueryEditor({ query, onChange, onRunQuery }: Props) {
const onQueryTextChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange({ ...query, queryText: event.target.value });
const onLicenseChange = (item: SelectableValue) => {
onChange({ ...query, licenseKey: item.value });
onRunQuery();
};

const onConstantChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange({ ...query, constant: parseFloat(event.target.value) });
// executes the query
const onFormatAsTimeSeriesChange = (event: ChangeEvent<HTMLInputElement>) => {
setIsTimeSeries(event.currentTarget.checked);
if (event.currentTarget.checked) {
onChange({ ...query, interval: 'AUTO' });
} else {
onChange({ ...query, interval: undefined });
}
onRunQuery();
};

const { queryText, constant } = query;
const onIntervalChange = (item: SelectableValue) => {
onChange({ ...query, interval: item.value });
onRunQuery();
};

const renderTimeSeriesOption = () => {
return (
<>
<InlineField label="Interval" labelWidth={20}>
<Select
defaultValue={DEFAULT_SELECTABLE_QUERY_INTERVAL}
onChange={(item) => onIntervalChange(item)}
width={40}
options={SELECTABLE_QUERY_INTERVALS}
/>
</InlineField>
</>
);
};

return (
<div className="gf-form">
<InlineField label="Constant">
<Input onChange={onConstantChange} value={constant} width={8} type="number" step="0.1" />
</InlineField>
<InlineField label="Query Text" labelWidth={16} tooltip="Not used yet">
<Input onChange={onQueryTextChange} value={queryText || ''} />
</InlineField>
<FieldSet>
<InlineField
label="License"
labelWidth={20}
invalid={licenseLoadingState === LoadingState.Error}
error={`Error when fetching Analytics Licenses: ${licenseErrorMessage}`}
disabled={licenseLoadingState === LoadingState.Error}
>
<Select
onChange={onLicenseChange}
width={40}
options={selectableLicenses}
noOptionsMessage="No Analytics Licenses found"
isLoading={licenseLoadingState === LoadingState.Loading}
placeholder={licenseLoadingState === LoadingState.Loading ? 'Loading Licenses' : 'Choose License'}
/>
</InlineField>
<InlineField label="Format as time series" labelWidth={20}>
<InlineSwitch value={isTimeSeries} onChange={onFormatAsTimeSeriesChange}></InlineSwitch>
</InlineField>
{isTimeSeries && renderTimeSeriesOption()}
</FieldSet>
</div>
);
}
60 changes: 33 additions & 27 deletions bitmovin-analytics-datasource/src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
import { getBackendSrv } from '@grafana/runtime';
import { catchError, lastValueFrom, map, Observable, of } from 'rxjs';

import { MixedDataRowList, MyDataSourceOptions, MyQuery, NumberDataRowList } from './types';
import { MixedDataRowList, MyDataSourceOptions, BitmovinAnalyticsDataQuery, NumberDataRowList } from './types';
import { transformGroupedTimeSeriesData, transformSimpleTimeSeries, transformTableData } from './utils/dataUtils';
import { QueryInterval } from './utils/intervalUtils';
import { calculateQueryInterval, QueryInterval } from './utils/intervalUtils';

type AnalyticsQuery = {
filters: Array<{ name: string; operator: string; value: number }>;
Expand All @@ -24,7 +24,7 @@ type AnalyticsQuery = {
interval?: QueryInterval;
};

export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
export class DataSource extends DataSourceApi<BitmovinAnalyticsDataQuery, MyDataSourceOptions> {
baseUrl: string;
apiKey: string;
tenantOrgId?: string;
Expand All @@ -48,34 +48,40 @@ export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
* - Interval is not set: All values up to the last one (not included) can be considered string values
* - The last value of each row is always be a number.
* */
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
async query(options: DataQueryRequest<BitmovinAnalyticsDataQuery>): Promise<DataQueryResponse> {
const { range } = options;
const from = new Date(range!.from.toDate().setSeconds(0, 0));
const from = range!.from.toDate();
const to = range!.to.toDate();

const query: AnalyticsQuery = {
filters: [
{
name: 'VIDEO_STARTUPTIME',
operator: 'GT',
value: 0,
},
],
groupBy: [],
orderBy: [
{
name: 'MINUTE',
order: 'DESC',
},
],
dimension: 'IMPRESSION_ID',
start: from,
end: to,
licenseKey: '',
interval: 'MINUTE',
};

const promises = options.targets.map(async (target) => {
const interval = target.interval
? calculateQueryInterval(target.interval!, from.getTime(), to.getTime())
: undefined;

const query: AnalyticsQuery = {
filters: [
{
name: 'VIDEO_STARTUPTIME',
operator: 'GT',
value: 0,
},
],
groupBy: [],
orderBy: interval
? [
{
name: interval,
order: 'DESC',
},
]
: [],
dimension: 'IMPRESSION_ID',
start: from,
end: to,
licenseKey: target.licenseKey,
interval: interval,
};

const response = await lastValueFrom(this.request(this.getRequestUrl(), 'POST', query));

const dataRows: MixedDataRowList = response.data.data.result.rows;
Expand Down
4 changes: 2 additions & 2 deletions bitmovin-analytics-datasource/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { DataSourcePlugin } from '@grafana/data';
import { DataSource } from './datasource';
import { ConfigEditor } from './components/ConfigEditor';
import { QueryEditor } from './components/QueryEditor';
import { MyQuery, MyDataSourceOptions } from './types';
import { BitmovinAnalyticsDataQuery, MyDataSourceOptions } from './types';

export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
export const plugin = new DataSourcePlugin<DataSource, BitmovinAnalyticsDataQuery, MyDataSourceOptions>(DataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor);
11 changes: 5 additions & 6 deletions bitmovin-analytics-datasource/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { DataSourceJsonData } from '@grafana/data';
import { DataQuery } from '@grafana/schema';
import { QueryInterval } from './utils/intervalUtils';

export interface MyQuery extends DataQuery {
queryText?: string;
constant: number;
export interface BitmovinAnalyticsDataQuery extends DataQuery {
licenseKey: string;
interval?: QueryInterval | 'AUTO';
}

export const DEFAULT_QUERY: Partial<MyQuery> = {
constant: 6.5,
};
export const DEFAULT_QUERY: Partial<BitmovinAnalyticsDataQuery> = {};

/**
* These are options configured for each DataSource instance
Expand Down
110 changes: 104 additions & 6 deletions bitmovin-analytics-datasource/src/utils/dataUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,150 @@ import {
import { FieldType } from '@grafana/data';

describe('padAndSortTimeSeries', () => {
it('should return sorted and padded data for simple time series data', () => {
it('should return sorted and padded data for simple time series data for MINUTE interval', () => {
//arrange
const data = [
[1712919540000, 1], //Friday, 12 April 2024 10:59:00
[1712919600000, 2], //Friday, 12 April 2024 11:00:00
[1712919720000, 5], //Friday, 12 April 2024 11:02:00
];

const startTimestamp = 1712919540000; //Friday, 12 April 2024 10:59:00
const startTimestamp = 1712919560000; //Friday, 12 April 2024 10:59:20
const endTimestamp = 1712919780000; //Friday, 12 April 2024 11:03:00

//act
const result = padAndSortTimeSeries(data, startTimestamp, endTimestamp, 'MINUTE');

//assert
expect(result).toEqual([
[1712919540000, 0],
[1712919600000, 2],
[1712919660000, 0],
[1712919720000, 5],
[1712919780000, 0],
]);
});

it('should return sorted and padded data for grouped time series data', () => {
it('should return sorted and padded data for simple time series data for HOUR Interval', () => {
//arrange
const data = [
[1712916000000, 1], //Friday, 12 April 2024 10:00:00
[1712919600000, 7], //Friday, 12 April 2024 11:00:00
[1712930400000, 5], //Friday, 12 April 2024 14:00:00
[1712934000000, 2], //Friday, 12 April 2024 15:00:00
];

const startTimestamp = 1712917444000; //Friday, 12 April 2024 10:24:04
const endTimestamp = 1712935444000; //Friday, 12 April 2024 15:24:04

//act
const result = padAndSortTimeSeries(data, startTimestamp, endTimestamp, 'HOUR');

//assert
expect(result).toEqual([
[1712919600000, 7], //Friday, 12 April 2024 11:00:00
[1712923200000, 0], //Friday, 12 April 2024 12:00:00
[1712926800000, 0], //Friday, 12 April 2024 13:00:00
[1712930400000, 5], //Friday, 12 April 2024 14:00:00
[1712934000000, 2], //Friday, 12 April 2024 15:00:00
]);
});

it('should return sorted and padded data for simple time series data for DAY interval', () => {
//arrange
const data = [
[1712917800000, 1], //Friday, 12 April 2024 10:30:00
[1713004200000, 2], //Saturday, 13 April 2024 10:30:00
[1713263400000, 5], //Tuesday, 16 April 2024 10:30:00
];

const startTimestamp = 1712919560000; //Friday, 12 April 2024 10:59:20
const endTimestamp = 1713351560000; //Wednesday, 17 April 2024 10:59:20

//act
const result = padAndSortTimeSeries(data, startTimestamp, endTimestamp, 'DAY');

//assert
expect(result).toEqual([
[1713004200000, 2], //Saturday, 13 April 2024 10:30:00
[1713090600000, 0], //Sunday, 14 April 2024 10:30:00
[1713177000000, 0], //Monday, 15 April 2024 10:30:00
[1713263400000, 5], //Tuesday, 16 April 2024 10:30:00
[1713349800000, 0], //Wednesday, 17 April 2024 10:30:00
]);
});

it('should return sorted and padded data for grouped time series data for MINUTE interval', () => {
//arrange
const data = [
[1712919540000, 'BROWSER', 'DEVICE_TYPE', 1], //Friday, 12 April 2024 10:59:00
[1712919600000, 'BROWSER', 'DEVICE_TYPE', 2], //Friday, 12 April 2024 11:00:00
[1712919720000, 'BROWSER', 'DEVICE_TYPE', 5], //Friday, 12 April 2024 11:02:00
];

const startTimestamp = 1712919540000; //Friday, 12 April 2024 10:59:00
const startTimestamp = 1712919560000; //Friday, 12 April 2024 10:59:20
const endTimestamp = 1712919780000; //Friday, 12 April 2024 11:03:00

//act
const result = padAndSortTimeSeries(data, startTimestamp, endTimestamp, 'MINUTE');

//assert
expect(result).toEqual([
[1712919540000, 'BROWSER', 'DEVICE_TYPE', 0],
[1712919600000, 'BROWSER', 'DEVICE_TYPE', 2],
[1712919660000, 'BROWSER', 'DEVICE_TYPE', 0],
[1712919720000, 'BROWSER', 'DEVICE_TYPE', 5],
[1712919780000, 'BROWSER', 'DEVICE_TYPE', 0],
]);
});

it('should return sorted and padded data for grouped time series data for HOUR interval', () => {
//arrange
const data = [
[1712916000000, 'BROWSER', 'DEVICE_TYPE', 1], //Friday, 12 April 2024 10:00:00
[1712919600000, 'BROWSER', 'DEVICE_TYPE', 7], //Friday, 12 April 2024 11:00:00
[1712930400000, 'BROWSER', 'DEVICE_TYPE', 5], //Friday, 12 April 2024 14:00:00
[1712934000000, 'BROWSER', 'DEVICE_TYPE', 2], //Friday, 12 April 2024 15:00:00
];

const startTimestamp = 1712917444000; //Friday, 12 April 2024 10:24:04
const endTimestamp = 1712935444000; //Friday, 12 April 2024 15:24:04

//act
const result = padAndSortTimeSeries(data, startTimestamp, endTimestamp, 'HOUR');

//assert
expect(result).toEqual([
[1712919600000, 'BROWSER', 'DEVICE_TYPE', 7], //Friday, 12 April 2024 11:00:00
[1712923200000, 'BROWSER', 'DEVICE_TYPE', 0], //Friday, 12 April 2024 12:00:00
[1712926800000, 'BROWSER', 'DEVICE_TYPE', 0], //Friday, 12 April 2024 13:00:00
[1712930400000, 'BROWSER', 'DEVICE_TYPE', 5], //Friday, 12 April 2024 14:00:00
[1712934000000, 'BROWSER', 'DEVICE_TYPE', 2], //Friday, 12 April 2024 15:00:00
]);
});

it('should return sorted and padded data for grouped time series data for DAY interval', () => {
//arrange
const data = [
[1712917800000, 'BROWSER', 'DEVICE_TYPE', 1], //Friday, 12 April 2024 10:30:00
[1713004200000, 'BROWSER', 'DEVICE_TYPE', 2], //Saturday, 13 April 2024 10:30:00
[1713263400000, 'BROWSER', 'DEVICE_TYPE', 5], //Tuesday, 16 April 2024 10:30:00
];

const startTimestamp = 1712919560000; //Friday, 12 April 2024 10:59:20
const endTimestamp = 1713351560000; //Wednesday, 17 April 2024 10:59:20

//act
const result = padAndSortTimeSeries(data, startTimestamp, endTimestamp, 'DAY');

//assert
expect(result).toEqual([
[1713004200000, 'BROWSER', 'DEVICE_TYPE', 2], //Saturday, 13 April 2024 10:30:00
[1713090600000, 'BROWSER', 'DEVICE_TYPE', 0], //Sunday, 14 April 2024 10:30:00
[1713177000000, 'BROWSER', 'DEVICE_TYPE', 0], //Monday, 15 April 2024 10:30:00
[1713263400000, 'BROWSER', 'DEVICE_TYPE', 5], //Tuesday, 16 April 2024 10:30:00
[1713349800000, 'BROWSER', 'DEVICE_TYPE', 0], //Wednesday, 17 April 2024 10:30:00
]);
});

it('should throw error when interval is not valid', () => {
//arrange
const data = [[0, 0]];
Expand Down
Loading

0 comments on commit 182e924

Please sign in to comment.