Skip to content

Commit

Permalink
feat(asset): add feature flag support (#79)
Browse files Browse the repository at this point in the history
Co-authored-by: NI\akerezsi <[email protected]>
  • Loading branch information
kkerezsi and alexkerezsini authored Oct 14, 2024
1 parent ad5d9e0 commit de4ee90
Show file tree
Hide file tree
Showing 23 changed files with 290 additions and 88 deletions.
5 changes: 3 additions & 2 deletions src/core/DataSourceBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import {
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
} from '@grafana/data';
import { BackendSrv, BackendSrvRequest, TemplateSrv, isFetchError } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { QuerySystemsResponse, QuerySystemsRequest, Workspace } from './types';
import { sleep } from './utils';
import { lastValueFrom } from 'rxjs';

export abstract class DataSourceBase<TQuery extends DataQuery> extends DataSourceApi<TQuery> {
export abstract class DataSourceBase<TQuery extends DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData> extends DataSourceApi<TQuery, TOptions> {
constructor(
readonly instanceSettings: DataSourceInstanceSettings,
readonly instanceSettings: DataSourceInstanceSettings<TOptions>,
readonly backendSrv: BackendSrv,
readonly templateSrv: TemplateSrv
) {
Expand Down
4 changes: 2 additions & 2 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function throwIfNullish<T>(value: T, error: string | Error): NonNullable<
}

/** Gets available workspaces as an array of {@link SelectableValue}. */
export function useWorkspaceOptions<DSType extends DataSourceBase<any>>(datasource: DSType) {
export function useWorkspaceOptions<DSType extends DataSourceBase<any, any>>(datasource: DSType) {
return useAsync(async () => {
const workspaces = await datasource.getWorkspaces();
const workspaceOptions = workspaces.map(w => ({ label: w.name, value: w.id }));
Expand All @@ -45,7 +45,7 @@ export function useWorkspaceOptions<DSType extends DataSourceBase<any>>(datasour
}

/** Gets Dashboard variables as an array of {@link SelectableValue}. */
export function getVariableOptions<DSType extends DataSourceBase<any>>(datasource: DSType) {
export function getVariableOptions<DSType extends DataSourceBase<any, any>>(datasource: DSType) {
return datasource.templateSrv
.getVariables()
.filter((variable: any) => !variable.datasource || variable.datasource.uid !== datasource.uid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
DataFrameDTO,
DataQueryRequest,
DataSourceInstanceSettings,
DataSourceJsonData,
FieldDTO,
TestDataSourceResponse,
} from '@grafana/data';
Expand All @@ -26,7 +27,7 @@ import { QueryBuilderOperations } from 'core/query-builder.constants';
import { Workspace } from 'core/types';
import { parseErrorMessage } from 'core/errors';

export class AssetCalibrationDataSource extends DataSourceBase<AssetCalibrationQuery> {
export class AssetCalibrationDataSource extends DataSourceBase<AssetCalibrationQuery, DataSourceJsonData> {
public defaultQuery = {
groupBy: [],
filter: ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ type Props = QueryEditorProps<AssetCalibrationDataSource, AssetCalibrationQuery>

export const AssetCalibrationQueryEditor = ({ query, onChange, onRunQuery, datasource }: Props) => {
query = datasource.prepareQuery(query) as AssetCalibrationQuery;

const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [systems, setSystems] = useState<SystemMetadata[]>([]);
const [areDependenciesLoaded, setAreDependenciesLoaded] = useState<boolean>(false);
Expand Down
62 changes: 62 additions & 0 deletions src/datasources/asset/AssetConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* AssetConfigEditor is a React component that implements the UI for editing the asset
* datasource configuration options.
*/
import React, { ChangeEvent, useCallback } from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { DataSourceHttpSettings, InlineField, InlineSegmentGroup, InlineSwitch, Tag, Text } from '@grafana/ui';
import { AssetDataSourceOptions, AssetFeatureTogglesDefaults } from './types/types';

interface Props extends DataSourcePluginOptionsEditorProps<AssetDataSourceOptions> { }

export const AssetConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
const handleFeatureChange = useCallback((featureKey: string) => (event: ChangeEvent<HTMLInputElement>) => {
const jsonData = {
...options.jsonData,
...{ featureToggles: { ...options.jsonData.featureToggles, [featureKey]: event.target.checked } }
};
onOptionsChange({ ...options, jsonData });
}, [options, onOptionsChange]);

return (
<>
<DataSourceHttpSettings
defaultUrl=""
dataSourceConfig={options}
showAccessOptions={false}
onChange={onOptionsChange}
/>
<>
<div style={{ paddingBottom: "10px" }}>
<Text element="h6">
Features
</Text>
</div>
<InlineSegmentGroup>
<InlineField label="Asset list" labelWidth={25}>
<InlineSwitch
value={options.jsonData?.featureToggles?.assetList ?? AssetFeatureTogglesDefaults.assetList}
onChange={handleFeatureChange('assetList')} />
</InlineField>
<Tag name='Beta' colorIndex={5} />
</InlineSegmentGroup>
<InlineSegmentGroup>
<InlineField label="Calibration forecast" labelWidth={25}>
<InlineSwitch
value={options.jsonData?.featureToggles?.calibrationForecast ?? AssetFeatureTogglesDefaults.calibrationForecast}
onChange={handleFeatureChange('calibrationForecast')} />
</InlineField>
<Tag name='Beta' colorIndex={5} />
</InlineSegmentGroup>
<InlineSegmentGroup>
<InlineField label="Asset summary" labelWidth={25}>
<InlineSwitch
value={options.jsonData?.featureToggles?.assetSummary ?? AssetFeatureTogglesDefaults.assetSummary}
onChange={handleFeatureChange('assetSummary')} />
</InlineField>
<Tag name='Beta' colorIndex={5} />
</InlineSegmentGroup>
</>
</>
);
}
7 changes: 6 additions & 1 deletion src/datasources/asset/AssetDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ import { AssetPresenceWithSystemConnectionModel, AssetsResponse } from "datasour
import { ListAssetsQuery } from "./types/ListAssets.types";

let ds: AssetDataSource, backendSrv: MockProxy<BackendSrv>
let assetOptions = {
assetListEnabled: true,
calibrationForecastEnabled: true,
assetSummaryEnabled: true,
}

beforeEach(() => {
[ds, backendSrv] = setupDataSource(AssetDataSource);
[ds, backendSrv] = setupDataSource(AssetDataSource, () => assetOptions);
});

const assetsResponseMock: AssetsResponse =
Expand Down
19 changes: 10 additions & 9 deletions src/datasources/asset/AssetDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ import {
import { BackendSrv, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { DataSourceBase } from 'core/DataSourceBase';
import {
AssetDataSourceOptions,
AssetQuery,
AssetQueryType,
} from './types/types';
import { ListAssetsDataSource } from './components/editors/list-assets/ListAssetsDataSource';
import { CalibrationForecastDataSource } from './components/editors/calibration-forecast/CalibrationForecastDataSource';
import { AssetSummaryDataSource } from './components/editors/asset-summary/AssetSummaryDataSource';
import { defaultAssetQuery, defaultAssetQueryType } from './defaults';
import { AssetSummaryQuery } from './types/AssetSummaryQuery.types';
import { CalibrationForecastQuery } from './types/CalibrationForecastQuery.types';
import { ListAssetsQuery } from './types/ListAssets.types';

export class AssetDataSource extends DataSourceBase<AssetQuery> {
export class AssetDataSource extends DataSourceBase<AssetQuery, AssetDataSourceOptions> {
private assetSummaryDataSource: AssetSummaryDataSource;
private calibrationForecastDataSource: CalibrationForecastDataSource;
private listAssetsDataSource: ListAssetsDataSource;

constructor(
readonly instanceSettings: DataSourceInstanceSettings,
readonly instanceSettings: DataSourceInstanceSettings<AssetDataSourceOptions>,
readonly backendSrv: BackendSrv = getBackendSrv(),
readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
Expand All @@ -37,32 +37,33 @@ export class AssetDataSource extends DataSourceBase<AssetQuery> {
baseUrl = this.instanceSettings.url + '/niapm/v1';

defaultQuery = {
queryType: defaultAssetQueryType,
...defaultAssetQuery
queryType: AssetQueryType.None,
};

async runQuery(query: AssetQuery, options: DataQueryRequest): Promise<DataFrameDTO> {
if (query.queryType === AssetQueryType.AssetSummary) {
return this.getAssetSummarySource().runQuery(query as AssetSummaryQuery, options);
}
else if (query.queryType === AssetQueryType.CalibrationForecast) {
if (query.queryType === AssetQueryType.CalibrationForecast) {
return this.getCalibrationForecastSource().runQuery(query as CalibrationForecastQuery, options);
}
else {
if (query.queryType === AssetQueryType.ListAssets) {
return this.getListAssetsSource().runQuery(query as ListAssetsQuery, options);
}
throw new Error('Unknown query type');
}

shouldRunQuery(query: AssetQuery): boolean {
if (query.queryType === AssetQueryType.AssetSummary) {
return this.getAssetSummarySource().shouldRunQuery(query as AssetSummaryQuery);
}
else if (query.queryType === AssetQueryType.CalibrationForecast) {
if (query.queryType === AssetQueryType.CalibrationForecast) {
return this.getCalibrationForecastSource().shouldRunQuery(query as CalibrationForecastQuery);
}
else {
if (query.queryType === AssetQueryType.ListAssets) {
return this.getListAssetsSource().shouldRunQuery(query as ListAssetsQuery);
}
return false;
}

async testDatasource(): Promise<TestDataSourceResponse> {
Expand Down
90 changes: 90 additions & 0 deletions src/datasources/asset/components/AssetQueryEditor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import { SystemMetadata } from '../../system/types';
import { AssetDataSource } from '../AssetDataSource';
import { setupRenderer } from '../../../test/fixtures';
import { ListAssetsDataSource } from './editors/list-assets/ListAssetsDataSource';
import { AssetSummaryDataSource } from './editors/asset-summary/AssetSummaryDataSource';
import { CalibrationForecastDataSource } from './editors/calibration-forecast/CalibrationForecastDataSource';
import { AssetQueryEditor } from './AssetQueryEditor';
import { ListAssetsQuery } from '../types/ListAssets.types';
import { CalibrationForecastQuery } from '../types/CalibrationForecastQuery.types';
import { select } from 'react-select-event';
import { AssetSummaryQuery } from '../types/AssetSummaryQuery.types';
import { AssetFeatureTogglesDefaults } from '../types/types';

const fakeSystems: SystemMetadata[] = [
{
id: '1',
state: 'CONNECTED',
workspace: '1',
},
{
id: '2',
state: 'CONNECTED',
workspace: '2',
},
];

let assetDatasourceOptions = {
featureToggles: { ...AssetFeatureTogglesDefaults }
}

class FakeAssetsSource extends ListAssetsDataSource {
querySystems(filter?: string, projection?: string[]): Promise<SystemMetadata[]> {
return Promise.resolve(fakeSystems);
}
}

class FakeAssetDataSource extends AssetDataSource {
getCalibrationForecastSource(): CalibrationForecastDataSource {
return new CalibrationForecastDataSource(this.instanceSettings, this.backendSrv, this.templateSrv);
}
getAssetSummarySource(): AssetSummaryDataSource {
return new AssetSummaryDataSource(this.instanceSettings, this.backendSrv, this.templateSrv);
}
getListAssetsSource(): ListAssetsDataSource {
return new FakeAssetsSource(this.instanceSettings, this.backendSrv, this.templateSrv);
}
}

const workspacesLoaded = () => waitForElementToBeRemoved(screen.getByTestId('Spinner'));
const render = setupRenderer(AssetQueryEditor, FakeAssetDataSource, () => assetDatasourceOptions);

beforeEach(() => {
assetDatasourceOptions = {
featureToggles: { ...AssetFeatureTogglesDefaults }
};
});

it('renders Asset list when feature is enabled', async () => {
assetDatasourceOptions.featureToggles.assetList = true;
render({} as ListAssetsQuery);
await workspacesLoaded();

expect(screen.getAllByRole('combobox').length).toBe(3);
expect(screen.getAllByRole('combobox')[1]).toHaveAccessibleDescription('Any workspace');
expect(screen.getAllByRole('combobox')[2]).toHaveAccessibleDescription('Select systems');
});

it('does not render when Asset list feature is not enabled', async () => {
assetDatasourceOptions.featureToggles.assetList = false;
render({} as ListAssetsQuery);

expect(screen.getAllByRole('combobox').length).toBe(1);
});

it('does not render when Asset calibration forecast feature is not enabled', async () => {
assetDatasourceOptions.featureToggles.calibrationForecast = true;
render({} as CalibrationForecastQuery);
const queryType = screen.getAllByRole('combobox')[0];
await select(queryType, "Calibration Forecast", { container: document.body });
expect(screen.getAllByText("Calibration Forecast").length).toBe(2)
});

it('does not render when Asset summary feature is not enabled', async () => {
assetDatasourceOptions.featureToggles.assetSummary = true;
render({} as AssetSummaryQuery);
const queryType = screen.getAllByRole('combobox')[0];
await select(queryType, "Asset Summary", { container: document.body });
expect(screen.getAllByText("Asset Summary").length).toBe(2)
});
Loading

0 comments on commit de4ee90

Please sign in to comment.