diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js
index 0fdf24d87ffad..89958fe08d6cc 100644
--- a/.buildkite/scripts/steps/storybooks/build_and_upload.js
+++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js
@@ -8,6 +8,7 @@ const STORYBOOKS = [
'canvas',
'codeeditor',
'ci_composite',
+ 'custom_integrations',
'url_template_editor',
'dashboard',
'dashboard_enhanced',
diff --git a/.ci/.storybook/main.js b/.ci/.storybook/main.js
index e399ec087e168..37f3391337308 100644
--- a/.ci/.storybook/main.js
+++ b/.ci/.storybook/main.js
@@ -11,6 +11,12 @@ const aliases = require('../../src/dev/storybook/aliases.ts').storybookAliases;
config.refs = {};
+// Required due to https://github.com/storybookjs/storybook/issues/13834
+config.babel = async (options) => ({
+ ...options,
+ plugins: ['@babel/plugin-transform-typescript', ...options.plugins],
+});
+
for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) {
// snake_case -> Title Case
const title = alias
diff --git a/.i18nrc.json b/.i18nrc.json
index 45016edc38dcd..46d2f8c6a23bf 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -5,6 +5,7 @@
"kbnConfig": "packages/kbn-config/src",
"console": "src/plugins/console",
"core": "src/core",
+ "customIntegrations": "src/plugins/custom_integrations",
"discover": "src/plugins/discover",
"bfetch": "src/plugins/bfetch",
"dashboard": "src/plugins/dashboard",
@@ -18,6 +19,7 @@
"home": "src/plugins/home",
"flot": "packages/kbn-ui-shared-deps-src/src/flot_charts",
"charts": "src/plugins/charts",
+ "customIntegrations": "src/plugins/custom_integrations",
"esUi": "src/plugins/es_ui_shared",
"devTools": "src/plugins/dev_tools",
"expressions": "src/plugins/expressions",
diff --git a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx
index 617944118cdbd..133b96f44da88 100644
--- a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx
+++ b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx
@@ -17,13 +17,13 @@ already existing applications. Did you know that almost everything you see in th
Kibana UI is built inside a plugin? If you removed all plugins from Kibana, you'd be left with an empty navigation menu, and a set of
developer tools. The Kibana platform is a blank canvas, just waiting for a developer to come along and create something!
-![Kibana personas](assets/kibana_platform_plugin_end_user.png)
+![Kibana personas](../assets/kibana_platform_plugin_end_user.png)
## 1,000 foot view
At a super high-level, Kibana is composed of **plugins**, **core**, and **Kibana packages**.
-![Kibana 1000 ft arch](assets/1000_ft_arch.png)
+![Kibana 1000 ft arch](../assets/1000_ft_arch.png)
**Plugins** provide the majority of all functionality in Kibana. All applications and UIs are defined here.
@@ -51,7 +51,7 @@ We try to put only the most stable and fundamental code into `Core`, while optio
Today it looks something like this.
-![Core vs platform plugins vs plugins](assets/platform_plugins_core.png)
+![Core vs platform plugins vs plugins](../assets/platform_plugins_core.png)
"Platform plugins" provide core-like functionality, just outside of core, and their public APIs tend to be more volatile. Other plugins may still expose shared services, but they are intended only for usage by a small subset of specific plugins, and may not be generic or "platform-like".
@@ -91,7 +91,7 @@ A plugin may register many applications, or none.
Applications are top level pages in the Kibana UI. Dashboard, Canvas, Maps, App Search, etc, are all examples of applications:
-![applications in kibana](./assets/applications.png)
+![applications in kibana](../assets/applications.png)
A plugin can register an application by
adding it to core's application .
diff --git a/docs/dev-tools/grokdebugger/index.asciidoc b/docs/dev-tools/grokdebugger/index.asciidoc
index 934452c54ccca..6a809c13fcb93 100644
--- a/docs/dev-tools/grokdebugger/index.asciidoc
+++ b/docs/dev-tools/grokdebugger/index.asciidoc
@@ -9,21 +9,22 @@ structure it. Grok is good for parsing syslog, apache, and other
webserver logs, mysql logs, and in general, any log format that is
written for human consumption.
-Grok patterns are supported in the ingest node
-{ref}/grok-processor.html[grok processor] and the Logstash
-{logstash-ref}/plugins-filters-grok.html[grok filter]. See
-{logstash-ref}/plugins-filters-grok.html#_grok_basics[grok basics]
-for more information on the syntax for a grok pattern.
-
-The Elastic Stack ships
-with more than 120 reusable grok patterns. See
-https://github.com/elastic/elasticsearch/tree/master/libs/grok/src/main/resources/patterns[Ingest node grok patterns] and https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[Logstash grok patterns]
-for the complete list of patterns.
+Grok patterns are supported in {es} {ref}/runtime.html[runtime fields], the {es}
+{ref}/grok-processor.html[grok ingest processor], and the {ls}
+{logstash-ref}/plugins-filters-grok.html[grok filter]. For syntax, see
+{ref}/grok.html[Grokking grok].
+
+The {stack} ships with more than 120 reusable grok patterns. For a complete
+list of patterns, see
+https://github.com/elastic/elasticsearch/tree/master/libs/grok/src/main/resources/patterns[{es}
+grok patterns] and
+https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[{ls}
+grok patterns].
Because
-ingest node and Logstash share the same grok implementation and pattern
+{es} and {ls} share the same grok implementation and pattern
libraries, any grok pattern that you create in the *Grok Debugger* will work
-in ingest node and Logstash.
+in both {es} and {ls}.
[float]
[[grokdebugger-getting-started]]
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 7f7041f7815cd..cbf46801fa86f 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -458,7 +458,7 @@ the infrastructure monitoring use-case within Kibana.
|{kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines]
-|The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details.
+|The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest pipelines.
|{kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens]
diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc
index 4c0c335b3c33e..60a65580501a6 100644
--- a/docs/migration/migrate_8_0.asciidoc
+++ b/docs/migration/migrate_8_0.asciidoc
@@ -364,4 +364,34 @@ Configuration management tools and automation will need to be updated to use the
=== `server.xsrf.token` is no longer valid
*Details:* The deprecated `server.xsrf.token` setting in the `kibana.yml` file has been removed.
+[float]
+=== `newsfeed.defaultLanguage` is no longer valid
+*Details:* Specifying a default language to retrieve newsfeed items is no longer supported.
+
+*Impact:* Newsfeed items will be retrieved based on the browser locale and fallback to 'en' if an item does not have a translation for the locale. Configure {kibana-ref}/i18n-settings-kb.html#general-i18n-settings-kb[`i18n.locale`] to override the default behavior.
+
+[float]
+=== `xpack.banners.placement` has changed value
+*Details:* `xpack.banners.placement: 'header'` setting in `kibana.yml` has changed value.
+
+*Impact:* Use {kibana-ref}/banners-settings-kb.html#banners-settings-kb[`xpack.banners.placement: 'top'`] instead.
+
+[float]
+=== `cpu.cgroup.path.override` is no longer valid
+*Details:* The deprecated `cpu.cgroup.path.override` setting is no longer supported.
+
+*Impact:* Configure {kibana-ref}/settings.html#ops-cGroupOverrides-cpuPath[`ops.cGroupOverrides.cpuPath`] instead.
+
+[float]
+=== `cpuacct.cgroup.path.override` is no longer valid
+*Details:* The deprecated `cpuacct.cgroup.path.override` setting is no longer supported.
+
+*Impact:* Configure {kibana-ref}/settings.html#ops-cGroupOverrides-cpuAcctPath[`ops.cGroupOverrides.cpuAcctPath`] instead.
+
+[float]
+=== `server.xsrf.whitelist` is no longer valid
+*Details:* The deprecated `server.xsrf.whitelist` setting is no longer supported.
+
+*Impact:* Use {kibana-ref}/settings.html#settings-xsrf-allowlist[`server.xsrf.allowlist`] instead.
+
// end::notable-breaking-changes[]
diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc
index 97a87506f2337..d5bc2ccd8ef7d 100644
--- a/docs/redirects.asciidoc
+++ b/docs/redirects.asciidoc
@@ -293,7 +293,7 @@ This content has moved. Refer to <>.
This content has moved. Refer to <>.
[role="exclude",id="ingest-node-pipelines"]
-== Ingest Node Pipelines
+== Ingest Pipelines
This content has moved. Refer to {ref}/ingest.html[Ingest pipelines].
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 92357a8800d67..3a94e652d2ea0 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -31,11 +31,6 @@ Be sure to back up the encryption key value somewhere safe, as your alerting rul
[[action-settings]]
==== Action settings
-`xpack.actions.enabled`::
-deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."]
-Feature toggle that enables Actions in {kib}.
-If `false`, all features dependent on Actions are disabled, including the *Observability* and *Security* apps. Default: `true`.
-
`xpack.actions.allowedHosts` {ess-icon}::
A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections.
+
@@ -179,3 +174,10 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`.
`xpack.alerting.maxEphemeralActionsPerAlert`::
Sets the number of actions that will be executed ephemerally. To use this, enable ephemeral tasks in task manager first with <>
+
+`xpack.alerting.defaultRuleTaskTimeout`::
+Specifies the default timeout for the all rule types tasks. The time is formatted as:
++
+`[ms,s,m,h,d,w,M,Y]`
++
+For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`.
diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc
index 694f8c53f6745..560f2d850c6d5 100644
--- a/docs/settings/reporting-settings.asciidoc
+++ b/docs/settings/reporting-settings.asciidoc
@@ -11,7 +11,6 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to:
* <>
* <>
-* <>
* <>
* <>
* <>
@@ -47,33 +46,6 @@ The static encryption key for reporting. Use an alphanumeric text string that is
xpack.reporting.encryptionKey: "something_secret"
--------------------------------------------------------------------------------
-[float]
-[[report-indices]]
-==== Reporting index setting
-
-
-
-`xpack.reporting.index`::
-deprecated:[7.11.0,This setting will be removed in 8.0.0.] Multitenancy by changing `kibana.index` is unsupported starting in 8.0.0. For more details, refer to https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes]. When you divide workspaces in an Elastic cluster using multiple {kib} instances with a different `kibana.index` setting per instance, you must set a unique `xpack.reporting.index` setting per `kibana.index`. Otherwise, report generation periodically fails if a report is queued through an instance with one `kibana.index` setting, and an instance with a different `kibana.index` attempts to claim the job. Reporting uses a weekly index in {es} to store the reporting job and the report content. The index is automatically created if it does not already exist. Configure a unique value for `xpack.reporting.index`, beginning with `.reporting-`, for every {kib} instance that has a unique <> setting. Defaults to `.reporting`.
-
-{kib} instance A:
-[source,yaml]
---------------------------------------------------------------------------------
-kibana.index: ".kibana-a"
-xpack.reporting.index: ".reporting-a"
-xpack.reporting.encryptionKey: "something_secret"
---------------------------------------------------------------------------------
-
-{kib} instance B:
-[source,yaml]
---------------------------------------------------------------------------------
-kibana.index: ".kibana-b"
-xpack.reporting.index: ".reporting-b"
-xpack.reporting.encryptionKey: "something_secret"
---------------------------------------------------------------------------------
-
-NOTE: If security is enabled, the `xpack.reporting.index` setting should begin with `.reporting-` for the `kibana_system` role to have the necessary privileges over the index.
-
[float]
[[reporting-kibana-server-settings]]
==== {kib} server settings
diff --git a/docs/setup/configuring-reporting.asciidoc b/docs/setup/configuring-reporting.asciidoc
index 6d209092d3338..38bf2955fb56e 100644
--- a/docs/setup/configuring-reporting.asciidoc
+++ b/docs/setup/configuring-reporting.asciidoc
@@ -148,56 +148,6 @@ reporting_user:
- "cn=Bill Murray,dc=example,dc=com"
--------------------------------------------------------------------------------
-[float]
-==== Grant access with a custom index
-
-If you are using a custom index, the `xpack.reporting.index` setting must begin with `.reporting-*`. The default {kib} system user has `all` privileges against the `.reporting-*` pattern of indices.
-
-If you use a different pattern for the `xpack.reporting.index` setting, you must create a custom `kibana_system` user with appropriate access to the index.
-
-NOTE: In the next major version of Kibana, granting access with a custom index is unsupported.
-
-. Create the reporting role.
-
-.. Open the main menu, then click *Stack Management*.
-
-.. Click *Roles > Create role*.
-
-. Specify the role settings.
-
-.. Enter the *Role name*. For example, `custom-reporting-user`.
-
-.. From the *Indices* dropdown, select the custom index.
-
-.. From the *Privileges* dropdown, select *all*.
-
-.. Click *Add Kibana privilege*.
-
-.. Select one or more *Spaces* that you want to grant reporting privileges to.
-
-.. Click *Customize*, then click *Analytics*.
-
-.. Next to each application you want to grant reporting privileges to, click *All*.
-
-.. Click *Add {kib} privilege*, then click *Create role*.
-
-. Assign the reporting role to a user.
-
-.. Open the main menu, then click *Stack Management*.
-
-.. Click *Users*, then click the user you want to assign the reporting role to.
-
-.. From the *Roles* dropdown, select *kibana_system* and *custom-reporting-user*.
-
-.. Click *Update user*.
-
-. Configure {kib} to use the new account.
-+
-[source,js]
---------------------------------------------------------------------------------
-elasticsearch.username: 'custom_kibana_system'
---------------------------------------------------------------------------------
-
[float]
[[securing-reporting]]
=== Secure the reporting endpoints
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index c098fb697de04..7a85411065db6 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -26,16 +26,6 @@ Toggling this causes the server to regenerate assets on the next startup,
which may cause a delay before pages start being served.
Set to `false` to disable Console. *Default: `true`*
-| `cpu.cgroup.path.override:`
- | deprecated:[7.10.0,"In 8.0 and later, this setting will no longer be supported."]
- This setting has been renamed to
- <>.
-
-| `cpuacct.cgroup.path.override:`
- | deprecated:[7.10.0,"In 8.0 and later, this setting will no longer be supported."]
- This setting has been renamed to
- <>.
-
| `csp.rules:`
| deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."]
A https://w3c.github.io/webappsec-csp/[Content Security Policy] template
diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc
index 4e5f70db9aef6..1f38d50e2d0bd 100644
--- a/docs/user/management.asciidoc
+++ b/docs/user/management.asciidoc
@@ -17,7 +17,7 @@ Consult your administrator if you do not have the appropriate access.
[cols="50, 50"]
|===
-| {ref}/ingest.html[Ingest Node Pipelines]
+| {ref}/ingest.html[Ingest Pipelines]
| Create and manage ingest pipelines that let you perform common transformations
and enrichments on your data.
diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc
index 5ef3b8177a9c5..101377e047588 100644
--- a/docs/user/monitoring/monitoring-metricbeat.asciidoc
+++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc
@@ -189,8 +189,9 @@ If you configured the monitoring cluster to use encrypted communications, you
must access it via HTTPS. For example, use a `hosts` setting like
`https://es-mon-1:9200`.
-IMPORTANT: The {es} {monitor-features} use ingest pipelines, therefore the
-cluster that stores the monitoring data must have at least one ingest node.
+IMPORTANT: The {es} {monitor-features} use ingest pipelines. The
+cluster that stores the monitoring data must have at least one node with the
+`ingest` role.
If the {es} {security-features} are enabled on the monitoring cluster, you
must provide a valid user ID and password so that {metricbeat} can send metrics
diff --git a/package.json b/package.json
index 37a5239cf068a..6539189ca994d 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,9 @@
"yarn": "^1.21.1"
},
"dependencies": {
+ "@dnd-kit/core": "^3.1.1",
+ "@dnd-kit/sortable": "^4.0.0",
+ "@dnd-kit/utilities": "^2.0.0",
"@babel/runtime": "^7.15.4",
"@elastic/apm-rum": "^5.9.1",
"@elastic/apm-rum-react": "^1.3.1",
diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts
index 86a036bbb9fe2..6ac897bbafb08 100644
--- a/packages/kbn-rule-data-utils/src/technical_field_names.ts
+++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts
@@ -52,6 +52,7 @@ const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const;
const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const;
const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const;
const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const;
+const ALERT_RULE_PARAMS = `${ALERT_RULE_NAMESPACE}.params` as const;
const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const;
const ALERT_RULE_RISK_SCORE = `${ALERT_RULE_NAMESPACE}.risk_score` as const;
const ALERT_RULE_RISK_SCORE_MAPPING = `${ALERT_RULE_NAMESPACE}.risk_score_mapping` as const;
@@ -109,6 +110,7 @@ const fields = {
ALERT_RULE_LICENSE,
ALERT_RULE_NAME,
ALERT_RULE_NOTE,
+ ALERT_RULE_PARAMS,
ALERT_RULE_REFERENCES,
ALERT_RULE_RISK_SCORE,
ALERT_RULE_RISK_SCORE_MAPPING,
@@ -164,6 +166,7 @@ export {
ALERT_RULE_LICENSE,
ALERT_RULE_NAME,
ALERT_RULE_NOTE,
+ ALERT_RULE_PARAMS,
ALERT_RULE_REFERENCES,
ALERT_RULE_RISK_SCORE,
ALERT_RULE_RISK_SCORE_MAPPING,
diff --git a/packages/kbn-utils/src/path/index.ts b/packages/kbn-utils/src/path/index.ts
index 9ee699c22c30c..15d6a3eddf01e 100644
--- a/packages/kbn-utils/src/path/index.ts
+++ b/packages/kbn-utils/src/path/index.ts
@@ -15,14 +15,12 @@ const isString = (v: any): v is string => typeof v === 'string';
const CONFIG_PATHS = [
process.env.KBN_PATH_CONF && join(process.env.KBN_PATH_CONF, 'kibana.yml'),
- process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), // deprecated
join(REPO_ROOT, 'config/kibana.yml'),
'/etc/kibana/kibana.yml',
].filter(isString);
const CONFIG_DIRECTORIES = [
process.env.KBN_PATH_CONF,
- process.env.KIBANA_PATH_CONF, // deprecated
join(REPO_ROOT, 'config'),
'/etc/kibana',
].filter(isString);
diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts
index 4e99f46ea05ff..95e23561a9378 100644
--- a/src/core/server/config/deprecation/core_deprecations.test.ts
+++ b/src/core/server/config/deprecation/core_deprecations.test.ts
@@ -19,38 +19,6 @@ describe('core deprecations', () => {
process.env = { ...initialEnv };
});
- describe('kibanaPathConf', () => {
- it('logs a warning if KIBANA_PATH_CONF environ variable is set', () => {
- process.env.KIBANA_PATH_CONF = 'somepath';
- const { messages } = applyCoreDeprecations();
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "Environment variable \\"KIBANA_PATH_CONF\\" is deprecated. It has been replaced with \\"KBN_PATH_CONF\\" pointing to a config folder",
- ]
- `);
- });
-
- it('does not log a warning if KIBANA_PATH_CONF environ variable is unset', () => {
- delete process.env.KIBANA_PATH_CONF;
- const { messages } = applyCoreDeprecations();
- expect(messages).toHaveLength(0);
- });
- });
-
- describe('xsrfDeprecation', () => {
- it('logs a warning if server.xsrf.whitelist is set', () => {
- const { migrated, messages } = applyCoreDeprecations({
- server: { xsrf: { whitelist: ['/path'] } },
- });
- expect(migrated.server.xsrf.allowlist).toEqual(['/path']);
- expect(messages).toMatchInlineSnapshot(`
- Array [
- "Setting \\"server.xsrf.whitelist\\" has been replaced by \\"server.xsrf.allowlist\\"",
- ]
- `);
- });
- });
-
describe('server.cors', () => {
it('renames server.cors to server.cors.enabled', () => {
const { migrated } = applyCoreDeprecations({
@@ -58,8 +26,9 @@ describe('core deprecations', () => {
});
expect(migrated.server.cors).toEqual({ enabled: true });
});
+
it('logs a warning message about server.cors renaming', () => {
- const { messages } = applyCoreDeprecations({
+ const { messages, levels } = applyCoreDeprecations({
server: { cors: true },
});
expect(messages).toMatchInlineSnapshot(`
@@ -67,7 +36,13 @@ describe('core deprecations', () => {
"\\"server.cors\\" is deprecated and has been replaced by \\"server.cors.enabled\\"",
]
`);
+ expect(levels).toMatchInlineSnapshot(`
+ Array [
+ "warning",
+ ]
+ `);
});
+
it('does not log deprecation message when server.cors.enabled set', () => {
const { migrated, messages } = applyCoreDeprecations({
server: { cors: { enabled: true } },
diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts
index 674812bd0957b..4e5f711fe9f3a 100644
--- a/src/core/server/config/deprecation/core_deprecations.ts
+++ b/src/core/server/config/deprecation/core_deprecations.ts
@@ -8,19 +8,6 @@
import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config';
-const kibanaPathConf: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
- if (process.env?.KIBANA_PATH_CONF) {
- addDeprecation({
- message: `Environment variable "KIBANA_PATH_CONF" is deprecated. It has been replaced with "KBN_PATH_CONF" pointing to a config folder`,
- correctiveActions: {
- manualSteps: [
- 'Use "KBN_PATH_CONF" instead of "KIBANA_PATH_CONF" to point to a config folder.',
- ],
- },
- });
- }
-};
-
const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => {
if (settings.server?.basePath && !settings.server?.rewriteBasePath) {
addDeprecation({
@@ -44,6 +31,7 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecati
if (typeof corsSettings === 'boolean') {
addDeprecation({
message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"',
+ level: 'warning',
correctiveActions: {
manualSteps: [
`Replace "server.cors: ${corsSettings}" with "server.cors.enabled: ${corsSettings}"`,
@@ -114,11 +102,7 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati
};
export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [
- rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'),
- rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'),
- rename('server.xsrf.whitelist', 'server.xsrf.allowlist'),
rewriteCorsSettings,
- kibanaPathConf,
rewriteBasePathDeprecation,
cspRulesDeprecation,
];
diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts
index e3f9ca7eb29f2..592ea0981682f 100644
--- a/src/core/server/config/test_utils.ts
+++ b/src/core/server/config/test_utils.ts
@@ -16,6 +16,7 @@ function collectDeprecations(
) {
const deprecations = provider(configDeprecationFactory);
const deprecationMessages: string[] = [];
+ const deprecationLevels: string[] = [];
const { config: migrated } = applyDeprecations(
settings,
deprecations.map((deprecation) => ({
@@ -23,11 +24,14 @@ function collectDeprecations(
path,
})),
() =>
- ({ message }) =>
- deprecationMessages.push(message)
+ ({ message, level }) => {
+ deprecationMessages.push(message);
+ deprecationLevels.push(level ?? '');
+ }
);
return {
messages: deprecationMessages,
+ levels: deprecationLevels,
migrated,
};
}
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index f671fa963079f..dd5b66af9ef21 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -26,8 +26,6 @@ kibana_vars=(
console.enabled
console.proxyConfig
console.proxyFilter
- cpu.cgroup.path.override
- cpuacct.cgroup.path.override
csp.rules
csp.strict
csp.warnLegacyBrowsers
@@ -175,7 +173,6 @@ kibana_vars=(
server.uuid
server.xsrf.allowlist
server.xsrf.disableProtection
- server.xsrf.whitelist
status.allowAnonymous
status.v6ApiFormat
telemetry.allowChangingOptInStatus
@@ -194,7 +191,6 @@ kibana_vars=(
vis_type_vega.enableExternalUrls
xpack.actions.allowedHosts
xpack.actions.customHostSettings
- xpack.actions.enabled
xpack.actions.enabledActionTypes
xpack.actions.maxResponseContentLength
xpack.actions.preconfigured
@@ -211,6 +207,7 @@ kibana_vars=(
xpack.alerting.healthCheck.interval
xpack.alerting.invalidateApiKeysTask.interval
xpack.alerting.invalidateApiKeysTask.removalDelay
+ xpack.alerting.defaultRuleTaskTimeout
xpack.alerts.healthCheck.interval
xpack.alerts.invalidateApiKeysTask.interval
xpack.alerts.invalidateApiKeysTask.removalDelay
@@ -251,7 +248,6 @@ kibana_vars=(
xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled
xpack.encryptedSavedObjects.encryptionKey
xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys
- xpack.event_log.enabled
xpack.event_log.indexEntries
xpack.event_log.logEntries
xpack.fleet.agentPolicies
@@ -318,7 +314,6 @@ kibana_vars=(
xpack.reporting.csv.useByteOrderMarkEncoding
xpack.reporting.enabled
xpack.reporting.encryptionKey
- xpack.reporting.index
xpack.reporting.kibanaApp
xpack.reporting.kibanaServer.hostname
xpack.reporting.kibanaServer.port
@@ -384,7 +379,6 @@ kibana_vars=(
xpack.securitySolution.prebuiltRulesFromSavedObjects
xpack.spaces.enabled
xpack.spaces.maxSpaces
- xpack.task_manager.enabled
xpack.task_manager.index
xpack.task_manager.max_attempts
xpack.task_manager.max_poll_inactivity_cycles
@@ -426,7 +420,7 @@ umask 0002
# paths. Therefore, Kibana provides a mechanism to override
# reading the cgroup path from /proc/self/cgroup and instead uses the
# cgroup path defined the configuration properties
-# cpu.cgroup.path.override and cpuacct.cgroup.path.override.
+# ops.cGroupOverrides.cpuPath and ops.cGroupOverrides.cpuAcctPath.
# Therefore, we set this value here so that cgroup statistics are
# available for the container this process will run in.
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index a61a2618d6428..c04f0d4f9320f 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -12,6 +12,7 @@ export const storybookAliases = {
canvas: 'x-pack/plugins/canvas/storybook',
codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook',
ci_composite: '.ci/.storybook',
+ custom_integrations: 'src/plugins/custom_integrations/storybook',
url_template_editor: 'src/plugins/kibana_react/public/url_template_editor/.storybook',
dashboard: 'src/plugins/dashboard/.storybook',
dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook',
diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts
index 73e15c91ce4bf..e2408d3124604 100755
--- a/src/plugins/custom_integrations/common/index.ts
+++ b/src/plugins/custom_integrations/common/index.ts
@@ -15,6 +15,7 @@ export interface IntegrationCategoryCount {
}
export const INTEGRATION_CATEGORY_DISPLAY = {
+ // Known EPR
aws: 'AWS',
azure: 'Azure',
cloud: 'Cloud',
@@ -39,8 +40,12 @@ export const INTEGRATION_CATEGORY_DISPLAY = {
ticketing: 'Ticketing',
version_control: 'Version control',
web: 'Web',
+
+ // Kibana added
upload_file: 'Upload a file',
+ language_client: 'Language client',
+ // Internal
updates_available: 'Updates available',
};
diff --git a/src/plugins/custom_integrations/kibana.json b/src/plugins/custom_integrations/kibana.json
index 3a78270d9ef09..cd58c1aec1ecb 100755
--- a/src/plugins/custom_integrations/kibana.json
+++ b/src/plugins/custom_integrations/kibana.json
@@ -12,5 +12,8 @@
"extraPublicDirs": [
"common"
],
+ "requiredPlugins": [
+ "presentationUtil"
+ ],
"optionalPlugins": []
}
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/dotnet.svg b/src/plugins/custom_integrations/public/assets/language_clients/dotnet.svg
new file mode 100755
index 0000000000000..92a7ad45d9f9c
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/dotnet.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/es.svg b/src/plugins/custom_integrations/public/assets/language_clients/es.svg
new file mode 100755
index 0000000000000..b1224e212e098
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/es.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/go.svg b/src/plugins/custom_integrations/public/assets/language_clients/go.svg
new file mode 100755
index 0000000000000..223a57194fd7c
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/go.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/java.svg b/src/plugins/custom_integrations/public/assets/language_clients/java.svg
new file mode 100644
index 0000000000000..d24d844695762
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/java.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/nodejs.svg b/src/plugins/custom_integrations/public/assets/language_clients/nodejs.svg
new file mode 100755
index 0000000000000..4dd358743bbff
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/nodejs.svg
@@ -0,0 +1,46 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/perl.svg b/src/plugins/custom_integrations/public/assets/language_clients/perl.svg
new file mode 100755
index 0000000000000..6ef322a3f58ae
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/perl.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/php.svg b/src/plugins/custom_integrations/public/assets/language_clients/php.svg
new file mode 100755
index 0000000000000..7a1c20116f466
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/php.svg
@@ -0,0 +1,18 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/python.svg b/src/plugins/custom_integrations/public/assets/language_clients/python.svg
new file mode 100755
index 0000000000000..b7234c439ced5
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/python.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/ruby.svg b/src/plugins/custom_integrations/public/assets/language_clients/ruby.svg
new file mode 100755
index 0000000000000..5e515bc0dd98e
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/ruby.svg
@@ -0,0 +1,120 @@
+
diff --git a/src/plugins/custom_integrations/public/assets/language_clients/rust.svg b/src/plugins/custom_integrations/public/assets/language_clients/rust.svg
new file mode 100755
index 0000000000000..82dcaf2ade93e
--- /dev/null
+++ b/src/plugins/custom_integrations/public/assets/language_clients/rust.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/plugins/custom_integrations/public/components/index.tsx b/src/plugins/custom_integrations/public/components/index.tsx
new file mode 100644
index 0000000000000..cfbec7d6d5ae5
--- /dev/null
+++ b/src/plugins/custom_integrations/public/components/index.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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, { Suspense, ComponentType, ReactElement, Ref } from 'react';
+import { EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui';
+
+/**
+ * A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors.
+ * @param Component A component deferred by `React.lazy`
+ * @param fallback A fallback component to render while things load; default is `EuiLoadingSpinner`
+ */
+export const withSuspense =
(
+ Component: ComponentType
,
+ fallback: ReactElement | null =
+) =>
+ React.forwardRef((props: P, ref: Ref) => {
+ return (
+
+
+
+
+
+ );
+ });
+
+export const LazyReplacementCard = React.lazy(() => import('./replacement_card'));
diff --git a/src/plugins/custom_integrations/public/components/replacement_card/index.ts b/src/plugins/custom_integrations/public/components/replacement_card/index.ts
new file mode 100644
index 0000000000000..631dc1fcb2ba2
--- /dev/null
+++ b/src/plugins/custom_integrations/public/components/replacement_card/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 { ReplacementCard } from './replacement_card';
+
+export { ReplacementCard, Props } from './replacement_card';
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default ReplacementCard;
diff --git a/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.component.tsx b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.component.tsx
new file mode 100644
index 0000000000000..f66d13fb911b5
--- /dev/null
+++ b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.component.tsx
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+/** @jsx jsx */
+
+import { css, jsx } from '@emotion/react';
+
+import {
+ htmlIdGenerator,
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiText,
+ EuiAccordion,
+ EuiLink,
+ useEuiTheme,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { CustomIntegration } from '../../../common';
+import { usePlatformService } from '../../services';
+
+export interface Props {
+ replacements: Array>;
+}
+
+// TODO - clintandrewhall: should use doc-links service
+const URL_COMPARISON = 'https://ela.st/beats-agent-comparison';
+
+const idGenerator = htmlIdGenerator('replacementCard');
+const alsoAvailable = i18n.translate('customIntegrations.components.replacementAccordionLabel', {
+ defaultMessage: 'Also available in Beats',
+});
+
+const link = (
+
+
+
+);
+
+/**
+ * A pure component, an accordion panel which can display information about replacements for a given EPR module.
+ */
+export const ReplacementCard = ({ replacements }: Props) => {
+ const { euiTheme } = useEuiTheme();
+ const { getAbsolutePath } = usePlatformService();
+
+ if (replacements.length === 0) {
+ return null;
+ }
+
+ const buttons = replacements.map((replacement) => (
+
+
+
+ {replacement.title}
+
+
+
+ ));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {buttons}
+
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.stories.tsx b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.stories.tsx
new file mode 100644
index 0000000000000..8fa0674c9b467
--- /dev/null
+++ b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.stories.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { Meta } from '@storybook/react';
+
+import { ReplacementCard as ConnectedComponent } from './replacement_card';
+import { ReplacementCard as PureComponent } from './replacement_card.component';
+
+export default {
+ title: 'Replacement Card',
+ description:
+ 'An accordion panel which can display information about Beats alternatives to a given EPR module, (if available)',
+ decorators: [
+ (storyFn, { globals }) => (
+
+ {storyFn()}
+
+ ),
+ ],
+} as Meta;
+
+interface Args {
+ eprPackageName: string;
+}
+
+const args: Args = {
+ eprPackageName: 'nginx',
+};
+
+const argTypes = {
+ eprPackageName: {
+ control: {
+ type: 'radio',
+ options: ['nginx', 'okta', 'aws', 'apache'],
+ },
+ },
+};
+
+export function ReplacementCard({ eprPackageName }: Args) {
+ return ;
+}
+
+ReplacementCard.args = args;
+ReplacementCard.argTypes = argTypes;
+
+export function Component() {
+ return (
+
+ );
+}
diff --git a/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.tsx b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.tsx
new file mode 100644
index 0000000000000..3e829270773a6
--- /dev/null
+++ b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 useAsync from 'react-use/lib/useAsync';
+import { useFindService } from '../../services';
+
+import { ReplacementCard as Component } from './replacement_card.component';
+
+export interface Props {
+ eprPackageName: string;
+}
+
+/**
+ * A data-connected component which can query about Beats-based replacement options for a given EPR module.
+ */
+export const ReplacementCard = ({ eprPackageName }: Props) => {
+ const { findReplacementIntegrations } = useFindService();
+ const integrations = useAsync(async () => {
+ return await findReplacementIntegrations({ shipper: 'beats', eprPackageName });
+ }, [eprPackageName]);
+
+ const { loading, value: replacements } = integrations;
+
+ if (loading || !replacements || replacements.length === 0) {
+ return null;
+ }
+
+ return ;
+};
diff --git a/src/plugins/custom_integrations/public/index.ts b/src/plugins/custom_integrations/public/index.ts
index 9e979dd6692bc..91da75c634a44 100755
--- a/src/plugins/custom_integrations/public/index.ts
+++ b/src/plugins/custom_integrations/public/index.ts
@@ -13,4 +13,8 @@ import { CustomIntegrationsPlugin } from './plugin';
export function plugin() {
return new CustomIntegrationsPlugin();
}
+
export { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
+
+export { withSuspense, LazyReplacementCard } from './components';
+export { filterCustomIntegrations } from './services/find';
diff --git a/src/plugins/custom_integrations/public/mocks.ts b/src/plugins/custom_integrations/public/mocks.ts
index 2e6bc491c2c5c..a8fedbbb712b2 100644
--- a/src/plugins/custom_integrations/public/mocks.ts
+++ b/src/plugins/custom_integrations/public/mocks.ts
@@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
-import { CustomIntegrationsSetup } from './types';
+import { pluginServices } from './services';
+import { PluginServiceRegistry } from '../../presentation_util/public';
+import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
+import { CustomIntegrationsServices } from './services';
+import { providers } from './services/stub';
function createCustomIntegrationsSetup(): jest.Mocked {
const mock: jest.Mocked = {
@@ -16,6 +20,17 @@ function createCustomIntegrationsSetup(): jest.Mocked {
return mock;
}
+function createCustomIntegrationsStart(): jest.Mocked {
+ const registry = new PluginServiceRegistry(providers);
+ pluginServices.setRegistry(registry.start({}));
+ const ContextProvider = pluginServices.getContextProvider();
+
+ return {
+ ContextProvider: jest.fn(ContextProvider),
+ };
+}
+
export const customIntegrationsMock = {
createSetup: createCustomIntegrationsSetup,
+ createStart: createCustomIntegrationsStart,
};
diff --git a/src/plugins/custom_integrations/public/plugin.ts b/src/plugins/custom_integrations/public/plugin.ts
index 7ea7a829e8072..a3470fefba46c 100755
--- a/src/plugins/custom_integrations/public/plugin.ts
+++ b/src/plugins/custom_integrations/public/plugin.ts
@@ -7,13 +7,20 @@
*/
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
-import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
+import {
+ CustomIntegrationsSetup,
+ CustomIntegrationsStart,
+ CustomIntegrationsStartDependencies,
+} from './types';
import {
CustomIntegration,
ROUTES_APPEND_CUSTOM_INTEGRATIONS,
ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
} from '../common';
+import { pluginServices } from './services';
+import { pluginServiceRegistry } from './services/kibana';
+
export class CustomIntegrationsPlugin
implements Plugin
{
@@ -30,8 +37,14 @@ export class CustomIntegrationsPlugin
};
}
- public start(core: CoreStart): CustomIntegrationsStart {
- return {};
+ public start(
+ coreStart: CoreStart,
+ startPlugins: CustomIntegrationsStartDependencies
+ ): CustomIntegrationsStart {
+ pluginServices.setRegistry(pluginServiceRegistry.start({ coreStart, startPlugins }));
+ return {
+ ContextProvider: pluginServices.getContextProvider(),
+ };
}
public stop() {}
diff --git a/src/plugins/custom_integrations/public/services/find.test.ts b/src/plugins/custom_integrations/public/services/find.test.ts
new file mode 100644
index 0000000000000..df52c22313b68
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/find.test.ts
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+/*
+ * 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 { filterCustomIntegrations } from './find';
+import { CustomIntegration } from '../../common';
+
+describe('Custom Integrations Find Service', () => {
+ const integrations: CustomIntegration[] = [
+ {
+ id: 'foo',
+ title: 'Foo',
+ description: 'test integration',
+ type: 'ui_link',
+ uiInternalPath: '/path/to/foo',
+ isBeta: false,
+ icons: [],
+ categories: ['aws', 'cloud'],
+ shipper: 'tests',
+ },
+ {
+ id: 'bar',
+ title: 'Bar',
+ description: 'test integration',
+ type: 'ui_link',
+ uiInternalPath: '/path/to/bar',
+ isBeta: false,
+ icons: [],
+ categories: ['aws'],
+ shipper: 'other',
+ eprOverlap: 'eprValue',
+ },
+ {
+ id: 'bar',
+ title: 'Bar',
+ description: 'test integration',
+ type: 'ui_link',
+ uiInternalPath: '/path/to/bar',
+ isBeta: false,
+ icons: [],
+ categories: ['cloud'],
+ shipper: 'other',
+ eprOverlap: 'eprValue',
+ },
+ {
+ id: 'baz',
+ title: 'Baz',
+ description: 'test integration',
+ type: 'ui_link',
+ uiInternalPath: '/path/to/baz',
+ isBeta: false,
+ icons: [],
+ categories: ['cloud'],
+ shipper: 'tests',
+ eprOverlap: 'eprOtherValue',
+ },
+ ];
+
+ describe('filterCustomIntegrations', () => {
+ test('filters on shipper', () => {
+ let result = filterCustomIntegrations(integrations, { shipper: 'other' });
+ expect(result.length).toBe(2);
+ result = filterCustomIntegrations(integrations, { shipper: 'tests' });
+ expect(result.length).toBe(2);
+ result = filterCustomIntegrations(integrations, { shipper: 'foobar' });
+ expect(result.length).toBe(0);
+ });
+ test('filters on eprOverlap', () => {
+ let result = filterCustomIntegrations(integrations, { eprPackageName: 'eprValue' });
+ expect(result.length).toBe(2);
+ result = filterCustomIntegrations(integrations, { eprPackageName: 'eprOtherValue' });
+ expect(result.length).toBe(1);
+ result = filterCustomIntegrations(integrations, { eprPackageName: 'otherValue' });
+ expect(result.length).toBe(0);
+ });
+ test('filters on categories and shipper, eprOverlap', () => {
+ const result = filterCustomIntegrations(integrations, {
+ shipper: 'other',
+ eprPackageName: 'eprValue',
+ });
+ expect(result.length).toBe(2);
+ });
+ });
+});
diff --git a/src/plugins/custom_integrations/public/services/find.ts b/src/plugins/custom_integrations/public/services/find.ts
new file mode 100644
index 0000000000000..4e69327c351b4
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/find.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { CustomIntegration } from '../../common';
+
+interface FindParams {
+ eprPackageName?: string;
+ shipper?: string;
+}
+
+/**
+ * A plugin service that finds and returns custom integrations.
+ */
+export interface CustomIntegrationsFindService {
+ findReplacementIntegrations(params?: FindParams): Promise;
+ findAppendedIntegrations(params?: FindParams): Promise;
+}
+
+/**
+ * Filter a set of integrations by eprPackageName, and/or shipper.
+ */
+export const filterCustomIntegrations = (
+ integrations: CustomIntegration[],
+ { eprPackageName, shipper }: FindParams = {}
+) => {
+ if (!eprPackageName && !shipper) {
+ return integrations;
+ }
+
+ let result = integrations;
+
+ if (eprPackageName) {
+ result = result.filter((integration) => integration.eprOverlap === eprPackageName);
+ }
+
+ if (shipper) {
+ result = result.filter((integration) => integration.shipper === shipper);
+ }
+
+ return result;
+};
diff --git a/src/plugins/custom_integrations/public/services/index.ts b/src/plugins/custom_integrations/public/services/index.ts
new file mode 100644
index 0000000000000..8a257ee1a2cd7
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/index.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { PluginServices } from '../../../presentation_util/public';
+
+import { CustomIntegrationsFindService } from './find';
+import { CustomIntegrationsPlatformService } from './platform';
+
+/**
+ * Services used by the custom integrations plugin.
+ */
+export interface CustomIntegrationsServices {
+ find: CustomIntegrationsFindService;
+ platform: CustomIntegrationsPlatformService;
+}
+
+/**
+ * The `PluginServices` object for the custom integrations plugin.
+ * @see /src/plugins/presentation_util/public/services/create/index.ts
+ */
+export const pluginServices = new PluginServices();
+
+/**
+ * A React hook that provides connections to the `CustomIntegrationsFindService`.
+ */
+export const useFindService = () => (() => pluginServices.getHooks().find.useService())();
+
+/**
+ * A React hook that provides connections to the `CustomIntegrationsPlatformService`.
+ */
+export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())();
diff --git a/src/plugins/custom_integrations/public/services/kibana/find.ts b/src/plugins/custom_integrations/public/services/kibana/find.ts
new file mode 100644
index 0000000000000..5fc7626baa1e1
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/kibana/find.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 {
+ CustomIntegration,
+ ROUTES_APPEND_CUSTOM_INTEGRATIONS,
+ ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
+} from '../../../common';
+import { KibanaPluginServiceFactory } from '../../../../presentation_util/public';
+
+import { CustomIntegrationsStartDependencies } from '../../types';
+import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find';
+
+/**
+ * A type definition for a factory to produce the `CustomIntegrationsFindService` for use in Kibana.
+ * @see /src/plugins/presentation_util/public/services/create/factory.ts
+ */
+export type CustomIntegrationsFindServiceFactory = KibanaPluginServiceFactory<
+ CustomIntegrationsFindService,
+ CustomIntegrationsStartDependencies
+>;
+
+/**
+ * A factory to produce the `CustomIntegrationsFindService` for use in Kibana.
+ */
+export const findServiceFactory: CustomIntegrationsFindServiceFactory = ({ coreStart }) => ({
+ findAppendedIntegrations: async (params) => {
+ const integrations: CustomIntegration[] = await coreStart.http.get(
+ ROUTES_APPEND_CUSTOM_INTEGRATIONS
+ );
+
+ return filterCustomIntegrations(integrations, params);
+ },
+ findReplacementIntegrations: async (params) => {
+ const replacements: CustomIntegration[] = await coreStart.http.get(
+ ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS
+ );
+
+ return filterCustomIntegrations(replacements, params);
+ },
+});
diff --git a/src/plugins/custom_integrations/public/services/kibana/index.ts b/src/plugins/custom_integrations/public/services/kibana/index.ts
new file mode 100644
index 0000000000000..d3cf27b9bc7c0
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/kibana/index.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 {
+ PluginServiceProviders,
+ PluginServiceProvider,
+ PluginServiceRegistry,
+ KibanaPluginServiceParams,
+} from '../../../../presentation_util/public';
+
+import { CustomIntegrationsServices } from '..';
+import { CustomIntegrationsStartDependencies } from '../../types';
+
+import { findServiceFactory } from './find';
+import { platformServiceFactory } from './platform';
+
+export { findServiceFactory } from './find';
+export { platformServiceFactory } from './platform';
+
+/**
+ * A set of `PluginServiceProvider`s for use in Kibana.
+ * @see /src/plugins/presentation_util/public/services/create/provider.tsx
+ */
+export const pluginServiceProviders: PluginServiceProviders<
+ CustomIntegrationsServices,
+ KibanaPluginServiceParams
+> = {
+ find: new PluginServiceProvider(findServiceFactory),
+ platform: new PluginServiceProvider(platformServiceFactory),
+};
+
+/**
+ * A `PluginServiceRegistry` for use in Kibana.
+ * @see /src/plugins/presentation_util/public/services/create/registry.tsx
+ */
+export const pluginServiceRegistry = new PluginServiceRegistry<
+ CustomIntegrationsServices,
+ KibanaPluginServiceParams
+>(pluginServiceProviders);
diff --git a/src/plugins/custom_integrations/public/services/kibana/platform.ts b/src/plugins/custom_integrations/public/services/kibana/platform.ts
new file mode 100644
index 0000000000000..e6fe89b68c975
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/kibana/platform.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { KibanaPluginServiceFactory } from '../../../../presentation_util/public';
+
+import type { CustomIntegrationsPlatformService } from '../platform';
+import type { CustomIntegrationsStartDependencies } from '../../types';
+
+/**
+ * A type definition for a factory to produce the `CustomIntegrationsPlatformService` for use in Kibana.
+ * @see /src/plugins/presentation_util/public/services/create/factory.ts
+ */
+export type CustomIntegrationsPlatformServiceFactory = KibanaPluginServiceFactory<
+ CustomIntegrationsPlatformService,
+ CustomIntegrationsStartDependencies
+>;
+
+/**
+ * A factory to produce the `CustomIntegrationsPlatformService` for use in Kibana.
+ */
+export const platformServiceFactory: CustomIntegrationsPlatformServiceFactory = ({
+ coreStart,
+}) => ({
+ getBasePath: coreStart.http.basePath.get,
+ getAbsolutePath: (path: string): string => coreStart.http.basePath.prepend(`${path}`),
+});
diff --git a/src/plugins/custom_integrations/public/services/platform.ts b/src/plugins/custom_integrations/public/services/platform.ts
new file mode 100644
index 0000000000000..0eb9c7d5c3c10
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/platform.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 interface CustomIntegrationsPlatformService {
+ getBasePath: () => string;
+ getAbsolutePath: (path: string) => string;
+}
diff --git a/src/plugins/custom_integrations/public/services/storybook/index.ts b/src/plugins/custom_integrations/public/services/storybook/index.ts
new file mode 100644
index 0000000000000..4dfed1b37e294
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/storybook/index.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 {
+ PluginServiceProviders,
+ PluginServiceProvider,
+ PluginServiceRegistry,
+} from '../../../../presentation_util/public';
+
+import { CustomIntegrationsServices } from '..';
+import { findServiceFactory } from '../stub/find';
+import { platformServiceFactory } from '../stub/platform';
+
+export { findServiceFactory } from '../stub/find';
+export { platformServiceFactory } from '../stub/platform';
+
+/**
+ * A set of `PluginServiceProvider`s for use in Storybook.
+ * @see /src/plugins/presentation_util/public/services/create/provider.tsx
+ */
+export const providers: PluginServiceProviders = {
+ find: new PluginServiceProvider(findServiceFactory),
+ platform: new PluginServiceProvider(platformServiceFactory),
+};
+
+/**
+ * A `PluginServiceRegistry` for use in Storybook.
+ * @see /src/plugins/presentation_util/public/services/create/registry.tsx
+ */
+export const registry = new PluginServiceRegistry(providers);
diff --git a/src/plugins/custom_integrations/public/services/stub/find.ts b/src/plugins/custom_integrations/public/services/stub/find.ts
new file mode 100644
index 0000000000000..08def4e63471d
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/stub/find.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { PluginServiceFactory } from '../../../../presentation_util/public';
+
+import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find';
+
+/**
+ * A type definition for a factory to produce the `CustomIntegrationsFindService` with stubbed output.
+ * @see /src/plugins/presentation_util/public/services/create/factory.ts
+ */
+export type CustomIntegrationsFindServiceFactory =
+ PluginServiceFactory;
+
+/**
+ * A factory to produce the `CustomIntegrationsFindService` with stubbed output.
+ */
+export const findServiceFactory: CustomIntegrationsFindServiceFactory = () => ({
+ findAppendedIntegrations: async (params) => {
+ const { integrations } = await import('./fixtures/integrations');
+ return filterCustomIntegrations(integrations, params);
+ },
+ findReplacementIntegrations: async (params) => {
+ const { integrations } = await import('./fixtures/integrations');
+ return filterCustomIntegrations(integrations, params);
+ },
+});
diff --git a/src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts b/src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts
new file mode 100644
index 0000000000000..7553deada9e26
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts
@@ -0,0 +1,1884 @@
+/*
+ * 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 { CustomIntegration } from '../../../../common';
+
+export const integrations: CustomIntegration[] = [
+ {
+ type: 'ui_link',
+ id: 'System logs',
+ title: 'System logs',
+ categories: ['os_system', 'security'],
+ uiInternalPath: '/app/home#/tutorial/systemLogs',
+ description: 'Collect system logs of common Unix/Linux based distributions.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogging',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'system',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'System metrics',
+ title: 'System metrics',
+ categories: ['os_system', 'security'],
+ uiInternalPath: '/app/home#/tutorial/systemMetrics',
+ description: 'Collect CPU, memory, network, and disk statistics from the host.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/system.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'system',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Apache logs',
+ title: 'Apache logs',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/apacheLogs',
+ description: 'Collect and parse access and error logs created by the Apache HTTP server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoApache',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'apache',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Apache metrics',
+ title: 'Apache metrics',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/apacheMetrics',
+ description: 'Fetch internal metrics from the Apache 2 HTTP server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoApache',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'apache',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Elasticsearch logs',
+ title: 'Elasticsearch logs',
+ categories: ['containers', 'os_system'],
+ uiInternalPath: '/app/home#/tutorial/elasticsearchLogs',
+ description: 'Collect and parse logs created by Elasticsearch.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoElasticsearch',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'elasticsearch',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'IIS logs',
+ title: 'IIS logs',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/iisLogs',
+ description: 'Collect and parse access and error logs created by the IIS HTTP server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/iis.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'iis',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Kafka logs',
+ title: 'Kafka logs',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/kafkaLogs',
+ description: 'Collect and parse logs created by Kafka.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoKafka',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'kafka',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Logstash logs',
+ title: 'Logstash logs',
+ categories: ['custom'],
+ uiInternalPath: '/app/home#/tutorial/logstashLogs',
+ description: 'Collect Logstash main and slow logs.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogstash',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'logstash',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Nginx logs',
+ title: 'Nginx logs',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/nginxLogs',
+ description: 'Collect and parse access and error logs created by the Nginx HTTP server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoNginx',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'nginx',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Nginx metrics',
+ title: 'Nginx metrics',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/nginxMetrics',
+ description: 'Fetch internal metrics from the Nginx HTTP server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoNginx',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'nginx',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'MySQL logs',
+ title: 'MySQL logs',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/mysqlLogs',
+ description: 'Collect and parse error and slow logs created by MySQL.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoMySQL',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'mysql',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'MySQL metrics',
+ title: 'MySQL metrics',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/mysqlMetrics',
+ description: 'Fetch internal metrics from MySQL.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoMySQL',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'mysql',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'MongoDB metrics',
+ title: 'MongoDB metrics',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/mongodbMetrics',
+ description: 'Fetch internal metrics from MongoDB.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoMongodb',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'mongodb',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Osquery logs',
+ title: 'Osquery logs',
+ categories: ['security', 'os_system'],
+ uiInternalPath: '/app/home#/tutorial/osqueryLogs',
+ description: 'Collect osquery logs in JSON format.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/osquery.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'osquery',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'PHP-FPM metrics',
+ title: 'PHP-FPM metrics',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/phpfpmMetrics',
+ description: 'Fetch internal metrics from PHP-FPM.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoPhp',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'php_fpm',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'PostgreSQL metrics',
+ title: 'PostgreSQL metrics',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/postgresqlMetrics',
+ description: 'Fetch internal metrics from PostgreSQL.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoPostgres',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'postgresql',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'PostgreSQL logs',
+ title: 'PostgreSQL logs',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/postgresqlLogs',
+ description: 'Collect and parse error and slow logs created by PostgreSQL.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoPostgres',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'postgresql',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'RabbitMQ metrics',
+ title: 'RabbitMQ metrics',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/rabbitmqMetrics',
+ description: 'Fetch internal metrics from the RabbitMQ server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoRabbitmq',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'rabbitmq',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Redis logs',
+ title: 'Redis logs',
+ categories: ['datastore', 'message_queue'],
+ uiInternalPath: '/app/home#/tutorial/redisLogs',
+ description: 'Collect and parse error and slow logs created by Redis.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoRedis',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'redis',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Redis metrics',
+ title: 'Redis metrics',
+ categories: ['datastore', 'message_queue'],
+ uiInternalPath: '/app/home#/tutorial/redisMetrics',
+ description: 'Fetch internal metrics from Redis.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoRedis',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'redis',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Suricata logs',
+ title: 'Suricata logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/suricataLogs',
+ description: 'Collect Suricata IDS/IPS/NSM logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/suricata.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'suricata',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Docker metrics',
+ title: 'Docker metrics',
+ categories: ['containers', 'os_system'],
+ uiInternalPath: '/app/home#/tutorial/dockerMetrics',
+ description: 'Fetch metrics about your Docker containers.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoDocker',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'docker',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Kubernetes metrics',
+ title: 'Kubernetes metrics',
+ categories: ['containers', 'kubernetes'],
+ uiInternalPath: '/app/home#/tutorial/kubernetesMetrics',
+ description: 'Fetch metrics from your Kubernetes installation.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoKubernetes',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'kubernetes',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'uWSGI metrics',
+ title: 'uWSGI metrics',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/uwsgiMetrics',
+ description: 'Fetch internal metrics from the uWSGI server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/uwsgi.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'uwsgi',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'NetFlow / IPFIX Collector',
+ title: 'NetFlow / IPFIX Collector',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/netflowLogs',
+ description: 'Collect NetFlow and IPFIX flow records.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoBeats',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'netflow',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Traefik logs',
+ title: 'Traefik logs',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/traefikLogs',
+ description: 'Collect Traefik access logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/traefik.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'traefik',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Ceph metrics',
+ title: 'Ceph metrics',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/cephMetrics',
+ description: 'Fetch internal metrics from the Ceph server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoCeph',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'ceph',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Aerospike metrics',
+ title: 'Aerospike metrics',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/aerospikeMetrics',
+ description: 'Fetch internal metrics from the Aerospike server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoAerospike',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'aerospike',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Couchbase metrics',
+ title: 'Couchbase metrics',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/couchbaseMetrics',
+ description: 'Fetch internal metrics from Couchbase.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoCouchbase',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'couchbase',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Dropwizard metrics',
+ title: 'Dropwizard metrics',
+ categories: ['elastic_stack', 'datastore'],
+ uiInternalPath: '/app/home#/tutorial/dropwizardMetrics',
+ description: 'Fetch internal metrics from Dropwizard Java application.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoDropwizard',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'dropwizard',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Elasticsearch metrics',
+ title: 'Elasticsearch metrics',
+ categories: ['elastic_stack', 'datastore'],
+ uiInternalPath: '/app/home#/tutorial/elasticsearchMetrics',
+ description: 'Fetch internal metrics from Elasticsearch.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoElasticsearch',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'elasticsearch',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Etcd metrics',
+ title: 'Etcd metrics',
+ categories: ['elastic_stack', 'datastore'],
+ uiInternalPath: '/app/home#/tutorial/etcdMetrics',
+ description: 'Fetch internal metrics from the Etcd server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoEtcd',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'etcd',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'HAProxy metrics',
+ title: 'HAProxy metrics',
+ categories: ['network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/haproxyMetrics',
+ description: 'Fetch internal metrics from the HAProxy server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoHAproxy',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'haproxy',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Kafka metrics',
+ title: 'Kafka metrics',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/kafkaMetrics',
+ description: 'Fetch internal metrics from the Kafka server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoKafka',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'kafka',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Kibana metrics',
+ title: 'Kibana metrics',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/kibanaMetrics',
+ description: 'Fetch internal metrics from Kibana.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoKibana',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'kibana',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Memcached metrics',
+ title: 'Memcached metrics',
+ categories: ['custom'],
+ uiInternalPath: '/app/home#/tutorial/memcachedMetrics',
+ description: 'Fetch internal metrics from the Memcached server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoMemcached',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'memcached',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Munin metrics',
+ title: 'Munin metrics',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/muninMetrics',
+ description: 'Fetch internal metrics from the Munin server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/munin.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'munin',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'vSphere metrics',
+ title: 'vSphere metrics',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/vsphereMetrics',
+ description: 'Fetch internal metrics from vSphere.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/vsphere.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'vsphere',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Windows metrics',
+ title: 'Windows metrics',
+ categories: ['os_system', 'security'],
+ uiInternalPath: '/app/home#/tutorial/windowsMetrics',
+ description: 'Fetch internal metrics from Windows.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoWindows',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'windows',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Windows Event Log',
+ title: 'Windows Event Log',
+ categories: ['os_system', 'security'],
+ uiInternalPath: '/app/home#/tutorial/windowsEventLogs',
+ description: 'Fetch logs from the Windows Event Log.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoWindows',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'windows',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Golang metrics',
+ title: 'Golang metrics',
+ categories: ['google_cloud', 'cloud', 'network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/golangMetrics',
+ description: 'Fetch internal metrics from a Golang app.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoGolang',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'golang',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Logstash metrics',
+ title: 'Logstash metrics',
+ categories: ['custom'],
+ uiInternalPath: '/app/home#/tutorial/logstashMetrics',
+ description: 'Fetch internal metrics from a Logstash server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogstash',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'logstash',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Prometheus metrics',
+ title: 'Prometheus metrics',
+ categories: ['monitoring', 'datastore'],
+ uiInternalPath: '/app/home#/tutorial/prometheusMetrics',
+ description: 'Fetch metrics from a Prometheus exporter.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoPrometheus',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'prometheus',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Zookeeper metrics',
+ title: 'Zookeeper metrics',
+ categories: ['datastore', 'config_management'],
+ uiInternalPath: '/app/home#/tutorial/zookeeperMetrics',
+ description: 'Fetch internal metrics from a Zookeeper server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/zookeeper.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'zookeeper',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Uptime Monitors',
+ title: 'Uptime Monitors',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/uptimeMonitors',
+ description: 'Monitor services for their availability',
+ icons: [
+ {
+ type: 'eui',
+ src: 'uptimeApp',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'uptime',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'AWS Cloudwatch logs',
+ title: 'AWS Cloudwatch logs',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/cloudwatchLogs',
+ description: 'Collect Cloudwatch logs with Functionbeat.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoAWS',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'aws',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'AWS metrics',
+ title: 'AWS metrics',
+ categories: ['aws', 'cloud', 'datastore', 'security', 'network'],
+ uiInternalPath: '/app/home#/tutorial/awsMetrics',
+ description: 'Fetch monitoring metrics for EC2 instances from the AWS APIs and Cloudwatch.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoAWS',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'aws',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Microsoft SQL Server Metrics',
+ title: 'Microsoft SQL Server Metrics',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/mssqlMetrics',
+ description: 'Fetch monitoring metrics from a Microsoft SQL Server instance',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/mssql.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'mssql',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'NATS metrics',
+ title: 'NATS metrics',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/natsMetrics',
+ description: 'Fetch monitoring metrics from the Nats server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/nats.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'nats',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'NATS logs',
+ title: 'NATS logs',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/natsLogs',
+ description: 'Collect and parse logs created by Nats.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/nats.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'nats',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Zeek logs',
+ title: 'Zeek logs',
+ categories: ['network', 'monitoring', 'security'],
+ uiInternalPath: '/app/home#/tutorial/zeekLogs',
+ description: 'Collect Zeek network security monitoring logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/zeek.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'zeek',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CoreDNS metrics',
+ title: 'CoreDNS metrics',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/corednsMetrics',
+ description: 'Fetch monitoring metrics from the CoreDNS server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/coredns.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'coredns',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CoreDNS logs',
+ title: 'CoreDNS logs',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/corednsLogs',
+ description: 'Collect CoreDNS logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/coredns.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'coredns',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Auditbeat',
+ title: 'Auditbeat',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/auditbeat',
+ description: 'Collect audit data from your hosts.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'securityAnalyticsApp',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'auditbeat',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Iptables logs',
+ title: 'Iptables logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/iptablesLogs',
+ description: 'Collect iptables and ip6tables logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/linux.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'iptables',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Cisco logs',
+ title: 'Cisco logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/ciscoLogs',
+ description: 'Collect Cisco network device logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/cisco.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'cisco',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Envoy Proxy logs',
+ title: 'Envoy Proxy logs',
+ categories: ['elastic_stack', 'datastore'],
+ uiInternalPath: '/app/home#/tutorial/envoyproxyLogs',
+ description: 'Collect Envoy Proxy logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/envoyproxy.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'envoyproxy',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CouchDB metrics',
+ title: 'CouchDB metrics',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/couchdbMetrics',
+ description: 'Fetch monitoring metrics from the CouchdB server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/couchdb.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'couchdb',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Consul metrics',
+ title: 'Consul metrics',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/consulMetrics',
+ description: 'Fetch monitoring metrics from the Consul server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/consul.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'consul',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CockroachDB metrics',
+ title: 'CockroachDB metrics',
+ categories: ['security', 'network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/cockroachdbMetrics',
+ description: 'Fetch monitoring metrics from the CockroachDB server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/cockroachdb.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'cockroachdb',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Traefik metrics',
+ title: 'Traefik metrics',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/traefikMetrics',
+ description: 'Fetch monitoring metrics from Traefik.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/traefik.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'traefik',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'AWS S3 based logs',
+ title: 'AWS S3 based logs',
+ categories: ['aws', 'cloud', 'datastore', 'security', 'network'],
+ uiInternalPath: '/app/home#/tutorial/awsLogs',
+ description: 'Collect AWS logs from S3 bucket with Filebeat.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoAWS',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'aws',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'ActiveMQ logs',
+ title: 'ActiveMQ logs',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/activemqLogs',
+ description: 'Collect ActiveMQ logs with Filebeat.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/activemq.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'activemq',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'ActiveMQ metrics',
+ title: 'ActiveMQ metrics',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/activemqMetrics',
+ description: 'Fetch monitoring metrics from ActiveMQ instances.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/activemq.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'activemq',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Azure metrics',
+ title: 'Azure metrics',
+ categories: ['azure', 'cloud', 'network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/azureMetrics',
+ description: 'Fetch Azure Monitor metrics.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoAzure',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'azure',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'IBM MQ logs',
+ title: 'IBM MQ logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/ibmmqLogs',
+ description: 'Collect IBM MQ logs with Filebeat.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/ibmmq.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'ibmmq',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'IBM MQ metrics',
+ title: 'IBM MQ metrics',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/ibmmqMetrics',
+ description: 'Fetch monitoring metrics from IBM MQ instances.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/ibmmq.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'ibmmq',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'STAN metrics',
+ title: 'STAN metrics',
+ categories: ['message_queue', 'kubernetes'],
+ uiInternalPath: '/app/home#/tutorial/stanMetrics',
+ description: 'Fetch monitoring metrics from the STAN server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/stan.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'stan',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Envoy Proxy metrics',
+ title: 'Envoy Proxy metrics',
+ categories: ['elastic_stack', 'datastore'],
+ uiInternalPath: '/app/home#/tutorial/envoyproxyMetrics',
+ description: 'Fetch monitoring metrics from Envoy Proxy.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/envoyproxy.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'envoyproxy',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Statsd metrics',
+ title: 'Statsd metrics',
+ categories: ['message_queue', 'kubernetes'],
+ uiInternalPath: '/app/home#/tutorial/statsdMetrics',
+ description: 'Fetch monitoring metrics from statsd.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/statsd.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'statsd',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Redis Enterprise metrics',
+ title: 'Redis Enterprise metrics',
+ categories: ['datastore', 'message_queue'],
+ uiInternalPath: '/app/home#/tutorial/redisenterpriseMetrics',
+ description: 'Fetch monitoring metrics from Redis Enterprise Server.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoRedis',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'redisenterprise',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'OpenMetrics metrics',
+ title: 'OpenMetrics metrics',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/openmetricsMetrics',
+ description: 'Fetch metrics from an endpoint that serves metrics in OpenMetrics format.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/openmetrics.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'openmetrics',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'oracle metrics',
+ title: 'oracle metrics',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/oracleMetrics',
+ description: 'Fetch internal metrics from a Oracle server.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/oracle.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'oracle',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'IIS Metrics',
+ title: 'IIS Metrics',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/iisMetrics',
+ description: 'Collect IIS server related metrics.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/iis.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'iis',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Azure logs',
+ title: 'Azure logs',
+ categories: ['azure', 'cloud', 'network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/azureLogs',
+ description: 'Collects Azure activity and audit related logs.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoAzure',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'azure',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Google Cloud metrics',
+ title: 'Google Cloud metrics',
+ categories: ['google_cloud', 'cloud', 'network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/gcpMetrics',
+ description:
+ 'Fetch monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoGCP',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'gcp',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Auditd logs',
+ title: 'Auditd logs',
+ categories: ['os_system'],
+ uiInternalPath: '/app/home#/tutorial/auditdLogs',
+ description: 'Collect logs from the Linux auditd daemon.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/linux.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'auditd',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Barracuda logs',
+ title: 'Barracuda logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/barracudaLogs',
+ description: 'Collect Barracuda Web Application Firewall logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/barracuda.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'barracuda',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Bluecoat logs',
+ title: 'Bluecoat logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/bluecoatLogs',
+ description: 'Collect Blue Coat Director logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogging',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'bluecoat',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CEF logs',
+ title: 'CEF logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/cefLogs',
+ description: 'Collect Common Event Format (CEF) log data over syslog.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogging',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'cef',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Check Point logs',
+ title: 'Check Point logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/checkpointLogs',
+ description: 'Collect Check Point firewall logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/checkpoint.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'checkpoint',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CrowdStrike logs',
+ title: 'CrowdStrike logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/crowdstrikeLogs',
+ description: 'Collect CrowdStrike Falcon logs using the Falcon SIEM Connector.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/crowdstrike.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'crowdstrike',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'CylancePROTECT logs',
+ title: 'CylancePROTECT logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/cylanceLogs',
+ description: 'Collect CylancePROTECT logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/cylance.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'cylance',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'F5 logs',
+ title: 'F5 logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/f5Logs',
+ description: 'Collect F5 Big-IP Access Policy Manager logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/f5.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'f5',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Fortinet logs',
+ title: 'Fortinet logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/fortinetLogs',
+ description: 'Collect Fortinet FortiOS logs over syslog.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/fortinet.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'fortinet',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Google Cloud logs',
+ title: 'Google Cloud logs',
+ categories: ['google_cloud', 'cloud', 'network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/gcpLogs',
+ description: 'Collect Google Cloud audit, firewall, and VPC flow logs.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoGoogleG',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'gcp',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'GSuite logs',
+ title: 'GSuite logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/gsuiteLogs',
+ description: 'Collect GSuite activity reports.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoGoogleG',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'gsuite',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'HAProxy logs',
+ title: 'HAProxy logs',
+ categories: ['network', 'web'],
+ uiInternalPath: '/app/home#/tutorial/haproxyLogs',
+ description: 'Collect HAProxy logs.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoHAproxy',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'haproxy',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Icinga logs',
+ title: 'Icinga logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/icingaLogs',
+ description: 'Collect Icinga main, debug, and startup logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/icinga.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'icinga',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Imperva logs',
+ title: 'Imperva logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/impervaLogs',
+ description: 'Collect Imperva SecureSphere logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogging',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'imperva',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Infoblox logs',
+ title: 'Infoblox logs',
+ categories: ['network'],
+ uiInternalPath: '/app/home#/tutorial/infobloxLogs',
+ description: 'Collect Infoblox NIOS logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/infoblox.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'infoblox',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Juniper Logs',
+ title: 'Juniper Logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/juniperLogs',
+ description: 'Collect Juniper JUNOS logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/juniper.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'juniper',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Kibana Logs',
+ title: 'Kibana Logs',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/kibanaLogs',
+ description: 'Collect Kibana logs.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoKibana',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'kibana',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Microsoft Defender ATP logs',
+ title: 'Microsoft Defender ATP logs',
+ categories: ['network', 'security', 'azure'],
+ uiInternalPath: '/app/home#/tutorial/microsoftLogs',
+ description: 'Collect Microsoft Defender ATP alerts.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/microsoft.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'microsoft',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'MISP threat intel logs',
+ title: 'MISP threat intel logs',
+ categories: ['network', 'security', 'azure'],
+ uiInternalPath: '/app/home#/tutorial/mispLogs',
+ description: 'Collect MISP threat intelligence data with Filebeat.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/misp.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'misp',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'MongoDB logs',
+ title: 'MongoDB logs',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/mongodbLogs',
+ description: 'Collect MongoDB logs.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoMongodb',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'mongodb',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'MSSQL logs',
+ title: 'MSSQL logs',
+ categories: ['datastore'],
+ uiInternalPath: '/app/home#/tutorial/mssqlLogs',
+ description: 'Collect MSSQL logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/microsoft.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'mssql',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Arbor Peakflow logs',
+ title: 'Arbor Peakflow logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/netscoutLogs',
+ description: 'Collect Netscout Arbor Peakflow SP logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/netscout.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'netscout',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Office 365 logs',
+ title: 'Office 365 logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/o365Logs',
+ description: 'Collect Office 365 activity logs via the Office 365 API.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/o365.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'o365',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Okta logs',
+ title: 'Okta logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/oktaLogs',
+ description: 'Collect the Okta system log via the Okta API.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/okta.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'okta',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Palo Alto Networks PAN-OS logs',
+ title: 'Palo Alto Networks PAN-OS logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/panwLogs',
+ description:
+ 'Collect Palo Alto Networks PAN-OS threat and traffic logs over syslog or from a log file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/paloalto.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'panw',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'RabbitMQ logs',
+ title: 'RabbitMQ logs',
+ categories: ['message_queue'],
+ uiInternalPath: '/app/home#/tutorial/rabbitmqLogs',
+ description: 'Collect RabbitMQ logs.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/rabbitmq.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'rabbitmq',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Radware DefensePro logs',
+ title: 'Radware DefensePro logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/radwareLogs',
+ description: 'Collect Radware DefensePro logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/radware.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'radware',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Google Santa logs',
+ title: 'Google Santa logs',
+ categories: ['security', 'os_system'],
+ uiInternalPath: '/app/home#/tutorial/santaLogs',
+ description: 'Collect Google Santa logs about process executions on MacOS.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogging',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'santa',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Sonicwall FW logs',
+ title: 'Sonicwall FW logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/sonicwallLogs',
+ description: 'Collect Sonicwall-FW logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/sonicwall.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'sonicwall',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Sophos logs',
+ title: 'Sophos logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/sophosLogs',
+ description: 'Collect Sophos XG SFOS logs over syslog.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/sophos.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'sophos',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Squid logs',
+ title: 'Squid logs',
+ categories: ['security'],
+ uiInternalPath: '/app/home#/tutorial/squidLogs',
+ description: 'Collect Squid logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'logoLogging',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'squid',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Tomcat logs',
+ title: 'Tomcat logs',
+ categories: ['web', 'security'],
+ uiInternalPath: '/app/home#/tutorial/tomcatLogs',
+ description: 'Collect Apache Tomcat logs over syslog or from a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/tomcat.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'tomcat',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'Zscaler Logs',
+ title: 'Zscaler Logs',
+ categories: ['network', 'security'],
+ uiInternalPath: '/app/home#/tutorial/zscalerLogs',
+ description: 'This is a module for receiving Zscaler NSS logs over Syslog or a file.',
+ icons: [
+ {
+ type: 'svg',
+ src: '/dqo/plugins/home/assets/logos/zscaler.svg',
+ },
+ ],
+ shipper: 'beats',
+ eprOverlap: 'zscaler',
+ isBeta: false,
+ },
+ {
+ type: 'ui_link',
+ id: 'apm',
+ title: 'APM',
+ categories: ['web'],
+ uiInternalPath: '/app/home#/tutorial/apm',
+ description: 'Collect in-depth performance metrics and errors from inside your applications.',
+ icons: [
+ {
+ type: 'eui',
+ src: 'apmApp',
+ },
+ ],
+ shipper: 'tutorial',
+ isBeta: false,
+ eprOverlap: 'apm',
+ },
+];
diff --git a/src/plugins/custom_integrations/public/services/stub/index.ts b/src/plugins/custom_integrations/public/services/stub/index.ts
new file mode 100644
index 0000000000000..fe7465949d565
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/stub/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 {
+ PluginServiceProviders,
+ PluginServiceProvider,
+ PluginServiceRegistry,
+} from '../../../../presentation_util/public';
+
+import { CustomIntegrationsServices } from '..';
+import { findServiceFactory } from './find';
+import { platformServiceFactory } from './platform';
+
+export { findServiceFactory } from './find';
+export { platformServiceFactory } from './platform';
+
+export const providers: PluginServiceProviders = {
+ find: new PluginServiceProvider(findServiceFactory),
+ platform: new PluginServiceProvider(platformServiceFactory),
+};
+
+export const registry = new PluginServiceRegistry(providers);
diff --git a/src/plugins/custom_integrations/public/services/stub/platform.ts b/src/plugins/custom_integrations/public/services/stub/platform.ts
new file mode 100644
index 0000000000000..60480f9905cb9
--- /dev/null
+++ b/src/plugins/custom_integrations/public/services/stub/platform.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { PluginServiceFactory } from '../../../../presentation_util/public';
+
+import type { CustomIntegrationsPlatformService } from '../platform';
+
+/**
+ * A type definition for a factory to produce the `CustomIntegrationsPlatformService` with stubbed output.
+ * @see /src/plugins/presentation_util/public/services/create/factory.ts
+ */
+export type CustomIntegrationsPlatformServiceFactory =
+ PluginServiceFactory;
+
+/**
+ * A factory to produce the `CustomIntegrationsPlatformService` with stubbed output.
+ */
+export const platformServiceFactory: CustomIntegrationsPlatformServiceFactory = () => ({
+ getBasePath: () => '/basePath',
+ getAbsolutePath: (path: string): string => `/basePath${path}`,
+});
diff --git a/src/plugins/custom_integrations/public/types.ts b/src/plugins/custom_integrations/public/types.ts
index 9a12af767ecbc..946115329e2b5 100755
--- a/src/plugins/custom_integrations/public/types.ts
+++ b/src/plugins/custom_integrations/public/types.ts
@@ -6,14 +6,19 @@
* Side Public License, v 1.
*/
+import type { PresentationUtilPluginStart } from '../../presentation_util/public';
+
import { CustomIntegration } from '../common';
export interface CustomIntegrationsSetup {
getAppendCustomIntegrations: () => Promise;
getReplacementCustomIntegrations: () => Promise;
}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface CustomIntegrationsStart {}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface AppPluginStartDependencies {}
+export interface CustomIntegrationsStart {
+ ContextProvider: React.FC;
+}
+
+export interface CustomIntegrationsStartDependencies {
+ presentationUtil: PresentationUtilPluginStart;
+}
diff --git a/src/plugins/custom_integrations/server/language_clients/index.ts b/src/plugins/custom_integrations/server/language_clients/index.ts
new file mode 100644
index 0000000000000..da61f804b4242
--- /dev/null
+++ b/src/plugins/custom_integrations/server/language_clients/index.ts
@@ -0,0 +1,181 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { CoreSetup } from 'kibana/server';
+import { CustomIntegrationRegistry } from '../custom_integration_registry';
+import { CustomIntegrationIcon, PLUGIN_ID } from '../../common';
+
+interface LanguageIntegration {
+ id: string;
+ title: string;
+ icon?: string;
+ euiIconName?: string;
+ description: string;
+ docUrlTemplate: string;
+}
+
+const ELASTIC_WEBSITE_URL = 'https://www.elastic.co';
+const ELASTICSEARCH_CLIENT_URL = `${ELASTIC_WEBSITE_URL}/guide/en/elasticsearch/client`;
+export const integrations: LanguageIntegration[] = [
+ {
+ id: 'all',
+ title: i18n.translate('customIntegrations.languageclients.AllTitle', {
+ defaultMessage: 'Elasticsearch Clients',
+ }),
+ euiIconName: 'logoElasticsearch',
+ description: i18n.translate('customIntegrations.languageclients.AllDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official language clients.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/index.html`,
+ },
+ {
+ id: 'javascript',
+ title: i18n.translate('customIntegrations.languageclients.JavascriptTitle', {
+ defaultMessage: 'Elasticsearch JavaScript Client',
+ }),
+ icon: 'nodejs.svg',
+ description: i18n.translate('customIntegrations.languageclients.JavascriptDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Node.js client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/javascript-api/{branch}/introduction.html`,
+ },
+ {
+ id: 'ruby',
+ title: i18n.translate('customIntegrations.languageclients.RubyTitle', {
+ defaultMessage: 'Elasticsearch Ruby Client',
+ }),
+ icon: 'ruby.svg',
+ description: i18n.translate('customIntegrations.languageclients.RubyDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Ruby client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/ruby-api/{branch}/ruby_client.html`,
+ },
+ {
+ id: 'go',
+ title: i18n.translate('customIntegrations.languageclients.GoTitle', {
+ defaultMessage: 'Elasticsearch Go Client',
+ }),
+ icon: 'go.svg',
+ description: i18n.translate('customIntegrations.languageclients.GoDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Go client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/go-api/{branch}/overview.html`,
+ },
+ {
+ id: 'dotnet',
+ title: i18n.translate('customIntegrations.languageclients.DotNetTitle', {
+ defaultMessage: 'Elasticsearch .NET Client',
+ }),
+ icon: 'dotnet.svg',
+ description: i18n.translate('customIntegrations.languageclients.DotNetDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official .NET client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/net-api/{branch}/index.html`,
+ },
+ {
+ id: 'php',
+ title: i18n.translate('customIntegrations.languageclients.PhpTitle', {
+ defaultMessage: 'Elasticsearch PHP Client',
+ }),
+ icon: 'php.svg',
+ description: i18n.translate('customIntegrations.languageclients.PhpDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official .PHP client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/php-api/{branch}/index.html`,
+ },
+ {
+ id: 'perl',
+ title: i18n.translate('customIntegrations.languageclients.PerlTitle', {
+ defaultMessage: 'Elasticsearch Perl Client',
+ }),
+ icon: 'perl.svg',
+ description: i18n.translate('customIntegrations.languageclients.PerlDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Perl client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/perl-api/{branch}/index.html`,
+ },
+ {
+ id: 'python',
+ title: i18n.translate('customIntegrations.languageclients.PythonTitle', {
+ defaultMessage: 'Elasticsearch Python Client',
+ }),
+ icon: 'python.svg',
+ description: i18n.translate('customIntegrations.languageclients.PythonDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Python client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/python-api/{branch}/index.html`,
+ },
+ {
+ id: 'rust',
+ title: i18n.translate('customIntegrations.languageclients.RustTitle', {
+ defaultMessage: 'Elasticsearch Rust Client',
+ }),
+ icon: 'rust.svg',
+ description: i18n.translate('customIntegrations.languageclients.RustDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Rust client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/rust-api/{branch}/index.html`,
+ },
+ {
+ id: 'java',
+ title: i18n.translate('customIntegrations.languageclients.JavaTitle', {
+ defaultMessage: 'Elasticsearch Java Client',
+ }),
+ icon: 'java.svg',
+ description: i18n.translate('customIntegrations.languageclients.JavaDescription', {
+ defaultMessage:
+ 'Start building your custom application on top of Elasticsearch with the official Java client.',
+ }),
+ docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/java-api-client/{branch}/index.html`,
+ },
+];
+
+export function registerLanguageClients(
+ core: CoreSetup,
+ registry: CustomIntegrationRegistry,
+ branch: string
+) {
+ integrations.forEach((integration: LanguageIntegration) => {
+ const icons: CustomIntegrationIcon[] = [];
+ if (integration.euiIconName) {
+ icons.push({
+ type: 'eui',
+ src: integration.euiIconName,
+ });
+ } else if (integration.icon) {
+ icons.push({
+ type: 'svg',
+ src: core.http.basePath.prepend(
+ `/plugins/${PLUGIN_ID}/assets/language_clients/${integration.icon}`
+ ),
+ });
+ }
+
+ registry.registerCustomIntegration({
+ id: `language_client.${integration.id}`,
+ title: integration.title,
+ description: integration.description,
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath: integration.docUrlTemplate.replace('{branch}', branch),
+ isBeta: false,
+ icons,
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ });
+ });
+}
diff --git a/src/plugins/custom_integrations/server/plugin.test.ts b/src/plugins/custom_integrations/server/plugin.test.ts
index 424eedf0603cd..8dee81ba6cba3 100644
--- a/src/plugins/custom_integrations/server/plugin.test.ts
+++ b/src/plugins/custom_integrations/server/plugin.test.ts
@@ -22,9 +22,145 @@ describe('CustomIntegrationsPlugin', () => {
initContext = coreMock.createPluginInitializerContext();
});
- test('wires up tutorials provider service and returns registerTutorial and addScopedTutorialContextFactory', () => {
+ test('should return setup contract', () => {
const setup = new CustomIntegrationsPlugin(initContext).setup(mockCoreSetup);
expect(setup).toHaveProperty('registerCustomIntegration');
+ expect(setup).toHaveProperty('getAppendCustomIntegrations');
+ });
+
+ test('should register language clients', () => {
+ const setup = new CustomIntegrationsPlugin(initContext).setup(mockCoreSetup);
+ expect(setup.getAppendCustomIntegrations()).toEqual([
+ {
+ id: 'language_client.all',
+ title: 'Elasticsearch Clients',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official language clients.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath: 'https://www.elastic.co/guide/en/elasticsearch/client/index.html',
+ isBeta: false,
+ icons: [{ type: 'eui', src: 'logoElasticsearch' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.javascript',
+ title: 'Elasticsearch JavaScript Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Node.js client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/branch/introduction.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.ruby',
+ title: 'Elasticsearch Ruby Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Ruby client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/branch/ruby_client.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.go',
+ title: 'Elasticsearch Go Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Go client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/go-api/branch/overview.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.dotnet',
+ title: 'Elasticsearch .NET Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official .NET client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/net-api/branch/index.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.php',
+ title: 'Elasticsearch PHP Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official .PHP client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/php-api/branch/index.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.perl',
+ title: 'Elasticsearch Perl Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Perl client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/perl-api/branch/index.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.python',
+ title: 'Elasticsearch Python Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Python client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/python-api/branch/index.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.rust',
+ title: 'Elasticsearch Rust Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Rust client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/rust-api/branch/index.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ {
+ id: 'language_client.java',
+ title: 'Elasticsearch Java Client',
+ description:
+ 'Start building your custom application on top of Elasticsearch with the official Java client.',
+ type: 'ui_link',
+ shipper: 'language_clients',
+ uiInternalPath:
+ 'https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/branch/index.html',
+ isBeta: false,
+ icons: [{ type: 'svg' }],
+ categories: ['elastic_stack', 'custom', 'language_client'],
+ },
+ ]);
});
});
});
diff --git a/src/plugins/custom_integrations/server/plugin.ts b/src/plugins/custom_integrations/server/plugin.ts
index 099650ee15a05..330a1288d05a2 100755
--- a/src/plugins/custom_integrations/server/plugin.ts
+++ b/src/plugins/custom_integrations/server/plugin.ts
@@ -12,12 +12,14 @@ import { CustomIntegrationsPluginSetup, CustomIntegrationsPluginStart } from './
import { CustomIntegration } from '../common';
import { CustomIntegrationRegistry } from './custom_integration_registry';
import { defineRoutes } from './routes/define_routes';
+import { registerLanguageClients } from './language_clients';
export class CustomIntegrationsPlugin
implements Plugin
{
private readonly logger: Logger;
private readonly customIngegrationRegistry: CustomIntegrationRegistry;
+ private readonly branch: string;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
@@ -25,6 +27,7 @@ export class CustomIntegrationsPlugin
this.logger,
initializerContext.env.mode.dev
);
+ this.branch = initializerContext.env.packageInfo.branch;
}
public setup(core: CoreSetup) {
@@ -33,6 +36,8 @@ export class CustomIntegrationsPlugin
const router = core.http.createRouter();
defineRoutes(router, this.customIngegrationRegistry);
+ registerLanguageClients(core, this.customIngegrationRegistry, this.branch);
+
return {
registerCustomIntegration: (integration: Omit) => {
this.customIngegrationRegistry.registerCustomIntegration({
@@ -40,6 +45,9 @@ export class CustomIntegrationsPlugin
...integration,
});
},
+ getAppendCustomIntegrations: () => {
+ return this.customIngegrationRegistry.getAppendCustomIntegrations();
+ },
} as CustomIntegrationsPluginSetup;
}
diff --git a/src/plugins/custom_integrations/storybook/decorator.tsx b/src/plugins/custom_integrations/storybook/decorator.tsx
new file mode 100644
index 0000000000000..c5fea9615ee47
--- /dev/null
+++ b/src/plugins/custom_integrations/storybook/decorator.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 { DecoratorFn } from '@storybook/react';
+import { I18nProvider } from '@kbn/i18n/react';
+
+import { PluginServiceRegistry } from '../../presentation_util/public';
+
+import { pluginServices } from '../public/services';
+import { CustomIntegrationsServices } from '../public/services';
+import { providers } from '../public/services/storybook';
+import { EuiThemeProvider } from '../../kibana_react/common/eui_styled_components';
+
+/**
+ * Returns a Storybook Decorator that provides both the `I18nProvider` and access to `PluginServices`
+ * for components rendered in Storybook.
+ */
+export const getCustomIntegrationsContextDecorator =
+ (): DecoratorFn =>
+ (story, { globals }) => {
+ const ContextProvider = getCustomIntegrationsContextProvider();
+ const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark';
+
+ return (
+
+
+ {story()}
+
+
+ );
+ };
+
+/**
+ * Prepares `PluginServices` for use in Storybook and returns a React `Context.Provider` element
+ * so components that access `PluginServices` can be rendered.
+ */
+export const getCustomIntegrationsContextProvider = () => {
+ const registry = new PluginServiceRegistry(providers);
+ pluginServices.setRegistry(registry.start({}));
+ return pluginServices.getContextProvider();
+};
diff --git a/src/plugins/custom_integrations/storybook/index.ts b/src/plugins/custom_integrations/storybook/index.ts
new file mode 100644
index 0000000000000..a9e34e1aeeb7e
--- /dev/null
+++ b/src/plugins/custom_integrations/storybook/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 {
+ getCustomIntegrationsContextDecorator as getStorybookContextDecorator,
+ getCustomIntegrationsContextProvider as getStorybookContextProvider,
+} from '../storybook/decorator';
diff --git a/src/plugins/custom_integrations/storybook/main.ts b/src/plugins/custom_integrations/storybook/main.ts
new file mode 100644
index 0000000000000..1261fe5a06f69
--- /dev/null
+++ b/src/plugins/custom_integrations/storybook/main.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.
+ */
+
+import { defaultConfig } from '@kbn/storybook';
+
+module.exports = defaultConfig;
diff --git a/src/plugins/custom_integrations/storybook/manager.ts b/src/plugins/custom_integrations/storybook/manager.ts
new file mode 100644
index 0000000000000..99c01efdddfdc
--- /dev/null
+++ b/src/plugins/custom_integrations/storybook/manager.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { addons } from '@storybook/addons';
+import { create } from '@storybook/theming';
+import { PANEL_ID } from '@storybook/addon-actions';
+
+addons.setConfig({
+ theme: create({
+ base: 'light',
+ brandTitle: 'Kibana Custom Integrations Storybook',
+ brandUrl: 'https://github.com/elastic/kibana/tree/master/src/plugins/custom_integrations',
+ }),
+ showPanel: true.valueOf,
+ selectedPanel: PANEL_ID,
+});
diff --git a/src/plugins/custom_integrations/storybook/preview.tsx b/src/plugins/custom_integrations/storybook/preview.tsx
new file mode 100644
index 0000000000000..c27390261c920
--- /dev/null
+++ b/src/plugins/custom_integrations/storybook/preview.tsx
@@ -0,0 +1,28 @@
+/*
+ * 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 { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks';
+
+import { getCustomIntegrationsContextDecorator } from './decorator';
+
+export const decorators = [getCustomIntegrationsContextDecorator()];
+
+export const parameters = {
+ docs: {
+ page: () => (
+ <>
+
+
+
+
+
+ >
+ ),
+ },
+};
diff --git a/src/plugins/custom_integrations/tsconfig.json b/src/plugins/custom_integrations/tsconfig.json
index 2ce7bf9c8112c..ccb75c358611b 100644
--- a/src/plugins/custom_integrations/tsconfig.json
+++ b/src/plugins/custom_integrations/tsconfig.json
@@ -6,8 +6,15 @@
"declaration": true,
"declarationMap": true
},
- "include": ["common/**/*", "public/**/*", "server/**/*"],
+ "include": [
+ "../../../typings/**/*",
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ "storybook/**/*"
+ ],
"references": [
- { "path": "../../core/tsconfig.json" }
+ { "path": "../../core/tsconfig.json" },
+ { "path": "../presentation_util/tsconfig.json" }
]
}
diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts
index a1d4b5b68d20d..06133fb2160c0 100644
--- a/src/plugins/embeddable/public/lib/containers/container.ts
+++ b/src/plugins/embeddable/public/lib/containers/container.ts
@@ -46,6 +46,7 @@ export abstract class Container<
parent?: Container
) {
super(input, output, parent);
+ this.getFactory = getFactory; // Currently required for using in storybook due to https://github.com/storybookjs/storybook/issues/13834
this.subscription = this.getInput$()
// At each update event, get both the previous and current state
.pipe(startWith(input), pairwise())
diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts
index 68edde1336728..a6c22073dbc90 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.test.ts
+++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts
@@ -308,6 +308,24 @@ describe('useRequest hook', () => {
expect(getSendRequestSpy().callCount).toBe(2);
});
+ it(`changing pollIntervalMs to undefined cancels the poll`, async () => {
+ const { setupErrorRequest, setErrorResponse, completeRequest, getSendRequestSpy } = helpers;
+ // Send initial request.
+ setupErrorRequest({ pollIntervalMs: REQUEST_TIME });
+
+ // Setting the poll to undefined will cancel subsequent requests.
+ setErrorResponse({ pollIntervalMs: undefined });
+
+ // Complete initial request.
+ await completeRequest();
+
+ // If there were another scheduled poll request, this would complete it.
+ await completeRequest();
+
+ // But because we canceled the poll, we only see 1 request instead of 2.
+ expect(getSendRequestSpy().callCount).toBe(1);
+ });
+
it('when the path changes after a request is scheduled, the scheduled request is sent with that path', async () => {
const {
setupSuccessRequest,
diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts
index 9523766596fed..c75ce4e83921c 100644
--- a/src/plugins/home/server/index.ts
+++ b/src/plugins/home/server/index.ts
@@ -18,9 +18,6 @@ export const config: PluginConfigDescriptor = {
disableWelcomeScreen: true,
},
schema: configSchema,
- deprecations: ({ renameFromRoot }) => [
- renameFromRoot('kibana.disableWelcomeScreen', 'home.disableWelcomeScreen'),
- ],
};
export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext);
diff --git a/src/plugins/newsfeed/server/config.ts b/src/plugins/newsfeed/server/config.ts
index f8924706b751c..f14f3452761e1 100644
--- a/src/plugins/newsfeed/server/config.ts
+++ b/src/plugins/newsfeed/server/config.ts
@@ -11,7 +11,6 @@ import {
NEWSFEED_DEFAULT_SERVICE_PATH,
NEWSFEED_DEFAULT_SERVICE_BASE_URL,
NEWSFEED_DEV_SERVICE_BASE_URL,
- NEWSFEED_FALLBACK_LANGUAGE,
} from '../common/constants';
export const configSchema = schema.object({
@@ -25,7 +24,6 @@ export const configSchema = schema.object({
schema.string({ defaultValue: NEWSFEED_DEV_SERVICE_BASE_URL })
),
}),
- defaultLanguage: schema.string({ defaultValue: NEWSFEED_FALLBACK_LANGUAGE }), // TODO: Deprecate since no longer used
mainInterval: schema.duration({ defaultValue: '2m' }), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote
fetchInterval: schema.duration({ defaultValue: '1d' }), // (1day) How often to fetch remote and reset the last fetched time
});
diff --git a/src/plugins/newsfeed/server/index.ts b/src/plugins/newsfeed/server/index.ts
index 460d48622af69..fefb725e2804e 100644
--- a/src/plugins/newsfeed/server/index.ts
+++ b/src/plugins/newsfeed/server/index.ts
@@ -17,7 +17,6 @@ export const config: PluginConfigDescriptor = {
mainInterval: true,
fetchInterval: true,
},
- deprecations: ({ unused }) => [unused('defaultLanguage')],
};
export function plugin() {
diff --git a/src/plugins/presentation_util/public/components/controls/__stories__/controls_service_stub.ts b/src/plugins/presentation_util/public/components/controls/__stories__/controls_service_stub.ts
new file mode 100644
index 0000000000000..59e7a44a83a17
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/__stories__/controls_service_stub.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { InputControlFactory } from '../types';
+import { ControlsService } from '../controls_service';
+import { flightFields, getEuiSelectableOptions } from './flights';
+import { OptionsListEmbeddableFactory } from '../control_types/options_list';
+
+export const getControlsServiceStub = () => {
+ const controlsServiceStub = new ControlsService();
+
+ const optionsListFactoryStub = new OptionsListEmbeddableFactory(
+ ({ field, search }) =>
+ new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)),
+ () => Promise.resolve(['demo data flights']),
+ () => Promise.resolve(flightFields)
+ );
+
+ // cast to unknown because the stub cannot use the embeddable start contract to transform the EmbeddableFactoryDefinition into an EmbeddableFactory
+ const optionsListControlFactory = optionsListFactoryStub as unknown as InputControlFactory;
+ optionsListControlFactory.getDefaultInput = () => ({});
+ controlsServiceStub.registerInputControlType(optionsListControlFactory);
+ return controlsServiceStub;
+};
diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx b/src/plugins/presentation_util/public/components/controls/__stories__/decorators.tsx
similarity index 95%
rename from src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx
rename to src/plugins/presentation_util/public/components/controls/__stories__/decorators.tsx
index 0aaa0e7a8a533..c5d3cf2c815be 100644
--- a/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx
+++ b/src/plugins/presentation_util/public/components/controls/__stories__/decorators.tsx
@@ -23,7 +23,7 @@ const panelStyle = {
const kqlBarStyle = { background: bar, padding: 16, minHeight, fontStyle: 'italic' };
-const inputBarStyle = { background: '#fff', padding: 4, minHeight };
+const inputBarStyle = { background: '#fff', padding: 4 };
const layout = (OptionStory: Story) => (
diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts b/src/plugins/presentation_util/public/components/controls/__stories__/flights.ts
similarity index 100%
rename from src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts
rename to src/plugins/presentation_util/public/components/controls/__stories__/flights.ts
diff --git a/src/plugins/presentation_util/public/components/controls/__stories__/input_controls.stories.tsx b/src/plugins/presentation_util/public/components/controls/__stories__/input_controls.stories.tsx
new file mode 100644
index 0000000000000..2a463fece18da
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/__stories__/input_controls.stories.tsx
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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, { useEffect, useMemo } from 'react';
+import uuid from 'uuid';
+
+import { decorators } from './decorators';
+import { providers } from '../../../services/storybook';
+import { getControlsServiceStub } from './controls_service_stub';
+import { ControlGroupContainerFactory } from '../control_group/control_group_container_factory';
+
+export default {
+ title: 'Controls',
+ description: '',
+ decorators,
+};
+
+const ControlGroupStoryComponent = () => {
+ const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []);
+
+ providers.overlays.start({});
+ const overlays = providers.overlays.getService();
+
+ const controlsServiceStub = getControlsServiceStub();
+
+ useEffect(() => {
+ (async () => {
+ const factory = new ControlGroupContainerFactory(controlsServiceStub, overlays);
+ const controlGroupContainerEmbeddable = await factory.create({
+ inheritParentState: {
+ useQuery: false,
+ useFilters: false,
+ useTimerange: false,
+ },
+ controlStyle: 'oneLine',
+ id: uuid.v4(),
+ panels: {},
+ });
+ if (controlGroupContainerEmbeddable && embeddableRoot.current) {
+ controlGroupContainerEmbeddable.render(embeddableRoot.current);
+ }
+ })();
+ }, [embeddableRoot, controlsServiceStub, overlays]);
+
+ return ;
+};
+
+export const ControlGroupStory = () => ;
diff --git a/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_component.tsx b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_component.tsx
new file mode 100644
index 0000000000000..240beea13b0e2
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_component.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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, { useEffect, useMemo, useState } from 'react';
+import classNames from 'classnames';
+import {
+ EuiButtonIcon,
+ EuiFormControlLayout,
+ EuiFormLabel,
+ EuiFormRow,
+ EuiToolTip,
+} from '@elastic/eui';
+import { ControlGroupContainer } from '../control_group/control_group_container';
+import { useChildEmbeddable } from '../hooks/use_child_embeddable';
+import { ControlStyle } from '../types';
+import { ControlFrameStrings } from './control_frame_strings';
+
+export interface ControlFrameProps {
+ container: ControlGroupContainer;
+ customPrepend?: JSX.Element;
+ controlStyle: ControlStyle;
+ enableActions?: boolean;
+ onRemove?: () => void;
+ embeddableId: string;
+ onEdit?: () => void;
+}
+
+export const ControlFrame = ({
+ customPrepend,
+ enableActions,
+ embeddableId,
+ controlStyle,
+ container,
+ onRemove,
+ onEdit,
+}: ControlFrameProps) => {
+ const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []);
+ const embeddable = useChildEmbeddable({ container, embeddableId });
+
+ const [title, setTitle] = useState();
+
+ const usingTwoLineLayout = controlStyle === 'twoLine';
+
+ useEffect(() => {
+ if (embeddableRoot.current && embeddable) {
+ embeddable.render(embeddableRoot.current);
+ }
+ const subscription = embeddable?.getInput$().subscribe((newInput) => setTitle(newInput.title));
+ return () => subscription?.unsubscribe();
+ }, [embeddable, embeddableRoot]);
+
+ const floatingActions = (
+
+
+
+
+
+
+
+
+ );
+
+ const form = (
+
+ {customPrepend ?? null}
+ {usingTwoLineLayout ? undefined : (
+
+ {title}
+
+ )}
+ >
+ }
+ >
+
+
+ );
+
+ return (
+ <>
+ {enableActions && floatingActions}
+
+ {form}
+
+ >
+ );
+};
diff --git a/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_strings.ts b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_strings.ts
new file mode 100644
index 0000000000000..5f9e89aa797cb
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_strings.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export const ControlFrameStrings = {
+ floatingActions: {
+ getEditButtonTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.editTitle', {
+ defaultMessage: 'Manage control',
+ }),
+ getRemoveButtonTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.removeTitle', {
+ defaultMessage: 'Remove control',
+ }),
+ },
+};
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_component.tsx b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_component.tsx
new file mode 100644
index 0000000000000..d683c0749d98d
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_component.tsx
@@ -0,0 +1,163 @@
+/*
+ * 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 '../control_group.scss';
+
+import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import React, { useEffect, useMemo, useState } from 'react';
+import classNames from 'classnames';
+import {
+ arrayMove,
+ SortableContext,
+ rectSortingStrategy,
+ sortableKeyboardCoordinates,
+} from '@dnd-kit/sortable';
+import {
+ closestCenter,
+ DndContext,
+ DragEndEvent,
+ DragOverlay,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ LayoutMeasuringStrategy,
+} from '@dnd-kit/core';
+
+import { ControlGroupStrings } from '../control_group_strings';
+import { ControlGroupContainer } from '../control_group_container';
+import { ControlClone, SortableControl } from './control_group_sortable_item';
+import { OPTIONS_LIST_CONTROL } from '../../control_types/options_list/options_list_embeddable';
+
+interface ControlGroupProps {
+ controlGroupContainer: ControlGroupContainer;
+}
+
+export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => {
+ const [controlIds, setControlIds] = useState([]);
+
+ // sync controlIds every time input panels change
+ useEffect(() => {
+ const subscription = controlGroupContainer.getInput$().subscribe(() => {
+ setControlIds((currentIds) => {
+ // sync control Ids with panels from container input.
+ const { panels } = controlGroupContainer.getInput();
+ const newIds: string[] = [];
+ const allIds = [...currentIds, ...Object.keys(panels)];
+ allIds.forEach((id) => {
+ const currentIndex = currentIds.indexOf(id);
+ if (!panels[id] && currentIndex !== -1) {
+ currentIds.splice(currentIndex, 1);
+ }
+ if (currentIndex === -1 && Boolean(panels[id])) {
+ newIds.push(id);
+ }
+ });
+ return [...currentIds, ...newIds];
+ });
+ });
+ return () => subscription.unsubscribe();
+ }, [controlGroupContainer]);
+
+ const [draggingId, setDraggingId] = useState(null);
+
+ const draggingIndex = useMemo(
+ () => (draggingId ? controlIds.indexOf(draggingId) : -1),
+ [controlIds, draggingId]
+ );
+
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
+ );
+
+ const onDragEnd = ({ over }: DragEndEvent) => {
+ if (over) {
+ const overIndex = controlIds.indexOf(over.id);
+ if (draggingIndex !== overIndex) {
+ const newIndex = overIndex;
+ setControlIds((currentControlIds) => arrayMove(currentControlIds, draggingIndex, newIndex));
+ }
+ }
+ setDraggingId(null);
+ };
+
+ return (
+
+
+ setDraggingId(active.id)}
+ onDragEnd={onDragEnd}
+ onDragCancel={() => setDraggingId(null)}
+ sensors={sensors}
+ collisionDetection={closestCenter}
+ layoutMeasuring={{
+ strategy: LayoutMeasuringStrategy.Always,
+ }}
+ >
+
+
+ {controlIds.map((controlId, index) => (
+ controlGroupContainer.editControl(controlId)}
+ onRemove={() => controlGroupContainer.removeEmbeddable(controlId)}
+ dragInfo={{ index, draggingIndex }}
+ container={controlGroupContainer}
+ controlStyle={controlGroupContainer.getInput().controlStyle}
+ embeddableId={controlId}
+ width={controlGroupContainer.getInput().panels[controlId].width}
+ key={controlId}
+ />
+ ))}
+
+
+
+ {draggingId ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+ controlGroupContainer.createNewControl(OPTIONS_LIST_CONTROL)} // use popover when there are multiple types of control
+ />
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx
new file mode 100644
index 0000000000000..3ae171a588da4
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx
@@ -0,0 +1,151 @@
+/*
+ * 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 { EuiFlexItem, EuiFormLabel, EuiIcon, EuiFlexGroup } from '@elastic/eui';
+import React, { forwardRef, HTMLAttributes } from 'react';
+import { useSortable } from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import classNames from 'classnames';
+
+import { ControlWidth } from '../../types';
+import { ControlGroupContainer } from '../control_group_container';
+import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
+import { ControlFrame, ControlFrameProps } from '../../control_frame/control_frame_component';
+
+interface DragInfo {
+ isOver?: boolean;
+ isDragging?: boolean;
+ draggingIndex?: number;
+ index?: number;
+}
+
+export type SortableControlProps = ControlFrameProps & {
+ dragInfo: DragInfo;
+ width: ControlWidth;
+};
+
+/**
+ * A sortable wrapper around the generic control frame.
+ */
+export const SortableControl = (frameProps: SortableControlProps) => {
+ const { embeddableId } = frameProps;
+ const { over, listeners, isSorting, transform, transition, attributes, isDragging, setNodeRef } =
+ useSortable({
+ id: embeddableId,
+ animateLayoutChanges: () => true,
+ });
+
+ frameProps.dragInfo = { ...frameProps.dragInfo, isOver: over?.id === embeddableId, isDragging };
+
+ return (
+
+ );
+};
+
+const SortableControlInner = forwardRef<
+ HTMLButtonElement,
+ SortableControlProps & { style: HTMLAttributes['style'] }
+>(
+ (
+ {
+ embeddableId,
+ controlStyle,
+ container,
+ dragInfo,
+ onRemove,
+ onEdit,
+ style,
+ width,
+ ...dragHandleProps
+ },
+ dragHandleRef
+ ) => {
+ const { isOver, isDragging, draggingIndex, index } = dragInfo;
+
+ const dragHandle = (
+
+ );
+
+ return (
+ (draggingIndex ?? -1),
+ })}
+ style={style}
+ >
+
+
+ );
+ }
+);
+
+/**
+ * A simplified clone version of the control which is dragged. This version only shows
+ * the title, because individual controls can be any size, and dragging a wide item
+ * can be quite cumbersome.
+ */
+export const ControlClone = ({
+ embeddableId,
+ container,
+ width,
+}: {
+ embeddableId: string;
+ container: ControlGroupContainer;
+ width: ControlWidth;
+}) => {
+ const embeddable = useChildEmbeddable({ embeddableId, container });
+ const layout = container.getInput().controlStyle;
+ return (
+
+ {layout === 'twoLine' ? (
+ {embeddable?.getInput().title}
+ ) : undefined}
+
+
+
+
+ {container.getInput().controlStyle === 'oneLine' ? (
+ {embeddable?.getInput().title}
+ ) : undefined}
+
+
+ );
+};
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group.scss b/src/plugins/presentation_util/public/components/controls/control_group/control_group.scss
new file mode 100644
index 0000000000000..f49efa7aab043
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group.scss
@@ -0,0 +1,184 @@
+$smallControl: $euiSize * 14;
+$mediumControl: $euiSize * 25;
+$largeControl: $euiSize * 50;
+$controlMinWidth: $euiSize * 14;
+
+.controlGroup {
+ margin-left: $euiSizeXS;
+ overflow-x: clip; // sometimes when using auto width, removing a control can cause a horizontal scrollbar to appear.
+ min-height: $euiSize * 4;
+ padding: $euiSize 0;
+}
+
+.controlFrame--cloneWrapper {
+ width: max-content;
+
+ .euiFormLabel {
+ padding-bottom: $euiSizeXS;
+ }
+
+ &-small {
+ width: $smallControl;
+ }
+
+ &-medium {
+ width: $mediumControl;
+ }
+
+ &-large {
+ width: $largeControl;
+ }
+
+ &-twoLine {
+ margin-top: -$euiSize * 1.25;
+ }
+
+ .euiFormLabel, div {
+ cursor: grabbing !important; // prevents cursor flickering while dragging the clone
+ }
+
+ .controlFrame--draggable {
+ cursor: grabbing;
+ height: $euiButtonHeight;
+ align-items: center;
+ border-radius: $euiBorderRadius;
+ @include euiFontSizeS;
+ font-weight: $euiFontWeightSemiBold;
+ @include euiFormControlDefaultShadow;
+ background-color: $euiFormInputGroupLabelBackground;
+ min-width: $controlMinWidth;
+ }
+
+ .controlFrame--formControlLayout, .controlFrame--draggable {
+ &-clone {
+ box-shadow: 0 0 0 1px $euiShadowColor,
+ 0 1px 6px 0 $euiShadowColor;
+ cursor: grabbing !important;
+ }
+
+ .controlFrame--dragHandle {
+ cursor: grabbing;
+ }
+ }
+}
+
+.controlFrame--wrapper {
+ flex-basis: auto;
+ position: relative;
+ display: block;
+
+ .controlFrame--formControlLayout {
+ width: 100%;
+ min-width: $controlMinWidth;
+ transition:background-color .1s, color .1s;
+
+ &__label {
+ @include euiTextTruncate;
+ max-width: 50%;
+ }
+
+ &:not(.controlFrame--formControlLayout-clone) {
+ .controlFrame--dragHandle {
+ cursor: grab;
+ }
+ }
+
+ .controlFrame--control {
+ height: 100%;
+ transition: opacity .1s;
+
+ &.controlFrame--twoLine {
+ width: 100%;
+ }
+ }
+ }
+
+ &-small {
+ width: $smallControl;
+ }
+
+ &-medium {
+ width: $mediumControl;
+ }
+
+ &-large {
+ width: $largeControl;
+ }
+
+ &-insertBefore,
+ &-insertAfter {
+ .controlFrame--formControlLayout:after {
+ content: '';
+ position: absolute;
+ background-color: transparentize($euiColorPrimary, .5);
+ border-radius: $euiBorderRadius;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ }
+ }
+
+ &-insertBefore {
+ .controlFrame--formControlLayout:after {
+ left: -$euiSizeS;
+ }
+ }
+
+ &-insertAfter {
+ .controlFrame--formControlLayout:after {
+ right: -$euiSizeS;
+ }
+ }
+
+ .controlFrame--floatingActions {
+ visibility: hidden;
+ opacity: 0;
+
+ // slower transition on hover leave in case the user accidentally stops hover
+ transition: visibility .3s, opacity .3s;
+
+ z-index: 1;
+ position: absolute;
+
+ &-oneLine {
+ right:$euiSizeXS;
+ top: -$euiSizeL;
+ padding: $euiSizeXS;
+ border-radius: $euiBorderRadius;
+ background-color: $euiColorEmptyShade;
+ box-shadow: 0 0 0 1pt $euiColorLightShade;
+ }
+
+ &-twoLine {
+ right:$euiSizeXS;
+ top: -$euiSizeXS;
+ }
+ }
+
+ &:hover {
+ .controlFrame--floatingActions {
+ transition:visibility .1s, opacity .1s;
+ visibility: visible;
+ opacity: 1;
+ }
+ }
+
+ &-isDragging {
+ .euiFormRow__labelWrapper {
+ opacity: 0;
+ }
+ .controlFrame--formControlLayout {
+ background-color: $euiColorEmptyShade !important;
+ color: transparent !important;
+ box-shadow: none;
+
+ .euiFormLabel {
+ opacity: 0;
+ }
+
+ .controlFrame--control {
+ opacity: 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_constants.ts b/src/plugins/presentation_util/public/components/controls/control_group/control_group_constants.ts
new file mode 100644
index 0000000000000..3c22b1ffbcd23
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_constants.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { ControlWidth } from '../types';
+import { ControlGroupStrings } from './control_group_strings';
+
+export const CONTROL_GROUP_TYPE = 'control_group';
+
+export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
+
+export const CONTROL_WIDTH_OPTIONS = [
+ {
+ id: `auto`,
+ label: ControlGroupStrings.management.controlWidth.getAutoWidthTitle(),
+ },
+ {
+ id: `small`,
+ label: ControlGroupStrings.management.controlWidth.getSmallWidthTitle(),
+ },
+ {
+ id: `medium`,
+ label: ControlGroupStrings.management.controlWidth.getMediumWidthTitle(),
+ },
+ {
+ id: `large`,
+ label: ControlGroupStrings.management.controlWidth.getLargeWidthTitle(),
+ },
+];
+
+export const CONTROL_LAYOUT_OPTIONS = [
+ {
+ id: `oneLine`,
+ label: ControlGroupStrings.management.controlStyle.getSingleLineTitle(),
+ },
+ {
+ id: `twoLine`,
+ label: ControlGroupStrings.management.controlStyle.getTwoLineTitle(),
+ },
+];
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_container.tsx b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container.tsx
new file mode 100644
index 0000000000000..03249889dfdea
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container.tsx
@@ -0,0 +1,224 @@
+/*
+ * 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 ReactDOM from 'react-dom';
+import { cloneDeep } from 'lodash';
+
+import {
+ Container,
+ EmbeddableFactory,
+ EmbeddableFactoryNotFoundError,
+} from '../../../../../embeddable/public';
+import {
+ InputControlEmbeddable,
+ InputControlInput,
+ InputControlOutput,
+ IEditableControlFactory,
+ ControlWidth,
+} from '../types';
+import { ControlsService } from '../controls_service';
+import { ControlGroupInput, ControlPanelState } from './types';
+import { ManageControlComponent } from './editor/manage_control';
+import { toMountPoint } from '../../../../../kibana_react/public';
+import { ControlGroup } from './component/control_group_component';
+import { PresentationOverlaysService } from '../../../services/overlays';
+import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_WIDTH } from './control_group_constants';
+import { ManageControlGroup } from './editor/manage_control_group_component';
+import { OverlayRef } from '../../../../../../core/public';
+import { ControlGroupStrings } from './control_group_strings';
+
+export class ControlGroupContainer extends Container {
+ public readonly type = CONTROL_GROUP_TYPE;
+
+ private nextControlWidth: ControlWidth = DEFAULT_CONTROL_WIDTH;
+
+ constructor(
+ initialInput: ControlGroupInput,
+ private readonly controlsService: ControlsService,
+ private readonly overlays: PresentationOverlaysService,
+ parent?: Container
+ ) {
+ super(initialInput, { embeddableLoaded: {} }, controlsService.getControlFactory, parent);
+ this.overlays = overlays;
+ this.controlsService = controlsService;
+ }
+
+ protected createNewPanelState(
+ factory: EmbeddableFactory,
+ partial: Partial = {}
+ ): ControlPanelState {
+ const panelState = super.createNewPanelState(factory, partial);
+ return {
+ order: 1,
+ width: this.nextControlWidth,
+ ...panelState,
+ } as ControlPanelState;
+ }
+
+ protected getInheritedInput(id: string): InputControlInput {
+ const { filters, query, timeRange, inheritParentState } = this.getInput();
+ return {
+ filters: inheritParentState.useFilters ? filters : undefined,
+ query: inheritParentState.useQuery ? query : undefined,
+ timeRange: inheritParentState.useTimerange ? timeRange : undefined,
+ id,
+ };
+ }
+
+ public createNewControl = async (type: string) => {
+ const factory = this.controlsService.getControlFactory(type);
+ if (!factory) throw new EmbeddableFactoryNotFoundError(type);
+
+ const initialInputPromise = new Promise>((resolve, reject) => {
+ let inputToReturn: Partial = {};
+
+ const onCancel = (ref: OverlayRef) => {
+ this.overlays
+ .openConfirm(ControlGroupStrings.management.discardNewControl.getSubtitle(), {
+ confirmButtonText: ControlGroupStrings.management.discardNewControl.getConfirm(),
+ cancelButtonText: ControlGroupStrings.management.discardNewControl.getCancel(),
+ title: ControlGroupStrings.management.discardNewControl.getTitle(),
+ buttonColor: 'danger',
+ })
+ .then((confirmed) => {
+ if (confirmed) {
+ reject();
+ ref.close();
+ }
+ });
+ };
+
+ const flyoutInstance = this.overlays.openFlyout(
+ toMountPoint(
+ (inputToReturn.title = newTitle)}
+ updateWidth={(newWidth) => (this.nextControlWidth = newWidth)}
+ controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
+ onChange: (partialInput) => {
+ inputToReturn = { ...inputToReturn, ...partialInput };
+ },
+ })}
+ onSave={() => {
+ resolve(inputToReturn);
+ flyoutInstance.close();
+ }}
+ onCancel={() => onCancel(flyoutInstance)}
+ />
+ ),
+ {
+ onClose: (flyout) => onCancel(flyout),
+ }
+ );
+ });
+ initialInputPromise.then(
+ async (explicitInput) => {
+ await this.addNewEmbeddable(type, explicitInput);
+ },
+ () => {} // swallow promise rejection because it can be part of normal flow
+ );
+ };
+
+ public editControl = async (embeddableId: string) => {
+ const panel = this.getInput().panels[embeddableId];
+ const factory = this.getFactory(panel.type);
+ const embeddable = await this.untilEmbeddableLoaded(embeddableId);
+
+ if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
+
+ const initialExplicitInput = cloneDeep(panel.explicitInput);
+ const initialWidth = panel.width;
+
+ const onCancel = (ref: OverlayRef) => {
+ this.overlays
+ .openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), {
+ confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(),
+ cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(),
+ title: ControlGroupStrings.management.discardChanges.getTitle(),
+ buttonColor: 'danger',
+ })
+ .then((confirmed) => {
+ if (confirmed) {
+ embeddable.updateInput(initialExplicitInput);
+ this.updateInput({
+ panels: {
+ ...this.getInput().panels,
+ [embeddableId]: { ...this.getInput().panels[embeddableId], width: initialWidth },
+ },
+ });
+ ref.close();
+ }
+ });
+ };
+
+ const flyoutInstance = this.overlays.openFlyout(
+ toMountPoint(
+ this.removeEmbeddable(embeddableId)}
+ updateTitle={(newTitle) => embeddable.updateInput({ title: newTitle })}
+ controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({
+ onChange: (partialInput) => embeddable.updateInput(partialInput),
+ initialInput: embeddable.getInput(),
+ })}
+ onCancel={() => onCancel(flyoutInstance)}
+ onSave={() => flyoutInstance.close()}
+ updateWidth={(newWidth) =>
+ this.updateInput({
+ panels: {
+ ...this.getInput().panels,
+ [embeddableId]: { ...this.getInput().panels[embeddableId], width: newWidth },
+ },
+ })
+ }
+ />
+ ),
+ {
+ onClose: (flyout) => onCancel(flyout),
+ }
+ );
+ };
+
+ public editControlGroup = () => {
+ const flyoutInstance = this.overlays.openFlyout(
+ toMountPoint(
+ this.updateInput({ controlStyle: newStyle })}
+ deleteAllEmbeddables={() => {
+ this.overlays
+ .openConfirm(ControlGroupStrings.management.deleteAllControls.getSubtitle(), {
+ confirmButtonText: ControlGroupStrings.management.deleteAllControls.getConfirm(),
+ cancelButtonText: ControlGroupStrings.management.deleteAllControls.getCancel(),
+ title: ControlGroupStrings.management.deleteAllControls.getTitle(),
+ buttonColor: 'danger',
+ })
+ .then((confirmed) => {
+ if (confirmed) {
+ Object.keys(this.getInput().panels).forEach((id) => this.removeEmbeddable(id));
+ flyoutInstance.close();
+ }
+ });
+ }}
+ setAllPanelWidths={(newWidth) => {
+ const newPanels = cloneDeep(this.getInput().panels);
+ Object.values(newPanels).forEach((panel) => (panel.width = newWidth));
+ this.updateInput({ panels: { ...newPanels, ...newPanels } });
+ }}
+ panels={this.getInput().panels}
+ />
+ )
+ );
+ };
+
+ public render(dom: HTMLElement) {
+ ReactDOM.render(, dom);
+ }
+}
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_container_factory.ts b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container_factory.ts
new file mode 100644
index 0000000000000..97ef48e6b240c
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container_factory.ts
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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 {
+ Container,
+ ContainerOutput,
+ EmbeddableFactory,
+ EmbeddableFactoryDefinition,
+ ErrorEmbeddable,
+} from '../../../../../embeddable/public';
+import { ControlGroupInput } from './types';
+import { ControlsService } from '../controls_service';
+import { ControlGroupStrings } from './control_group_strings';
+import { CONTROL_GROUP_TYPE } from './control_group_constants';
+import { ControlGroupContainer } from './control_group_container';
+import { PresentationOverlaysService } from '../../../services/overlays';
+
+export type DashboardContainerFactory = EmbeddableFactory<
+ ControlGroupInput,
+ ContainerOutput,
+ ControlGroupContainer
+>;
+export class ControlGroupContainerFactory
+ implements EmbeddableFactoryDefinition
+{
+ public readonly isContainerType = true;
+ public readonly type = CONTROL_GROUP_TYPE;
+ public readonly controlsService: ControlsService;
+ private readonly overlays: PresentationOverlaysService;
+
+ constructor(controlsService: ControlsService, overlays: PresentationOverlaysService) {
+ this.overlays = overlays;
+ this.controlsService = controlsService;
+ }
+
+ public isEditable = async () => false;
+
+ public readonly getDisplayName = () => {
+ return ControlGroupStrings.getEmbeddableTitle();
+ };
+
+ public getDefaultInput(): Partial {
+ return {
+ panels: {},
+ inheritParentState: {
+ useFilters: true,
+ useQuery: true,
+ useTimerange: true,
+ },
+ };
+ }
+
+ public create = async (
+ initialInput: ControlGroupInput,
+ parent?: Container
+ ): Promise => {
+ return new ControlGroupContainer(initialInput, this.controlsService, this.overlays, parent);
+ };
+}
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_strings.ts b/src/plugins/presentation_util/public/components/controls/control_group/control_group_strings.ts
new file mode 100644
index 0000000000000..78e50d8651931
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_strings.ts
@@ -0,0 +1,176 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export const ControlGroupStrings = {
+ getEmbeddableTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.title', {
+ defaultMessage: 'Control group',
+ }),
+ manageControl: {
+ getFlyoutTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.flyoutTitle', {
+ defaultMessage: 'Manage control',
+ }),
+ getTitleInputTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.titleInputTitle', {
+ defaultMessage: 'Title',
+ }),
+ getWidthInputTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.widthInputTitle', {
+ defaultMessage: 'Control width',
+ }),
+ getSaveChangesTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.saveChangesTitle', {
+ defaultMessage: 'Save and close',
+ }),
+ getCancelTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.cancelTitle', {
+ defaultMessage: 'Cancel',
+ }),
+ },
+ management: {
+ getAddControlTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.addControl', {
+ defaultMessage: 'Add control',
+ }),
+ getManageButtonTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.buttonTitle', {
+ defaultMessage: 'Manage controls',
+ }),
+ getFlyoutTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.flyoutTitle', {
+ defaultMessage: 'Manage controls',
+ }),
+ getDesignTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.designTitle', {
+ defaultMessage: 'Design',
+ }),
+ getWidthTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.widthTitle', {
+ defaultMessage: 'Width',
+ }),
+ getLayoutTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layoutTitle', {
+ defaultMessage: 'Layout',
+ }),
+ getDeleteButtonTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.delete', {
+ defaultMessage: 'Delete control',
+ }),
+ getDeleteAllButtonTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll', {
+ defaultMessage: 'Delete all',
+ }),
+ controlWidth: {
+ getChangeAllControlWidthsTitle: () =>
+ i18n.translate(
+ 'presentationUtil.inputControls.controlGroup.management.layout.changeAllControlWidths',
+ {
+ defaultMessage: 'Set width for all controls',
+ }
+ ),
+ getWidthSwitchLegend: () =>
+ i18n.translate(
+ 'presentationUtil.inputControls.controlGroup.management.layout.controlWidthLegend',
+ {
+ defaultMessage: 'Change individual control width',
+ }
+ ),
+ getAutoWidthTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.auto', {
+ defaultMessage: 'Auto',
+ }),
+ getSmallWidthTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.small', {
+ defaultMessage: 'Small',
+ }),
+ getMediumWidthTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.medium', {
+ defaultMessage: 'Medium',
+ }),
+ getLargeWidthTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.large', {
+ defaultMessage: 'Large',
+ }),
+ },
+ controlStyle: {
+ getDesignSwitchLegend: () =>
+ i18n.translate(
+ 'presentationUtil.inputControls.controlGroup.management.layout.designSwitchLegend',
+ {
+ defaultMessage: 'Switch control designs',
+ }
+ ),
+ getSingleLineTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.singleLine', {
+ defaultMessage: 'Single line layout',
+ }),
+ getTwoLineTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.twoLine', {
+ defaultMessage: 'Two line layout',
+ }),
+ },
+ deleteAllControls: {
+ getTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.title', {
+ defaultMessage: 'Delete all?',
+ }),
+ getSubtitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.sub', {
+ defaultMessage: 'Controls are not recoverable once removed.',
+ }),
+ getConfirm: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.confirm', {
+ defaultMessage: 'Delete',
+ }),
+ getCancel: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.cancel', {
+ defaultMessage: 'Cancel',
+ }),
+ },
+ discardChanges: {
+ getTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.title', {
+ defaultMessage: 'Discard?',
+ }),
+ getSubtitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.sub', {
+ defaultMessage:
+ 'Discard changes to this control? Controls are not recoverable once removed.',
+ }),
+ getConfirm: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.confirm', {
+ defaultMessage: 'Discard',
+ }),
+ getCancel: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.cancel', {
+ defaultMessage: 'Cancel',
+ }),
+ },
+ discardNewControl: {
+ getTitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.title', {
+ defaultMessage: 'Discard?',
+ }),
+ getSubtitle: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.sub', {
+ defaultMessage: 'Discard new control? Controls are not recoverable once removed.',
+ }),
+ getConfirm: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.confirm', {
+ defaultMessage: 'Discard',
+ }),
+ getCancel: () =>
+ i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.cancel', {
+ defaultMessage: 'Cancel',
+ }),
+ },
+ },
+};
diff --git a/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control.tsx b/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control.tsx
new file mode 100644
index 0000000000000..6d80a6e0b31f6
--- /dev/null
+++ b/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control.tsx
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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, { useEffect, useState } from 'react';
+import {
+ EuiFlyoutHeader,
+ EuiButtonGroup,
+ EuiFlyoutBody,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiFieldText,
+ EuiFlyoutFooter,
+ EuiButton,
+ EuiFormRow,
+ EuiForm,
+ EuiButtonEmpty,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { ControlGroupStrings } from '../control_group_strings';
+import { ControlEditorComponent, ControlWidth } from '../../types';
+import { CONTROL_WIDTH_OPTIONS } from '../control_group_constants';
+
+interface ManageControlProps {
+ title?: string;
+ onSave: () => void;
+ width: ControlWidth;
+ onCancel: () => void;
+ removeControl?: () => void;
+ controlEditorComponent?: ControlEditorComponent;
+ updateTitle: (title: string) => void;
+ updateWidth: (newWidth: ControlWidth) => void;
+}
+
+export const ManageControlComponent = ({
+ controlEditorComponent,
+ removeControl,
+ updateTitle,
+ updateWidth,
+ onCancel,
+ onSave,
+ title,
+ width,
+}: ManageControlProps) => {
+ const [currentTitle, setCurrentTitle] = useState(title);
+ const [currentWidth, setCurrentWidth] = useState(width);
+
+ const [controlEditorValid, setControlEditorValid] = useState(false);
+ const [editorValid, setEditorValid] = useState(false);
+
+ useEffect(() => setEditorValid(Boolean(currentTitle)), [currentTitle]);
+
+ return (
+ <>
+
+
+