[RAM] Feature flagging for triggers actions UI plugin (#126957)

* Feature flagging for triggers actions UI

Co-authored-by: Zacqary Adam Xeper <zacqary.xeper@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2022-03-16 10:55:08 -07:00 committed by GitHub
parent 2577d36a08
commit 6084c972ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 221 additions and 4 deletions

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type ExperimentalFeatures = typeof allowedExperimentalValues;
/**
* A list of allowed values that can be used in `xpack.triggersActionsUi.enableExperimental`.
* This object is then used to validate and parse the value entered.
*/
export const allowedExperimentalValues = Object.freeze({
rulesListDatagrid: true,
rulesDetailLogs: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
const TriggersActionsUIInvalidExperimentalValue = class extends Error {};
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
/**
* Parses the string value used in `xpack.triggersActionsUi.enableExperimental` kibana configuration,
* which should be a string of values delimited by a comma (`,`)
*
* @param configValue
* @throws TriggersActionsUIInvalidExperimentalValue
*/
export const parseExperimentalConfigValue = (configValue: string[]): ExperimentalFeatures => {
const enabledFeatures: Mutable<Partial<ExperimentalFeatures>> = {};
for (const value of configValue) {
if (!isValidExperimentalValue(value)) {
throw new TriggersActionsUIInvalidExperimentalValue(`[${value}] is not valid.`);
}
enabledFeatures[value as keyof ExperimentalFeatures] = true;
}
return {
...allowedExperimentalValues,
...enabledFeatures,
};
};
export const isValidExperimentalValue = (value: string): boolean => {
return allowedKeys.includes(value as keyof ExperimentalFeatures);
};
export const getExperimentalAllowedValues = (): string[] => [...allowedKeys];

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface TriggersActionsUiConfigType {
enableExperimental: string[];
}

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExperimentalFeatures } from '../../common/experimental_features';
export class ExperimentalFeaturesService {
private static experimentalFeatures?: ExperimentalFeatures;
public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) {
this.experimentalFeatures = experimentalFeatures;
}
public static get(): ExperimentalFeatures {
if (!this.experimentalFeatures) {
this.throwUninitializedError();
}
return this.experimentalFeatures;
}
private static throwUninitializedError(): never {
throw new Error(
'Experimental features services not initialized - are you trying to import this module from outside of the triggers actions UI app?'
);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExperimentalFeaturesService } from './experimental_features_service';
import { getIsExperimentalFeatureEnabled } from './get_experimental_features';
describe('getIsExperimentalFeatureEnabled', () => {
it('getIsExperimentalFeatureEnabled returns the flag enablement', async () => {
ExperimentalFeaturesService.init({
experimentalFeatures: {
rulesListDatagrid: true,
rulesDetailLogs: false,
},
});
let result = getIsExperimentalFeatureEnabled('rulesListDatagrid');
expect(result).toEqual(true);
result = getIsExperimentalFeatureEnabled('rulesDetailLogs');
expect(result).toEqual(false);
expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError(
'Invalid enable value doesNotExist. Allowed values are: rulesListDatagrid, rulesDetailLogs'
);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
ExperimentalFeatures,
allowedExperimentalValues,
isValidExperimentalValue,
getExperimentalAllowedValues,
} from '../../common/experimental_features';
const allowedExperimentalValueKeys = getExperimentalAllowedValues();
export const getIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => {
if (!isValidExperimentalValue(feature)) {
throw new Error(
`Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValueKeys.join(
', '
)}`
);
}
return allowedExperimentalValues[feature];
};

View file

@ -8,6 +8,7 @@
// TODO: https://github.com/elastic/kibana/issues/110895
/* eslint-disable @kbn/eslint/no_export_all */
import { PluginInitializerContext } from 'kibana/server';
import { Plugin } from './plugin';
export type {
@ -42,8 +43,8 @@ export { AlertConditions, AlertConditionsGroup } from './application/sections';
export * from './common';
export function plugin() {
return new Plugin();
export function plugin(context: PluginInitializerContext) {
return new Plugin(context);
}
export { Plugin };

View file

@ -9,6 +9,7 @@ import { CoreSetup, CoreStart, Plugin as CorePlugin } from 'src/core/public';
import { i18n } from '@kbn/i18n';
import { ReactElement } from 'react';
import { PluginInitializerContext } from 'kibana/public';
import { FeaturesPluginStart } from '../../features/public';
import { KibanaFeature } from '../../features/common';
import { registerBuiltInActionTypes } from './application/components/builtin_action_types';
@ -31,6 +32,11 @@ import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout';
import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout';
import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import {
ExperimentalFeatures,
parseExperimentalConfigValue,
} from '../common/experimental_features';
import type {
ActionTypeModel,
@ -40,6 +46,7 @@ import type {
ConnectorAddFlyoutProps,
ConnectorEditFlyoutProps,
} from './types';
import { TriggersActionsUiConfigType } from '../common/types';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
@ -89,16 +96,22 @@ export class Plugin
{
private actionTypeRegistry: TypeRegistry<ActionTypeModel>;
private ruleTypeRegistry: TypeRegistry<RuleTypeModel>;
private config: TriggersActionsUiConfigType;
readonly experimentalFeatures: ExperimentalFeatures;
constructor() {
constructor(ctx: PluginInitializerContext) {
this.actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
this.ruleTypeRegistry = new TypeRegistry<RuleTypeModel>();
this.config = ctx.config.get();
this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental || []);
}
public setup(core: CoreSetup, plugins: PluginsSetup): TriggersAndActionsUIPublicPluginSetup {
const actionTypeRegistry = this.actionTypeRegistry;
const ruleTypeRegistry = this.ruleTypeRegistry;
ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures });
const featureTitle = i18n.translate('xpack.triggersActionsUI.managementSection.displayName', {
defaultMessage: 'Rules and Connectors',
});

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext } from '../../../../src/core/server';
import {
ExperimentalFeatures,
getExperimentalAllowedValues,
isValidExperimentalValue,
parseExperimentalConfigValue,
} from '../common/experimental_features';
const allowedExperimentalValues = getExperimentalAllowedValues();
export const configSchema = schema.object({
enableGeoTrackingThresholdAlert: schema.maybe(schema.boolean({ defaultValue: false })),
enableExperimental: schema.arrayOf(schema.string(), {
defaultValue: () => [],
validate(list) {
for (const key of list) {
if (!isValidExperimentalValue(key)) {
return `[${key}] is not allowed. Allowed values are: ${allowedExperimentalValues.join(
', '
)}`;
}
}
},
}),
});
export type ConfigSchema = TypeOf<typeof configSchema>;
export type ConfigType = ConfigSchema & {
experimentalFeatures: ExperimentalFeatures;
};
export const createConfig = (context: PluginInitializerContext): ConfigType => {
const pluginConfig = context.config.get<TypeOf<typeof configSchema>>();
const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental);
return {
...pluginConfig,
experimentalFeatures,
};
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
import { configSchema, ConfigSchema } from '../config';
import { configSchema, ConfigSchema } from './config';
import { TriggersActionsPlugin } from './plugin';
export type { PluginStartContract } from './plugin';
@ -22,6 +22,7 @@ export {
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {
enableGeoTrackingThresholdAlert: true,
enableExperimental: true,
},
schema: configSchema,
};