Skip to content
Open
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
111 changes: 99 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SystemLink Grafana Plugins

[![Push to
main](https://github.com/ni/systemlink-grafana-plugins/actions/workflows/push.yml/badge.svg)](https://github.com/ni/systemlink-grafana-plugins/actions/workflows/push.yml)

Expand Down Expand Up @@ -80,6 +81,92 @@ You should now be able to use the data source when building a dashboard or in
the Explore mode. Navigating to Explore is the easiest way to begin testing the
plugin.

### Feature Toggle implementation

The SystemLink Grafana plugins now support a feature flag infrastructure to enable or disable specific features dynamically. This implementation is inspired by the feature flag system used in SLE Angular Apps and is designed to work seamlessly in both development and production environments. Features can be toggled on or off without requiring a redeployment.

#### Steps to Add a New Feature Toggle

**Define the Feature Toggle**

1. Add new entry to the `FeatureToggleNames` enum present in [feature-toggle.ts](/src/core/feature-toggle.ts).

```ts
export enum FeatureToggleNames {
locations = 'locations',
newFeature = 'newFeature', // Add your new feature toggle here
}
```

2. Add the default configuration for the new feature toggle in the `FeatureTogglesDefaults` object.

```ts
export const FeatureTogglesDefaults: Record<FeatureToggleNames, FeatureToggle> = {
[FeatureToggleNames.locations]: {
name: 'locations',
enabledByDefault: false,
description: 'Enables location support in Asset queries.',
},
// Add your new feature toggle configuration
[FeatureToggleNames.newFeature]: {
name: 'newFeature',
enabledByDefault: false,
description: 'Description of the new feature.',
},
};
```

**Use the Feature Flag in Code**

1. Use the getFeatureFlagValue function to check if the feature is enabled before executing the related logic.

```ts
const isNewFeatureEnabled = getFeatureFlagValue(this.instanceSettings.jsonData, FeatureToggleNames.newFeature);

if (isNewFeatureEnabled) {
// Execute logic for the new feature
} else {
// Fallback logic
}
```

**Update the Data Source Configuration UI (Optional)**

1. This step is needed only if you want to toggle the feature flag from the data source configuration UI.
2. Open `ConfigEditor.tsx` file of your data source and add a new `InlineField` for the feature flag in the **Features** section:

```ts
<InlineSegmentGroup>
<InlineField label="New Feature" labelWidth={25}>
<InlineSwitch
value={options.jsonData?.featureToggles?.newFeature ?? FeatureTogglesDefaults.newFeature.enabledByDefault}
onChange={handleFeatureChange('newFeature')}
/>
</InlineField>
<Tag name="Beta" colorIndex={5} />
</InlineSegmentGroup>
```

**Test the Feature Flag**

1. Testing from local storage
1. Open the inspect mode and go to the Application tab. You can update the boolean value of the feature toggle.
2. Check whether the feature is enabled in the data source when building a dashboard.
2. Testing from data source configuration UI
1. Navigate to the data sources configuration page (/datasources). You can get there by clicking the gear icon in the sidebar.
2. Select the plugin in the list and click on it to enter the data source settings view.
3. If you have created `ConfigEditor.tsx`, you can see the **Features** section in the configuration UI.
4. Toggle a feature and click **Save & test**.
5. Check whether the feature is enabled in the data source when building a dashboard.

> **Note:**
> When adding a new feature flag, it is recommended to set `enabledByDefault: false` initially and use local storage for testing purposes. This ensures that the feature is only accessible to developers or testers during the development phase.
> Once the feature is released to customers and deemed stable, you should either:
>
> - Remove the feature flag from the source code entirely, or
> - Set enabledByDefault: true to make the feature permanently available.
> This approach helps maintain clean and maintainable code while ensuring a smooth feature rollout process.

### Testing

If you followed the steps above, a live reload script was injected into the
Expand Down Expand Up @@ -121,17 +208,17 @@ optional.

`<type>` must be one of the following:

| Type | When to use | Automatic version bump |
| --- | --- | --- |
| `build` | Changes that affect the build system or external dependencies | None |
| `ci` | Changes to our CI configuration files and scripts | None |
| `docs` | Documentation only changes | None |
| `feat` | A new feature | Minor |
| `fix` | A bug fix | Maintenance |
| `perf`| A code change that improves performance | None |
| `refactor`| A code change that neither fixes a bug nor adds a feature | None |
| `test`| Adding missing tests or correcting existing tests | None |
| `chore` | Changes that don't fit into the above categories | None |
| Type | When to use | Automatic version bump |
| ---------- | ------------------------------------------------------------- | ---------------------- |
| `build` | Changes that affect the build system or external dependencies | None |
| `ci` | Changes to our CI configuration files and scripts | None |
| `docs` | Documentation only changes | None |
| `feat` | A new feature | Minor |
| `fix` | A bug fix | Maintenance |
| `perf` | A code change that improves performance | None |
| `refactor` | A code change that neither fixes a bug nor adds a feature | None |
| `test` | Adding missing tests or correcting existing tests | None |
| `chore` | Changes that don't fit into the above categories | None |

For example, if you're making a bug fix to the [Data
frame](src/datasources/data-frame/) plugin, your PR title (and therefore the
Expand Down Expand Up @@ -175,4 +262,4 @@ follow the instructions.
### Helpful links

- [Grafana plugin developer's
guide](https://grafana.com/docs/grafana/latest/developers/plugins/)
guide](https://grafana.com/docs/grafana/latest/developers/plugins/)
1 change: 1 addition & 0 deletions provisioning/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ apiVersion: 1

config: &config
access: proxy
editable: true
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added this configuration to enable the Save & test option for local development purposes.
image

url: MY_SYSTEMLINK_API_URL
jsonData:
httpHeaderName1: 'x-ni-api-key'
Expand Down
62 changes: 62 additions & 0 deletions src/core/feature-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DataSourceJsonData } from "@grafana/data";

interface FeatureToggle {
name: string;
enabledByDefault: boolean;
description?: string;
}

export interface FeatureToggleDataSourceOptions extends DataSourceJsonData {
featureToggles: { [key: string]: boolean };
}

export enum FeatureToggleNames {
assetList = 'assetList',
calibrationForecast = 'calibrationForecast',
assetSummary = 'assetSummary',
locations = 'locations'
}

export const FeatureTogglesDefaults: Record<FeatureToggleNames, FeatureToggle> = {
[FeatureToggleNames.assetList]: {
name: 'assetList',
enabledByDefault: true,
description: 'Enables the Asset List query type.'
},
[FeatureToggleNames.calibrationForecast]: {
name: 'calibrationForecast',
enabledByDefault: true,
description: 'Enables the Calibration Forecast query type.'
},
[FeatureToggleNames.assetSummary]: {
name: 'assetSummary',
enabledByDefault: true,
description: 'Enables the Asset Summary query type.'
},
[FeatureToggleNames.locations]: {
name: 'locations',
enabledByDefault: false,
description: 'Enables location support in Asset queries.'
}
};

export function getFeatureFlagValue(options: FeatureToggleDataSourceOptions, flagName: FeatureToggleNames): boolean {
// Check if the feature flag is set in the datasource options.
const optionValue = options?.featureToggles && options?.featureToggles[flagName];
if (optionValue !== undefined && optionValue) {
return optionValue;
}

// Check if the feature flag is set in local storage.
const localValue = localStorage.getItem(`${flagName}`);
if (localValue !== null) {
return localValue === 'true';
}

// If not set in options or local storage, use the default value and set it to local storage.
localStorage.setItem(
FeatureTogglesDefaults[flagName].name,
FeatureTogglesDefaults[flagName].enabledByDefault.toString()
);
return FeatureTogglesDefaults[flagName].enabledByDefault;
}
10 changes: 5 additions & 5 deletions src/datasources/asset/AssetConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
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';
import { FeatureToggleDataSourceOptions, FeatureTogglesDefaults } from 'core/feature-toggle';

interface Props extends DataSourcePluginOptionsEditorProps<AssetDataSourceOptions> { }
interface Props extends DataSourcePluginOptionsEditorProps<FeatureToggleDataSourceOptions> { }

export const AssetConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
const handleFeatureChange = useCallback((featureKey: string) => (event: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -35,23 +35,23 @@ export const AssetConfigEditor: React.FC<Props> = ({ options, onOptionsChange })
<InlineSegmentGroup>
<InlineField label="Asset list" labelWidth={25}>
<InlineSwitch
value={options.jsonData?.featureToggles?.assetList ?? AssetFeatureTogglesDefaults.assetList}
value={options.jsonData?.featureToggles?.assetList ?? FeatureTogglesDefaults.assetList.enabledByDefault}
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}
value={options.jsonData?.featureToggles?.calibrationForecast ?? FeatureTogglesDefaults.calibrationForecast.enabledByDefault}
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}
value={options.jsonData?.featureToggles?.assetSummary ?? FeatureTogglesDefaults.assetSummary.enabledByDefault}
onChange={handleFeatureChange('assetSummary')} />
</InlineField>
<Tag name='Beta' colorIndex={5} />
Expand Down
6 changes: 3 additions & 3 deletions src/datasources/asset/AssetDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { BackendSrv, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { DataSourceBase } from 'core/DataSourceBase';
import {
AssetDataSourceOptions,
AssetQuery,
AssetQueryType,
AssetQueryReturnType
Expand All @@ -25,15 +24,16 @@ import { transformComputedFieldsQuery } from 'core/query-builder.utils';
import { AssetVariableQuery } from './types/AssetVariableQuery.types';
import { defaultListAssetsVariable, defaultProjectionForListAssetsVariable } from './defaults';
import { TAKE_LIMIT } from './constants/ListAssets.constants';
import { FeatureToggleDataSourceOptions } from 'core/feature-toggle';

export class AssetDataSource extends DataSourceBase<AssetQuery, AssetDataSourceOptions> {
export class AssetDataSource extends DataSourceBase<AssetQuery, FeatureToggleDataSourceOptions> {
private assetSummaryDataSource: AssetSummaryDataSource;
private calibrationForecastDataSource: CalibrationForecastDataSource;
private listAssetsDataSource: ListAssetsDataSource;
private assetQueryReturnType: AssetQueryReturnType = AssetQueryReturnType.AssetTagPath;

constructor(
readonly instanceSettings: DataSourceInstanceSettings<AssetDataSourceOptions>,
readonly instanceSettings: DataSourceInstanceSettings<FeatureToggleDataSourceOptions>,
readonly backendSrv: BackendSrv = getBackendSrv(),
readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
Expand Down
13 changes: 7 additions & 6 deletions src/datasources/asset/components/AssetQueryEditor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ 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, AssetQueryType } from '../types/types';
import { AssetQueryType } from '../types/types';
import { ListAssetsDataSource } from '../data-sources/list-assets/ListAssetsDataSource';
import { AssetSummaryDataSource } from '../data-sources/asset-summary/AssetSummaryDataSource';
import { LocationModel } from '../types/ListLocations.types';
import { FeatureTogglesDefaults } from 'core/feature-toggle';

const fakeSystems: SystemProperties[] = [
{
Expand All @@ -38,7 +39,7 @@ const fakeLocations: LocationModel[] = [
]

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

class FakeAssetsSource extends ListAssetsDataSource {
Expand Down Expand Up @@ -67,28 +68,28 @@ const render = setupRenderer(AssetQueryEditor, FakeAssetDataSource, () => assetD

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

it('renders Asset list when feature is enabled', async () => {
assetDatasourceOptions.featureToggles.assetList = true;
localStorage.setItem('assetList', 'true');
render({ type: AssetQueryType.ListAssets } as ListAssetsQuery);
const queryType = screen.getAllByRole('combobox')[0];
await select(queryType, "List Assets", { container: document.body });
await waitFor(() => expect(screen.getAllByText("List Assets").length).toBe(1));
});

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

it('renders Asset summary when feature is enabled', async () => {
assetDatasourceOptions.featureToggles.assetSummary = true;
localStorage.setItem('assetSummary', 'true');
render({ type: AssetQueryType.AssetSummary } as AssetSummaryQuery);
const queryType = screen.getAllByRole('combobox')[0];
await select(queryType, "Asset Summary", { container: document.body });
Expand Down
13 changes: 7 additions & 6 deletions src/datasources/asset/components/AssetQueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import { ListAssetsEditor } from './editors/list-assets/ListAssetsEditor';
import { defaultAssetSummaryQuery, defaultCalibrationForecastQuery, defaultListAssetsQuery } from '../defaults';
import { ListAssetsQuery } from '../types/ListAssets.types';
import { AssetSummaryQuery } from '../types/AssetSummaryQuery.types';
import { AssetDataSourceOptions, AssetFeatureToggles, AssetFeatureTogglesDefaults, AssetQuery, AssetQueryType } from '../types/types';
import { AssetQuery, AssetQueryType } from '../types/types';
import { CalibrationForecastQuery } from '../types/CalibrationForecastQuery.types';
import { FeatureToggleDataSourceOptions, FeatureToggleNames, getFeatureFlagValue } from 'core/feature-toggle';

type Props = QueryEditorProps<AssetDataSource, AssetQuery, AssetDataSourceOptions>;
type Props = QueryEditorProps<AssetDataSource, AssetQuery, FeatureToggleDataSourceOptions>;

export function AssetQueryEditor({ query, onChange, onRunQuery, datasource }: Props) {
const [queryType, setQueryType] = useState(query.type);
const assetFeatures = useRef<AssetFeatureToggles>({
assetList: datasource.instanceSettings.jsonData?.featureToggles?.assetList ?? AssetFeatureTogglesDefaults.assetList,
calibrationForecast: datasource.instanceSettings.jsonData?.featureToggles?.calibrationForecast ?? AssetFeatureTogglesDefaults.calibrationForecast,
assetSummary: datasource.instanceSettings.jsonData?.featureToggles?.assetSummary ?? AssetFeatureTogglesDefaults.assetSummary
const assetFeatures = useRef<{ [key: string]: boolean }>({
assetList: getFeatureFlagValue(datasource.instanceSettings.jsonData, FeatureToggleNames.assetList),
calibrationForecast: getFeatureFlagValue(datasource.instanceSettings.jsonData, FeatureToggleNames.calibrationForecast),
assetSummary: getFeatureFlagValue(datasource.instanceSettings.jsonData, FeatureToggleNames.assetSummary),
});

const handleQueryChange = useCallback((value: AssetQuery, runQuery = false): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { BackendSrv, TemplateSrv } from '@grafana/runtime';
import { mock } from 'jest-mock-extended';

import { AssetSummaryResponse } from 'datasources/asset/types/AssetSummaryQuery.types';
import { AssetDataSourceOptions, AssetQuery, AssetQueryType } from 'datasources/asset/types/types';
import { AssetQuery, AssetQueryType } from 'datasources/asset/types/types';
import { AssetSummaryDataSource } from '../../../data-sources/asset-summary/AssetSummaryDataSource';
import { assetSummaryFields } from '../../../constants/AssetSummaryQuery.constants';
import { FeatureToggleDataSourceOptions } from 'core/feature-toggle';

describe('AssetSummaryDataSource', () => {
let dataSource: AssetSummaryDataSource;
const instanceSettings = mock<DataSourceInstanceSettings<AssetDataSourceOptions>>();
const instanceSettings = mock<DataSourceInstanceSettings<FeatureToggleDataSourceOptions>>();
const backendSrv = mock<BackendSrv>();
const templateSrv = mock<TemplateSrv>();
const assetSummary: AssetSummaryResponse = {
Expand Down
Loading