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-4041 implement license and interval selection #64

Merged
merged 8 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
},
Comment on lines +63 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe a comment why is this one filter included always by default ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's just temporary, I put it in for testing the editor. As soon as the filter selection implementation is ready this will be substituted by the filter selection value.

],
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
Loading