OpenFeature: Create basic frontend client + example usage (#110587)
* initial basic OpenFeature client for datasource class * add dep * update, use a wrapping function to enforce types * move init OF to grafana-runtime * docs * Fix circular dependency causing tests to fail * codeowners * use toggle in datasourcewithbackend * Fix CUJs group-by test * Comments * update docs, make default value mandatory * revert using for queryServiceFromUI toggle
This commit is contained in:
@@ -629,6 +629,7 @@
|
||||
/packages/grafana-runtime/rollup.config.ts @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/src/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
||||
/packages/grafana-runtime/src/internal/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
||||
/packages/grafana-runtime/src/internal/openFeature @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/src/unstable.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
||||
/packages/grafana-runtime/tsconfig.build.json @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/tsconfig.json @grafana/grafana-frontend-platform
|
||||
|
||||
@@ -14,7 +14,30 @@ Once your feature toggle is defined, you can then wrap your feature around a che
|
||||
Examples:
|
||||
|
||||
- [Backend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/pkg/services/authn/clients/session.go#L57): Use the `IsEnabled` function and pass in your feature toggle.
|
||||
- [Frontend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/public/app/features/search/service/folders.ts#L14): Check the config for your feature toggle.
|
||||
|
||||
### Frontend
|
||||
|
||||
Use the new OpenFeature-based feature flag client for all new feature flags. There are some differences compared to the legacy `config.featureToggles` system:
|
||||
|
||||
- Feature flag initialisation is async, but will be finished by the time the UI is rendered. This means you cannot get the value of a feature flag at the 'top level' of a module/file
|
||||
- Call `evaluateBooleanFlag("flagName")` from `@grafana/runtime/internal` instead to get the value of a feature flag
|
||||
- Feature flag values _may_ change over the lifetime of the session. Do not store the value in a variable that is used for longer than a single render - always call `evaluateBooleanFlag` lazily when you use the value.
|
||||
|
||||
e.g.
|
||||
|
||||
```ts
|
||||
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
|
||||
|
||||
// BAD - Don't do this. The feature toggle will not evaluate correctly
|
||||
const isEnabled = evaluateBooleanFlag('newPreferences', false);
|
||||
|
||||
function makeAPICall() {
|
||||
// GOOD - The feature toggle should be called after app initialisation
|
||||
if (evaluateBooleanFlag('newPreferences', false)) {
|
||||
// do new things
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Enabling toggles in development
|
||||
|
||||
|
||||
@@ -71,9 +71,12 @@ test.describe(
|
||||
await test.step('3.Edit and restore default groupBy', async () => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
// Wait for the page to load
|
||||
const groupByVariable = getGroupByInput(dashboardPage, selectors);
|
||||
await expect(groupByVariable).toBeVisible();
|
||||
|
||||
const initialSelectedOptionsCount = await groupByValues.count();
|
||||
|
||||
const groupByVariable = getGroupByInput(dashboardPage, selectors);
|
||||
await groupByVariable.click();
|
||||
|
||||
const groupByOption = groupByOptions.nth(1);
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"@locker/near-membrane-shared-dom": "0.14.0",
|
||||
"@msagl/core": "^1.1.19",
|
||||
"@msagl/parser": "^1.1.19",
|
||||
"@openfeature/web-sdk": "^1.6.1",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"@opentelemetry/exporter-collector": "0.25.0",
|
||||
"@opentelemetry/semantic-conventions": "1.37.0",
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
"@grafana/faro-web-sdk": "^1.13.2",
|
||||
"@grafana/schema": "12.3.0-pre",
|
||||
"@grafana/ui": "12.3.0-pre",
|
||||
"@openfeature/core": "^1.9.0",
|
||||
"@openfeature/ofrep-web-provider": "^0.3.3",
|
||||
"@openfeature/web-sdk": "^1.6.1",
|
||||
"@types/systemjs": "6.15.3",
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -27,3 +27,5 @@ export {
|
||||
} from '../services/pluginExtensions/getObservablePluginLinks';
|
||||
|
||||
export { UserStorage } from '../utils/userStorage';
|
||||
|
||||
export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { OFREPWebProvider } from '@openfeature/ofrep-web-provider';
|
||||
import { OpenFeature } from '@openfeature/web-sdk';
|
||||
|
||||
import { FeatureToggles } from '@grafana/data';
|
||||
|
||||
import { config } from '../../config';
|
||||
|
||||
export type FeatureFlagName = keyof FeatureToggles;
|
||||
|
||||
export async function initOpenFeature() {
|
||||
/**
|
||||
* Note: Currently we don't have a way to override OpenFeature flags for tests or localStorage.
|
||||
* A few improvements we could make:
|
||||
* - When running in tests (unit or e2e?), we could use InMemoryProvider instead
|
||||
* - Use Multi-Provider to combine InMemoryProvider (for localStorage) with OFREPWebProvider
|
||||
* to allow for overrides https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider
|
||||
*/
|
||||
|
||||
const ofProvider = new OFREPWebProvider({
|
||||
baseUrl: '/apis/features.grafana.app/v0alpha1/namespaces/' + config.namespace,
|
||||
pollInterval: -1, // disable polling
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
|
||||
await OpenFeature.setProviderAndWait(ofProvider, {
|
||||
targetingKey: config.namespace,
|
||||
namespace: config.namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export function evaluateBooleanFlag(flagName: FeatureFlagName, defaultValue: boolean): boolean {
|
||||
return OpenFeature.getClient().getBooleanValue(flagName, defaultValue);
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
setMegaMenuOpenHook,
|
||||
} from '@grafana/runtime';
|
||||
import {
|
||||
initOpenFeature,
|
||||
setGetObservablePluginComponents,
|
||||
setGetObservablePluginLinks,
|
||||
setPanelDataErrorView,
|
||||
@@ -129,8 +130,20 @@ export class GrafanaApp {
|
||||
async init() {
|
||||
try {
|
||||
await preInitTasks();
|
||||
|
||||
// Let iframe container know grafana has started loading
|
||||
window.parent.postMessage('GrafanaAppInit', '*');
|
||||
|
||||
// Currently the OpenFeature API requires a signed in user. This means feature flags cannot be used
|
||||
// on the login page.
|
||||
if (contextSrv.user.isSignedIn) {
|
||||
try {
|
||||
await initOpenFeature();
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize OpenFeature provider', err);
|
||||
}
|
||||
}
|
||||
|
||||
const regionalFormat = config.featureToggles.localeFormatPreference
|
||||
? config.regionalFormat
|
||||
: contextSrv.user.language;
|
||||
|
||||
@@ -3489,6 +3489,9 @@ __metadata:
|
||||
"@grafana/faro-web-sdk": "npm:^1.13.2"
|
||||
"@grafana/schema": "npm:12.3.0-pre"
|
||||
"@grafana/ui": "npm:12.3.0-pre"
|
||||
"@openfeature/core": "npm:^1.9.0"
|
||||
"@openfeature/ofrep-web-provider": "npm:^0.3.3"
|
||||
"@openfeature/web-sdk": "npm:^1.6.1"
|
||||
"@rollup/plugin-node-resolve": "npm:16.0.1"
|
||||
"@rollup/plugin-terser": "npm:0.4.4"
|
||||
"@testing-library/dom": "npm:10.4.1"
|
||||
@@ -5752,6 +5755,42 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/core@npm:^1.9.0":
|
||||
version: 1.9.0
|
||||
resolution: "@openfeature/core@npm:1.9.0"
|
||||
checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/ofrep-core@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "@openfeature/ofrep-core@npm:1.1.0"
|
||||
peerDependencies:
|
||||
"@openfeature/core": ^1.6.0
|
||||
checksum: 10/4198f2f1abf974822bf14530a7f514292d8235552d6e61465d29ffe42d092f675e7f56a9e9da5aa7d45dfbd98cc36316efbdaadaf83e8f510f35865612f5f24f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/ofrep-web-provider@npm:^0.3.3":
|
||||
version: 0.3.3
|
||||
resolution: "@openfeature/ofrep-web-provider@npm:0.3.3"
|
||||
dependencies:
|
||||
"@openfeature/ofrep-core": "npm:^1.0.0"
|
||||
peerDependencies:
|
||||
"@openfeature/web-sdk": ^1.4.0
|
||||
checksum: 10/85f362e3ebaa9d421be91e4d966284e28850649e417d5db81181960b95ac693c9faa4a0bdc84eeb03fa694a8cd1e1f5d9de9bf0fba62f8e2669f9637836f0884
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/web-sdk@npm:^1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@openfeature/web-sdk@npm:1.6.1"
|
||||
peerDependencies:
|
||||
"@openfeature/core": ^1.9.0
|
||||
checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/api-logs@npm:0.202.0":
|
||||
version: 0.202.0
|
||||
resolution: "@opentelemetry/api-logs@npm:0.202.0"
|
||||
@@ -18218,6 +18257,7 @@ __metadata:
|
||||
"@msagl/core": "npm:^1.1.19"
|
||||
"@msagl/parser": "npm:^1.1.19"
|
||||
"@npmcli/package-json": "npm:^6.0.0"
|
||||
"@openfeature/web-sdk": "npm:^1.6.1"
|
||||
"@opentelemetry/api": "npm:1.9.0"
|
||||
"@opentelemetry/exporter-collector": "npm:0.25.0"
|
||||
"@opentelemetry/semantic-conventions": "npm:1.37.0"
|
||||
|
||||
Reference in New Issue
Block a user