[RAM] [Rule Form v2] Add feature flag (#179184)

## Summary

Closes #179110 

This adds a series of feature flags to all plugins that are currently
using the **Rule Form Flyout**, in preparation for development of the
new Create Rule Flow V2 (#175634).

Unfortunately true Kibana feature-flagging is still not yet implemented,
so we need to implement this as a config option for every individual
affected plugin. The following `kibana.yml` config will enable the
upcoming V2 rule form:

```yaml
xpack.trigger_actions_ui.enableExperimental: ['ruleFormV2']
xpack.infra.featureFlags.ruleFormV2Enabled: true
xpack.ml.experimental.ruleFormV2.enabled: true
xpack.transform.experimental.ruleFormV2Enabled: true
xpack.legacy_uptime.experimental.ruleFormV2Enabled: true
xpack.observability.unsafe.ruleFormV2.enabled: true
xpack.slo.experimental.ruleFormV2.enabled: true
xpack.apm.featureFlags.ruleFormV2Enabled: true

discover.experimental.ruleFormV2Enabled: true
``` 

**These feature flags will not enable anything yet.** This PR is for the
sole purpose of adding these browser-enabled flags now, so that we can
reduce the amount of code committed later.

### Why feature flag every plugin?

The V1 rule form is currently exported from Triggers Actions UI. In V2,
we're moving away from this model and exporting the rule form in a KBN
package. Therefore we can't simply export the results of a single
feature flag from Triggers Actions UI, we need to signal to each
individual plugin whether it should be pulling from the new KBN package
or from the old cross-plugin export.

### Plugins affected

#### Triggers Actions UI, Infra, Observability, APM

These plugins already contained interfaces for experimental features or
feature flags, so this PR simply adds a new flag to these existing
structures.

#### Discover, ML, Transform, Uptime, SLO

These plugins did **not** have existing browser-exposed feature flag
systems, so this PR adds these interfaces. The transform plugin notably
did not yet have any config options configured at all.
This commit is contained in:
Zacqary Adam Xeper 2024-03-27 15:17:21 -05:00 committed by GitHub
parent d54200bc42
commit bfc91b0cb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 300 additions and 36 deletions

View file

@ -0,0 +1,21 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({
enableUiSettingsValidations: schema.boolean({ defaultValue: false }),
experimental: schema.maybe(
schema.object({
ruleFormV2Enabled: schema.maybe(schema.boolean({ defaultValue: false })),
})
),
});
export type ConfigSchema = TypeOf<typeof configSchema>;
export type ExperimentalFeatures = ConfigSchema['experimental'];

View file

@ -49,9 +49,12 @@ const gatherRoutes = (wrapper: ShallowWrapper) => {
});
};
const mockExperimentalFeatures = {};
const props: DiscoverRoutesProps = {
customizationCallbacks: [],
customizationContext: mockCustomizationContext,
experimentalFeatures: mockExperimentalFeatures,
};
describe('DiscoverRoutes', () => {
@ -156,6 +159,7 @@ describe('CustomDiscoverRoutes', () => {
<CustomDiscoverRoutes
profileRegistry={profileRegistry}
customizationContext={mockCustomizationContext}
experimentalFeatures={mockExperimentalFeatures}
/>
);
expect(component.find(DiscoverRoutes).getElement()).toMatchObject(
@ -163,6 +167,7 @@ describe('CustomDiscoverRoutes', () => {
prefix={addProfile('', mockProfile)}
customizationCallbacks={callbacks}
customizationContext={mockCustomizationContext}
experimentalFeatures={mockExperimentalFeatures}
/>
);
});
@ -173,6 +178,7 @@ describe('CustomDiscoverRoutes', () => {
<CustomDiscoverRoutes
profileRegistry={profileRegistry}
customizationContext={mockCustomizationContext}
experimentalFeatures={mockExperimentalFeatures}
/>
);
expect(component.find(NotFoundRoute).getElement()).toMatchObject(<NotFoundRoute />);
@ -191,6 +197,7 @@ describe('DiscoverRouter', () => {
history={history}
profileRegistry={profileRegistry}
customizationContext={mockCustomizationContext}
experimentalFeatures={mockExperimentalFeatures}
/>
);
gatherRoutes(component);
@ -201,6 +208,7 @@ describe('DiscoverRouter', () => {
<DiscoverRoutes
customizationCallbacks={callbacks}
customizationContext={mockCustomizationContext}
experimentalFeatures={mockExperimentalFeatures}
/>
);
});
@ -210,6 +218,7 @@ describe('DiscoverRouter', () => {
<CustomDiscoverRoutes
profileRegistry={profileRegistry}
customizationContext={mockCustomizationContext}
experimentalFeatures={mockExperimentalFeatures}
/>
);
});

View file

@ -12,6 +12,7 @@ import React, { useCallback, useMemo } from 'react';
import { History } from 'history';
import { EuiErrorBoundary } from '@elastic/eui';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { ExperimentalFeatures } from '../../common/config';
import { ContextAppRoute } from './context';
import { SingleDocRoute } from './doc';
import { DiscoverMainRoute } from './main';
@ -26,6 +27,7 @@ export interface DiscoverRoutesProps {
prefix?: string;
customizationCallbacks: CustomizationCallback[];
customizationContext: DiscoverCustomizationContext;
experimentalFeatures: ExperimentalFeatures;
}
export const DiscoverRoutes = ({ prefix, ...mainRouteProps }: DiscoverRoutesProps) => {
@ -67,6 +69,7 @@ export const DiscoverRoutes = ({ prefix, ...mainRouteProps }: DiscoverRoutesProp
interface CustomDiscoverRoutesProps {
profileRegistry: DiscoverProfileRegistry;
customizationContext: DiscoverCustomizationContext;
experimentalFeatures: ExperimentalFeatures;
}
export const CustomDiscoverRoutes = ({ profileRegistry, ...props }: CustomDiscoverRoutesProps) => {
@ -93,6 +96,7 @@ export interface DiscoverRouterProps {
services: DiscoverServices;
profileRegistry: DiscoverProfileRegistry;
customizationContext: DiscoverCustomizationContext;
experimentalFeatures: ExperimentalFeatures;
history: History;
}

View file

@ -9,6 +9,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { ExperimentalFeatures } from '../../common/config';
import { DiscoverRouter } from './discover_router';
import { DiscoverServices } from '../build_services';
import type { DiscoverProfileRegistry } from '../customizations/profile_registry';
@ -19,6 +20,7 @@ export interface RenderAppProps {
services: DiscoverServices;
profileRegistry: DiscoverProfileRegistry;
customizationContext: DiscoverCustomizationContext;
experimentalFeatures: ExperimentalFeatures;
}
export const renderApp = ({
@ -26,6 +28,7 @@ export const renderApp = ({
services,
profileRegistry,
customizationContext,
experimentalFeatures,
}: RenderAppProps) => {
const { history, capabilities, chrome, data, core } = services;
@ -45,6 +48,7 @@ export const renderApp = ({
services={services}
profileRegistry={profileRegistry}
customizationContext={customizationContext}
experimentalFeatures={experimentalFeatures}
history={history}
/>,
{

View file

@ -78,6 +78,7 @@ import {
} from './components/discover_container';
import { getESQLSearchProvider } from './global_search/search_provider';
import { HistoryService } from './history_service';
import { ConfigSchema, ExperimentalFeatures } from '../common/config';
/**
* @public
@ -207,7 +208,10 @@ export interface DiscoverStartPlugins {
export class DiscoverPlugin
implements Plugin<DiscoverSetup, DiscoverStart, DiscoverSetupPlugins, DiscoverStartPlugins>
{
constructor(private readonly initializerContext: PluginInitializerContext) {}
constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {
this.experimentalFeatures =
initializerContext.config.get().experimental ?? this.experimentalFeatures;
}
private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private historyService = new HistoryService();
@ -222,6 +226,9 @@ export class DiscoverPlugin
enabled: false,
showLogsExplorerTabs: false,
};
private experimentalFeatures: ExperimentalFeatures = {
ruleFormV2Enabled: false,
};
setup(
core: CoreSetup<DiscoverStartPlugins, DiscoverStart>,
@ -355,6 +362,7 @@ export class DiscoverPlugin
displayMode: 'standalone',
inlineTopNav: this.inlineTopNav,
},
experimentalFeatures: this.experimentalFeatures,
});
return () => {

View file

@ -6,15 +6,12 @@
* Side Public License, v 1.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from '@kbn/core-plugins-server';
const configSchema = schema.object({
enableUiSettingsValidations: schema.boolean({ defaultValue: false }),
});
export type ConfigSchema = TypeOf<typeof configSchema>;
import { configSchema, type ConfigSchema } from '../common/config';
export const config: PluginConfigDescriptor<ConfigSchema> = {
schema: configSchema,
exposeToBrowser: {
experimental: true,
},
};

View file

@ -20,7 +20,7 @@ import { createSearchEmbeddableFactory } from './embeddable';
import { initializeLocatorServices } from './locator';
import { registerSampleData } from './sample_data';
import { getUiSettings } from './ui_settings';
import { ConfigSchema } from './config';
import { ConfigSchema } from '../common/config';
export class DiscoverServerPlugin
implements Plugin<object, DiscoverServerPluginStart, object, DiscoverServerPluginStartDeps>

View file

@ -340,6 +340,18 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.observability_onboarding.ui.enabled (boolean)',
'xpack.observabilityLogsExplorer.navigation.showAppLink (any)', // conditional, is actually a boolean
'share.new_version.enabled (boolean)',
/**
* Rule form V2 feature flags
*/
'discover.experimental.ruleFormV2Enabled (boolean)',
'xpack.infra.featureFlags.ruleFormV2Enabled (boolean)',
'xpack.legacy_uptime.experimental.ruleFormV2Enabled (boolean)',
'xpack.ml.experimental.ruleFormV2.enabled (boolean)',
'xpack.transform.experimental.ruleFormV2Enabled (boolean)',
'xpack.apm.featureFlags.ruleFormV2Enabled (boolean)',
'xpack.observability.unsafe.ruleFormV2.enabled (boolean)',
'xpack.slo.experimental.ruleFormV2.enabled (boolean)',
/**/
];
// We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large
// arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's

View file

@ -19,12 +19,16 @@ export const ML_EXTERNAL_BASE_PATH = '/api/ml';
export type MlFeatures = Record<'ad' | 'dfa' | 'nlp', boolean>;
export type CompatibleModule = 'security' | 'observability' | 'search';
export type ExperimentalFeatures = Record<'ruleFormV2', boolean>;
export interface ConfigSchema {
ad?: { enabled: boolean };
dfa?: { enabled: boolean };
nlp?: { enabled: boolean };
compatibleModuleType?: CompatibleModule;
experimental?: {
ruleFormV2?: { enabled: boolean };
};
}
export function initEnabledFeatures(enabledFeatures: MlFeatures, config: ConfigSchema) {
@ -38,3 +42,12 @@ export function initEnabledFeatures(enabledFeatures: MlFeatures, config: ConfigS
enabledFeatures.nlp = config.nlp.enabled;
}
}
export function initExperimentalFeatures(
experimentalFeatures: ExperimentalFeatures,
config: ConfigSchema
) {
if (config.experimental?.ruleFormV2?.enabled !== undefined) {
experimentalFeatures.ruleFormV2 = config.experimental.ruleFormV2.enabled;
}
}

View file

@ -20,7 +20,7 @@ import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-pl
import { StorageContextProvider } from '@kbn/ml-local-storage';
import useLifecycles from 'react-use/lib/useLifecycles';
import useObservable from 'react-use/lib/useObservable';
import type { MlFeatures } from '../../common/constants/app';
import type { ExperimentalFeatures, MlFeatures } from '../../common/constants/app';
import { MlLicense } from '../../common/license';
import { MlCapabilitiesService } from './capabilities/check_capabilities';
import { ML_STORAGE_KEYS } from '../../common/types/storage';
@ -49,6 +49,7 @@ interface AppProps {
appMountParams: AppMountParameters;
isServerless: boolean;
mlFeatures: MlFeatures;
experimentalFeatures: ExperimentalFeatures;
}
const localStorage = new Storage(window.localStorage);
@ -91,7 +92,14 @@ export interface MlServicesContext {
export type MlGlobalServices = ReturnType<typeof getMlGlobalServices>;
const App: FC<AppProps> = ({ coreStart, deps, appMountParams, isServerless, mlFeatures }) => {
const App: FC<AppProps> = ({
coreStart,
deps,
appMountParams,
isServerless,
mlFeatures,
experimentalFeatures,
}) => {
const pageDeps: PageDependencies = {
history: appMountParams.history,
setHeaderActionMenu: appMountParams.setHeaderActionMenu,
@ -171,6 +179,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams, isServerless, mlFe
isServerless={isServerless}
mlFeatures={mlFeatures}
showMLNavMenu={chromeStyle === 'classic'}
experimentalFeatures={experimentalFeatures}
>
<MlRouter pageDeps={pageDeps} />
</EnabledFeaturesContextProvider>
@ -188,7 +197,8 @@ export const renderApp = (
deps: MlDependencies,
appMountParams: AppMountParameters,
isServerless: boolean,
mlFeatures: MlFeatures
mlFeatures: MlFeatures,
experimentalFeatures: ExperimentalFeatures
) => {
setDependencyCache({
timefilter: deps.data.query.timefilter,
@ -211,6 +221,7 @@ export const renderApp = (
appMountParams={appMountParams}
isServerless={isServerless}
mlFeatures={mlFeatures}
experimentalFeatures={experimentalFeatures}
/>,
appMountParams.element
);

View file

@ -7,7 +7,7 @@
import type { FC } from 'react';
import React, { createContext, useContext, useMemo } from 'react';
import type { MlFeatures } from '../../../../common/constants/app';
import type { ExperimentalFeatures, MlFeatures } from '../../../../common/constants/app';
export interface EnabledFeatures {
showNodeInfo: boolean;
@ -16,6 +16,7 @@ export interface EnabledFeatures {
isADEnabled: boolean;
isDFAEnabled: boolean;
isNLPEnabled: boolean;
showRuleFormV2: boolean;
}
export const EnabledFeaturesContext = createContext({
showNodeInfo: true,
@ -30,6 +31,7 @@ interface Props {
isServerless: boolean;
mlFeatures: MlFeatures;
showMLNavMenu?: boolean;
experimentalFeatures?: ExperimentalFeatures;
}
export const EnabledFeaturesContextProvider: FC<Props> = ({
@ -37,6 +39,7 @@ export const EnabledFeaturesContextProvider: FC<Props> = ({
isServerless,
showMLNavMenu = true,
mlFeatures,
experimentalFeatures,
}) => {
const features: EnabledFeatures = {
showNodeInfo: !isServerless,
@ -45,6 +48,7 @@ export const EnabledFeaturesContextProvider: FC<Props> = ({
isADEnabled: mlFeatures.ad,
isDFAEnabled: mlFeatures.dfa,
isNLPEnabled: mlFeatures.nlp,
showRuleFormV2: experimentalFeatures?.ruleFormV2 ?? false,
};
return (

View file

@ -65,6 +65,8 @@ import {
PLUGIN_ICON_SOLUTION,
PLUGIN_ID,
type ConfigSchema,
type ExperimentalFeatures,
initExperimentalFeatures,
} from '../common/constants/app';
import type { MlCapabilities } from './shared';
import type { ElasticModels } from './application/services/elastic_models_service';
@ -127,10 +129,14 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
dfa: true,
nlp: true,
};
private experimentalFeatures: ExperimentalFeatures = {
ruleFormV2: false,
};
constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {
this.isServerless = initializerContext.env.packageInfo.buildFlavor === 'serverless';
initEnabledFeatures(this.enabledFeatures, initializerContext.config.get());
initExperimentalFeatures(this.experimentalFeatures, initializerContext.config.get());
}
setup(
@ -183,7 +189,8 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
},
params,
this.isServerless,
this.enabledFeatures
this.enabledFeatures,
this.experimentalFeatures
);
},
});

View file

@ -25,4 +25,9 @@ export const configSchema = schema.object({
dfa: enabledSchema,
nlp: enabledSchema,
compatibleModuleType: compatibleModuleTypeSchema,
experimental: schema.maybe(
schema.object({
ruleFormV2: enabledSchema,
})
),
});

View file

@ -34,6 +34,7 @@ export const config: PluginConfigDescriptor<ConfigSchema> = {
ad: true,
dfa: true,
nlp: true,
experimental: true,
},
};

View file

@ -17,6 +17,7 @@ export enum ApmFeatureFlagName {
SourcemapApiAvailable = 'sourcemapApiAvailable',
StorageExplorerAvailable = 'storageExplorerAvailable',
ProfilingIntegrationAvailable = 'profilingIntegrationAvailable',
RuleFormV2Enabled = 'ruleFormV2Enabled',
}
const apmFeatureFlagMap = {
@ -52,6 +53,10 @@ const apmFeatureFlagMap = {
default: false,
type: t.boolean,
},
[ApmFeatureFlagName.RuleFormV2Enabled]: {
default: false,
type: t.boolean,
},
};
type ApmFeatureFlagMap = typeof apmFeatureFlagMap;

View file

@ -86,6 +86,7 @@ const mockConfig: ConfigSchema = {
sourcemapApiAvailable: true,
storageExplorerAvailable: true,
profilingIntegrationAvailable: false,
ruleFormV2Enabled: false,
},
serverless: { enabled: false },
};

View file

@ -25,6 +25,7 @@ export interface ConfigSchema {
sourcemapApiAvailable: boolean;
storageExplorerAvailable: boolean;
profilingIntegrationAvailable: boolean;
ruleFormV2Enabled: boolean;
};
serverless: {
enabled: boolean;

View file

@ -78,6 +78,7 @@ const configSchema = schema.object({
* enabling this feature flag.
*/
profilingIntegrationAvailable: schema.boolean({ defaultValue: false }),
ruleFormV2Enabled: schema.boolean({ defaultValue: false }),
}),
serverless: schema.object({
enabled: offeringBasedSchema({

View file

@ -35,6 +35,7 @@ export interface InfraConfig {
logThresholdAlertRuleEnabled: boolean;
alertsAndRulesDropdownEnabled: boolean;
profilingEnabled: boolean;
ruleFormV2Enabled: boolean;
};
}

View file

@ -188,6 +188,7 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => {
logThresholdAlertRuleEnabled: true,
alertsAndRulesDropdownEnabled: true,
profilingEnabled: false,
ruleFormV2Enabled: false,
},
};

View file

@ -29,6 +29,7 @@ describe('usePluginConfig()', () => {
logThresholdAlertRuleEnabled: true,
alertsAndRulesDropdownEnabled: true,
profilingEnabled: false,
ruleFormV2Enabled: false,
},
};
const { result } = renderHook(() => usePluginConfig(), {

View file

@ -2424,6 +2424,7 @@ const createMockStaticConfiguration = (sources: any): InfraConfig => ({
logThresholdAlertRuleEnabled: true,
alertsAndRulesDropdownEnabled: true,
profilingEnabled: false,
ruleFormV2Enabled: false,
},
enabled: true,
sources,

View file

@ -102,6 +102,7 @@ const createMockStaticConfiguration = (): InfraConfig => ({
logThresholdAlertRuleEnabled: true,
alertsAndRulesDropdownEnabled: true,
profilingEnabled: false,
ruleFormV2Enabled: false,
},
enabled: true,
});

View file

@ -122,6 +122,7 @@ export const config: PluginConfigDescriptor<InfraConfig> = {
* enabling this feature flag.
*/
profilingEnabled: schema.boolean({ defaultValue: false }),
ruleFormV2Enabled: schema.boolean({ defaultValue: false }),
}),
}),
exposeToBrowser: publicConfigKeys,

View file

@ -106,6 +106,9 @@ export interface ConfigSchema {
thresholdRule?: {
enabled: boolean;
};
ruleFormV2?: {
enabled: boolean;
};
};
}
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;

View file

@ -52,6 +52,9 @@ const configSchema = schema.object({
traditional: schema.boolean({ defaultValue: false }),
}),
}),
ruleFormV2: schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),
}),
customThresholdRule: schema.object({
groupByPageSize: schema.number({ defaultValue: 10_000 }),

View file

@ -0,0 +1,29 @@
/*
* 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';
export const configSchema = schema.object({
sloOrphanSummaryCleanUpTaskEnabled: schema.boolean({ defaultValue: true }),
enabled: schema.boolean({ defaultValue: true }),
experimental: schema.maybe(
schema.object({
ruleFormV2: schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),
})
),
});
export const config = {
schema: configSchema,
exposeToBrowser: {
experimental: true,
},
};
export type SloConfig = TypeOf<typeof configSchema>;
export type ExperimentalFeatures = SloConfig['experimental'];

View file

@ -24,6 +24,7 @@ import { PluginContext } from './context/plugin_context';
import { SloPublicPluginsStart } from './types';
import { routes } from './routes/routes';
import { ExperimentalFeatures } from '../common/config';
function App() {
return (
@ -52,6 +53,7 @@ export const renderApp = ({
kibanaVersion,
isServerless,
observabilityRuleTypeRegistry,
experimentalFeatures,
}: {
core: CoreStart;
plugins: SloPublicPluginsStart;
@ -62,6 +64,7 @@ export const renderApp = ({
isDev?: boolean;
kibanaVersion: string;
isServerless?: boolean;
experimentalFeatures: ExperimentalFeatures;
}) => {
const { element, history, theme$ } = appMountParameters;
const i18nCore = core.i18n;
@ -100,6 +103,7 @@ export const renderApp = ({
appMountParameters,
ObservabilityPageTemplate,
observabilityRuleTypeRegistry,
experimentalFeatures,
}}
>
<Router history={history}>

View file

@ -16,7 +16,6 @@ export function HeaderMenu(): React.ReactElement | null {
const { http, theme } = useKibana().services;
const { appMountParameters } = usePluginContext();
return (
<HeaderMenuPortal
setHeaderActionMenu={appMountParameters?.setHeaderActionMenu!}

View file

@ -9,12 +9,14 @@ import { createContext } from 'react';
import type { AppMountParameters } from '@kbn/core/public';
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
import { ExperimentalFeatures } from '../../common/config';
export interface PluginContextValue {
isDev?: boolean;
appMountParameters?: AppMountParameters;
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
ObservabilityPageTemplate: React.ComponentType<LazyObservabilityPageTemplateProps>;
experimentalFeatures?: ExperimentalFeatures;
}
export const PluginContext = createContext({} as PluginContextValue);

View file

@ -14,6 +14,7 @@ import { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-pl
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { RecursivePartial } from '@kbn/utility-types';
import { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
import { ExperimentalFeatures } from '../../../../common/config';
import { CreateSLOForm } from '../types';
import { PluginContext } from '../../../context/plugin_context';
import { SloPublicPluginsStart } from '../../../types';
@ -27,6 +28,7 @@ export const getCreateSLOFlyoutLazy = ({
isDev,
kibanaVersion,
isServerless,
experimentalFeatures,
}: {
core: CoreStart;
plugins: SloPublicPluginsStart;
@ -35,6 +37,7 @@ export const getCreateSLOFlyoutLazy = ({
isDev?: boolean;
kibanaVersion: string;
isServerless?: boolean;
experimentalFeatures: ExperimentalFeatures;
}) => {
return ({
onClose,
@ -60,6 +63,7 @@ export const getCreateSLOFlyoutLazy = ({
isDev,
observabilityRuleTypeRegistry,
ObservabilityPageTemplate,
experimentalFeatures,
}}
>
<QueryClientProvider client={queryClient}>

View file

@ -24,13 +24,18 @@ import { SloListLocatorDefinition } from './locators/slo_list';
import { SLOS_BASE_PATH } from '../common/locators/paths';
import { getCreateSLOFlyoutLazy } from './pages/slo_edit/shared_flyout/get_create_slo_flyout';
import { registerBurnRateRuleType } from './rules/register_burn_rate_rule_type';
import { ExperimentalFeatures, SloConfig } from '../common/config';
export class SloPlugin
implements Plugin<SloPublicSetup, SloPublicStart, SloPublicPluginsSetup, SloPublicPluginsStart>
{
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
private experimentalFeatures: ExperimentalFeatures = { ruleFormV2: { enabled: false } };
constructor(private readonly initContext: PluginInitializerContext) {}
constructor(private readonly initContext: PluginInitializerContext<SloConfig>) {
this.experimentalFeatures =
this.initContext.config.get().experimental ?? this.experimentalFeatures;
}
public setup(
coreSetup: CoreSetup<SloPublicPluginsStart, SloPublicStart>,
@ -60,6 +65,7 @@ export class SloPlugin
ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate,
plugins: { ...pluginsStart, ruleTypeRegistry, actionTypeRegistry },
isServerless: !!pluginsStart.serverless,
experimentalFeatures: this.experimentalFeatures,
});
};
const appUpdater$ = this.appUpdater$;
@ -145,6 +151,7 @@ export class SloPlugin
ObservabilityPageTemplate: pluginsStart.observabilityShared.navigation.PageTemplate,
plugins: { ...pluginsStart, ruleTypeRegistry, actionTypeRegistry },
isServerless: !!pluginsStart.serverless,
experimentalFeatures: this.experimentalFeatures,
}),
};
}

View file

@ -6,7 +6,7 @@
*/
import { PluginInitializerContext } from '@kbn/core/server';
import { schema, TypeOf } from '@kbn/config-schema';
import { configSchema } from '../common/config';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
@ -18,12 +18,10 @@ export async function plugin(initializerContext: PluginInitializerContext) {
export type { PluginSetup, PluginStart } from './plugin';
const configSchema = schema.object({
sloOrphanSummaryCleanUpTaskEnabled: schema.boolean({ defaultValue: true }),
enabled: schema.boolean({ defaultValue: true }),
});
export const config = {
schema: configSchema,
exposeToBrowser: {
experimental: true,
},
};
export type SloConfig = TypeOf<typeof configSchema>;
export type { SloConfig } from '../common/config';

View file

@ -11,10 +11,19 @@ import { schema, TypeOf } from '@kbn/config-schema';
const uptimeConfig = schema.object({
index: schema.maybe(schema.string()),
enabled: schema.boolean({ defaultValue: true }),
experimental: schema.maybe(
schema.object({
ruleFormV2Enabled: schema.maybe(schema.boolean({ defaultValue: false })),
})
),
});
export const config: PluginConfigDescriptor = {
schema: uptimeConfig,
exposeToBrowser: {
experimental: true,
},
};
export type UptimeConfig = TypeOf<typeof uptimeConfig>;
export type ExperimentalFeatures = UptimeConfig['experimental'];

View file

@ -11,6 +11,7 @@ import { i18n as i18nFormatter } from '@kbn/i18n';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { getIntegratedAppAvailability } from '../lib/adapters/framework/capabilities_adapter';
import { DEFAULT_TIMEPICKER_QUICK_RANGES, INTEGRATED_SOLUTIONS } from '../../../common/constants';
import type { ExperimentalFeatures } from '../../../common/config';
import { UptimeApp, UptimeAppProps } from './uptime_app';
import { ClientPluginsSetup, ClientPluginsStart } from '../../plugin';
@ -19,7 +20,8 @@ export function renderApp(
plugins: ClientPluginsSetup,
startPlugins: ClientPluginsStart,
appMountParameters: AppMountParameters,
isDev: boolean
isDev: boolean,
experimentalFeatures: ExperimentalFeatures
) {
const {
application: { capabilities },

View file

@ -56,6 +56,7 @@ import {
ObservabilityAIAssistantPublicSetup,
} from '@kbn/observability-ai-assistant-plugin/public';
import { PLUGIN } from '../common/constants/plugin';
import { UptimeConfig } from '../common/config';
import {
LazySyntheticsPolicyCreateExtension,
LazySyntheticsPolicyEditExtension,
@ -119,9 +120,15 @@ export type ClientStart = void;
export class UptimePlugin
implements Plugin<ClientSetup, ClientStart, ClientPluginsSetup, ClientPluginsStart>
{
constructor(private readonly initContext: PluginInitializerContext) {}
constructor(private readonly initContext: PluginInitializerContext<UptimeConfig>) {
this.experimentalFeatures =
this.initContext.config.get().experimental || this.experimentalFeatures;
}
private uptimeAppUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private experimentalFeatures: UptimeConfig['experimental'] = {
ruleFormV2Enabled: false,
};
public setup(core: CoreSetup<ClientPluginsStart, unknown>, plugins: ClientPluginsSetup): void {
if (plugins.home) {
@ -202,7 +209,14 @@ export class UptimePlugin
mount: async (params: AppMountParameters) => {
const [coreStart, corePlugins] = await core.getStartServices();
const { renderApp } = await import('./legacy_uptime/app/render_app');
return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev);
return renderApp(
coreStart,
plugins,
corePlugins,
params,
this.initContext.env.mode.dev,
this.experimentalFeatures
);
},
updater$: this.uptimeAppUpdater,
});

View file

@ -0,0 +1,20 @@
/*
* 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 type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
export const configSchema = schema.object({
experimental: schema.maybe(
schema.object({
ruleFormV2Enabled: schema.maybe(schema.boolean()),
})
),
});
export type ConfigSchema = TypeOf<typeof configSchema>;
export type ExperimentalFeatures = ConfigSchema['experimental'];

View file

@ -15,6 +15,7 @@ import { Router, Routes, Route } from '@kbn/shared-ux-router';
import type { ScopedHistory } from '@kbn/core/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { ExperimentalFeatures } from '../../common/config';
import { SECTION_SLUG } from './common/constants';
import type { AppDependencies } from './app_dependencies';
import { CloneTransformSection } from './sections/clone_transform';
@ -22,6 +23,7 @@ import { CreateTransformSection } from './sections/create_transform';
import { TransformManagementSection } from './sections/transform_management';
import {
EnabledFeaturesContextProvider,
ExperimentalFeaturesContextProvider,
type TransformEnabledFeatures,
} from './serverless_context';
@ -44,7 +46,8 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => (
export const renderApp = (
element: HTMLElement,
appDependencies: AppDependencies,
enabledFeatures: TransformEnabledFeatures
enabledFeatures: TransformEnabledFeatures,
experimentalFeatures: ExperimentalFeatures
) => {
const I18nContext = appDependencies.i18n.Context;
@ -64,7 +67,9 @@ export const renderApp = (
<KibanaContextProvider services={appDependencies}>
<I18nContext>
<EnabledFeaturesContextProvider enabledFeatures={enabledFeatures}>
<App history={appDependencies.history} />
<ExperimentalFeaturesContextProvider experimentalFeatures={experimentalFeatures}>
<App history={appDependencies.history} />
</ExperimentalFeaturesContextProvider>
</EnabledFeaturesContextProvider>
</I18nContext>
</KibanaContextProvider>

View file

@ -13,6 +13,7 @@ import { type TransformEnabledFeatures } from './serverless_context';
import type { PluginsDependencies } from '../plugin';
import { getMlSharedImports } from '../shared_imports';
import type { ExperimentalFeatures } from '../../common/config';
import type { AppDependencies } from './app_dependencies';
import { breadcrumbService } from './services/navigation';
import { docTitleService } from './services/navigation';
@ -24,7 +25,8 @@ const localStorage = new Storage(window.localStorage);
export async function mountManagementSection(
coreSetup: CoreSetup<PluginsDependencies>,
params: ManagementAppMountParams,
isServerless: boolean
isServerless: boolean,
experimentalFeatures: ExperimentalFeatures
) {
const { element, setBreadcrumbs, history } = params;
const { http, getStartServices } = coreSetup;
@ -99,7 +101,12 @@ export async function mountManagementSection(
const enabledFeatures: TransformEnabledFeatures = {
showNodeInfo: !isServerless,
};
const unmountAppCallback = renderApp(element, appDependencies, enabledFeatures);
const unmountAppCallback = renderApp(
element,
appDependencies,
enabledFeatures,
experimentalFeatures
);
return () => {
docTitle.reset();

View file

@ -7,6 +7,7 @@
import type { FC } from 'react';
import React, { createContext, useContext, useMemo } from 'react';
import type { ExperimentalFeatures } from '../../common/config';
export interface TransformEnabledFeatures {
showNodeInfo: boolean;
@ -15,9 +16,9 @@ export const EnabledFeaturesContext = createContext({
showNodeInfo: true,
});
export const EnabledFeaturesContextProvider: FC<{ enabledFeatures: TransformEnabledFeatures }> = (
props
) => {
export const EnabledFeaturesContextProvider: FC<{
enabledFeatures: TransformEnabledFeatures;
}> = (props) => {
const { children, enabledFeatures } = props;
return (
<EnabledFeaturesContext.Provider value={enabledFeatures}>
@ -26,9 +27,30 @@ export const EnabledFeaturesContextProvider: FC<{ enabledFeatures: TransformEnab
);
};
export const ExperimentalFeaturesContext = createContext<ExperimentalFeatures>({
ruleFormV2Enabled: false,
});
export const ExperimentalFeaturesContextProvider: FC<{
experimentalFeatures: ExperimentalFeatures;
}> = (props) => {
const { children, experimentalFeatures } = props;
return (
<ExperimentalFeaturesContext.Provider value={experimentalFeatures}>
{children}
</ExperimentalFeaturesContext.Provider>
);
};
export function useEnabledFeatures() {
const context = useContext(EnabledFeaturesContext);
return useMemo(() => {
return context;
}, [context]);
}
export function useExperimentalFeatures() {
const context = useContext(ExperimentalFeaturesContext);
return useMemo(() => {
return context;
}, [context]);
}

View file

@ -25,6 +25,7 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { PluginInitializerContext } from '@kbn/core/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { ConfigSchema } from '../common/config';
import { registerFeature } from './register_feature';
import { getTransformHealthRuleType } from './alerting';
@ -49,8 +50,13 @@ export interface PluginsDependencies {
export class TransformUiPlugin {
private isServerless: boolean = false;
constructor(initializerContext: PluginInitializerContext) {
private experimentalFeatures: ConfigSchema['experimental'] = {
ruleFormV2Enabled: false,
};
constructor(initializerContext: PluginInitializerContext<ConfigSchema>) {
this.isServerless = initializerContext.env.packageInfo.buildFlavor === 'serverless';
this.experimentalFeatures =
initializerContext.config.get().experimental ?? this.experimentalFeatures;
}
public setup(coreSetup: CoreSetup<PluginsDependencies>, pluginsSetup: PluginsDependencies): void {
@ -66,7 +72,12 @@ export class TransformUiPlugin {
order: 5,
mount: async (params) => {
const { mountManagementSection } = await import('./app/mount_management_section');
return mountManagementSection(coreSetup, params, this.isServerless);
return mountManagementSection(
coreSetup,
params,
this.isServerless,
this.experimentalFeatures
);
},
});
registerFeature(home);

View file

@ -5,11 +5,19 @@
* 2.0.
*/
import type { PluginInitializerContext } from '@kbn/core/server';
import type { PluginInitializerContext, PluginConfigDescriptor } from '@kbn/core/server';
import { configSchema, type ConfigSchema } from '../common/config';
export const plugin = async (ctx: PluginInitializerContext) => {
const { TransformServerPlugin } = await import('./plugin');
return new TransformServerPlugin(ctx);
};
export const config: PluginConfigDescriptor<ConfigSchema> = {
schema: configSchema,
exposeToBrowser: {
experimental: true,
},
};
export { registerTransformHealthRuleType } from './lib/alerting';

View file

@ -66,6 +66,7 @@ export const StorybookContextDecorator: React.FC<StorybookContextDecoratorProps>
ruleKqlBar: true,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
ruleFormV2: false,
},
});
return (

View file

@ -21,6 +21,7 @@ export const allowedExperimentalValues = Object.freeze({
ruleKqlBar: false,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
ruleFormV2: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -24,6 +24,7 @@ describe('getIsExperimentalFeatureEnabled', () => {
ruleKqlBar: true,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
ruleFormV2: false,
},
});
@ -63,6 +64,10 @@ describe('getIsExperimentalFeatureEnabled', () => {
expect(result).toEqual(false);
result = getIsExperimentalFeatureEnabled('ruleFormV2');
expect(result).toEqual(false);
expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError(
`Invalid enable value doesNotExist. Allowed values are: ${allowedExperimentalValueKeys.join(
', '