From 62f9b56c4cb0061682d857c3fb35150b13d91c0a Mon Sep 17 00:00:00 2001
From: Maxim Kholod <maxim.kholod@elastic.co>
Date: Tue, 26 Sep 2023 10:28:13 +0200
Subject: [PATCH 1/4] [Cloud Security] use only available agent versions for
 Cloudformation and Cloud Shell parameters (#166198)

## Summary

fixes
- https://github.com/elastic/security-team/issues/7557

instead of using Kibana version for Cloudformation and Cloud Shell
params in CNVM and CSPM integrations, check if the version of an agent
that matches the current Kibana version actually available as an
artifact. Relevant for serverless, where Kibana version points to
not-yet released versions of Elastic Agent.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
 .../public/hooks/use_agent_version.test.ts    | 125 ++++++++++++++++++
 .../fleet/public/hooks/use_agent_version.ts   |  27 +++-
 2 files changed, 146 insertions(+), 6 deletions(-)
 create mode 100644 x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts

diff --git a/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts b/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts
new file mode 100644
index 0000000000000..6cb1c8ee42248
--- /dev/null
+++ b/x-pack/plugins/fleet/public/hooks/use_agent_version.test.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+
+import { useAgentVersion } from './use_agent_version';
+import { useKibanaVersion } from './use_kibana_version';
+import { sendGetAgentsAvailableVersions } from './use_request';
+
+jest.mock('./use_kibana_version');
+jest.mock('./use_request');
+
+describe('useAgentVersion', () => {
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should return agent version that matches Kibana version if released', async () => {
+    const mockKibanaVersion = '8.8.1';
+    const mockAvailableVersions = ['8.9.0', '8.8.1', '8.8.0', '8.7.0'];
+
+    (useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
+    (sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
+      data: { items: mockAvailableVersions },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());
+
+    expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();
+
+    await waitForNextUpdate();
+
+    expect(result.current).toEqual(mockKibanaVersion);
+  });
+
+  it('should return the latest availeble agent version if a version that matches Kibana version is not released', async () => {
+    const mockKibanaVersion = '8.11.0';
+    const mockAvailableVersions = ['8.8.0', '8.7.0', '8.9.2', '7.16.0'];
+
+    (useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
+    (sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
+      data: { items: mockAvailableVersions },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());
+
+    expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();
+
+    await waitForNextUpdate();
+
+    expect(result.current).toEqual('8.9.2');
+  });
+
+  it('should return the agent version that is <= Kibana version if an agent version that matches Kibana version is not released', async () => {
+    const mockKibanaVersion = '8.8.3';
+    const mockAvailableVersions = ['8.8.0', '8.8.1', '8.8.2', '8.7.0', '8.9.2', '7.16.0'];
+
+    (useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
+    (sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
+      data: { items: mockAvailableVersions },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());
+
+    expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();
+
+    await waitForNextUpdate();
+
+    expect(result.current).toEqual('8.8.2');
+  });
+
+  it('should return the latest availeble agent version if a snapshot version', async () => {
+    const mockKibanaVersion = '8.10.0-SNAPSHOT';
+    const mockAvailableVersions = ['8.8.0', '8.7.0', '8.9.2', '7.16.0'];
+
+    (useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
+    (sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
+      data: { items: mockAvailableVersions },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());
+
+    expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();
+
+    await waitForNextUpdate();
+
+    expect(result.current).toEqual('8.9.2');
+  });
+
+  it('should return kibana version if no agent versions available', async () => {
+    const mockKibanaVersion = '8.11.0';
+    const mockAvailableVersions: string[] = [];
+
+    (useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
+    (sendGetAgentsAvailableVersions as jest.Mock).mockResolvedValue({
+      data: { items: mockAvailableVersions },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());
+
+    expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();
+
+    await waitForNextUpdate();
+
+    expect(result.current).toEqual('8.11.0');
+  });
+
+  it('should return kibana version if the list of available agent versions is not available', async () => {
+    const mockKibanaVersion = '8.11.0';
+
+    (useKibanaVersion as jest.Mock).mockReturnValue(mockKibanaVersion);
+    (sendGetAgentsAvailableVersions as jest.Mock).mockRejectedValue(new Error('Fetching error'));
+
+    const { result, waitForNextUpdate } = renderHook(() => useAgentVersion());
+
+    expect(sendGetAgentsAvailableVersions).toHaveBeenCalled();
+    await waitForNextUpdate();
+
+    expect(result.current).toEqual(mockKibanaVersion);
+  });
+});
diff --git a/x-pack/plugins/fleet/public/hooks/use_agent_version.ts b/x-pack/plugins/fleet/public/hooks/use_agent_version.ts
index 32d0ee128ddcc..8c198dbc7773e 100644
--- a/x-pack/plugins/fleet/public/hooks/use_agent_version.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_agent_version.ts
@@ -4,14 +4,15 @@
  * 2.0; you may not use this file except in compliance with the Elastic License
  * 2.0.
  */
-
 import { useEffect, useState } from 'react';
+import semverRcompare from 'semver/functions/rcompare';
+import semverLt from 'semver/functions/lt';
 
 import { useKibanaVersion } from './use_kibana_version';
 import { sendGetAgentsAvailableVersions } from './use_request';
 
 /**
- * @returns The most recent agent version available to install or upgrade to.
+ * @returns The most compatible agent version available to install or upgrade to.
  */
 export const useAgentVersion = (): string | undefined => {
   const kibanaVersion = useKibanaVersion();
@@ -21,12 +22,26 @@ export const useAgentVersion = (): string | undefined => {
     const getVersions = async () => {
       try {
         const res = await sendGetAgentsAvailableVersions();
-        // if the endpoint returns an error, use the fallback versions
-        const versionsList = res?.data?.items ? res.data.items : [kibanaVersion];
+        const availableVersions = res?.data?.items;
+        let agentVersionToUse;
+
+        if (
+          availableVersions &&
+          availableVersions.length > 0 &&
+          availableVersions.indexOf(kibanaVersion) === -1
+        ) {
+          availableVersions.sort(semverRcompare);
+          agentVersionToUse =
+            availableVersions.find((version) => {
+              return semverLt(version, kibanaVersion);
+            }) || availableVersions[0];
+        } else {
+          agentVersionToUse = kibanaVersion;
+        }
 
-        setAgentVersion(versionsList[0]);
+        setAgentVersion(agentVersionToUse);
       } catch (err) {
-        return;
+        setAgentVersion(kibanaVersion);
       }
     };
 

From fd1a1f93f300308392cdba3dd84dfb42dc492ae5 Mon Sep 17 00:00:00 2001
From: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com>
Date: Tue, 26 Sep 2023 10:50:41 +0100
Subject: [PATCH 2/4] [serverless] Advanced Settings - Form component (#166460)

Addresses https://github.com/elastic/kibana/issues/160411

## Summary

This PR adds a package that contains a form component for the Advanced
Settings UI in serverless.
This implementation was extracted from the the `Form` component in the
`advancedSettings` plugin, excluding some functionalities:
- The form doesn't support search queries.
- The form doesn't divide the settings into categories.

### Testing

The form can be tested in the Storybook Preview from the CI build. Some
things to be tested:

- Making changes to any of the fields displays the bottom bar.
- Clicking the Cancel button clears the changes.
- Clicking the Save button triggers a `saveChanges` action with the
correct changes.
- The bottom bar correctly shows the number of unsaved settings.
- Toggling the `isSavingEnabled` control to `false` disables all fields.
- Toggling the `requirePageReload` control to `true` causes saving of
changes to any of the fields to trigger a `showReloadPagePrompt` action.

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

<!--
### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Clint Andrew Hall <clint@clintandrewhall.com>
---
 .github/CODEOWNERS                            |   1 +
 package.json                                  |   1 +
 .../settings/components/form/README.mdx       |  18 +++
 .../form/bottom_bar/bottom_bar.test.tsx       |  79 ++++++++++
 .../components/form/bottom_bar/bottom_bar.tsx | 104 +++++++++++++
 .../components/form/bottom_bar/index.tsx      |   9 ++
 .../form/bottom_bar/unsaved_count.tsx         |  41 +++++
 .../settings/components/form/form.styles.ts   |  33 ++++
 .../settings/components/form/form.test.tsx    | 144 ++++++++++++++++++
 .../settings/components/form/form.tsx         |  86 +++++++++++
 .../settings/components/form/index.ts         |  12 ++
 .../settings/components/form/kibana.jsonc     |   5 +
 .../components/form/mocks/context.tsx         |  54 +++++++
 .../settings/components/form/mocks/index.ts   |  11 ++
 .../components/form/mocks/settings.ts         | 114 ++++++++++++++
 .../components/form/mocks/settings_client.ts  |  17 +++
 .../settings/components/form/package.json     |   6 +
 .../components/form/reload_page_toast.tsx     |  49 ++++++
 .../settings/components/form/services.tsx     |  78 ++++++++++
 .../form/storybook/form.stories.tsx           |  67 ++++++++
 .../settings/components/form/tsconfig.json    |  34 +++++
 .../settings/components/form/types.ts         |  51 +++++++
 .../settings/components/form/use_save.ts      |  52 +++++++
 .../field_definition/get_definitions.ts       |   4 +-
 tsconfig.base.json                            |   2 +
 yarn.lock                                     |   4 +
 26 files changed, 1075 insertions(+), 1 deletion(-)
 create mode 100644 packages/kbn-management/settings/components/form/README.mdx
 create mode 100644 packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.test.tsx
 create mode 100644 packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.tsx
 create mode 100644 packages/kbn-management/settings/components/form/bottom_bar/index.tsx
 create mode 100644 packages/kbn-management/settings/components/form/bottom_bar/unsaved_count.tsx
 create mode 100644 packages/kbn-management/settings/components/form/form.styles.ts
 create mode 100644 packages/kbn-management/settings/components/form/form.test.tsx
 create mode 100644 packages/kbn-management/settings/components/form/form.tsx
 create mode 100644 packages/kbn-management/settings/components/form/index.ts
 create mode 100644 packages/kbn-management/settings/components/form/kibana.jsonc
 create mode 100644 packages/kbn-management/settings/components/form/mocks/context.tsx
 create mode 100644 packages/kbn-management/settings/components/form/mocks/index.ts
 create mode 100644 packages/kbn-management/settings/components/form/mocks/settings.ts
 create mode 100644 packages/kbn-management/settings/components/form/mocks/settings_client.ts
 create mode 100644 packages/kbn-management/settings/components/form/package.json
 create mode 100644 packages/kbn-management/settings/components/form/reload_page_toast.tsx
 create mode 100644 packages/kbn-management/settings/components/form/services.tsx
 create mode 100644 packages/kbn-management/settings/components/form/storybook/form.stories.tsx
 create mode 100644 packages/kbn-management/settings/components/form/tsconfig.json
 create mode 100644 packages/kbn-management/settings/components/form/types.ts
 create mode 100644 packages/kbn-management/settings/components/form/use_save.ts

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 647de24ad4920..9a6a585abd68c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -484,6 +484,7 @@ packages/kbn-management/cards_navigation @elastic/platform-deployment-management
 src/plugins/management @elastic/platform-deployment-management
 packages/kbn-management/settings/components/field_input @elastic/platform-deployment-management
 packages/kbn-management/settings/components/field_row @elastic/platform-deployment-management
+packages/kbn-management/settings/components/form @elastic/platform-deployment-management
 packages/kbn-management/settings/field_definition @elastic/platform-deployment-management
 packages/kbn-management/settings/setting_ids @elastic/appex-sharedux @elastic/platform-deployment-management
 packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/platform-deployment-management
diff --git a/package.json b/package.json
index 2749e9b2143c4..8d29cd024e96f 100644
--- a/package.json
+++ b/package.json
@@ -506,6 +506,7 @@
     "@kbn/management-plugin": "link:src/plugins/management",
     "@kbn/management-settings-components-field-input": "link:packages/kbn-management/settings/components/field_input",
     "@kbn/management-settings-components-field-row": "link:packages/kbn-management/settings/components/field_row",
+    "@kbn/management-settings-components-form": "link:packages/kbn-management/settings/components/form",
     "@kbn/management-settings-field-definition": "link:packages/kbn-management/settings/field_definition",
     "@kbn/management-settings-ids": "link:packages/kbn-management/settings/setting_ids",
     "@kbn/management-settings-section-registry": "link:packages/kbn-management/settings/section_registry",
diff --git a/packages/kbn-management/settings/components/form/README.mdx b/packages/kbn-management/settings/components/form/README.mdx
new file mode 100644
index 0000000000000..163f476284f89
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/README.mdx
@@ -0,0 +1,18 @@
+---
+id: management/settings/components/form
+slug: /management/settings/components/form
+title: Management Settings Form Component
+description: A package containing a component for rendering the form in the Advanced Settings UI.
+tags: ['management', 'settings']
+date: 2023-09-12
+---
+
+## Description
+
+This package contains a component for rendering the Advanced Settings UI form that contains `FieldRow` components, each of which displays a single UiSetting field row.
+The form also handles the logic for saving any changes to the UiSettings values by directly communicating with the uiSettings service.
+
+
+## Notes
+
+- This implementation was extracted from the `Form` component in the `advancedSettings` plugin.
\ No newline at end of file
diff --git a/packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.test.tsx b/packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.test.tsx
new file mode 100644
index 0000000000000..ddb3502bb2009
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import {
+  BottomBar,
+  BottomBarProps,
+  DATA_TEST_SUBJ_SAVE_BUTTON,
+  DATA_TEST_SUBJ_CANCEL_BUTTON,
+} from './bottom_bar';
+import { wrap } from '../mocks';
+
+const saveAll = jest.fn();
+const clearAllUnsaved = jest.fn();
+const unsavedChangesCount = 3;
+
+const defaultProps: BottomBarProps = {
+  onSaveAll: saveAll,
+  onClearAllUnsaved: clearAllUnsaved,
+  hasInvalidChanges: false,
+  unsavedChangesCount,
+  isLoading: false,
+};
+
+const unsavedChangesCountText = unsavedChangesCount + ' unsaved settings';
+
+describe('BottomBar', () => {
+  it('renders without errors', () => {
+    const { container } = render(wrap(<BottomBar {...defaultProps} />));
+    expect(container).toBeInTheDocument();
+  });
+
+  it('fires saveAll when the Save button is clicked', () => {
+    const { getByTestId } = render(wrap(<BottomBar {...defaultProps} />));
+
+    const input = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
+    fireEvent.click(input);
+    expect(saveAll).toHaveBeenCalled();
+  });
+
+  it('fires clearAllUnsaved when the Cancel button is clicked', () => {
+    const { getByTestId } = render(wrap(<BottomBar {...defaultProps} />));
+
+    const input = getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON);
+    fireEvent.click(input);
+    expect(saveAll).toHaveBeenCalled();
+  });
+
+  it('renders unsaved changes count', () => {
+    const { getByText } = render(wrap(<BottomBar {...defaultProps} />));
+
+    expect(getByText(unsavedChangesCountText)).toBeInTheDocument();
+  });
+
+  it('save button is disabled when there are invalid changes', () => {
+    const { getByTestId } = render(
+      wrap(<BottomBar {...{ ...defaultProps, hasInvalidChanges: true }} />)
+    );
+
+    const input = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
+    expect(input).toBeDisabled();
+  });
+
+  it('save button is loading when in loading state', () => {
+    const { getByTestId, getByLabelText } = render(
+      wrap(<BottomBar {...{ ...defaultProps, isLoading: true }} />)
+    );
+
+    const input = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
+    expect(input).toBeDisabled();
+    expect(getByLabelText('Loading')).toBeInTheDocument();
+  });
+});
diff --git a/packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.tsx b/packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.tsx
new file mode 100644
index 0000000000000..818c86b78109a
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/bottom_bar/bottom_bar.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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 React from 'react';
+
+import {
+  EuiBottomBar,
+  EuiButton,
+  EuiButtonEmpty,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiToolTip,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { UnsavedCount } from './unsaved_count';
+import { useFormStyles } from '../form.styles';
+
+export const DATA_TEST_SUBJ_SAVE_BUTTON = 'settings-save-button';
+export const DATA_TEST_SUBJ_CANCEL_BUTTON = 'settings-cancel-button';
+
+/**
+ * Props for a {@link BottomBar} component.
+ */
+export interface BottomBarProps {
+  onSaveAll: () => void;
+  onClearAllUnsaved: () => void;
+  hasInvalidChanges: boolean;
+  isLoading: boolean;
+  unsavedChangesCount: number;
+}
+
+/**
+ * Component for displaying the bottom bar of a {@link Form}.
+ */
+export const BottomBar = ({
+  onSaveAll,
+  onClearAllUnsaved,
+  hasInvalidChanges,
+  isLoading,
+  unsavedChangesCount,
+}: BottomBarProps) => {
+  const { cssFormButton, cssFormUnsavedCount } = useFormStyles();
+
+  return (
+    <EuiBottomBar>
+      <EuiFlexGroup
+        justifyContent="spaceBetween"
+        alignItems="center"
+        responsive={false}
+        gutterSize="s"
+      >
+        <EuiFlexItem grow={false} css={cssFormUnsavedCount}>
+          <UnsavedCount unsavedCount={unsavedChangesCount} />
+        </EuiFlexItem>
+        <EuiFlexItem />
+        <EuiFlexItem grow={false}>
+          <EuiButtonEmpty
+            css={cssFormButton}
+            color="ghost"
+            size="s"
+            iconType="cross"
+            onClick={onClearAllUnsaved}
+            data-test-subj={DATA_TEST_SUBJ_CANCEL_BUTTON}
+          >
+            {i18n.translate('management.settings.form.cancelButtonLabel', {
+              defaultMessage: 'Cancel changes',
+            })}
+          </EuiButtonEmpty>
+        </EuiFlexItem>
+        <EuiFlexItem grow={false}>
+          <EuiToolTip
+            content={
+              hasInvalidChanges &&
+              i18n.translate('management.settings.form.saveButtonTooltipWithInvalidChanges', {
+                defaultMessage: 'Fix invalid settings before saving.',
+              })
+            }
+          >
+            <EuiButton
+              css={cssFormButton}
+              disabled={hasInvalidChanges}
+              color="success"
+              fill
+              size="s"
+              iconType="check"
+              onClick={onSaveAll}
+              isLoading={isLoading}
+              data-test-subj={DATA_TEST_SUBJ_SAVE_BUTTON}
+            >
+              {i18n.translate('management.settings.form.saveButtonLabel', {
+                defaultMessage: 'Save changes',
+              })}
+            </EuiButton>
+          </EuiToolTip>
+        </EuiFlexItem>
+      </EuiFlexGroup>
+    </EuiBottomBar>
+  );
+};
diff --git a/packages/kbn-management/settings/components/form/bottom_bar/index.tsx b/packages/kbn-management/settings/components/form/bottom_bar/index.tsx
new file mode 100644
index 0000000000000..0abbe24520157
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/bottom_bar/index.tsx
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+export { BottomBar } from './bottom_bar';
diff --git a/packages/kbn-management/settings/components/form/bottom_bar/unsaved_count.tsx b/packages/kbn-management/settings/components/form/bottom_bar/unsaved_count.tsx
new file mode 100644
index 0000000000000..c5bb501ce98f4
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/bottom_bar/unsaved_count.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 React from 'react';
+
+import { EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useFormStyles } from '../form.styles';
+
+/**
+ * Props for a {@link UnsavedCount} component.
+ */
+interface UnsavedCountProps {
+  unsavedCount: number;
+}
+
+/**
+ * Component for displaying the count of unsaved changes in a {@link BottomBar}.
+ */
+export const UnsavedCount = ({ unsavedCount }: UnsavedCountProps) => {
+  const { cssFormUnsavedCountMessage } = useFormStyles();
+  return (
+    <EuiText size="s" color="ghost" css={cssFormUnsavedCountMessage}>
+      <FormattedMessage
+        id="management.settings.form.countOfSettingsChanged"
+        defaultMessage="{unsavedCount} unsaved {unsavedCount, plural,
+              one {setting}
+              other {settings}
+            }"
+        values={{
+          unsavedCount,
+        }}
+      />
+    </EuiText>
+  );
+};
diff --git a/packages/kbn-management/settings/components/form/form.styles.ts b/packages/kbn-management/settings/components/form/form.styles.ts
new file mode 100644
index 0000000000000..994bab4530f1f
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/form.styles.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { useEuiTheme, euiBreakpoint } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+/**
+ * A React hook that provides stateful `css` classes for the {@link Form} component.
+ */
+export const useFormStyles = () => {
+  const euiTheme = useEuiTheme();
+  const { size, colors } = euiTheme.euiTheme;
+
+  return {
+    cssFormButton: css`
+      width: 100%;
+    `,
+    cssFormUnsavedCount: css`
+      ${euiBreakpoint(euiTheme, ['xs'])} {
+        display: none;
+      }
+    `,
+    cssFormUnsavedCountMessage: css`
+      box-shadow: -${size.xs} 0 ${colors.warning};
+      padding-left: ${size.s};
+    `,
+  };
+};
diff --git a/packages/kbn-management/settings/components/form/form.test.tsx b/packages/kbn-management/settings/components/form/form.test.tsx
new file mode 100644
index 0000000000000..2f1cbdb80bcac
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/form.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * 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 React from 'react';
+import { fireEvent, render, waitFor } from '@testing-library/react';
+
+import { FieldDefinition, SettingType } from '@kbn/management-settings-types';
+import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
+
+import { Form } from './form';
+import { wrap, getSettingsMock, createFormServicesMock, uiSettingsClientMock } from './mocks';
+import { TEST_SUBJ_PREFIX_FIELD } from '@kbn/management-settings-components-field-input/input';
+import { DATA_TEST_SUBJ_SAVE_BUTTON, DATA_TEST_SUBJ_CANCEL_BUTTON } from './bottom_bar/bottom_bar';
+import { FormServices } from './types';
+
+const settingsMock = getSettingsMock();
+const fields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
+  settingsMock,
+  uiSettingsClientMock
+);
+
+describe('Form', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('renders without errors', () => {
+    const { container } = render(wrap(<Form fields={fields} isSavingEnabled={true} />));
+
+    expect(container).toBeInTheDocument();
+  });
+
+  it('renders as read only if saving is disabled', () => {
+    const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={false} />));
+
+    (Object.keys(settingsMock) as SettingType[]).forEach((type) => {
+      if (type === 'json' || type === 'markdown') {
+        return;
+      }
+
+      const inputTestSubj = `${TEST_SUBJ_PREFIX_FIELD}-${type}`;
+
+      if (type === 'color') {
+        expect(getByTestId(`euiColorPickerAnchor ${inputTestSubj}`)).toBeDisabled();
+      } else {
+        expect(getByTestId(inputTestSubj)).toBeDisabled();
+      }
+    });
+  });
+
+  it('renders bottom bar when a field is changed', () => {
+    const { getByTestId, queryByTestId } = render(
+      wrap(<Form fields={fields} isSavingEnabled={true} />)
+    );
+
+    expect(queryByTestId(DATA_TEST_SUBJ_SAVE_BUTTON)).not.toBeInTheDocument();
+    expect(queryByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON)).not.toBeInTheDocument();
+
+    const testFieldType = 'string';
+    const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
+    fireEvent.change(input, { target: { value: 'test' } });
+
+    expect(getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON)).toBeInTheDocument();
+    expect(getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON)).toBeInTheDocument();
+  });
+
+  it('fires saveChanges when Save button is clicked', async () => {
+    const services: FormServices = createFormServicesMock();
+    const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={true} />, services));
+
+    const testFieldType = 'string';
+    const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
+    fireEvent.change(input, { target: { value: 'test' } });
+
+    const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
+    fireEvent.click(saveButton);
+
+    expect(services.saveChanges).toHaveBeenCalledWith({
+      string: { type: 'string', unsavedValue: 'test' },
+    });
+  });
+
+  it('clears changes when Cancel button is clicked', () => {
+    const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={false} />));
+
+    const testFieldType = 'string';
+    const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
+    fireEvent.change(input, { target: { value: 'test' } });
+
+    const cancelButton = getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON);
+    fireEvent.click(cancelButton);
+
+    expect(input).toHaveValue(settingsMock[testFieldType].value);
+  });
+
+  it('fires showError when saving is unsuccessful', () => {
+    const services: FormServices = createFormServicesMock();
+    const saveChangesWithError = jest.fn(() => {
+      throw new Error('Unable to save');
+    });
+    const testServices = { ...services, saveChanges: saveChangesWithError };
+
+    const { getByTestId } = render(
+      wrap(<Form fields={fields} isSavingEnabled={true} />, testServices)
+    );
+
+    const testFieldType = 'string';
+    const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
+    fireEvent.change(input, { target: { value: 'test' } });
+
+    const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
+    fireEvent.click(saveButton);
+
+    expect(testServices.showError).toHaveBeenCalled();
+  });
+
+  it('fires showReloadPagePrompt when changing a reloadPageRequired setting', async () => {
+    const services: FormServices = createFormServicesMock();
+    // Make all settings require a page reload
+    const testFields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
+      getSettingsMock(true),
+      uiSettingsClientMock
+    );
+    const { getByTestId } = render(
+      wrap(<Form fields={testFields} isSavingEnabled={true} />, services)
+    );
+
+    const testFieldType = 'string';
+    const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
+    fireEvent.change(input, { target: { value: 'test' } });
+
+    const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
+    fireEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(services.showReloadPagePrompt).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/packages/kbn-management/settings/components/form/form.tsx b/packages/kbn-management/settings/components/form/form.tsx
new file mode 100644
index 0000000000000..fabc80755cad8
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/form.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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 React, { Fragment } from 'react';
+
+import type { FieldDefinition } from '@kbn/management-settings-types';
+import { FieldRow, RowOnChangeFn } from '@kbn/management-settings-components-field-row';
+import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
+import { isEmpty } from 'lodash';
+import { BottomBar } from './bottom_bar';
+import { useSave } from './use_save';
+
+/**
+ * Props for a {@link Form} component.
+ */
+export interface FormProps {
+  /** A list of {@link FieldDefinition} corresponding to settings to be displayed in the form. */
+  fields: Array<FieldDefinition<SettingType>>;
+  /** True if saving settings is enabled, false otherwise. */
+  isSavingEnabled: boolean;
+}
+
+/**
+ * Component for displaying a set of {@link FieldRow} in a form.
+ * @param props The {@link FormProps} for the {@link Form} component.
+ */
+export const Form = (props: FormProps) => {
+  const { fields, isSavingEnabled } = props;
+
+  const [unsavedChanges, setUnsavedChanges] = React.useState<
+    Record<string, UnsavedFieldChange<SettingType>>
+  >({});
+
+  const [isLoading, setIsLoading] = React.useState<boolean>(false);
+
+  const unsavedChangesCount = Object.keys(unsavedChanges).length;
+  const hasInvalidChanges = Object.values(unsavedChanges).some(({ isInvalid }) => isInvalid);
+
+  const clearAllUnsaved = () => {
+    setUnsavedChanges({});
+  };
+
+  const saveChanges = useSave({ fields, clearChanges: clearAllUnsaved });
+
+  const saveAll = async () => {
+    setIsLoading(true);
+    await saveChanges(unsavedChanges);
+    setIsLoading(false);
+  };
+
+  const onChange: RowOnChangeFn<SettingType> = (id, change) => {
+    if (!change) {
+      const { [id]: unsavedChange, ...rest } = unsavedChanges;
+      setUnsavedChanges(rest);
+      return;
+    }
+
+    setUnsavedChanges((changes) => ({ ...changes, [id]: change }));
+  };
+
+  const fieldRows = fields.map((field) => {
+    const { id: key } = field;
+    const unsavedChange = unsavedChanges[key];
+    return <FieldRow {...{ key, field, unsavedChange, onChange, isSavingEnabled }} />;
+  });
+
+  return (
+    <Fragment>
+      <div>{fieldRows}</div>
+      {!isEmpty(unsavedChanges) && (
+        <BottomBar
+          onSaveAll={saveAll}
+          onClearAllUnsaved={clearAllUnsaved}
+          hasInvalidChanges={hasInvalidChanges}
+          isLoading={isLoading}
+          unsavedChangesCount={unsavedChangesCount}
+        />
+      )}
+    </Fragment>
+  );
+};
diff --git a/packages/kbn-management/settings/components/form/index.ts b/packages/kbn-management/settings/components/form/index.ts
new file mode 100644
index 0000000000000..d674990322a09
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+export { Form } from './form';
+
+export type { FormKibanaDependencies, FormServices } from './types';
+export { FormProvider, FormKibanaProvider } from './services';
diff --git a/packages/kbn-management/settings/components/form/kibana.jsonc b/packages/kbn-management/settings/components/form/kibana.jsonc
new file mode 100644
index 0000000000000..5db9d203e37f3
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+  "type": "shared-common",
+  "id": "@kbn/management-settings-components-form",
+  "owner": "@elastic/platform-deployment-management"
+}
diff --git a/packages/kbn-management/settings/components/form/mocks/context.tsx b/packages/kbn-management/settings/components/form/mocks/context.tsx
new file mode 100644
index 0000000000000..2af26a8f0aaf1
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/mocks/context.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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 React, { ReactChild } from 'react';
+import { I18nProvider } from '@kbn/i18n-react';
+
+import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
+import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
+import { I18nStart } from '@kbn/core-i18n-browser';
+
+import { createFieldRowServicesMock } from '@kbn/management-settings-components-field-row/mocks';
+import { FormProvider } from '../services';
+import type { FormServices } from '../types';
+
+const createRootMock = () => {
+  const i18n: I18nStart = {
+    Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
+  };
+  const theme = themeServiceMock.createStartContract();
+  return {
+    i18n,
+    theme,
+  };
+};
+
+export const createFormServicesMock = (): FormServices => ({
+  ...createFieldRowServicesMock(),
+  saveChanges: jest.fn(),
+  showError: jest.fn(),
+  showReloadPagePrompt: jest.fn(),
+});
+
+export const TestWrapper = ({
+  children,
+  services = createFormServicesMock(),
+}: {
+  children: ReactChild;
+  services?: FormServices;
+}) => {
+  return (
+    <KibanaRootContextProvider {...createRootMock()}>
+      <FormProvider {...services}>{children}</FormProvider>
+    </KibanaRootContextProvider>
+  );
+};
+
+export const wrap = (component: JSX.Element, services: FormServices = createFormServicesMock()) => (
+  <TestWrapper services={services} children={component} />
+);
diff --git a/packages/kbn-management/settings/components/form/mocks/index.ts b/packages/kbn-management/settings/components/form/mocks/index.ts
new file mode 100644
index 0000000000000..80e92448a3bb4
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/mocks/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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.
+ */
+
+export { TestWrapper, createFormServicesMock, wrap } from './context';
+export { getSettingsMock } from './settings';
+export { uiSettingsClientMock } from './settings_client';
diff --git a/packages/kbn-management/settings/components/form/mocks/settings.ts b/packages/kbn-management/settings/components/form/mocks/settings.ts
new file mode 100644
index 0000000000000..e22f24e4a1a09
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/mocks/settings.ts
@@ -0,0 +1,114 @@
+/*
+ * 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 { KnownTypeToMetadata, SettingType } from '@kbn/management-settings-types';
+
+type Settings = {
+  [key in SettingType]: KnownTypeToMetadata<key>;
+};
+
+/**
+ * A utility function returning a representative set of UiSettings.
+ * @param requirePageReload The value of the `requirePageReload` param for all settings.
+ */
+export const getSettingsMock = (requirePageReload: boolean = false): Settings => {
+  const defaults = {
+    requiresPageReload: requirePageReload,
+    readonly: false,
+    category: ['category'],
+  };
+
+  return {
+    array: {
+      description: 'Description for Array test setting',
+      name: 'array:test:setting',
+      type: 'array',
+      userValue: null,
+      value: ['example_value'],
+      ...defaults,
+    },
+    boolean: {
+      description: 'Description for Boolean test setting',
+      name: 'boolean:test:setting',
+      type: 'boolean',
+      userValue: null,
+      value: true,
+      ...defaults,
+    },
+    color: {
+      description: 'Description for Color test setting',
+      name: 'color:test:setting',
+      type: 'color',
+      userValue: null,
+      value: '#FF00CC',
+      ...defaults,
+    },
+    image: {
+      description: 'Description for Image test setting',
+      name: 'image:test:setting',
+      type: 'image',
+      userValue: null,
+      value: '',
+      ...defaults,
+    },
+    number: {
+      description: 'Description for Number test setting',
+      name: 'number:test:setting',
+      type: 'number',
+      userValue: null,
+      value: 1,
+      ...defaults,
+    },
+    json: {
+      name: 'json:test:setting',
+      description: 'Description for Json test setting',
+      type: 'json',
+      userValue: null,
+      value: '{"foo": "bar"}',
+      ...defaults,
+    },
+    markdown: {
+      name: 'markdown:test:setting',
+      description: 'Description for Markdown test setting',
+      type: 'markdown',
+      userValue: null,
+      value: '',
+      ...defaults,
+    },
+    select: {
+      description: 'Description for Select test setting',
+      name: 'select:test:setting',
+      options: ['apple', 'orange', 'banana'],
+      optionLabels: {
+        apple: 'Apple',
+        orange: 'Orange',
+        banana: 'Banana',
+      },
+      type: 'select',
+      userValue: null,
+      value: 'apple',
+      ...defaults,
+    },
+    string: {
+      description: 'Description for String test setting',
+      name: 'string:test:setting',
+      type: 'string',
+      userValue: null,
+      value: 'hello world',
+      ...defaults,
+    },
+    undefined: {
+      description: 'Description for Undefined test setting',
+      name: 'undefined:test:setting',
+      type: 'undefined',
+      userValue: null,
+      value: undefined,
+      ...defaults,
+    },
+  };
+};
diff --git a/packages/kbn-management/settings/components/form/mocks/settings_client.ts b/packages/kbn-management/settings/components/form/mocks/settings_client.ts
new file mode 100644
index 0000000000000..7087add88a943
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/mocks/settings_client.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
+
+/**
+ * Mock of the portion of the {@link IUiSettingsClient} used as a parameter in the {@link getFieldDefinitions} function.
+ */
+export const uiSettingsClientMock: Pick<IUiSettingsClient, 'isCustom' | 'isOverridden'> = {
+  isCustom: () => false,
+  isOverridden: () => false,
+};
diff --git a/packages/kbn-management/settings/components/form/package.json b/packages/kbn-management/settings/components/form/package.json
new file mode 100644
index 0000000000000..3e14acc7378dd
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "@kbn/management-settings-components-form",
+  "private": true,
+  "version": "1.0.0",
+  "license": "SSPL-1.0 OR Elastic License 2.0"
+}
\ No newline at end of file
diff --git a/packages/kbn-management/settings/components/form/reload_page_toast.tsx b/packages/kbn-management/settings/components/form/reload_page_toast.tsx
new file mode 100644
index 0000000000000..a8414dd4ccbe8
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/reload_page_toast.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 React from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { toMountPoint } from '@kbn/react-kibana-mount';
+import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { ToastInput } from '@kbn/core-notifications-browser';
+import { I18nStart } from '@kbn/core-i18n-browser';
+import { ThemeServiceStart } from '@kbn/core-theme-browser';
+
+export const DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON = 'pageReloadButton';
+
+/**
+ * Utility function for returning a {@link ToastInput} for displaying a prompt for reloading the page.
+ * @param theme The {@link ThemeServiceStart} contract.
+ * @param i18nStart The {@link I18nStart} contract.
+ * @returns A toast.
+ */
+export const reloadPageToast = (theme: ThemeServiceStart, i18nStart: I18nStart): ToastInput => {
+  return {
+    title: i18n.translate('management.settings.form.requiresPageReloadToastDescription', {
+      defaultMessage: 'One or more settings require you to reload the page to take effect.',
+    }),
+    text: toMountPoint(
+      <EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
+        <EuiFlexItem grow={false}>
+          <EuiButton
+            size="s"
+            onClick={() => window.location.reload()}
+            data-test-subj={DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON}
+          >
+            {i18n.translate('management.settings.form.requiresPageReloadToastButtonLabel', {
+              defaultMessage: 'Reload page',
+            })}
+          </EuiButton>
+        </EuiFlexItem>
+      </EuiFlexGroup>,
+      { i18n: i18nStart, theme }
+    ),
+    color: 'success',
+  };
+};
diff --git a/packages/kbn-management/settings/components/form/services.tsx b/packages/kbn-management/settings/components/form/services.tsx
new file mode 100644
index 0000000000000..bdbfbdc88c33b
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/services.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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 {
+  FieldRowProvider,
+  FieldRowKibanaProvider,
+} from '@kbn/management-settings-components-field-row';
+import React, { FC, useContext } from 'react';
+import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
+
+import type { FormServices, FormKibanaDependencies, Services } from './types';
+import { reloadPageToast } from './reload_page_toast';
+
+const FormContext = React.createContext<Services | null>(null);
+
+/**
+ * Props for {@link FormProvider}.
+ */
+export interface FormProviderProps extends FormServices {
+  children: React.ReactNode;
+}
+
+/**
+ * React Provider that provides services to a {@link Form} component and its dependents.
+ */
+export const FormProvider = ({ children, ...services }: FormProviderProps) => {
+  const { saveChanges, showError, showReloadPagePrompt, ...rest } = services;
+
+  return (
+    <FormContext.Provider value={{ saveChanges, showError, showReloadPagePrompt }}>
+      <FieldRowProvider {...rest}>{children}</FieldRowProvider>
+    </FormContext.Provider>
+  );
+};
+
+/**
+ * Kibana-specific Provider that maps Kibana plugins and services to a {@link FormProvider}.
+ */
+export const FormKibanaProvider: FC<FormKibanaDependencies> = ({ children, ...deps }) => {
+  const { settings, toasts, docLinks, theme, i18nStart } = deps;
+
+  return (
+    <FormContext.Provider
+      value={{
+        saveChanges: (changes: Record<string, UnsavedFieldChange<SettingType>>) => {
+          const arr = Object.entries(changes).map(([key, value]) =>
+            settings.client.set(key, value.unsavedValue)
+          );
+          return Promise.all(arr);
+        },
+        showError: (message: string) => toasts.addDanger(message),
+        showReloadPagePrompt: () => toasts.add(reloadPageToast(theme, i18nStart)),
+      }}
+    >
+      <FieldRowKibanaProvider {...{ docLinks, toasts }}>{children}</FieldRowKibanaProvider>
+    </FormContext.Provider>
+  );
+};
+
+/**
+ * React hook for accessing pre-wired services.
+ */
+export const useServices = () => {
+  const context = useContext(FormContext);
+
+  if (!context) {
+    throw new Error(
+      'FormContext is missing. Ensure your component or React root is wrapped with FormProvider.'
+    );
+  }
+
+  return context;
+};
diff --git a/packages/kbn-management/settings/components/form/storybook/form.stories.tsx b/packages/kbn-management/settings/components/form/storybook/form.stories.tsx
new file mode 100644
index 0000000000000..5ba4a57d8a2f3
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/storybook/form.stories.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 React from 'react';
+import { EuiPanel } from '@elastic/eui';
+import { action } from '@storybook/addon-actions';
+import { ComponentMeta } from '@storybook/react';
+import { FieldDefinition, SettingType } from '@kbn/management-settings-types';
+import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
+import { getSettingsMock, uiSettingsClientMock } from '../mocks';
+import { Form as Component } from '../form';
+import { FormProvider } from '../services';
+
+export default {
+  title: `Settings/Form/Form`,
+  description: 'A form with field rows',
+  argTypes: {
+    isSavingEnabled: {
+      name: 'Saving is enabled?',
+      control: { type: 'boolean' },
+    },
+    requirePageReload: {
+      name: 'Settings require page reload?',
+      control: { type: 'boolean' },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <FormProvider
+        showDanger={action('showDanger')}
+        links={{ deprecationKey: 'link/to/deprecation/docs' }}
+        saveChanges={action('saveChanges')}
+        showError={action('showError')}
+        showReloadPagePrompt={action('showReloadPagePrompt')}
+      >
+        <EuiPanel>
+          <Story />
+        </EuiPanel>
+      </FormProvider>
+    ),
+  ],
+} as ComponentMeta<typeof Component>;
+
+interface FormStoryProps {
+  /** True if saving settings is enabled, false otherwise. */
+  isSavingEnabled: boolean;
+  /** True if settings require page reload, false otherwise. */
+  requirePageReload: boolean;
+}
+
+export const Form = ({ isSavingEnabled, requirePageReload }: FormStoryProps) => {
+  const fields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
+    getSettingsMock(requirePageReload),
+    uiSettingsClientMock
+  );
+
+  return <Component {...{ fields, isSavingEnabled }} />;
+};
+
+Form.args = {
+  isSavingEnabled: true,
+  requirePageReload: false,
+};
diff --git a/packages/kbn-management/settings/components/form/tsconfig.json b/packages/kbn-management/settings/components/form/tsconfig.json
new file mode 100644
index 0000000000000..359e5560fd2e4
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/tsconfig.json
@@ -0,0 +1,34 @@
+{
+  "extends": "../../../../../tsconfig.base.json",
+  "compilerOptions": {
+    "outDir": "target/types",
+    "types": [
+      "jest",
+      "node",
+      "react",
+      "@testing-library/jest-dom",
+    ]
+  },
+  "include": [
+    "**/*.ts",
+    "**/*.tsx",
+  ],
+  "exclude": [
+    "target/**/*"
+  ],
+  "kbn_references": [
+    "@kbn/management-settings-types",
+    "@kbn/management-settings-field-definition",
+    "@kbn/i18n",
+    "@kbn/i18n-react",
+    "@kbn/management-settings-components-field-row",
+    "@kbn/react-kibana-context-root",
+    "@kbn/core-theme-browser-mocks",
+    "@kbn/core-i18n-browser",
+    "@kbn/react-kibana-mount",
+    "@kbn/core-notifications-browser",
+    "@kbn/core-theme-browser",
+    "@kbn/core-ui-settings-browser",
+    "@kbn/management-settings-components-field-input",
+  ]
+}
diff --git a/packages/kbn-management/settings/components/form/types.ts b/packages/kbn-management/settings/components/form/types.ts
new file mode 100644
index 0000000000000..2e803cbd74cb5
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/types.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 type {
+  FieldRowKibanaDependencies,
+  FieldRowServices,
+} from '@kbn/management-settings-components-field-row';
+import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
+import { SettingsStart } from '@kbn/core-ui-settings-browser';
+import { I18nStart } from '@kbn/core-i18n-browser';
+import { ThemeServiceStart } from '@kbn/core-theme-browser';
+import { ToastsStart } from '@kbn/core-notifications-browser';
+
+/**
+ * Contextual services used by a {@link Form} component.
+ */
+export interface Services {
+  saveChanges: (changes: Record<string, UnsavedFieldChange<SettingType>>) => void;
+  showError: (message: string) => void;
+  showReloadPagePrompt: () => void;
+}
+
+/**
+ * Contextual services used by a {@link Form} component and its dependents.
+ */
+export type FormServices = FieldRowServices & Services;
+
+/**
+ * An interface containing a collection of Kibana plugins and services required to
+ * render a {@link Form} component.
+ */
+interface KibanaDependencies {
+  settings: {
+    client: SettingsStart['client'];
+  };
+  theme: ThemeServiceStart;
+  i18nStart: I18nStart;
+  /** The portion of the {@link ToastsStart} contract used by this component. */
+  toasts: Pick<ToastsStart, 'addError' | 'add'>;
+}
+
+/**
+ * An interface containing a collection of Kibana plugins and services required to
+ * render a {@link Form} component and its dependents.
+ */
+export type FormKibanaDependencies = KibanaDependencies & FieldRowKibanaDependencies;
diff --git a/packages/kbn-management/settings/components/form/use_save.ts b/packages/kbn-management/settings/components/form/use_save.ts
new file mode 100644
index 0000000000000..ebd1981eb57d9
--- /dev/null
+++ b/packages/kbn-management/settings/components/form/use_save.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 type { FieldDefinition, SettingType } from '@kbn/management-settings-types';
+import { isEmpty } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { UnsavedFieldChange } from '@kbn/management-settings-types';
+import { useServices } from './services';
+
+export interface UseSaveParameters {
+  /** All {@link FieldDefinition} in the form. */
+  fields: Array<FieldDefinition<SettingType>>;
+  /** The function to invoke for clearing all unsaved changes. */
+  clearChanges: () => void;
+}
+
+/**
+ * Hook to provide a function that will save all given {@link UnsavedFieldChange}.
+ *
+ * @param params The {@link UseSaveParameters} to use.
+ * @returns A function that will save all {@link UnsavedFieldChange} that are passed as an argument.
+ */
+export const useSave = (params: UseSaveParameters) => {
+  const { saveChanges, showError, showReloadPagePrompt } = useServices();
+
+  return async (changes: Record<string, UnsavedFieldChange<SettingType>>) => {
+    if (isEmpty(changes)) {
+      return;
+    }
+    try {
+      await saveChanges(changes);
+      params.clearChanges();
+      const requiresReload = params.fields.some(
+        (setting) => changes.hasOwnProperty(setting.id) && setting.requiresPageReload
+      );
+      if (requiresReload) {
+        showReloadPagePrompt();
+      }
+    } catch (e) {
+      showError(
+        i18n.translate('management.settings.form.saveErrorMessage', {
+          defaultMessage: 'Unable to save',
+        })
+      );
+    }
+  };
+};
diff --git a/packages/kbn-management/settings/field_definition/get_definitions.ts b/packages/kbn-management/settings/field_definition/get_definitions.ts
index c42613c8c2ce1..83d604db294cf 100644
--- a/packages/kbn-management/settings/field_definition/get_definitions.ts
+++ b/packages/kbn-management/settings/field_definition/get_definitions.ts
@@ -10,6 +10,8 @@ import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
 import { FieldDefinition, SettingType, UiSettingMetadata } from '@kbn/management-settings-types';
 import { getFieldDefinition } from './get_definition';
 
+type SettingsClient = Pick<IUiSettingsClient, 'isCustom' | 'isOverridden'>;
+
 /**
  * Convenience function to convert settings taken from a UiSettingsClient into
  * {@link FieldDefinition} objects.
@@ -20,7 +22,7 @@ import { getFieldDefinition } from './get_definition';
  */
 export const getFieldDefinitions = (
   settings: Record<string, UiSettingMetadata<SettingType>>,
-  client: IUiSettingsClient
+  client: SettingsClient
 ): Array<FieldDefinition<SettingType>> =>
   Object.entries(settings).map(([id, setting]) =>
     getFieldDefinition({
diff --git a/tsconfig.base.json b/tsconfig.base.json
index a33f7b1cd960b..08afa3562fdd2 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -962,6 +962,8 @@
       "@kbn/management-settings-components-field-input/*": ["packages/kbn-management/settings/components/field_input/*"],
       "@kbn/management-settings-components-field-row": ["packages/kbn-management/settings/components/field_row"],
       "@kbn/management-settings-components-field-row/*": ["packages/kbn-management/settings/components/field_row/*"],
+      "@kbn/management-settings-components-form": ["packages/kbn-management/settings/components/form"],
+      "@kbn/management-settings-components-form/*": ["packages/kbn-management/settings/components/form/*"],
       "@kbn/management-settings-field-definition": ["packages/kbn-management/settings/field_definition"],
       "@kbn/management-settings-field-definition/*": ["packages/kbn-management/settings/field_definition/*"],
       "@kbn/management-settings-ids": ["packages/kbn-management/settings/setting_ids"],
diff --git a/yarn.lock b/yarn.lock
index 84df6498ff250..950fcebf83bb0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4867,6 +4867,10 @@
   version "0.0.0"
   uid ""
 
+"@kbn/management-settings-components-form@link:packages/kbn-management/settings/components/form":
+  version "0.0.0"
+  uid ""
+
 "@kbn/management-settings-field-definition@link:packages/kbn-management/settings/field_definition":
   version "0.0.0"
   uid ""

From 59c5eb0988c61dfd5f589e656dc4096102eade33 Mon Sep 17 00:00:00 2001
From: Shahzad <shahzad31comp@gmail.com>
Date: Tue, 26 Sep 2023 12:12:24 +0200
Subject: [PATCH 3/4] [Synthetics] Refactor cardinality test runs query
 (#166608)

---
 .../configurations/lens_attributes.ts         |  1 +
 .../synthetics/kpi_over_time_config.ts        |  5 ++--
 .../synthetics/single_metric_config.ts        |  7 +++---
 .../test_data/sample_attribute_cwv.ts         |  3 +++
 .../test_data/sample_attribute_kpi.ts         |  1 +
 .../monitor_add_edit/form/index.tsx           |  2 +-
 .../monitor_add_edit/form/submit.tsx          |  2 +-
 .../monitor_test_runs_sparkline.tsx           |  4 +--
 .../status_bar/monitor_redirects.tsx          | 25 ++++++++++---------
 .../uptime/server/legacy_uptime/lib/lib.ts    |  5 ----
 .../routes/uptime_route_wrapper.ts            |  4 +--
 11 files changed, 31 insertions(+), 28 deletions(-)

diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts
index 10225de1a1ad4..69080c22a13d0 100644
--- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts
+++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/lens_attributes.ts
@@ -858,6 +858,7 @@ export class LensAttributes {
       dataType: 'number',
       isBucketed: false,
       label: label || 'Count of records',
+      customLabel: true,
       operationType: 'count',
       scale: 'ratio',
       sourceField: RECORDS_FIELD,
diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
index b13f072cc5e64..6fdb18750990e 100644
--- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
+++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts
@@ -14,6 +14,7 @@ import {
   PERCENTILE,
   ReportTypes,
   FORMULA_COLUMN,
+  RECORDS_FIELD,
 } from '../constants';
 import {
   CLS_LABEL,
@@ -124,8 +125,8 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig
       },
       {
         label: 'Total runs',
-        id: 'monitor.check_group',
-        field: 'monitor.check_group',
+        id: 'total_test_runs',
+        field: RECORDS_FIELD,
         columnType: OPERATION_COLUMN,
         columnFilters: [
           {
diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts
index cae5b16a41584..ed0df54219a29 100644
--- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts
+++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/synthetics/single_metric_config.ts
@@ -103,8 +103,9 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri
           titlePosition: 'bottom',
         },
         columnType: FORMULA_COLUMN,
-        formula: "unique_count(monitor.check_group, kql='summary: *')",
         format: 'number',
+        field: RECORDS_FIELD,
+        columnFilter: { language: 'kuery', query: 'summary: *' },
       },
       {
         id: 'monitor_successful',
@@ -114,9 +115,9 @@ export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): Seri
         metricStateOptions: {
           titlePosition: 'bottom',
         },
-        columnType: FORMULA_COLUMN,
-        formula: 'unique_count(monitor.check_group, kql=\'monitor.status: "up"\')',
         format: 'number',
+        field: RECORDS_FIELD,
+        columnFilter: { language: 'kuery', query: 'summary.down: 0' },
       },
       {
         id: 'monitor_errors',
diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
index 5c09be2382425..9364977451b48 100644
--- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
+++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts
@@ -54,6 +54,7 @@ export const sampleAttributeCoreWebVital = {
                 sourceField: 'user_agent.os.name',
               },
               'y-axis-column-1': {
+                customLabel: true,
                 dataType: 'number',
                 filter: {
                   language: 'kuery',
@@ -67,6 +68,7 @@ export const sampleAttributeCoreWebVital = {
                 sourceField: RECORDS_FIELD,
               },
               'y-axis-column-2': {
+                customLabel: true,
                 dataType: 'number',
                 filter: {
                   language: 'kuery',
@@ -79,6 +81,7 @@ export const sampleAttributeCoreWebVital = {
                 sourceField: RECORDS_FIELD,
               },
               'y-axis-column-layer0-0': {
+                customLabel: true,
                 dataType: 'number',
                 filter: {
                   language: 'kuery',
diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
index 49787f1304859..af5d7a91a541d 100644
--- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
+++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts
@@ -50,6 +50,7 @@ export const sampleAttributeKpi = {
                 },
                 isBucketed: false,
                 label: 'test-series',
+                customLabel: true,
                 operationType: 'count',
                 scale: 'ratio',
                 sourceField: RECORDS_FIELD,
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx
index bfd7ac5149a88..f63cb3b7f6dc5 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx
@@ -18,7 +18,7 @@ export const MonitorForm: React.FC<{
   defaultValues?: SyntheticsMonitor;
   space?: string;
   readOnly?: boolean;
-  canUsePublicLocations: boolean;
+  canUsePublicLocations?: boolean;
 }> = ({ children, defaultValues, space, readOnly = false, canUsePublicLocations }) => {
   const methods = useFormWrapped({
     mode: 'onSubmit',
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx
index 4a8cca5410baa..fdda2247f15a4 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/submit.tsx
@@ -26,7 +26,7 @@ export const ActionBar = ({
   canUsePublicLocations = true,
 }: {
   readOnly: boolean;
-  canUsePublicLocations: boolean;
+  canUsePublicLocations?: boolean;
 }) => {
   const { monitorId } = useParams<{ monitorId: string }>();
   const history = useHistory();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx
index d8ba47ef7f39c..c2930a1d22ffb 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx
@@ -32,11 +32,11 @@ export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[]
           'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty
         },
         dataType: 'synthetics' as const,
-        selectedMetricField: 'monitor.check_group',
+        selectedMetricField: 'total_test_runs',
         filters: [],
         name: labels.TEST_RUNS_LABEL,
         color: theme.eui.euiColorVis1,
-        operationType: 'unique_count',
+        operationType: 'count',
       },
     ];
     // eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/status_details/status_bar/monitor_redirects.tsx b/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/status_details/status_bar/monitor_redirects.tsx
index deac2b712f61e..a0db4b2b6a32c 100644
--- a/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/status_details/status_bar/monitor_redirects.tsx
+++ b/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/status_details/status_bar/monitor_redirects.tsx
@@ -7,31 +7,32 @@
 
 import React, { useState } from 'react';
 import { i18n } from '@kbn/i18n';
-import { EuiPopover } from '@elastic/eui';
-import styled from 'styled-components';
+import {
+  EuiPopover,
+  EuiDescriptionListTitle,
+  EuiDescriptionListDescription,
+  EuiButtonEmpty,
+} from '@elastic/eui';
 import { Ping } from '../../../../../../common/runtime_types';
 import { PingRedirects } from '../../ping_list/ping_redirects';
-import { MonListDescription, MonListTitle } from './status_bar';
 
 interface Props {
   monitorStatus: Ping | null;
 }
 
-const RedirectBtn = styled.span`
-  cursor: pointer;
-`;
-
 export const MonitorRedirects: React.FC<Props> = ({ monitorStatus }) => {
   const list = monitorStatus?.http?.response?.redirects;
 
   const [isPopoverOpen, setIsPopoverOpen] = useState(false);
 
   const button = (
-    <MonListDescription>
-      <RedirectBtn
+    <EuiDescriptionListDescription>
+      <EuiButtonEmpty
         className="euiLink euiLink--primary"
         onClick={() => setIsPopoverOpen(!isPopoverOpen)}
         data-test-subj="uptimeMonitorRedirectInfo"
+        iconType="arrowDown"
+        iconSide="right"
       >
         {i18n.translate('xpack.uptime.monitorList.redirects.title.number', {
           defaultMessage: '{number}',
@@ -39,13 +40,13 @@ export const MonitorRedirects: React.FC<Props> = ({ monitorStatus }) => {
             number: list?.length ?? 0,
           },
         })}
-      </RedirectBtn>
-    </MonListDescription>
+      </EuiButtonEmpty>
+    </EuiDescriptionListDescription>
   );
 
   return list ? (
     <>
-      <MonListTitle>Redirects</MonListTitle>
+      <EuiDescriptionListTitle>Redirects</EuiDescriptionListTitle>
       <EuiPopover
         button={button}
         isOpen={isPopoverOpen}
diff --git a/x-pack/plugins/uptime/server/legacy_uptime/lib/lib.ts b/x-pack/plugins/uptime/server/legacy_uptime/lib/lib.ts
index 9008ff9b33100..ae5ceca74fdd1 100644
--- a/x-pack/plugins/uptime/server/legacy_uptime/lib/lib.ts
+++ b/x-pack/plugins/uptime/server/legacy_uptime/lib/lib.ts
@@ -24,7 +24,6 @@ import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../../constants/settings';
 import { DynamicSettingsAttributes } from '../../runtime_types/settings';
 import { settingsObjectId, umDynamicSettings } from './saved_objects/uptime_settings';
 import { API_URLS } from '../../../common/constants';
-import { UptimeServerSetup } from './adapters';
 
 export type { UMServerLibs } from '../uptime_server';
 
@@ -287,7 +286,3 @@ export function debugESCall({
   }
   console.log(`\n`);
 }
-
-export const isTestUser = (server: UptimeServerSetup) => {
-  return server.config.service?.username === 'localKibanaIntegrationTestsUser';
-};
diff --git a/x-pack/plugins/uptime/server/legacy_uptime/routes/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/legacy_uptime/routes/uptime_route_wrapper.ts
index 3e92f73efab10..5beaa563790e8 100644
--- a/x-pack/plugins/uptime/server/legacy_uptime/routes/uptime_route_wrapper.ts
+++ b/x-pack/plugins/uptime/server/legacy_uptime/routes/uptime_route_wrapper.ts
@@ -7,7 +7,7 @@
 
 import { KibanaResponse } from '@kbn/core-http-router-server-internal';
 import { UMKibanaRouteWrapper } from './types';
-import { isTestUser, UptimeEsClient } from '../lib/lib';
+import { UptimeEsClient } from '../lib/lib';
 
 export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => ({
   ...uptimeRoute,
@@ -24,7 +24,7 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) =>
       {
         request,
         uiSettings: coreContext.uiSettings,
-        isDev: Boolean(server.isDev && !isTestUser(server)),
+        isDev: Boolean(server.isDev),
       }
     );
 

From a8de9ebfdf1c2cb83b301879350e242485616874 Mon Sep 17 00:00:00 2001
From: Marco Vettorello <marco.vettorello@elastic.co>
Date: Tue, 26 Sep 2023 12:21:10 +0200
Subject: [PATCH 4/4] Remove unused d3-cloud library (#166601)

## Summary

This PR removes the d3-cloud library specified as a dependency in Kibana
`package.json`
The current tag-cloud uses the d3-cloud version library that is a
dependency of elastic-charts.

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
---
 package.json | 1 -
 yarn.lock    | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 8d29cd024e96f..a08963d37feea 100644
--- a/package.json
+++ b/package.json
@@ -868,7 +868,6 @@
     "d3": "3.5.17",
     "d3-array": "2.12.1",
     "d3-brush": "^3.0.0",
-    "d3-cloud": "1.2.5",
     "d3-interpolate": "^3.0.1",
     "d3-scale": "^3.3.0",
     "d3-selection": "^3.0.0",
diff --git a/yarn.lock b/yarn.lock
index 950fcebf83bb0..7e1992e29ade1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13968,7 +13968,7 @@ d3-brush@^3.0.0:
     d3-selection "3"
     d3-transition "3"
 
-d3-cloud@1.2.5, d3-cloud@^1.2.5:
+d3-cloud@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/d3-cloud/-/d3-cloud-1.2.5.tgz#3e91564f2d27fba47fcc7d812eb5081ea24c603d"
   integrity sha512-4s2hXZgvs0CoUIw31oBAGrHt9Kt/7P9Ik5HIVzISFiWkD0Ga2VLAuO/emO/z1tYIpE7KG2smB4PhMPfFMJpahw==