Skip to content

Commit 76b054c

Browse files
Merge branch '8.18' into backport/8.18/pr-218354
2 parents 357bb03 + 06b5855 commit 76b054c

16 files changed

Lines changed: 174 additions & 80 deletions

File tree

examples/feature_flags_example/common/feature_flags.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
export const FeatureFlagExampleBoolean = 'example-boolean';
11-
export const FeatureFlagExampleString = 'example-string';
12-
export const FeatureFlagExampleNumber = 'example-number';
10+
export const FeatureFlagExampleBoolean = 'featureFlagsExample.exampleBoolean';
11+
export const FeatureFlagExampleString = 'featureFlagsExample.exampleString';
12+
export const FeatureFlagExampleNumber = 'featureFlagsExample.exampleNumber';

src/core/packages/feature-flags/README.mdx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ id: kibFeatureFlagsService
33
slug: /kibana-dev-docs/tutorials/feature-flags-service
44
title: Feature Flags service
55
description: The Feature Flags service provides the necessary APIs to evaluate dynamic feature flags.
6-
date: 2024-07-26
6+
date: 2024-10-16
77
tags: ['kibana', 'dev', 'contributor', 'api docs', 'a/b testing', 'feature flags', 'flags']
88
---
99

@@ -12,7 +12,13 @@ tags: ['kibana', 'dev', 'contributor', 'api docs', 'a/b testing', 'feature flags
1212
The Feature Flags service provides the necessary APIs to evaluate dynamic feature flags.
1313

1414
The service is always enabled, however, it will return the fallback value if a feature flags provider hasn't been attached.
15-
Kibana only registers a provider when running on Elastic Cloud Hosted/Serverless.
15+
Kibana only registers a provider when running on Elastic Cloud Hosted/Serverless. And even in those scenarios, we expect that some customers might
16+
have network restrictions that might not allow the flags to evaluate. The fallback value must provide a non-broken experience to users.
17+
18+
:warning: Feature Flags are considered dynamic configuration and cannot be used for settings that require restarting Kibana.
19+
One example of invalid use cases are settings used during the `setup` lifecycle of the plugin, such as settings that define
20+
if an HTTP route is registered or not. Instead, you should always register the route, and return `404 - Not found` in the route
21+
handler if the feature flag returns a _disabled_ state.
1622

1723
For a code example, refer to the [Feature Flags Example plugin](../../../examples/feature_flags_example)
1824

@@ -28,7 +34,7 @@ import type { PluginInitializerContext } from '@kbn/core-plugins-server';
2834

2935
export const featureFlags: FeatureFlagDefinitions = [
3036
{
31-
key: 'my-cool-feature',
37+
key: 'myPlugin.myCoolFeature',
3238
name: 'My cool feature',
3339
description: 'Enables the cool feature to auto-hide the navigation bar',
3440
tags: ['my-plugin', 'my-service', 'ui'],
@@ -114,7 +120,7 @@ async (context, request, response) => {
114120
const { featureFlags } = await context.core;
115121
return response.ok({
116122
body: {
117-
number: await featureFlags.getNumberValue('example-number', 1),
123+
number: await featureFlags.getNumberValue('myPlugin.exampleNumber', 1),
118124
},
119125
});
120126
}
@@ -138,7 +144,7 @@ provider. In the `kibana.yml`, the following config sets the overrides:
138144

139145
```yaml
140146
feature_flags.overrides:
141-
my-feature-flag: 'my-forced-value'
147+
myPlugin.myFeatureFlag: 'my-forced-value'
142148
```
143149
144150
> [!WARNING]

src/core/packages/feature-flags/browser-internal/src/feature_flags_service.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,11 @@ describe('FeatureFlagsService Browser', () => {
244244
beforeEach(async () => {
245245
addHandlerSpy = jest.spyOn(featureFlagsClient, 'addHandler');
246246
injectedMetadata.getFeatureFlags.mockReturnValue({
247-
overrides: { 'my-overridden-flag': true },
247+
overrides: {
248+
'my-overridden-flag': true,
249+
'myPlugin.myOverriddenFlag': true,
250+
myDestructuredObjPlugin: { myOverriddenFlag: true },
251+
},
248252
});
249253
featureFlagsService.setup({ injectedMetadata });
250254
startContract = await featureFlagsService.start();
@@ -344,5 +348,14 @@ describe('FeatureFlagsService Browser', () => {
344348
expect(getBooleanValueSpy).toHaveBeenCalledTimes(1);
345349
expect(getBooleanValueSpy).toHaveBeenCalledWith('another-flag', false);
346350
});
351+
352+
test('overrides with dotted names', async () => {
353+
const getBooleanValueSpy = jest.spyOn(featureFlagsClient, 'getBooleanValue');
354+
expect(startContract.getBooleanValue('myPlugin.myOverriddenFlag', false)).toEqual(true);
355+
expect(
356+
startContract.getBooleanValue('myDestructuredObjPlugin.myOverriddenFlag', false)
357+
).toEqual(true);
358+
expect(getBooleanValueSpy).not.toHaveBeenCalled();
359+
});
347360
});
348361
});

src/core/packages/feature-flags/server-internal/src/feature_flags_service.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ describe('FeatureFlagsService Server', () => {
2727
atPath: {
2828
overrides: {
2929
'my-overridden-flag': true,
30+
'myPlugin.myOverriddenFlag': true,
31+
myDestructuredObjPlugin: { myOverriddenFlag: true },
3032
},
3133
},
3234
}),
@@ -253,10 +255,25 @@ describe('FeatureFlagsService Server', () => {
253255
expect(getBooleanValueSpy).toHaveBeenCalledTimes(1);
254256
expect(getBooleanValueSpy).toHaveBeenCalledWith('another-flag', false);
255257
});
258+
259+
test('overrides with dotted names', async () => {
260+
const getBooleanValueSpy = jest.spyOn(featureFlagsClient, 'getBooleanValue');
261+
await expect(
262+
startContract.getBooleanValue('myPlugin.myOverriddenFlag', false)
263+
).resolves.toEqual(true);
264+
await expect(
265+
startContract.getBooleanValue('myDestructuredObjPlugin.myOverriddenFlag', false)
266+
).resolves.toEqual(true);
267+
expect(getBooleanValueSpy).not.toHaveBeenCalled();
268+
});
256269
});
257270

258271
test('returns overrides', () => {
259272
const { getOverrides } = featureFlagsService.setup();
260-
expect(getOverrides()).toStrictEqual({ 'my-overridden-flag': true });
273+
expect(getOverrides()).toStrictEqual({
274+
'my-overridden-flag': true,
275+
'myPlugin.myOverriddenFlag': true,
276+
myDestructuredObjPlugin: { myOverriddenFlag: true },
277+
});
261278
});
262279
});

src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '@openfeature/server-sdk';
2525
import deepMerge from 'deepmerge';
2626
import { filter, switchMap, startWith, Subject } from 'rxjs';
27+
import { get } from 'lodash';
2728
import { createOpenFeatureLogger } from './create_open_feature_logger';
2829
import { setProviderWithRetries } from './set_provider_with_retries';
2930
import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
@@ -167,9 +168,10 @@ export class FeatureFlagsService {
167168
flagName: string,
168169
fallbackValue: T
169170
): Promise<T> {
171+
const override = get(this.overrides, flagName); // using lodash get because flagName can come with dots and the config parser might structure it in objects.
170172
const value =
171-
typeof this.overrides[flagName] !== 'undefined'
172-
? (this.overrides[flagName] as T)
173+
typeof override !== 'undefined'
174+
? (override as T)
173175
: // We have to bind the evaluation or the client will lose its internal context
174176
await evaluationFn.bind(this.featureFlagsClient)(flagName, fallbackValue);
175177
apm.addLabels({ [`flag_${flagName}`]: value });

src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,9 +516,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
516516
updatePrebuiltDetectionRules: isServerless
517517
? `${SERVERLESS_DOCS}security-prebuilt-rules-management.html#update-prebuilt-rules`
518518
: `${SECURITY_SOLUTION_DOCS}prebuilt-rules-management.html#update-prebuilt-rules`,
519-
prebuiltRuleCustomizationPromoBlog: isServerless
520-
? '' // URL for Serverless to be added later, once the blog post is published. Issue: https://github.com/elastic/kibana/issues/209000
521-
: `${ELASTIC_WEBSITE_URL}blog/security-prebuilt-rules-editing`,
519+
prebuiltRuleCustomizationPromoBlog: `${ELASTIC_WEBSITE_URL}blog/security-prebuilt-rules-editing`,
522520
createEsqlRuleType: `${SECURITY_SOLUTION_DOCS}rules-ui-create.html#create-esql-rule`,
523521
ruleUiAdvancedParams: `${SECURITY_SOLUTION_DOCS}rules-ui-create.html#rule-ui-advanced-params`,
524522
entityAnalytics: {

x-pack/platform/plugins/shared/spaces/public/management/edit_space/edit_space_roles_tab.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export const EditSpaceAssignedRolesTab: FC<Props> = ({ space, features, isReadOn
106106
size: 'm',
107107
maxWidth: true,
108108
maskProps: { headerZindexLocation: 'below' },
109+
'aria-labelledby': 'space-assign-role-privilege-form-title',
109110
}
110111
);
111112
},

x-pack/platform/plugins/shared/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
663663
<React.Fragment>
664664
<EuiFlyoutHeader hasBorder>
665665
<EuiTitle size="m">
666-
<h2>
666+
<h2 id="space-assign-role-privilege-form-title">
667667
{isEditOperation.current
668668
? i18n.translate('xpack.spaces.management.spaceDetails.roles.assignRoleButton', {
669669
defaultMessage: 'Edit role privileges for space',

x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,13 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({
207207
}, []);
208208

209209
return (
210-
<EuiFlyout onClose={onClose} data-test-subj="create-connector-flyout">
210+
<EuiFlyout
211+
onClose={onClose}
212+
data-test-subj="create-connector-flyout"
213+
aria-label={i18n.translate('xpack.triggersActionsUI.createConnectorFlyout', {
214+
defaultMessage: 'create connector flyout',
215+
})}
216+
>
211217
<FlyoutHeader
212218
icon={actionTypeModel?.iconClass}
213219
actionTypeName={actionType?.name}

x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,55 +8,101 @@
88
import React from 'react';
99
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
1010
import { UseArray, useFormData } from '../../../../shared_imports';
11+
import type { FormHook, ArrayItem } from '../../../../shared_imports';
1112
import { RelatedIntegrationsHelpInfo } from './related_integrations_help_info';
1213
import { RelatedIntegrationFieldRow } from './related_integration_field_row';
1314
import * as i18n from './translations';
1415
import { OptionalFieldLabel } from '../optional_field_label';
16+
import { getFlattenedArrayFieldNames } from '../utils';
1517

1618
interface RelatedIntegrationsProps {
1719
path: string;
1820
dataTestSubj?: string;
1921
}
2022

21-
export function RelatedIntegrations({ path, dataTestSubj }: RelatedIntegrationsProps): JSX.Element {
23+
function RelatedIntegrationsComponent({
24+
path,
25+
dataTestSubj,
26+
}: RelatedIntegrationsProps): JSX.Element {
27+
return (
28+
<UseArray path={path} initialNumberOfItems={0}>
29+
{({ items, addItem, removeItem, form }) => (
30+
<RelatedIntegrationsList
31+
items={items}
32+
addItem={addItem}
33+
removeItem={removeItem}
34+
path={path}
35+
form={form}
36+
dataTestSubj={dataTestSubj}
37+
/>
38+
)}
39+
</UseArray>
40+
);
41+
}
42+
43+
interface RelatedIntegrationsListProps {
44+
items: ArrayItem[];
45+
addItem: () => void;
46+
removeItem: (id: number) => void;
47+
path: string;
48+
form: FormHook;
49+
dataTestSubj?: string;
50+
}
51+
52+
const RelatedIntegrationsList = ({
53+
items,
54+
addItem,
55+
removeItem,
56+
path,
57+
form,
58+
dataTestSubj,
59+
}: RelatedIntegrationsListProps) => {
60+
const flattenedFieldNames = getFlattenedArrayFieldNames(form, path);
61+
62+
/*
63+
Not using "watch" for the initial render, to let row components render and initialize form fields.
64+
Then we can use the "watch" feature to track their changes.
65+
*/
66+
const hasRenderedInitially = flattenedFieldNames.length > 0;
67+
const fieldsToWatch = hasRenderedInitially ? flattenedFieldNames : [];
68+
69+
const [formData] = useFormData({ watch: fieldsToWatch });
70+
2271
const label = (
2372
<>
2473
{i18n.RELATED_INTEGRATIONS_LABEL}
2574
<RelatedIntegrationsHelpInfo />
2675
</>
2776
);
28-
const [formData] = useFormData();
2977

3078
return (
31-
<UseArray path={path} initialNumberOfItems={0}>
32-
{({ items, addItem, removeItem }) => (
33-
<EuiFormRow
34-
label={label}
35-
labelAppend={OptionalFieldLabel}
36-
labelType="legend"
37-
fullWidth
38-
data-test-subj={dataTestSubj}
39-
hasChildLabel={false}
40-
>
41-
<>
42-
<EuiFlexGroup direction="column" gutterSize="s">
43-
{items.map((item) => (
44-
<EuiFlexItem key={item.id} data-test-subj="relatedIntegrationRow">
45-
<RelatedIntegrationFieldRow
46-
item={item}
47-
relatedIntegrations={formData[path] ?? []}
48-
removeItem={removeItem}
49-
/>
50-
</EuiFlexItem>
51-
))}
52-
</EuiFlexGroup>
53-
{items.length > 0 && <EuiSpacer size="s" />}
54-
<EuiButtonEmpty size="xs" iconType="plusInCircle" onClick={addItem}>
55-
{i18n.ADD_INTEGRATION}
56-
</EuiButtonEmpty>
57-
</>
58-
</EuiFormRow>
59-
)}
60-
</UseArray>
79+
<EuiFormRow
80+
label={label}
81+
labelAppend={OptionalFieldLabel}
82+
labelType="legend"
83+
fullWidth
84+
data-test-subj={dataTestSubj}
85+
hasChildLabel={false}
86+
>
87+
<>
88+
<EuiFlexGroup direction="column" gutterSize="s">
89+
{items.map((item) => (
90+
<EuiFlexItem key={item.id} data-test-subj="relatedIntegrationRow">
91+
<RelatedIntegrationFieldRow
92+
item={item}
93+
relatedIntegrations={formData[path] ?? []}
94+
removeItem={removeItem}
95+
/>
96+
</EuiFlexItem>
97+
))}
98+
</EuiFlexGroup>
99+
{items.length > 0 && <EuiSpacer size="s" />}
100+
<EuiButtonEmpty size="xs" iconType="plusInCircle" onClick={addItem}>
101+
{i18n.ADD_INTEGRATION}
102+
</EuiButtonEmpty>
103+
</>
104+
</EuiFormRow>
61105
);
62-
}
106+
};
107+
108+
export const RelatedIntegrations = React.memo(RelatedIntegrationsComponent);

0 commit comments

Comments
 (0)