) {
return Object.entries(apmIndices)
.map(([key, value]) => [key, value?.trim()])
- .filter(([key, value]) => !!value)
+ .filter(([_, value]) => !!value)
.reduce((obj, [key, value]) => ({ ...obj, [key as string]: value }), {});
}
diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts
index 7e45f412d4bdb7..1615550027d3cd 100644
--- a/x-pack/plugins/apm/server/routes/errors.ts
+++ b/x-pack/plugins/apm/server/routes/errors.ts
@@ -12,7 +12,7 @@ import { getErrorGroups } from '../lib/errors/get_error_groups';
import { setupRequest } from '../lib/helpers/setup_request';
import { uiFiltersRt, rangeRt } from './default_api_types';
-export const errorsRoute = createRoute((core) => ({
+export const errorsRoute = createRoute(() => ({
path: '/api/apm/services/{serviceName}/errors',
params: {
path: t.type({
diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts
index a6e9175fcb651a..87214076718256 100644
--- a/x-pack/plugins/apm/server/routes/service_nodes.ts
+++ b/x-pack/plugins/apm/server/routes/service_nodes.ts
@@ -9,7 +9,7 @@ import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceNodes } from '../lib/service_nodes';
import { rangeRt, uiFiltersRt } from './default_api_types';
-export const serviceNodesRoute = createRoute((core) => ({
+export const serviceNodesRoute = createRoute(() => ({
path: '/api/apm/services/{serviceName}/serviceNodes',
params: {
path: t.type({
diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts
index 996bfbd9184d1c..8672c6c108c4cf 100644
--- a/x-pack/plugins/apm/server/routes/services.ts
+++ b/x-pack/plugins/apm/server/routes/services.ts
@@ -17,7 +17,7 @@ import { uiFiltersRt, rangeRt } from './default_api_types';
import { getServiceAnnotations } from '../lib/services/annotations';
import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt';
-export const servicesRoute = createRoute((core) => ({
+export const servicesRoute = createRoute(() => ({
path: '/api/apm/services',
params: {
query: t.intersection([uiFiltersRt, rangeRt]),
diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
index 6fd864a3371653..f5c9cc2adf2388 100644
--- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
+++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
@@ -24,7 +24,7 @@ import {
import { jsonRt } from '../../../common/runtime_types/json_rt';
// get list of configurations
-export const agentConfigurationRoute = createRoute((core) => ({
+export const agentConfigurationRoute = createRoute(() => ({
path: '/api/apm/settings/agent-configuration',
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
@@ -137,7 +137,7 @@ export const createOrUpdateAgentConfigurationRoute = createRoute(() => ({
}));
// Lookup single configuration (used by APM Server)
-export const agentConfigurationSearchRoute = createRoute((core) => ({
+export const agentConfigurationSearchRoute = createRoute(() => ({
method: 'POST',
path: '/api/apm/settings/agent-configuration/search',
params: {
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
index 2d5722744f93e7..e52ce760e026a5 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
@@ -34,7 +34,7 @@ export const apmIndicesRoute = createRoute(() => ({
}));
// save ui indices
-export const saveApmIndicesRoute = createRoute((core) => ({
+export const saveApmIndicesRoute = createRoute(() => ({
method: 'POST',
path: '/api/apm/settings/apm-indices/save',
options: {
@@ -50,7 +50,7 @@ export const saveApmIndicesRoute = createRoute((core) => ({
'apm_oss.metricsIndices': t.string,
}),
},
- handler: async ({ context, request }) => {
+ handler: async ({ context }) => {
const { body } = context.params;
const savedObjectsClient = context.core.savedObjects.client;
return await saveApmIndices(savedObjectsClient, body);
diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts
index f32840fe08b9c6..83c23a75e999d5 100644
--- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts
+++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts
@@ -17,7 +17,7 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction';
import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links';
import { createRoute } from '../create_route';
-export const customLinkTransactionRoute = createRoute((core) => ({
+export const customLinkTransactionRoute = createRoute(() => ({
path: '/api/apm/settings/custom_links/transaction',
params: {
query: filterOptionsRt,
@@ -31,7 +31,7 @@ export const customLinkTransactionRoute = createRoute((core) => ({
},
}));
-export const listCustomLinksRoute = createRoute((core) => ({
+export const listCustomLinksRoute = createRoute(() => ({
path: '/api/apm/settings/custom_links',
params: {
query: filterOptionsRt,
diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx
index 8751d8102ad379..c799f36a283c15 100644
--- a/x-pack/plugins/canvas/public/application.tsx
+++ b/x-pack/plugins/canvas/public/application.tsx
@@ -33,7 +33,7 @@ import { CapabilitiesStrings } from '../i18n';
import { startServices, services } from './services';
// @ts-ignore Untyped local
-import { destroyHistory } from './lib/history_provider';
+import { createHistory, destroyHistory } from './lib/history_provider';
// @ts-ignore Untyped local
import { stopRouter } from './lib/router_provider';
import { initFunctions } from './functions';
@@ -97,6 +97,9 @@ export const initializeCanvas = async (
services.expressions.getService().registerFunction(fn);
}
+ // Re-initialize our history
+ createHistory();
+
// Create Store
const canvasStore = await createStore(coreSetup, setupPlugins);
diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
index 47b461f22ad651..3014369d948579 100644
--- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
+++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx
@@ -69,6 +69,7 @@ export const withUnconnectedElementsLoadedTelemetry = (
) =>
function ElementsLoadedTelemetry(props: ElementsLoadedTelemetryProps) {
const { telemetryElementCounts, workpad, telemetryResolvedArgs, ...other } = props;
+ const { error, pending } = telemetryElementCounts;
const [currentWorkpadId, setWorkpadId] = useState(undefined);
const [hasReported, setHasReported] = useState(false);
@@ -87,27 +88,20 @@ export const withUnconnectedElementsLoadedTelemetry = (
0
);
- if (
- workpadElementCount === 0 ||
- (resolvedArgsAreForWorkpad && telemetryElementCounts.pending === 0)
- ) {
+ if (workpadElementCount === 0 || (resolvedArgsAreForWorkpad && pending === 0)) {
setHasReported(true);
} else {
setHasReported(false);
}
- } else if (
- !hasReported &&
- telemetryElementCounts.pending === 0 &&
- resolvedArgsAreForWorkpad
- ) {
- if (telemetryElementCounts.error > 0) {
+ } else if (!hasReported && pending === 0 && resolvedArgsAreForWorkpad) {
+ if (error > 0) {
trackMetric(METRIC_TYPE.LOADED, [WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]);
} else {
trackMetric(METRIC_TYPE.LOADED, WorkpadLoadedMetric);
}
setHasReported(true);
}
- });
+ }, [currentWorkpadId, hasReported, error, pending, telemetryResolvedArgs, workpad]);
return ;
};
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot
index 6601f570209e97..14791cd3d8b250 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot
+++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot
@@ -63,6 +63,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = `
>
@@ -88,6 +89,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = `
>
@@ -118,6 +120,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = `
>
@@ -148,6 +151,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = `
>
@@ -237,6 +241,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
>
@@ -262,6 +267,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
>
@@ -292,6 +298,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
>
@@ -322,6 +329,7 @@ exports[`Storyshots components/Assets/Asset marker 1`] = `
>
diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot
index aff630b21c7705..1b8f1480759f65 100644
--- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot
@@ -422,6 +422,7 @@ Array [
>
@@ -447,6 +448,7 @@ Array [
>
@@ -477,6 +479,7 @@ Array [
>
@@ -507,6 +510,7 @@ Array [
>
@@ -585,6 +589,7 @@ Array [
>
@@ -610,6 +615,7 @@ Array [
>
@@ -640,6 +646,7 @@ Array [
>
@@ -670,6 +677,7 @@ Array [
>
diff --git a/x-pack/plugins/canvas/public/components/router/index.ts b/x-pack/plugins/canvas/public/components/router/index.ts
index 5e014870f5158a..fa857c6f0cd3c6 100644
--- a/x-pack/plugins/canvas/public/components/router/index.ts
+++ b/x-pack/plugins/canvas/public/components/router/index.ts
@@ -11,7 +11,6 @@ import {
enableAutoplay,
setRefreshInterval,
setAutoplayInterval,
- // @ts-ignore untyped local
} from '../../state/actions/workpad';
// @ts-ignore untyped local
import { Router as Component } from './router';
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot
index 6f12f683564672..408b0679c415fb 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot
@@ -16,6 +16,7 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button
>
@@ -42,6 +43,7 @@ exports[`Storyshots components/SavedElementsModal/ElementControls has two button
>
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot
index be0fb0573c394c..1c506819df1fbe 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot
@@ -66,6 +66,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = `
>
@@ -92,6 +93,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = `
>
@@ -170,6 +172,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = `
>
@@ -196,6 +199,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = `
>
@@ -274,6 +278,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = `
>
@@ -300,6 +305,7 @@ exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = `
>
diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot
index 03093b41300b88..04b2184f274620 100644
--- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot
@@ -362,6 +362,7 @@ Array [
>
@@ -388,6 +389,7 @@ Array [
>
@@ -466,6 +468,7 @@ Array [
>
@@ -492,6 +495,7 @@ Array [
>
@@ -570,6 +574,7 @@ Array [
>
@@ -596,6 +601,7 @@ Array [
>
@@ -851,6 +857,7 @@ Array [
>
@@ -877,6 +884,7 @@ Array [
>
diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot
index 4d5b9570ee20f3..16263aa7ea3847 100644
--- a/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot
@@ -55,6 +55,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = `
>
@@ -80,6 +81,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = `
>
@@ -105,6 +107,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = `
>
@@ -130,6 +133,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = `
>
diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts
index c6dddab3b5dd16..abd40731078ec0 100644
--- a/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/index.ts
@@ -5,7 +5,6 @@
*/
import { connect } from 'react-redux';
-// @ts-ignore
import { addColor, removeColor } from '../../state/actions/workpad';
import { getWorkpadColors } from '../../state/selectors/workpad';
diff --git a/x-pack/plugins/canvas/public/components/workpad_config/index.js b/x-pack/plugins/canvas/public/components/workpad_config/index.ts
similarity index 63%
rename from x-pack/plugins/canvas/public/components/workpad_config/index.js
rename to x-pack/plugins/canvas/public/components/workpad_config/index.ts
index 913cf7093e726b..e417821fd4f67d 100644
--- a/x-pack/plugins/canvas/public/components/workpad_config/index.js
+++ b/x-pack/plugins/canvas/public/components/workpad_config/index.ts
@@ -7,28 +7,29 @@
import { connect } from 'react-redux';
import { get } from 'lodash';
-import { sizeWorkpad, setName, setWorkpadCSS } from '../../state/actions/workpad';
+import { sizeWorkpad as setSize, setName, setWorkpadCSS } from '../../state/actions/workpad';
import { getWorkpad } from '../../state/selectors/workpad';
import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants';
import { WorkpadConfig as Component } from './workpad_config';
+import { State } from '../../../types';
-const mapStateToProps = (state) => {
+const mapStateToProps = (state: State) => {
const workpad = getWorkpad(state);
return {
- name: get(workpad, 'name'),
+ name: get(workpad, 'name'),
size: {
- width: get(workpad, 'width'),
- height: get(workpad, 'height'),
+ width: get(workpad, 'width'),
+ height: get(workpad, 'height'),
},
- css: get(workpad, 'css', DEFAULT_WORKPAD_CSS),
+ css: get(workpad, 'css', DEFAULT_WORKPAD_CSS),
};
};
const mapDispatchToProps = {
- setSize: (size) => sizeWorkpad(size),
- setName: (name) => setName(name),
- setWorkpadCSS: (css) => setWorkpadCSS(css),
+ setSize,
+ setName,
+ setWorkpadCSS,
};
export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component);
diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.js b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.js
deleted file mode 100644
index 45758c9965653c..00000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiFieldText,
- EuiFieldNumber,
- EuiBadge,
- EuiButtonIcon,
- EuiFormRow,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiTitle,
- EuiToolTip,
- EuiTextArea,
- EuiAccordion,
- EuiText,
- EuiButton,
-} from '@elastic/eui';
-import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadConfig: strings } = ComponentStrings;
-
-export class WorkpadConfig extends PureComponent {
- static propTypes = {
- size: PropTypes.object.isRequired,
- name: PropTypes.string.isRequired,
- css: PropTypes.string,
- setSize: PropTypes.func.isRequired,
- setName: PropTypes.func.isRequired,
- setWorkpadCSS: PropTypes.func.isRequired,
- };
-
- state = {
- css: this.props.css,
- };
-
- render() {
- const { size, name, setSize, setName, setWorkpadCSS } = this.props;
- const { css } = this.state;
- const rotate = () => setSize({ width: size.height, height: size.width });
-
- const badges = [
- {
- name: '1080p',
- size: { height: 1080, width: 1920 },
- },
- {
- name: '720p',
- size: { height: 720, width: 1280 },
- },
- {
- name: 'A4',
- size: { height: 842, width: 590 },
- },
- {
- name: strings.getUSLetterButtonLabel(),
- size: { height: 792, width: 612 },
- },
- ];
-
- return (
-
-
-
- {strings.getTitle()}
-
-
-
-
-
-
- setName(e.target.value)} />
-
-
-
-
-
-
-
- setSize({ width: Number(e.target.value), height: size.height })}
- value={size.width}
- />
-
-
-
-
-
-
-
-
-
-
-
- setSize({ height: Number(e.target.value), width: size.width })}
- value={size.height}
- />
-
-
-
-
-
-
-
- {badges.map((badge, i) => (
- setSize(badge.size)}
- aria-label={strings.getPageSizeBadgeAriaLabel(badge.name)}
- onClickAriaLabel={strings.getPageSizeBadgeOnClickAriaLabel(badge.name)}
- >
- {badge.name}
-
- ))}
-
-
-
-
-
-
- {strings.getGlobalCSSLabel()}
-
-
- }
- >
-
- this.setState({ css: e.target.value })}
- rows={10}
- />
-
- setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}>
- {strings.getApplyStylesheetButtonLabel()}
-
-
-
-
-
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx
new file mode 100644
index 00000000000000..7b7a1e08b2c5da
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.tsx
@@ -0,0 +1,175 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent, useState } from 'react';
+import PropTypes from 'prop-types';
+import {
+ EuiFieldText,
+ EuiFieldNumber,
+ EuiBadge,
+ EuiButtonIcon,
+ EuiFormRow,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiTitle,
+ EuiToolTip,
+ EuiTextArea,
+ EuiAccordion,
+ EuiText,
+ EuiButton,
+} from '@elastic/eui';
+import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants';
+import { ComponentStrings } from '../../../i18n';
+
+const { WorkpadConfig: strings } = ComponentStrings;
+
+interface Props {
+ size: {
+ height: number;
+ width: number;
+ };
+ name: string;
+ css?: string;
+ setSize: ({ height, width }: { height: number; width: number }) => void;
+ setName: (name: string) => void;
+ setWorkpadCSS: (css: string) => void;
+}
+
+export const WorkpadConfig: FunctionComponent = (props) => {
+ const [css, setCSS] = useState(props.css);
+ const { size, name, setSize, setName, setWorkpadCSS } = props;
+ const rotate = () => setSize({ width: size.height, height: size.width });
+
+ const badges = [
+ {
+ name: '1080p',
+ size: { height: 1080, width: 1920 },
+ },
+ {
+ name: '720p',
+ size: { height: 720, width: 1280 },
+ },
+ {
+ name: 'A4',
+ size: { height: 842, width: 590 },
+ },
+ {
+ name: strings.getUSLetterButtonLabel(),
+ size: { height: 792, width: 612 },
+ },
+ ];
+
+ return (
+
+
+
+ {strings.getTitle()}
+
+
+
+
+
+
+ setName(e.target.value)} />
+
+
+
+
+
+
+
+ setSize({ width: Number(e.target.value), height: size.height })}
+ value={size.width}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ setSize({ height: Number(e.target.value), width: size.width })}
+ value={size.height}
+ />
+
+
+
+
+
+
+
+ {badges.map((badge, i) => (
+ setSize(badge.size)}
+ aria-label={strings.getPageSizeBadgeAriaLabel(badge.name)}
+ onClickAriaLabel={strings.getPageSizeBadgeOnClickAriaLabel(badge.name)}
+ >
+ {badge.name}
+
+ ))}
+
+
+
+
+
+
+ {strings.getGlobalCSSLabel()}
+
+
+ }
+ >
+
+ setCSS(e.target.value)}
+ rows={10}
+ />
+
+ setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}>
+ {strings.getApplyStylesheetButtonLabel()}
+
+
+
+
+
+
+ );
+};
+
+WorkpadConfig.propTypes = {
+ size: PropTypes.object.isRequired,
+ name: PropTypes.string.isRequired,
+ css: PropTypes.string,
+ setSize: PropTypes.func.isRequired,
+ setName: PropTypes.func.isRequired,
+ setWorkpadCSS: PropTypes.func.isRequired,
+};
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts
index e561607cb101e1..0765973915f776 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts
@@ -14,13 +14,11 @@ import { State, CanvasWorkpadBoundingBox } from '../../../../types';
import { fetchAllRenderables } from '../../../state/actions/elements';
// @ts-ignore Untyped local
import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient';
-// @ts-ignore Untyped local
import {
setWriteable,
setRefreshInterval,
enableAutoplay,
setAutoplayInterval,
- // @ts-ignore Untyped local
} from '../../../state/actions/workpad';
import { getZoomScale, canUserWrite } from '../../../state/selectors/app';
import {
@@ -75,7 +73,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
},
doRefresh: () => dispatch(fetchAllRenderables()),
setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)),
- enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(autoplay)),
+ enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)),
setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)),
});
diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot
index 14466cab1a698a..f8583d7cd0dc03 100644
--- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot
+++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot
@@ -169,6 +169,7 @@ exports[`Storyshots arguments/SeriesStyle/components simple: no series 1`] = `
>
diff --git a/x-pack/plugins/canvas/public/lib/create_thunk.ts b/x-pack/plugins/canvas/public/lib/create_thunk.ts
new file mode 100644
index 00000000000000..cbcaeeccc8b931
--- /dev/null
+++ b/x-pack/plugins/canvas/public/lib/create_thunk.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Dispatch, Action } from 'redux';
+// @ts-ignore untyped dependency
+import { createThunk as createThunkFn } from 'redux-thunks/cjs';
+import { State } from '../../types';
+
+type CreateThunk = (
+ type: string,
+ fn: (
+ params: { type: string; dispatch: Dispatch; getState: () => State },
+ ...args: Arguments
+ ) => void
+) => (...args: Arguments) => Action;
+
+// This declaration exists because redux-thunks is not typed, and has a dependency on
+// Canvas State. Therefore, creating a wrapper that strongly-types the function-- and creates
+// a single point of replacement, should the need arise-- is a nice workaround.
+export const createThunk = createThunkFn as CreateThunk;
diff --git a/x-pack/plugins/canvas/public/lib/history_provider.js b/x-pack/plugins/canvas/public/lib/history_provider.js
index 649f101126012e..396372eeb85c11 100644
--- a/x-pack/plugins/canvas/public/lib/history_provider.js
+++ b/x-pack/plugins/canvas/public/lib/history_provider.js
@@ -134,7 +134,7 @@ function wrapHistoryInstance(history) {
return wrappedHistory;
}
-let instances = new WeakMap();
+const instances = new WeakMap();
const getHistoryInstance = (win) => {
// if no window object, use memory module
@@ -144,6 +144,15 @@ const getHistoryInstance = (win) => {
return createHashStateHistory();
};
+export const createHistory = (win = getWindow()) => {
+ // create and cache wrapped history instance
+ const historyInstance = getHistoryInstance(win);
+ const wrappedInstance = wrapHistoryInstance(historyInstance);
+ instances.set(win, wrappedInstance);
+
+ return wrappedInstance;
+};
+
export const historyProvider = (win = getWindow()) => {
// return cached instance if one exists
const instance = instances.get(win);
@@ -151,14 +160,13 @@ export const historyProvider = (win = getWindow()) => {
return instance;
}
- // create and cache wrapped history instance
- const historyInstance = getHistoryInstance(win);
- const wrappedInstance = wrapHistoryInstance(historyInstance);
- instances.set(win, wrappedInstance);
-
- return wrappedInstance;
+ return createHistory(win);
};
-export const destroyHistory = () => {
- instances = new WeakMap();
+export const destroyHistory = (win = getWindow()) => {
+ const instance = instances.get(win);
+
+ if (instance) {
+ instance.resetOnChange();
+ }
};
diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js
index 47fbc782f90d36..e89e62917da390 100644
--- a/x-pack/plugins/canvas/public/state/actions/elements.js
+++ b/x-pack/plugins/canvas/public/state/actions/elements.js
@@ -5,10 +5,10 @@
*/
import { createAction } from 'redux-actions';
-import { createThunk } from 'redux-thunks/cjs';
import immutable from 'object-path-immutable';
import { get, pick, cloneDeep, without } from 'lodash';
import { toExpression, safeElementFromExpression } from '@kbn/interpreter/common';
+import { createThunk } from '../../lib/create_thunk';
import { getPages, getNodeById, getNodes, getSelectedPageIndex } from '../selectors/workpad';
import { getValue as getResolvedArgsValue } from '../selectors/resolved_args';
import { getDefaultElement } from '../defaults';
diff --git a/x-pack/plugins/canvas/public/state/actions/embeddable.ts b/x-pack/plugins/canvas/public/state/actions/embeddable.ts
index e2cf588ec20a9c..a153cb7f4354de 100644
--- a/x-pack/plugins/canvas/public/state/actions/embeddable.ts
+++ b/x-pack/plugins/canvas/public/state/actions/embeddable.ts
@@ -6,8 +6,7 @@
import { Dispatch } from 'redux';
import { createAction } from 'redux-actions';
-// @ts-ignore Untyped
-import { createThunk } from 'redux-thunks';
+import { createThunk } from '../../lib/create_thunk';
// @ts-ignore Untyped Local
import { fetchRenderable } from './elements';
import { State } from '../../../types';
diff --git a/x-pack/plugins/canvas/public/state/actions/workpad.js b/x-pack/plugins/canvas/public/state/actions/workpad.ts
similarity index 54%
rename from x-pack/plugins/canvas/public/state/actions/workpad.js
rename to x-pack/plugins/canvas/public/state/actions/workpad.ts
index 167c156dce9985..47df38838f8907 100644
--- a/x-pack/plugins/canvas/public/state/actions/workpad.js
+++ b/x-pack/plugins/canvas/public/state/actions/workpad.ts
@@ -5,26 +5,28 @@
*/
import { createAction } from 'redux-actions';
-import { createThunk } from 'redux-thunks/cjs';
import { without, includes } from 'lodash';
+import { createThunk } from '../../lib/create_thunk';
import { getWorkpadColors } from '../selectors/workpad';
+// @ts-ignore
import { fetchAllRenderables } from './elements';
+import { CanvasWorkpad } from '../../../types';
-export const sizeWorkpad = createAction('sizeWorkpad');
-export const setName = createAction('setName');
-export const setWriteable = createAction('setWriteable');
-export const setColors = createAction('setColors');
-export const setRefreshInterval = createAction('setRefreshInterval');
-export const setWorkpadCSS = createAction('setWorkpadCSS');
-export const enableAutoplay = createAction('enableAutoplay');
-export const setAutoplayInterval = createAction('setAutoplayInterval');
-export const resetWorkpad = createAction('resetWorkpad');
+export const sizeWorkpad = createAction<{ height: number; width: number }>('sizeWorkpad');
+export const setName = createAction('setName');
+export const setWriteable = createAction('setWriteable');
+export const setColors = createAction('setColors');
+export const setRefreshInterval = createAction('setRefreshInterval');
+export const setWorkpadCSS = createAction('setWorkpadCSS');
+export const enableAutoplay = createAction('enableAutoplay');
+export const setAutoplayInterval = createAction('setAutoplayInterval');
+export const resetWorkpad = createAction('resetWorkpad');
export const initializeWorkpad = createThunk('initializeWorkpad', ({ dispatch }) => {
dispatch(fetchAllRenderables());
});
-export const addColor = createThunk('addColor', ({ dispatch, getState }, color) => {
+export const addColor = createThunk('addColor', ({ dispatch, getState }, color: string) => {
const colors = getWorkpadColors(getState()).slice(0);
if (!includes(colors, color)) {
colors.push(color);
@@ -32,16 +34,20 @@ export const addColor = createThunk('addColor', ({ dispatch, getState }, color)
dispatch(setColors(colors));
});
-export const removeColor = createThunk('removeColor', ({ dispatch, getState }, color) => {
+export const removeColor = createThunk('removeColor', ({ dispatch, getState }, color: string) => {
dispatch(setColors(without(getWorkpadColors(getState()), color)));
});
export const setWorkpad = createThunk(
'setWorkpad',
- ({ dispatch, type }, workpad, { loadPages = true } = {}) => {
+ (
+ { dispatch, type },
+ workpad: CanvasWorkpad,
+ { loadPages = true }: { loadPages?: boolean } = {}
+ ) => {
dispatch(createAction(type)(workpad)); // set the workpad object in state
if (loadPages) {
dispatch(initializeWorkpad());
- } // load all the elements on the workpad
+ }
}
);
diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts
index 72e0817eea8df2..879c73587ed965 100644
--- a/x-pack/plugins/data_enhanced/public/plugin.ts
+++ b/x-pack/plugins/data_enhanced/public/plugin.ts
@@ -29,19 +29,20 @@ export interface DataEnhancedStartDependencies {
export type DataEnhancedSetup = ReturnType;
export type DataEnhancedStart = ReturnType;
-export class DataEnhancedPlugin implements Plugin {
- constructor() {}
-
- public setup(core: CoreSetup, { data }: DataEnhancedSetupDependencies) {
+export class DataEnhancedPlugin
+ implements Plugin {
+ public setup(
+ core: CoreSetup,
+ { data }: DataEnhancedSetupDependencies
+ ) {
data.autocomplete.addQuerySuggestionProvider(
KUERY_LANGUAGE_NAME,
setupKqlQuerySuggestionProvider(core)
);
- data.search.registerSearchStrategyProvider(ASYNC_SEARCH_STRATEGY, asyncSearchStrategyProvider);
- data.search.registerSearchStrategyProvider(
- ES_SEARCH_STRATEGY,
- enhancedEsSearchStrategyProvider
- );
+ const asyncSearchStrategy = asyncSearchStrategyProvider(core);
+ const esSearchStrategy = enhancedEsSearchStrategyProvider(core, asyncSearchStrategy);
+ data.search.registerSearchStrategy(ASYNC_SEARCH_STRATEGY, asyncSearchStrategy);
+ data.search.registerSearchStrategy(ES_SEARCH_STRATEGY, esSearchStrategy);
}
public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {
diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
index 6c635cc5b4489a..3013f9966f068c 100644
--- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
@@ -6,35 +6,37 @@
import { of } from 'rxjs';
import { AbortController } from 'abort-controller';
+import { CoreSetup } from '../../../../../src/core/public';
import { coreMock } from '../../../../../src/core/public/mocks';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { asyncSearchStrategyProvider } from './async_search_strategy';
-import { IAsyncSearchOptions } from './types';
-import { CoreStart } from 'kibana/public';
+import { IAsyncSearchOptions } from '.';
+import { DataEnhancedStartDependencies } from '../plugin';
describe('Async search strategy', () => {
- let mockCoreStart: MockedKeys;
+ let mockCoreSetup: jest.Mocked>;
+ let mockDataStart: jest.Mocked;
const mockSearch = jest.fn();
const mockRequest = { params: {}, serverStrategy: 'foo' };
const mockOptions: IAsyncSearchOptions = { pollInterval: 0 };
beforeEach(() => {
- mockCoreStart = coreMock.createStart();
+ mockCoreSetup = coreMock.createSetup();
+ mockDataStart = dataPluginMock.createStartContract();
+ (mockDataStart.search.getSearchStrategy as jest.Mock).mockReturnValue({ search: mockSearch });
+ mockCoreSetup.getStartServices.mockResolvedValue([
+ undefined as any,
+ { data: mockDataStart },
+ undefined,
+ ]);
mockSearch.mockReset();
});
it('only sends one request if the first response is complete', async () => {
mockSearch.mockReturnValueOnce(of({ id: 1, total: 1, loaded: 1 }));
- const asyncSearch = asyncSearchStrategyProvider({
- core: mockCoreStart,
- getSearchStrategy: jest.fn().mockImplementation(() => {
- return () => {
- return {
- search: mockSearch,
- };
- };
- }),
- });
+ const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
@@ -51,17 +53,7 @@ describe('Async search strategy', () => {
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
- const asyncSearch = asyncSearchStrategyProvider({
- core: mockCoreStart,
- getSearchStrategy: jest.fn().mockImplementation(() => {
- return () => {
- return {
- search: mockSearch,
- };
- };
- }),
- });
-
+ const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch.search(mockRequest, mockOptions).toPromise();
@@ -75,17 +67,7 @@ describe('Async search strategy', () => {
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true }));
- const asyncSearch = asyncSearchStrategyProvider({
- core: mockCoreStart,
- getSearchStrategy: jest.fn().mockImplementation(() => {
- return () => {
- return {
- search: mockSearch,
- };
- };
- }),
- });
-
+ const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
await asyncSearch
@@ -104,16 +86,7 @@ describe('Async search strategy', () => {
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
- const asyncSearch = asyncSearchStrategyProvider({
- core: mockCoreStart,
- getSearchStrategy: jest.fn().mockImplementation(() => {
- return () => {
- return {
- search: mockSearch,
- };
- };
- }),
- });
+ const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
@@ -131,16 +104,7 @@ describe('Async search strategy', () => {
of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })
);
- const asyncSearch = asyncSearchStrategyProvider({
- core: mockCoreStart,
- getSearchStrategy: jest.fn().mockImplementation(() => {
- return () => {
- return {
- search: mockSearch,
- };
- };
- }),
- });
+ const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
expect(mockSearch).toBeCalledTimes(0);
@@ -157,16 +121,7 @@ describe('Async search strategy', () => {
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }))
.mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 }));
- const asyncSearch = asyncSearchStrategyProvider({
- core: mockCoreStart,
- getSearchStrategy: jest.fn().mockImplementation(() => {
- return () => {
- return {
- search: mockSearch,
- };
- };
- }),
- });
+ const asyncSearch = asyncSearchStrategyProvider(mockCoreSetup);
const abortController = new AbortController();
const options = { ...mockOptions, signal: abortController.signal };
@@ -178,7 +133,7 @@ describe('Async search strategy', () => {
} catch (e) {
expect(e.name).toBe('AbortError');
expect(mockSearch).toBeCalledTimes(1);
- expect(mockCoreStart.http.delete).toBeCalled();
+ expect(mockCoreSetup.http.delete).toBeCalled();
}
});
});
diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
index 18b5b976b3c1b5..7de4dd28ad3d7b 100644
--- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
@@ -4,17 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EMPTY, fromEvent, NEVER, Observable, throwError, timer } from 'rxjs';
-import { mergeMap, expand, takeUntil } from 'rxjs/operators';
+import { EMPTY, fromEvent, NEVER, throwError, timer, Observable, from } from 'rxjs';
+import { mergeMap, expand, takeUntil, share, flatMap } from 'rxjs/operators';
+import { CoreSetup } from '../../../../../src/core/public';
import { AbortError } from '../../../../../src/plugins/data/common';
import {
- IKibanaSearchResponse,
- ISearchContext,
+ ISearch,
ISearchStrategy,
+ ISyncSearchRequest,
SYNC_SEARCH_STRATEGY,
- TSearchStrategyProvider,
} from '../../../../../src/plugins/data/public';
-import { IAsyncSearchRequest, IAsyncSearchOptions, IAsyncSearchResponse } from './types';
+import { IAsyncSearchOptions, IAsyncSearchResponse, IAsyncSearchRequest } from './types';
+import { DataEnhancedStartDependencies } from '../plugin';
export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY';
@@ -24,55 +25,59 @@ declare module '../../../../../src/plugins/data/public' {
}
}
-export const asyncSearchStrategyProvider: TSearchStrategyProvider = (
- context: ISearchContext
-): ISearchStrategy => {
- const syncStrategyProvider = context.getSearchStrategy(SYNC_SEARCH_STRATEGY);
- const { search } = syncStrategyProvider(context);
- return {
- search: (
- request: IAsyncSearchRequest,
- { pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
- ): Observable => {
- const { serverStrategy } = request;
- let id: string | undefined = request.id;
+export function asyncSearchStrategyProvider(
+ core: CoreSetup
+): ISearchStrategy {
+ const startServices$ = from(core.getStartServices()).pipe(share());
- const aborted$ = options.signal
- ? fromEvent(options.signal, 'abort').pipe(
- mergeMap(() => {
- // If we haven't received the response to the initial request, including the ID, then
- // we don't need to send a follow-up request to delete this search. Otherwise, we
- // send the follow-up request to delete this search, then throw an abort error.
- if (id !== undefined) {
- context.core.http.delete(`/internal/search/${request.serverStrategy}/${id}`);
- }
- return throwError(new AbortError());
- })
- )
- : NEVER;
+ const search: ISearch = (
+ request: ISyncSearchRequest,
+ { pollInterval = 1000, ...options }: IAsyncSearchOptions = {}
+ ) => {
+ const { serverStrategy } = request;
+ let { id } = request;
- return search(request, options).pipe(
- expand((response: IAsyncSearchResponse) => {
- // If the response indicates of an error, stop polling and complete the observable
- if (!response || (response.is_partial && !response.is_running)) {
+ const aborted$ = options.signal
+ ? fromEvent(options.signal, 'abort').pipe(
+ mergeMap(() => {
+ // If we haven't received the response to the initial request, including the ID, then
+ // we don't need to send a follow-up request to delete this search. Otherwise, we
+ // send the follow-up request to delete this search, then throw an abort error.
+ if (id !== undefined) {
+ core.http.delete(`/internal/search/${request.serverStrategy}/${id}`);
+ }
return throwError(new AbortError());
- }
+ })
+ )
+ : NEVER;
+
+ return startServices$.pipe(
+ flatMap((startServices) => {
+ const syncSearch = startServices[1].data.search.getSearchStrategy(SYNC_SEARCH_STRATEGY);
+ return (syncSearch.search(request, options) as Observable).pipe(
+ expand((response) => {
+ // If the response indicates of an error, stop polling and complete the observable
+ if (!response || (response.is_partial && !response.is_running)) {
+ return throwError(new AbortError());
+ }
- // If the response indicates it is complete, stop polling and complete the observable
- if (!response.is_running) return EMPTY;
+ // If the response indicates it is complete, stop polling and complete the observable
+ if (!response.is_running) return EMPTY;
- id = response.id;
+ id = response.id;
- // Delay by the given poll interval
- return timer(pollInterval).pipe(
- // Send future requests using just the ID from the response
- mergeMap(() => {
- return search({ id, serverStrategy }, options);
- })
- );
- }),
- takeUntil(aborted$)
- );
- },
+ // Delay by the given poll interval
+ return timer(pollInterval).pipe(
+ // Send future requests using just the ID from the response
+ mergeMap(() => {
+ return search({ id, serverStrategy }, options);
+ })
+ );
+ }),
+ takeUntil(aborted$)
+ );
+ })
+ );
};
-};
+ return { search };
+}
diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.test.ts
new file mode 100644
index 00000000000000..5d6bd53e2c9453
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreSetup } from '../../../../../src/core/public';
+import { coreMock } from '../../../../../src/core/public/mocks';
+import { ES_SEARCH_STRATEGY } from '../../../../../src/plugins/data/common';
+import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
+import { IAsyncSearchOptions } from '.';
+
+describe('Enhanced ES search strategy', () => {
+ let mockCoreSetup: jest.Mocked;
+ const mockSearch = { search: jest.fn() };
+
+ beforeEach(() => {
+ mockCoreSetup = coreMock.createSetup();
+ mockSearch.search.mockClear();
+ });
+
+ it('returns a strategy with `search` that calls the async search `search`', () => {
+ const request = { params: {} };
+ const options: IAsyncSearchOptions = { pollInterval: 0 };
+
+ const esSearch = enhancedEsSearchStrategyProvider(mockCoreSetup, mockSearch);
+ esSearch.search(request, options);
+
+ expect(mockSearch.search.mock.calls[0][0]).toEqual({
+ ...request,
+ serverStrategy: ES_SEARCH_STRATEGY,
+ });
+ expect(mockSearch.search.mock.calls[0][1]).toEqual(options);
+ });
+});
diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts
index 3a511c7b5a1767..c4b293a52a1043 100644
--- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts
@@ -5,42 +5,40 @@
*/
import { Observable } from 'rxjs';
+import { CoreSetup } from '../../../../../src/core/public';
import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../../../src/plugins/data/common';
import {
- TSearchStrategyProvider,
- ISearchContext,
ISearch,
getEsPreference,
+ ISearchStrategy,
UI_SETTINGS,
} from '../../../../../src/plugins/data/public';
import { IEnhancedEsSearchRequest, EnhancedSearchParams } from '../../common';
import { ASYNC_SEARCH_STRATEGY } from './async_search_strategy';
import { IAsyncSearchOptions } from './types';
-export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider = (
- context: ISearchContext
-) => {
- const asyncStrategyProvider = context.getSearchStrategy(ASYNC_SEARCH_STRATEGY);
- const { search: asyncSearch } = asyncStrategyProvider(context);
-
+export function enhancedEsSearchStrategyProvider(
+ core: CoreSetup,
+ asyncStrategy: ISearchStrategy
+) {
const search: ISearch = (
request: IEnhancedEsSearchRequest,
options
) => {
const params: EnhancedSearchParams = {
- ignoreThrottled: !context.core.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
- preference: getEsPreference(context.core.uiSettings),
+ ignoreThrottled: !core.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
+ preference: getEsPreference(core.uiSettings),
...request.params,
};
request.params = params;
const asyncOptions: IAsyncSearchOptions = { pollInterval: 0, ...options };
- return asyncSearch(
+ return asyncStrategy.search(
{ ...request, serverStrategy: ES_SEARCH_STRATEGY },
asyncOptions
) as Observable;
};
return { search };
-};
+}
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
index da461609f0b836..75d1b69eb61570 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts
@@ -12,7 +12,7 @@ type HttpResponse = Record | any[];
// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const setLoadTemplatesResponse = (response: HttpResponse = []) => {
- server.respondWith('GET', `${API_BASE_PATH}/index-templates`, [
+ server.respondWith('GET', `${API_BASE_PATH}/index_templates`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
@@ -28,7 +28,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
const setDeleteTemplateResponse = (response: HttpResponse = []) => {
- server.respondWith('POST', `${API_BASE_PATH}/delete-index-templates`, [
+ server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
@@ -39,7 +39,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const status = error ? error.status || 400 : 200;
const body = error ? error.body : response;
- server.respondWith('GET', `${API_BASE_PATH}/index-templates/:id`, [
+ server.respondWith('GET', `${API_BASE_PATH}/index_templates/:id`, [
status,
{ 'Content-Type': 'application/json' },
JSON.stringify(body),
@@ -50,7 +50,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const status = error ? error.body.status || 400 : 200;
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
- server.respondWith('POST', `${API_BASE_PATH}/index-templates`, [
+ server.respondWith('POST', `${API_BASE_PATH}/index_templates`, [
status,
{ 'Content-Type': 'application/json' },
body,
@@ -61,7 +61,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const status = error ? error.status || 400 : 200;
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
- server.respondWith('PUT', `${API_BASE_PATH}/index-templates/:name`, [
+ server.respondWith('PUT', `${API_BASE_PATH}/index_templates/:name`, [
status,
{ 'Content-Type': 'application/json' },
body,
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index 8f6a8dddeb195e..7c79c7e61174ea 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -188,7 +188,7 @@ describe('Index Templates tab', () => {
expect(server.requests.length).toBe(totalRequests + 1);
expect(server.requests[server.requests.length - 1].url).toBe(
- `${API_BASE_PATH}/index-templates`
+ `${API_BASE_PATH}/index_templates`
);
});
@@ -318,7 +318,7 @@ describe('Index Templates tab', () => {
const latestRequest = server.requests[server.requests.length - 1];
expect(latestRequest.method).toBe('POST');
- expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete-index-templates`);
+ expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`);
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
templates: [{ name: legacyTemplates[0].name, isLegacy }],
});
diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts
index 3792e322ae40be..4ad428744deab7 100644
--- a/x-pack/plugins/index_management/common/index.ts
+++ b/x-pack/plugins/index_management/common/index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { PLUGIN, API_BASE_PATH, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from './constants';
+export { PLUGIN, API_BASE_PATH, CREATE_LEGACY_TEMPLATE_BY_DEFAULT, BASE_PATH } from './constants';
export { getTemplateParameter } from './lib';
diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts
new file mode 100644
index 00000000000000..eaa7f24017a2f8
--- /dev/null
+++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { deserializeComponentTemplate } from './component_template_serialization';
+
+describe('deserializeComponentTemplate', () => {
+ test('deserializes a component template', () => {
+ expect(
+ deserializeComponentTemplate(
+ {
+ name: 'my_component_template',
+ component_template: {
+ version: 1,
+ _meta: {
+ serialization: {
+ id: 10,
+ class: 'MyComponentTemplate',
+ },
+ description: 'set number of shards to one',
+ },
+ template: {
+ settings: {
+ number_of_shards: 1,
+ },
+ mappings: {
+ _source: {
+ enabled: false,
+ },
+ properties: {
+ host_name: {
+ type: 'keyword',
+ },
+ created_at: {
+ type: 'date',
+ format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ },
+ },
+ },
+ },
+ },
+ },
+ [
+ {
+ name: 'my_index_template',
+ index_template: {
+ index_patterns: ['foo'],
+ template: {
+ settings: {
+ number_of_replicas: 2,
+ },
+ },
+ composed_of: ['my_component_template'],
+ },
+ },
+ ]
+ )
+ ).toEqual({
+ name: 'my_component_template',
+ version: 1,
+ _meta: {
+ serialization: {
+ id: 10,
+ class: 'MyComponentTemplate',
+ },
+ description: 'set number of shards to one',
+ },
+ template: {
+ settings: {
+ number_of_shards: 1,
+ },
+ mappings: {
+ _source: {
+ enabled: false,
+ },
+ properties: {
+ host_name: {
+ type: 'keyword',
+ },
+ created_at: {
+ type: 'date',
+ format: 'EEE MMM dd HH:mm:ss Z yyyy',
+ },
+ },
+ },
+ },
+ _kbnMeta: {
+ usedBy: ['my_index_template'],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts
new file mode 100644
index 00000000000000..0db81bf81d3002
--- /dev/null
+++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import {
+ TemplateFromEs,
+ ComponentTemplateFromEs,
+ ComponentTemplateDeserialized,
+ ComponentTemplateListItem,
+} from '../types';
+
+const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
+
+/**
+ * Normalize a list of component templates to a map where each key
+ * is a component template name, and the value is an array of index templates name using it
+ *
+ * @example
+ *
+ {
+ "comp-1": [
+ "template-1",
+ "template-2"
+ ],
+ "comp2": [
+ "template-1",
+ "template-2"
+ ]
+ }
+ *
+ * @param indexTemplatesEs List of component templates
+ */
+
+const getIndexTemplatesToUsedBy = (indexTemplatesEs: TemplateFromEs[]) => {
+ return indexTemplatesEs.reduce((acc, item) => {
+ if (item.index_template.composed_of) {
+ item.index_template.composed_of.forEach((component) => {
+ acc[component] = acc[component] ? [...acc[component], item.name] : [item.name];
+ });
+ }
+ return acc;
+ }, {} as { [key: string]: string[] });
+};
+
+export function deserializeComponentTemplate(
+ componentTemplateEs: ComponentTemplateFromEs,
+ indexTemplatesEs: TemplateFromEs[]
+) {
+ const { name, component_template: componentTemplate } = componentTemplateEs;
+ const { template, _meta, version } = componentTemplate;
+
+ const indexTemplatesToUsedBy = getIndexTemplatesToUsedBy(indexTemplatesEs);
+
+ const deserializedComponentTemplate: ComponentTemplateDeserialized = {
+ name,
+ template,
+ version,
+ _meta,
+ _kbnMeta: {
+ usedBy: indexTemplatesToUsedBy[name] || [],
+ },
+ };
+
+ return deserializedComponentTemplate;
+}
+
+export function deserializeComponenTemplateList(
+ componentTemplateEs: ComponentTemplateFromEs,
+ indexTemplatesEs: TemplateFromEs[]
+) {
+ const { name, component_template: componentTemplate } = componentTemplateEs;
+ const { template } = componentTemplate;
+
+ const indexTemplatesToUsedBy = getIndexTemplatesToUsedBy(indexTemplatesEs);
+
+ const componentTemplateListItem: ComponentTemplateListItem = {
+ name,
+ usedBy: indexTemplatesToUsedBy[name] || [],
+ hasSettings: hasEntries(template.settings),
+ hasMappings: hasEntries(template.mappings),
+ hasAliases: hasEntries(template.aliases),
+ };
+
+ return componentTemplateListItem;
+}
diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts
index 16eb544c56a089..c67d28da2c24b4 100644
--- a/x-pack/plugins/index_management/common/lib/index.ts
+++ b/x-pack/plugins/index_management/common/lib/index.ts
@@ -11,3 +11,8 @@ export {
} from './template_serialization';
export { getTemplateParameter } from './utils';
+
+export {
+ deserializeComponentTemplate,
+ deserializeComponenTemplateList,
+} from './component_template_serialization';
diff --git a/x-pack/plugins/index_management/common/types/component_templates.ts b/x-pack/plugins/index_management/common/types/component_templates.ts
new file mode 100644
index 00000000000000..bc7ebdc2753dde
--- /dev/null
+++ b/x-pack/plugins/index_management/common/types/component_templates.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IndexSettings } from './indices';
+import { Aliases } from './aliases';
+import { Mappings } from './mappings';
+
+export interface ComponentTemplateSerialized {
+ template: {
+ settings?: IndexSettings;
+ aliases?: Aliases;
+ mappings?: Mappings;
+ };
+ version?: number;
+ _meta?: { [key: string]: any };
+}
+
+export interface ComponentTemplateDeserialized extends ComponentTemplateSerialized {
+ name: string;
+ _kbnMeta: {
+ usedBy: string[];
+ };
+}
+
+export interface ComponentTemplateFromEs {
+ name: string;
+ component_template: ComponentTemplateSerialized;
+}
+
+export interface ComponentTemplateListItem {
+ name: string;
+ usedBy: string[];
+ hasMappings: boolean;
+ hasAliases: boolean;
+ hasSettings: boolean;
+}
diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts
index b467f020978a56..81a06156dd291d 100644
--- a/x-pack/plugins/index_management/common/types/index.ts
+++ b/x-pack/plugins/index_management/common/types/index.ts
@@ -11,3 +11,5 @@ export * from './indices';
export * from './mappings';
export * from './templates';
+
+export * from './component_templates';
diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts
index f113aa44d058f8..006a2d9dea8f23 100644
--- a/x-pack/plugins/index_management/common/types/templates.ts
+++ b/x-pack/plugins/index_management/common/types/templates.ts
@@ -49,6 +49,11 @@ export interface TemplateDeserialized {
};
}
+export interface TemplateFromEs {
+ name: string;
+ index_template: TemplateSerialized;
+}
+
/**
* Interface for the template list in our UI table
* we don't include the mappings, settings and aliases
diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx
index 10bbe3ced64da4..bfd99de6949e58 100644
--- a/x-pack/plugins/index_management/public/application/app.tsx
+++ b/x-pack/plugins/index_management/public/application/app.tsx
@@ -5,10 +5,12 @@
*/
import React, { useEffect } from 'react';
+
import { Router, Switch, Route, Redirect } from 'react-router-dom';
import { ScopedHistory } from 'kibana/public';
+
import { UIM_APP_LOAD } from '../../common/constants';
-import { IndexManagementHome } from './sections/home';
+import { IndexManagementHome, homeSections } from './sections/home';
import { TemplateCreate } from './sections/template_create';
import { TemplateClone } from './sections/template_clone';
import { TemplateEdit } from './sections/template_edit';
@@ -32,7 +34,7 @@ export const AppWithoutRouter = () => (
-
+
);
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
new file mode 100644
index 00000000000000..830cc0ee6a9800
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -0,0 +1,174 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { act } from 'react-dom/test-utils';
+
+import { setupEnvironment, pageHelpers } from './helpers';
+import { ComponentTemplateListTestBed } from './helpers/component_template_list.helpers';
+import { API_BASE_PATH } from '../../../../../../common/constants';
+import { ComponentTemplateListItem } from '../../types';
+
+const { setup } = pageHelpers.componentTemplateList;
+
+jest.mock('ui/i18n', () => {
+ const I18nContext = ({ children }: any) => children;
+ return { I18nContext };
+});
+
+describe('', () => {
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+ let testBed: ComponentTemplateListTestBed;
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ });
+
+ describe('With component templates', () => {
+ const componentTemplate1: ComponentTemplateListItem = {
+ name: 'test_component_template_1',
+ hasMappings: true,
+ hasAliases: true,
+ hasSettings: true,
+ usedBy: [],
+ };
+
+ const componentTemplate2: ComponentTemplateListItem = {
+ name: 'test_component_template_2',
+ hasMappings: true,
+ hasAliases: true,
+ hasSettings: true,
+ usedBy: ['test_index_template_1'],
+ };
+
+ const componentTemplates = [componentTemplate1, componentTemplate2];
+
+ httpRequestsMockHelpers.setLoadComponentTemplatesResponse(componentTemplates);
+
+ test('should render the list view', async () => {
+ const { table } = testBed;
+
+ // Verify table content
+ const { tableCellsValues } = table.getMetaData('componentTemplatesTable');
+ tableCellsValues.forEach((row, i) => {
+ const { name, usedBy } = componentTemplates[i];
+ const usedByText = usedBy.length === 0 ? 'Not in use' : usedBy.length.toString();
+
+ expect(row).toEqual(['', name, usedByText, '', '', '', '']);
+ });
+ });
+
+ test('should reload the component templates data', async () => {
+ const { component, actions } = testBed;
+ const totalRequests = server.requests.length;
+
+ await act(async () => {
+ actions.clickReloadButton();
+ });
+
+ component.update();
+
+ expect(server.requests.length).toBe(totalRequests + 1);
+ expect(server.requests[server.requests.length - 1].url).toBe(
+ `${API_BASE_PATH}/component_templates`
+ );
+ });
+
+ test('should delete a component template', async () => {
+ const { actions, component } = testBed;
+ const { name: componentTemplateName } = componentTemplate1;
+
+ await act(async () => {
+ actions.clickDeleteActionAt(0);
+ });
+
+ // We need to read the document "body" as the modal is added there and not inside
+ // the component DOM tree.
+ const modal = document.body.querySelector(
+ '[data-test-subj="deleteComponentTemplatesConfirmation"]'
+ );
+ const confirmButton: HTMLButtonElement | null = modal!.querySelector(
+ '[data-test-subj="confirmModalConfirmButton"]'
+ );
+
+ expect(modal).not.toBe(null);
+ expect(modal!.textContent).toContain('Delete component template');
+
+ httpRequestsMockHelpers.setDeleteComponentTemplateResponse({
+ itemsDeleted: [componentTemplateName],
+ errors: [],
+ });
+
+ await act(async () => {
+ confirmButton!.click();
+ });
+
+ component.update();
+
+ const deleteRequest = server.requests[server.requests.length - 2];
+
+ expect(deleteRequest.method).toBe('DELETE');
+ expect(deleteRequest.url).toBe(
+ `${API_BASE_PATH}/component_templates/${componentTemplateName}`
+ );
+ expect(deleteRequest.status).toEqual(200);
+ });
+ });
+
+ describe('No component templates', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadComponentTemplatesResponse([]);
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ });
+
+ test('should display an empty prompt', async () => {
+ const { exists, find } = testBed;
+
+ expect(exists('sectionLoading')).toBe(false);
+ expect(exists('emptyList')).toBe(true);
+ expect(find('emptyList.title').text()).toEqual('Start by creating a component template');
+ });
+ });
+
+ describe('Error handling', () => {
+ beforeEach(async () => {
+ const error = {
+ status: 500,
+ error: 'Internal server error',
+ message: 'Internal server error',
+ };
+
+ httpRequestsMockHelpers.setLoadComponentTemplatesResponse(undefined, { body: error });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ });
+
+ test('should render an error message if error fetching component templates', async () => {
+ const { exists, find } = testBed;
+
+ expect(exists('componentTemplatesLoadError')).toBe(true);
+ expect(find('componentTemplatesLoadError').text()).toContain(
+ 'Unable to load component templates. Try again.'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.ts
new file mode 100644
index 00000000000000..8fb4dcff0bcea6
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_list.helpers.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { act } from 'react-dom/test-utils';
+
+import { BASE_PATH } from '../../../../../../../common';
+import {
+ registerTestBed,
+ TestBed,
+ TestBedConfig,
+ findTestSubject,
+ nextTick,
+} from '../../../../../../../../../test_utils';
+import { WithAppDependencies } from './setup_environment';
+import { ComponentTemplateList } from '../../../component_template_list';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`${BASE_PATH}component_templates`],
+ componentRoutePath: `${BASE_PATH}component_templates`,
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateList), testBedConfig);
+
+export type ComponentTemplateListTestBed = TestBed & {
+ actions: ReturnType;
+};
+
+const createActions = (testBed: TestBed) => {
+ const { find } = testBed;
+
+ /**
+ * User Actions
+ */
+ const clickReloadButton = () => {
+ find('reloadButton').simulate('click');
+ };
+
+ const clickComponentTemplateAt = async (index: number) => {
+ const { component, table, router } = testBed;
+ const { rows } = table.getMetaData('componentTemplatesTable');
+ const componentTemplateLink = findTestSubject(
+ rows[index].reactWrapper,
+ 'componentTemplateDetailsLink'
+ );
+
+ await act(async () => {
+ const { href } = componentTemplateLink.props();
+ router.navigateTo(href!);
+ await nextTick();
+ component.update();
+ });
+ };
+
+ const clickDeleteActionAt = (index: number) => {
+ const { table } = testBed;
+
+ const { rows } = table.getMetaData('componentTemplatesTable');
+ const deleteButton = findTestSubject(rows[index].reactWrapper, 'deleteComponentTemplateButton');
+
+ deleteButton.simulate('click');
+ };
+
+ return {
+ clickReloadButton,
+ clickComponentTemplateAt,
+ clickDeleteActionAt,
+ };
+};
+
+export const setup = async (): Promise => {
+ const testBed = await initTestBed();
+
+ return {
+ ...testBed,
+ actions: createActions(testBed),
+ };
+};
+
+export type ComponentTemplateTestSubjects =
+ | 'componentTemplatesTable'
+ | 'componentTemplateDetails'
+ | 'componentTemplateDetails.title'
+ | 'deleteComponentTemplatesConfirmation'
+ | 'emptyList'
+ | 'emptyList.title'
+ | 'sectionLoading'
+ | 'componentTemplatesLoadError'
+ | 'deleteComponentTemplateButton'
+ | 'reloadButton';
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts
new file mode 100644
index 00000000000000..8473041ee0af3d
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import sinon, { SinonFakeServer } from 'sinon';
+import { API_BASE_PATH } from '../../../../../../../common';
+
+// Register helpers to mock HTTP Requests
+const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
+ const setLoadComponentTemplatesResponse = (response?: any[], error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? error.body : response;
+
+ server.respondWith('GET', `${API_BASE_PATH}/component_templates`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(body),
+ ]);
+ };
+
+ const setDeleteComponentTemplateResponse = (response?: object) => {
+ server.respondWith('DELETE', `${API_BASE_PATH}/component_templates/:name`, [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
+ return {
+ setLoadComponentTemplatesResponse,
+ setDeleteComponentTemplateResponse,
+ };
+};
+
+export const init = () => {
+ const server = sinon.fakeServer.create();
+ server.respondImmediately = true;
+
+ // Define default response for unhandled requests.
+ // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
+ // and we can mock them all with a 200 instead of mocking each one individually.
+ server.respondWith([200, {}, 'DefaultMockedResponse']);
+
+ const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
+
+ return {
+ server,
+ httpRequestsMockHelpers,
+ };
+};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/index.ts
new file mode 100644
index 00000000000000..c1d75b3c2dd9b6
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setup as componentTemplatesListSetup } from './component_template_list.helpers';
+
+export { nextTick, getRandomString, findTestSubject } from '../../../../../../../../../test_utils';
+
+export { setupEnvironment } from './setup_environment';
+
+export const pageHelpers = {
+ componentTemplateList: { setup: componentTemplatesListSetup },
+};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx
new file mode 100644
index 00000000000000..c0aeb70166b5ba
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* eslint-disable @kbn/eslint/no-restricted-paths */
+import React from 'react';
+import axios from 'axios';
+import axiosXhrAdapter from 'axios/lib/adapters/xhr';
+
+import { HttpSetup } from 'kibana/public';
+import { BASE_PATH, API_BASE_PATH } from '../../../../../../../common/constants';
+import {
+ notificationServiceMock,
+ docLinksServiceMock,
+} from '../../../../../../../../../../src/core/public/mocks';
+
+import { init as initHttpRequests } from './http_requests';
+import { ComponentTemplatesProvider } from '../../../component_templates_context';
+
+const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
+
+const appDependencies = {
+ httpClient: (mockHttpClient as unknown) as HttpSetup,
+ apiBasePath: API_BASE_PATH,
+ appBasePath: BASE_PATH,
+ trackMetric: () => {},
+ docLinks: docLinksServiceMock.createStartContract(),
+ toasts: notificationServiceMock.createSetupContract().toasts,
+};
+
+export const setupEnvironment = () => {
+ const { server, httpRequestsMockHelpers } = initHttpRequests();
+
+ return {
+ server,
+ httpRequestsMockHelpers,
+ };
+};
+
+export const WithAppDependencies = (Comp: any) => (props: any) => (
+
+
+
+);
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
new file mode 100644
index 00000000000000..41fa608ef538be
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useEffect } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { SectionLoading } from '../shared_imports';
+import { useComponentTemplatesContext } from '../component_templates_context';
+import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants';
+
+import { EmptyPrompt } from './empty_prompt';
+import { ComponentTable } from './table';
+import { LoadError } from './error';
+import { ComponentTemplatesDeleteModal } from './delete_modal';
+
+export const ComponentTemplateList: React.FunctionComponent = () => {
+ const { api, trackMetric } = useComponentTemplatesContext();
+
+ const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates();
+
+ const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]);
+
+ // Track component loaded
+ useEffect(() => {
+ trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD);
+ }, [trackMetric]);
+
+ if (data && data.length === 0) {
+ return ;
+ }
+
+ let content: React.ReactNode;
+
+ if (isLoading) {
+ content = (
+
+
+
+ );
+ } else if (data?.length) {
+ content = (
+
+ );
+ } else if (error) {
+ content = ;
+ }
+
+ return (
+
+ {content}
+ {componentTemplatesToDelete?.length > 0 ? (
+ {
+ if (deleteResponse?.hasDeletedComponentTemplates) {
+ // refetch the component templates
+ sendRequest();
+ }
+ setComponentTemplatesToDelete([]);
+ }}
+ componentTemplatesToDelete={componentTemplatesToDelete}
+ />
+ ) : null}
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx
new file mode 100644
index 00000000000000..bf621065842b56
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx
@@ -0,0 +1,128 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { useComponentTemplatesContext } from '../component_templates_context';
+
+export const ComponentTemplatesDeleteModal = ({
+ componentTemplatesToDelete,
+ callback,
+}: {
+ componentTemplatesToDelete: string[];
+ callback: (data?: { hasDeletedComponentTemplates: boolean }) => void;
+}) => {
+ const { toasts, api } = useComponentTemplatesContext();
+ const numComponentTemplatesToDelete = componentTemplatesToDelete.length;
+
+ const handleDeleteComponentTemplates = () => {
+ api
+ .deleteComponentTemplates(componentTemplatesToDelete)
+ .then(({ data: { itemsDeleted, errors }, error }) => {
+ const hasDeletedComponentTemplates = itemsDeleted && itemsDeleted.length;
+
+ if (hasDeletedComponentTemplates) {
+ const successMessage =
+ itemsDeleted.length === 1
+ ? i18n.translate(
+ 'xpack.idxMgmt.home.componentTemplates.deleteModal.successDeleteSingleNotificationMessageText',
+ {
+ defaultMessage: "Deleted component template '{componentTemplateName}'",
+ values: { componentTemplateName: componentTemplatesToDelete[0] },
+ }
+ )
+ : i18n.translate(
+ 'xpack.idxMgmt.home.componentTemplates.deleteModal.successDeleteMultipleNotificationMessageText',
+ {
+ defaultMessage:
+ 'Deleted {numSuccesses, plural, one {# component template} other {# component templates}}',
+ values: { numSuccesses: itemsDeleted.length },
+ }
+ );
+
+ callback({ hasDeletedComponentTemplates });
+ toasts.addSuccess(successMessage);
+ }
+
+ if (error || errors?.length) {
+ const hasMultipleErrors =
+ errors?.length > 1 || (error && componentTemplatesToDelete.length > 1);
+ const errorMessage = hasMultipleErrors
+ ? i18n.translate(
+ 'xpack.idxMgmt.home.componentTemplates.deleteModal.multipleErrorsNotificationMessageText',
+ {
+ defaultMessage: 'Error deleting {count} component templates',
+ values: {
+ count: errors?.length || componentTemplatesToDelete.length,
+ },
+ }
+ )
+ : i18n.translate(
+ 'xpack.idxMgmt.home.componentTemplates.deleteModal.errorNotificationMessageText',
+ {
+ defaultMessage: "Error deleting component template '{name}'",
+ values: { name: (errors && errors[0].name) || componentTemplatesToDelete[0] },
+ }
+ );
+ toasts.addDanger(errorMessage);
+ }
+ });
+ };
+
+ const handleOnCancel = () => {
+ callback();
+ };
+
+ return (
+
+
+ }
+ onCancel={handleOnCancel}
+ onConfirm={handleDeleteComponentTemplates}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ >
+ <>
+
+
+
+
+
+ {componentTemplatesToDelete.map((name) => (
+ - {name}
+ ))}
+
+ >
+
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx
new file mode 100644
index 00000000000000..edd9f77cbf635d
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { FunctionComponent } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiEmptyPrompt, EuiLink } from '@elastic/eui';
+
+import { useComponentTemplatesContext } from '../component_templates_context';
+
+export const EmptyPrompt: FunctionComponent = () => {
+ const { documentation } = useComponentTemplatesContext();
+
+ return (
+
+ {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptTitle', {
+ defaultMessage: 'Start by creating a component template',
+ })}
+
+ }
+ body={
+
+
+
+
+ {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink', {
+ defaultMessage: 'Learn more',
+ })}
+
+
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
new file mode 100644
index 00000000000000..aa37b9ce5767c0
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { FunctionComponent } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink, EuiCallOut } from '@elastic/eui';
+
+export interface Props {
+ onReloadClick: () => void;
+}
+
+export const LoadError: FunctionComponent = ({ onReloadClick }) => {
+ return (
+
+
+
+ ),
+ }}
+ />
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts
similarity index 79%
rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts
rename to x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts
index 3a25359373aa6a..84ee48d14bb8c2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { CreateAnalyticsFlyout } from './create_analytics_flyout';
+export { ComponentTemplateList } from './component_template_list';
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
new file mode 100644
index 00000000000000..2d9557e64e6e76
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx
@@ -0,0 +1,205 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { FunctionComponent, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiInMemoryTable,
+ EuiButton,
+ EuiInMemoryTableProps,
+ EuiTextColor,
+ EuiIcon,
+} from '@elastic/eui';
+
+import { ComponentTemplateListItem } from '../types';
+
+export interface Props {
+ componentTemplates: ComponentTemplateListItem[];
+ onReloadClick: () => void;
+ onDeleteClick: (componentTemplateName: string[]) => void;
+}
+
+export const ComponentTable: FunctionComponent = ({
+ componentTemplates,
+ onReloadClick,
+ onDeleteClick,
+}) => {
+ const [selection, setSelection] = useState([]);
+
+ const tableProps: EuiInMemoryTableProps = {
+ itemId: 'name',
+ isSelectable: true,
+ 'data-test-subj': 'componentTemplatesTable',
+ sorting: { sort: { field: 'name', direction: 'asc' } },
+ selection: {
+ onSelectionChange: setSelection,
+ selectable: ({ usedBy }) => usedBy.length === 0,
+ selectableMessage: (selectable) =>
+ selectable
+ ? i18n.translate('xpack.idxMgmt.componentTemplatesList.table.selectionLabel', {
+ defaultMessage: 'Select this component template',
+ })
+ : i18n.translate('xpack.idxMgmt.componentTemplatesList.table.disabledSelectionLabel', {
+ defaultMessage: 'Component template is in use and cannot be deleted',
+ }),
+ },
+ rowProps: () => ({
+ 'data-test-subj': 'componentTemplateTableRow',
+ }),
+ search: {
+ toolsLeft:
+ selection.length > 0 ? (
+ onDeleteClick(selection.map(({ name }) => name))}
+ color="danger"
+ >
+
+
+ ) : undefined,
+ toolsRight: [
+
+ {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.reloadButtonLabel', {
+ defaultMessage: 'Reload',
+ })}
+ ,
+ ],
+ box: {
+ incremental: true,
+ },
+ filters: [
+ {
+ type: 'field_value_toggle_group',
+ field: 'usedBy.length',
+ items: [
+ {
+ value: 1,
+ name: i18n.translate(
+ 'xpack.idxMgmt.componentTemplatesList.table.inUseFilterOptionLabel',
+ {
+ defaultMessage: 'In use',
+ }
+ ),
+ operator: 'gte',
+ },
+ {
+ value: 0,
+ name: i18n.translate(
+ 'xpack.idxMgmt.componentTemplatesList.table.notInUseFilterOptionLabel',
+ {
+ defaultMessage: 'Not in use',
+ }
+ ),
+ operator: 'eq',
+ },
+ ],
+ },
+ ],
+ },
+ pagination: {
+ initialPageSize: 10,
+ pageSizeOptions: [10, 20, 50],
+ },
+ columns: [
+ {
+ field: 'name',
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.nameColumnTitle', {
+ defaultMessage: 'Name',
+ }),
+ sortable: true,
+ },
+ {
+ field: 'usedBy',
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.isInUseColumnTitle', {
+ defaultMessage: 'Index templates',
+ }),
+ sortable: true,
+ render: (usedBy: string[]) => {
+ if (usedBy.length) {
+ return usedBy.length;
+ }
+
+ return (
+
+
+
+
+
+ );
+ },
+ },
+ {
+ field: 'hasMappings',
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.mappingsColumnTitle', {
+ defaultMessage: 'Mappings',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (hasMappings: boolean) => (hasMappings ? : null),
+ },
+ {
+ field: 'hasSettings',
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.settingsColumnTitle', {
+ defaultMessage: 'Settings',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (hasSettings: boolean) => (hasSettings ? : null),
+ },
+ {
+ field: 'hasAliases',
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.aliasesColumnTitle', {
+ defaultMessage: 'Aliases',
+ }),
+ truncateText: true,
+ sortable: true,
+ render: (hasAliases: boolean) => (hasAliases ? : null),
+ },
+ {
+ name: (
+
+ ),
+ actions: [
+ {
+ 'data-test-subj': 'deleteComponentTemplateButton',
+ isPrimary: true,
+ name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.deleteActionLabel', {
+ defaultMessage: 'Delete',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription',
+ { defaultMessage: 'Delete this component template' }
+ ),
+ type: 'icon',
+ icon: 'trash',
+ color: 'danger',
+ onClick: ({ name }) => onDeleteClick([name]),
+ enabled: ({ usedBy }) => usedBy.length === 0,
+ },
+ ],
+ },
+ ],
+ items: componentTemplates ?? [],
+ };
+
+ return ;
+};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
new file mode 100644
index 00000000000000..6f5f5bdebd6d06
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { createContext, useContext } from 'react';
+import { HttpSetup, DocLinksSetup, NotificationsSetup } from 'src/core/public';
+
+import { getApi, getUseRequest, getSendRequest, getDocumentation } from './lib';
+
+const ComponentTemplatesContext = createContext(undefined);
+
+interface Props {
+ httpClient: HttpSetup;
+ apiBasePath: string;
+ appBasePath: string;
+ trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
+ docLinks: DocLinksSetup;
+ toasts: NotificationsSetup['toasts'];
+}
+
+interface Context {
+ api: ReturnType;
+ documentation: ReturnType;
+ trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void;
+ toasts: NotificationsSetup['toasts'];
+ appBasePath: string;
+}
+
+export const ComponentTemplatesProvider = ({
+ children,
+ value,
+}: {
+ value: Props;
+ children: React.ReactNode;
+}) => {
+ const { httpClient, apiBasePath, trackMetric, docLinks, toasts, appBasePath } = value;
+
+ const useRequest = getUseRequest(httpClient);
+ const sendRequest = getSendRequest(httpClient);
+
+ const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric);
+ const documentation = getDocumentation(docLinks);
+
+ return (
+