interactionStart('drag', e)}
- onMouseUp={(e) => interactionStart('drop', e)}
- >
-
-
- {/* Resize handle */}
- interactionStart('resize', e)}
- onMouseUp={(e) => interactionStart('drop', e)}
- css={css`
- right: 0;
- bottom: 0;
- opacity: 0;
- margin: -2px;
- position: absolute;
- width: ${euiThemeVars.euiSizeL};
- height: ${euiThemeVars.euiSizeL};
- transition: opacity 0.2s, border 0.2s;
- border-radius: 7px 0 7px 0;
- border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
- border-right: 2px solid ${euiThemeVars.euiColorSuccess};
- :hover {
- background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
- cursor: se-resize;
+ /** Set initial styles based on state at mount to prevent styles from "blipping" */
+ const initialStyles = useMemo(() => {
+ const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId];
+ return css`
+ grid-column-start: ${initialPanel.column + 1};
+ grid-column-end: ${initialPanel.column + 1 + initialPanel.width};
+ grid-row-start: ${initialPanel.row + 1};
+ grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
+ `;
+ }, [gridLayoutStateManager, rowIndex, panelId]);
+
+ useEffect(
+ () => {
+ /** Update the styles of the panel via a subscription to prevent re-renders */
+ const styleSubscription = combineLatest([
+ gridLayoutStateManager.activePanel$,
+ gridLayoutStateManager.gridLayout$,
+ gridLayoutStateManager.runtimeSettings$,
+ ])
+ .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
+ .subscribe(([activePanel, gridLayout, runtimeSettings]) => {
+ const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
+ const panel = gridLayout[rowIndex].panels[panelId];
+ if (!ref || !panel) return;
+
+ const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
+ if (panelId === activePanel?.id) {
+ // if the current panel is active, give it fixed positioning depending on the interaction event
+ const { position: draggingPosition } = activePanel;
+
+ ref.style.zIndex = `${euiThemeVars.euiZModal}`;
+ if (currentInteractionEvent?.type === 'resize') {
+ // if the current panel is being resized, ensure it is not shrunk past the size of a single cell
+ ref.style.width = `${Math.max(
+ draggingPosition.right - draggingPosition.left,
+ runtimeSettings.columnPixelWidth
+ )}px`;
+ ref.style.height = `${Math.max(
+ draggingPosition.bottom - draggingPosition.top,
+ runtimeSettings.rowHeight
+ )}px`;
+
+ // undo any "lock to grid" styles **except** for the top left corner, which stays locked
+ ref.style.gridColumnStart = `${panel.column + 1}`;
+ ref.style.gridRowStart = `${panel.row + 1}`;
+ ref.style.gridColumnEnd = ``;
+ ref.style.gridRowEnd = ``;
+ } else {
+ // if the current panel is being dragged, render it with a fixed position + size
+ ref.style.position = 'fixed';
+ ref.style.left = `${draggingPosition.left}px`;
+ ref.style.top = `${draggingPosition.top}px`;
+ ref.style.width = `${draggingPosition.right - draggingPosition.left}px`;
+ ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
+
+ // undo any "lock to grid" styles
+ ref.style.gridColumnStart = ``;
+ ref.style.gridRowStart = ``;
+ ref.style.gridColumnEnd = ``;
+ ref.style.gridRowEnd = ``;
+ }
+ } else {
+ ref.style.zIndex = '0';
+
+ // if the panel is not being dragged and/or resized, undo any fixed position styles
+ ref.style.position = '';
+ ref.style.left = ``;
+ ref.style.top = ``;
+ ref.style.width = ``;
+ ref.style.height = ``;
+
+ // and render the panel locked to the grid
+ ref.style.gridColumnStart = `${panel.column + 1}`;
+ ref.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
+ ref.style.gridRowStart = `${panel.row + 1}`;
+ ref.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
}
- `}
- />
-
{
+ styleSubscription.unsubscribe();
+ };
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+
+ return (
+
+
- {renderPanelContents(panelData.id)}
-
-
-
- );
-});
+ {/* drag handle */}
+
interactionStart('drag', e)}
+ onMouseUp={(e) => interactionStart('drop', e)}
+ >
+
+
+ {/* Resize handle */}
+
interactionStart('resize', e)}
+ onMouseUp={(e) => interactionStart('drop', e)}
+ css={css`
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+ margin: -2px;
+ position: absolute;
+ width: ${euiThemeVars.euiSizeL};
+ height: ${euiThemeVars.euiSizeL};
+ transition: opacity 0.2s, border 0.2s;
+ border-radius: 7px 0 7px 0;
+ border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
+ border-right: 2px solid ${euiThemeVars.euiColorSuccess};
+ :hover {
+ background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
+ cursor: se-resize;
+ }
+ `}
+ />
+
+ {renderPanelContents(panelId)}
+
+
+
+ );
+ }
+);
diff --git a/packages/kbn-grid-layout/grid/grid_row.tsx b/packages/kbn-grid-layout/grid/grid_row.tsx
index 917f661c91740..e797cd570550a 100644
--- a/packages/kbn-grid-layout/grid/grid_row.tsx
+++ b/packages/kbn-grid-layout/grid/grid_row.tsx
@@ -7,41 +7,23 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import React, { forwardRef, useMemo, useRef } from 'react';
+import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
+import { combineLatest, map, pairwise, skip } from 'rxjs';
import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
-import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
+import { DragPreview } from './drag_preview';
import { GridPanel } from './grid_panel';
-import {
- GridLayoutStateManager,
- GridRowData,
- PanelInteractionEvent,
- RuntimeGridSettings,
-} from './types';
-
-const gridColor = transparentize(euiThemeVars.euiColorSuccess, 0.2);
-const getGridBackgroundCSS = (settings: RuntimeGridSettings) => {
- const { gutterSize, columnPixelWidth, rowHeight } = settings;
- return css`
- background-position: top -${gutterSize / 2}px left -${gutterSize / 2}px;
- background-size: ${columnPixelWidth + gutterSize}px ${rowHeight + gutterSize}px;
- background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px),
- linear-gradient(to bottom, ${gridColor} 1px, transparent 1px);
- `;
-};
+import { GridLayoutStateManager, GridRowData, PanelInteractionEvent } from './types';
export const GridRow = forwardRef<
HTMLDivElement,
{
rowIndex: number;
- rowData: GridRowData;
toggleIsCollapsed: () => void;
- targetRowIndex: number | undefined;
- runtimeSettings: RuntimeGridSettings;
renderPanelContents: (panelId: string) => React.ReactNode;
setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
gridLayoutStateManager: GridLayoutStateManager;
@@ -49,10 +31,7 @@ export const GridRow = forwardRef<
>(
(
{
- rowData,
rowIndex,
- targetRowIndex,
- runtimeSettings,
toggleIsCollapsed,
renderPanelContents,
setInteractionEvent,
@@ -60,19 +39,122 @@ export const GridRow = forwardRef<
},
gridRef
) => {
- const dragPreviewRef = useRef
(null);
- const activePanel = useStateFromPublishingSubject(gridLayoutStateManager.activePanel$);
+ const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
+ const [panelIds, setPanelIds] = useState(Object.keys(currentRow.panels));
+ const [rowTitle, setRowTitle] = useState(currentRow.title);
+ const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed);
- const { gutterSize, columnCount, rowHeight } = runtimeSettings;
- const isGridTargeted = activePanel?.id && targetRowIndex === rowIndex;
+ const getRowCount = useCallback(
+ (row: GridRowData) => {
+ const maxRow = Object.values(row.panels).reduce((acc, panel) => {
+ return Math.max(acc, panel.row + panel.height);
+ }, 0);
+ return maxRow || 1;
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [rowIndex]
+ );
+
+ /** Set initial styles based on state at mount to prevent styles from "blipping" */
+ const initialStyles = useMemo(() => {
+ const initialRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex];
+ const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
+ const { gutterSize, columnCount, rowHeight } = runtimeSettings;
+
+ return css`
+ gap: ${gutterSize}px;
+ grid-template-columns: repeat(
+ ${columnCount},
+ calc((100% - ${gutterSize * (columnCount - 1)}px) / ${columnCount})
+ );
+ grid-template-rows: repeat(${getRowCount(initialRow)}, ${rowHeight}px);
+ `;
+ }, [gridLayoutStateManager, getRowCount, rowIndex]);
+
+ useEffect(
+ () => {
+ /** Update the styles of the grid row via a subscription to prevent re-renders */
+ const styleSubscription = combineLatest([
+ gridLayoutStateManager.interactionEvent$,
+ gridLayoutStateManager.gridLayout$,
+ gridLayoutStateManager.runtimeSettings$,
+ ])
+ .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
+ .subscribe(([interactionEvent, gridLayout, runtimeSettings]) => {
+ const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex];
+ if (!rowRef) return;
+
+ const { gutterSize, rowHeight, columnPixelWidth } = runtimeSettings;
- // calculate row count based on the number of rows needed to fit all panels
- const rowCount = useMemo(() => {
- const maxRow = Object.values(rowData.panels).reduce((acc, panel) => {
- return Math.max(acc, panel.row + panel.height);
- }, 0);
- return maxRow || 1;
- }, [rowData]);
+ rowRef.style.gridTemplateRows = `repeat(${getRowCount(
+ gridLayout[rowIndex]
+ )}, ${rowHeight}px)`;
+
+ const targetRow = interactionEvent?.targetRowIndex;
+ if (rowIndex === targetRow && interactionEvent?.type !== 'drop') {
+ // apply "targetted row" styles
+ const gridColor = transparentize(euiThemeVars.euiColorSuccess, 0.2);
+ rowRef.style.backgroundPosition = `top -${gutterSize / 2}px left -${
+ gutterSize / 2
+ }px`;
+ rowRef.style.backgroundSize = ` ${columnPixelWidth + gutterSize}px ${
+ rowHeight + gutterSize
+ }px`;
+ rowRef.style.backgroundImage = `linear-gradient(to right, ${gridColor} 1px, transparent 1px),
+ linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`;
+ rowRef.style.backgroundColor = `${transparentize(
+ euiThemeVars.euiColorSuccess,
+ 0.05
+ )}`;
+ } else {
+ // undo any "targetted row" styles
+ rowRef.style.backgroundPosition = ``;
+ rowRef.style.backgroundSize = ``;
+ rowRef.style.backgroundImage = ``;
+ rowRef.style.backgroundColor = `transparent`;
+ }
+ });
+
+ /**
+ * The things that should trigger a re-render are title, collapsed state, and panel ids - panel positions
+ * are being controlled via CSS styles, so they do not need to trigger a re-render. This subscription ensures
+ * that the row will re-render when one of those three things changes.
+ */
+ const rowStateSubscription = gridLayoutStateManager.gridLayout$
+ .pipe(
+ skip(1), // we are initializing all row state with a value, so skip the initial emit
+ map((gridLayout) => {
+ return {
+ title: gridLayout[rowIndex].title,
+ isCollapsed: gridLayout[rowIndex].isCollapsed,
+ panelIds: Object.keys(gridLayout[rowIndex].panels),
+ };
+ }),
+ pairwise()
+ )
+ .subscribe(([oldRowData, newRowData]) => {
+ if (oldRowData.title !== newRowData.title) setRowTitle(newRowData.title);
+ if (oldRowData.isCollapsed !== newRowData.isCollapsed)
+ setIsCollapsed(newRowData.isCollapsed);
+ if (
+ oldRowData.panelIds.length !== newRowData.panelIds.length ||
+ !(
+ oldRowData.panelIds.every((p) => newRowData.panelIds.includes(p)) &&
+ newRowData.panelIds.every((p) => oldRowData.panelIds.includes(p))
+ )
+ ) {
+ setPanelIds(newRowData.panelIds);
+ }
+ });
+
+ return () => {
+ styleSubscription.unsubscribe();
+ rowStateSubscription.unsubscribe();
+ };
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [rowIndex]
+ );
return (
<>
@@ -85,51 +167,43 @@ export const GridRow = forwardRef<
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
defaultMessage: 'Toggle collapse',
})}
- iconType={rowData.isCollapsed ? 'arrowRight' : 'arrowDown'}
+ iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
onClick={toggleIsCollapsed}
/>
- {rowData.title}
+ {rowTitle}
>
)}
- {!rowData.isCollapsed && (
+ {!isCollapsed && (
- {Object.values(rowData.panels).map((panelData) => (
+ {panelIds.map((panelId) => (
{
e.preventDefault();
e.stopPropagation();
- const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelData.id];
+ const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
if (!panelRef) return;
const panelRect = panelRef.getBoundingClientRect();
setInteractionEvent({
type,
- id: panelData.id,
+ id: panelId,
panelDiv: panelRef,
targetRowIndex: rowIndex,
mouseOffsets: {
@@ -144,32 +218,12 @@ export const GridRow = forwardRef<
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
}
- gridLayoutStateManager.panelRefs.current[rowIndex][panelData.id] = element;
+ gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
}}
/>
))}
- {/* render the drag preview if this row is currently being targetted */}
- {isGridTargeted && (
-
- )}
+
)}
>
diff --git a/packages/kbn-grid-layout/grid/use_grid_layout_events.ts b/packages/kbn-grid-layout/grid/use_grid_layout_events.ts
index dfbd013d3642a..bd6343b9e5652 100644
--- a/packages/kbn-grid-layout/grid/use_grid_layout_events.ts
+++ b/packages/kbn-grid-layout/grid/use_grid_layout_events.ts
@@ -8,6 +8,7 @@
*/
import { useEffect, useRef } from 'react';
+import deepEqual from 'fast-deep-equal';
import { resolveGridRow } from './resolve_grid_row';
import { GridLayoutStateManager, GridPanelData } from './types';
@@ -121,6 +122,7 @@ export const useGridLayoutEvents = ({
maxColumn
);
const targetRow = Math.max(Math.round(localYCoordinate / (rowHeight + gutterSize)), 0);
+
const requestedGridData = { ...currentGridData };
if (isResize) {
requestedGridData.width = Math.max(targetColumn - requestedGridData.column, 1);
@@ -154,8 +156,9 @@ export const useGridLayoutEvents = ({
const resolvedOriginGrid = resolveGridRow(originGrid);
nextLayout[lastRowIndex] = resolvedOriginGrid;
}
-
- gridLayout$.next(nextLayout);
+ if (!deepEqual(currentLayout, nextLayout)) {
+ gridLayout$.next(nextLayout);
+ }
}
};
diff --git a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
index cdb99a9ebbfd0..fe657ae253107 100644
--- a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
+++ b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
@@ -7,10 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { i18n } from '@kbn/i18n';
import { useEffect, useMemo, useRef } from 'react';
-import { BehaviorSubject, combineLatest, debounceTime, map, retry } from 'rxjs';
+import { BehaviorSubject, debounceTime } from 'rxjs';
+
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
+
import {
ActivePanel,
GridLayoutData,
@@ -43,10 +44,14 @@ export const useGridLayoutState = ({
...gridSettings,
columnPixelWidth: 0,
});
+ const panelIds$ = new BehaviorSubject(
+ initialLayout.map(({ panels }) => Object.keys(panels))
+ );
return {
rowRefs,
panelRefs,
+ panelIds$,
gridLayout$,
activePanel$,
gridDimensions$,
@@ -67,117 +72,12 @@ export const useGridLayoutState = ({
const columnPixelWidth =
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
gridSettings.columnCount;
- gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
- });
-
- /**
- * on layout change, update the styles of every panel so that it renders as expected
- */
- const onLayoutChangeSubscription = combineLatest([
- gridLayoutStateManager.gridLayout$,
- gridLayoutStateManager.activePanel$,
- ])
- .pipe(
- map(([gridLayout, activePanel]) => {
- // wait for all panel refs to be ready before continuing
- for (let rowIndex = 0; rowIndex < gridLayout.length; rowIndex++) {
- const currentRow = gridLayout[rowIndex];
- Object.keys(currentRow.panels).forEach((key) => {
- const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][key];
- if (!panelRef && !currentRow.isCollapsed) {
- throw new Error(
- i18n.translate('kbnGridLayout.panelRefNotFoundError', {
- defaultMessage: 'Panel reference does not exist', // the retry will catch this error
- })
- );
- }
- });
- }
- return { gridLayout, activePanel };
- }),
- retry({ delay: 10 }) // retry until panel references all exist
- )
- .subscribe(({ gridLayout, activePanel }) => {
- const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
- const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
- for (let rowIndex = 0; rowIndex < gridLayout.length; rowIndex++) {
- if (activePanel && rowIndex !== currentInteractionEvent?.targetRowIndex) {
- /**
- * If there is an interaction event happening but the current row is not being targetted, it
- * does not need to be re-rendered; so, skip setting the panel styles of this row.
- *
- * If there is **no** interaction event, then this is the initial render so the styles of every
- * panel should be initialized; so, don't skip setting the panel styles.
- */
- continue;
- }
-
- // re-render the targetted row
- const currentRow = gridLayout[rowIndex];
- Object.keys(currentRow.panels).forEach((key) => {
- const panel = currentRow.panels[key];
- const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][key];
- if (!panelRef) {
- return;
- }
-
- const isResize = currentInteractionEvent?.type === 'resize';
- if (panel.id === activePanel?.id) {
- // if the current panel is active, give it fixed positioning depending on the interaction event
- const { position: draggingPosition } = activePanel;
-
- if (isResize) {
- // if the current panel is being resized, ensure it is not shrunk past the size of a single cell
- panelRef.style.width = `${Math.max(
- draggingPosition.right - draggingPosition.left,
- runtimeSettings.columnPixelWidth
- )}px`;
- panelRef.style.height = `${Math.max(
- draggingPosition.bottom - draggingPosition.top,
- runtimeSettings.rowHeight
- )}px`;
-
- // undo any "lock to grid" styles **except** for the top left corner, which stays locked
- panelRef.style.gridColumnStart = `${panel.column + 1}`;
- panelRef.style.gridRowStart = `${panel.row + 1}`;
- panelRef.style.gridColumnEnd = ``;
- panelRef.style.gridRowEnd = ``;
- } else {
- // if the current panel is being dragged, render it with a fixed position + size
- panelRef.style.position = 'fixed';
- panelRef.style.left = `${draggingPosition.left}px`;
- panelRef.style.top = `${draggingPosition.top}px`;
- panelRef.style.width = `${draggingPosition.right - draggingPosition.left}px`;
- panelRef.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
-
- // undo any "lock to grid" styles
- panelRef.style.gridColumnStart = ``;
- panelRef.style.gridRowStart = ``;
- panelRef.style.gridColumnEnd = ``;
- panelRef.style.gridRowEnd = ``;
- }
- } else {
- // if the panel is not being dragged and/or resized, undo any fixed position styles
- panelRef.style.position = '';
- panelRef.style.left = ``;
- panelRef.style.top = ``;
- panelRef.style.width = ``;
- panelRef.style.height = ``;
-
- // and render the panel locked to the grid
- panelRef.style.gridColumnStart = `${panel.column + 1}`;
- panelRef.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
- panelRef.style.gridRowStart = `${panel.row + 1}`;
- panelRef.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
- }
- });
- }
+ gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
});
return () => {
resizeSubscription.unsubscribe();
- onLayoutChangeSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/packages/kbn-grid-layout/tsconfig.json b/packages/kbn-grid-layout/tsconfig.json
index 5a1888db0dc64..f0dd3232a42d5 100644
--- a/packages/kbn-grid-layout/tsconfig.json
+++ b/packages/kbn-grid-layout/tsconfig.json
@@ -17,7 +17,6 @@
"target/**/*"
],
"kbn_references": [
- "@kbn/presentation-publishing",
"@kbn/ui-theme",
"@kbn/i18n",
]
diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts
index 551e99e4ef131..b146be6f6e252 100644
--- a/packages/kbn-management/settings/setting_ids/index.ts
+++ b/packages/kbn-management/settings/setting_ids/index.ts
@@ -83,7 +83,6 @@ export const DISCOVER_SAMPLE_SIZE_ID = 'discover:sampleSize';
export const DISCOVER_SEARCH_FIELDS_FROM_SOURCE_ID = 'discover:searchFieldsFromSource';
export const DISCOVER_SEARCH_ON_PAGE_LOAD_ID = 'discover:searchOnPageLoad';
export const DISCOVER_SHOW_FIELD_STATISTICS_ID = 'discover:showFieldStatistics';
-export const DISCOVER_SHOW_LEGACY_FIELD_TOP_VALUES_ID = 'discover:showLegacyFieldTopValues';
export const DISCOVER_SHOW_MULTI_FIELDS_ID = 'discover:showMultiFields';
export const DISCOVER_SORT_DEFAULT_ORDER_ID = 'discover:sort:defaultOrder';
export const DOC_TABLE_HIDE_TIME_COLUMNS_ID = 'doc_table:hideTimeColumn';
diff --git a/packages/kbn-test/src/jest/resolver.js b/packages/kbn-test/src/jest/resolver.js
index 8f985e9463962..aab1b0f597284 100644
--- a/packages/kbn-test/src/jest/resolver.js
+++ b/packages/kbn-test/src/jest/resolver.js
@@ -51,6 +51,13 @@ module.exports = (request, options) => {
});
}
+ if (request === '@launchdarkly/js-sdk-common') {
+ return resolve.sync('@launchdarkly/js-sdk-common/dist/cjs/index.cjs', {
+ basedir: options.basedir,
+ extensions: options.extensions,
+ });
+ }
+
if (request === `elastic-apm-node`) {
return APM_AGENT_MOCK;
}
diff --git a/packages/kbn-test/src/mocha/junit_report_generation.test.js b/packages/kbn-test/src/mocha/junit_report_generation.test.js
index caf023a795154..6dbc8bf6cf1f8 100644
--- a/packages/kbn-test/src/mocha/junit_report_generation.test.js
+++ b/packages/kbn-test/src/mocha/junit_report_generation.test.js
@@ -55,9 +55,12 @@ describe('dev/mocha/junit report generation', () => {
const [testsuite] = report.testsuites.testsuite;
expect(testsuite.$.time).toMatch(DURATION_REGEX);
expect(testsuite.$.timestamp).toMatch(ISO_DATE_SEC_REGEX);
- expect(testsuite.$).toEqual({
- 'command-line':
- 'node scripts/jest --config=packages/kbn-test/jest.config.js --runInBand --coverage=false --passWithNoTests',
+ const expectedCommandLine = process.env.CI
+ ? 'node scripts/jest --config=packages/kbn-test/jest.config.js --runInBand --coverage=false --passWithNoTests'
+ : 'node node_modules/jest-worker/build/workers/processChild.js';
+
+ expect(testsuite.$).toMatchObject({
+ 'command-line': expectedCommandLine,
failures: '2',
name: 'test',
skipped: '1',
diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts
index d764978c9db92..a3925b3a04f24 100644
--- a/src/dev/build/tasks/os_packages/docker_generator/run.ts
+++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts
@@ -51,7 +51,7 @@ export async function runDockerGenerator(
*/
if (flags.baseImage === 'wolfi')
baseImageName =
- 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:de4d5b06ee2074eb716f29e72b170346fd4715e5f083fc83a378603ce5bd9ced';
+ 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:18153942f0d6e97bc6131cd557c7ed3be6e892846a5df0760896eb8d15b1b236';
let imageFlavor = '';
if (flags.baseImage === 'ubi') imageFlavor += `-ubi`;
diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.test.ts b/src/plugins/data_views/public/data_views/data_views_api_client.test.ts
index 8e1261802fbbc..4eaf2e88f56d9 100644
--- a/src/plugins/data_views/public/data_views/data_views_api_client.test.ts
+++ b/src/plugins/data_views/public/data_views/data_views_api_client.test.ts
@@ -30,9 +30,6 @@ describe('IndexPatternsApiClient', () => {
expect(fetchSpy).toHaveBeenCalledWith(expectedPath, {
// not sure what asResponse is but the rest of the results are useful
asResponse: true,
- headers: {
- 'user-hash': '',
- },
query: {
allow_hidden: undefined,
allow_no_index: undefined,
diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.ts b/src/plugins/data_views/public/data_views/data_views_api_client.ts
index e569e7f25bff6..233b05ea7bc22 100644
--- a/src/plugins/data_views/public/data_views/data_views_api_client.ts
+++ b/src/plugins/data_views/public/data_views/data_views_api_client.ts
@@ -56,6 +56,7 @@ export class DataViewsApiClient implements IDataViewsApiClient {
const userId = await this.getCurrentUserId();
const userHash = userId ? await sha1(userId) : '';
+ const headers = userHash ? { 'user-hash': userHash } : undefined;
const request = body
? this.http.post(url, { query, body, version, asResponse })
@@ -64,7 +65,7 @@ export class DataViewsApiClient implements IDataViewsApiClient {
version,
...cacheOptions,
asResponse,
- headers: { 'user-hash': userHash },
+ headers,
});
return request.catch((resp) => {
diff --git a/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor_natural_language.tsx b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor_natural_language.tsx
index 0b84b504d9507..bdef8c5e3a079 100644
--- a/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor_natural_language.tsx
+++ b/x-pack/packages/kbn-ai-assistant/src/prompt_editor/prompt_editor_natural_language.tsx
@@ -109,6 +109,15 @@ export function PromptEditorNaturalLanguage({
}
}, [handleResizeTextArea, prompt]);
+ useEffect(() => {
+ // Attach the event listener to the window to catch mouseup outside the browser window
+ window.addEventListener('mouseup', handleResizeTextArea);
+
+ return () => {
+ window.removeEventListener('mouseup', handleResizeTextArea);
+ };
+ }, [handleResizeTextArea]);
+
return (
{
const onCloseMock = jest.fn();
@@ -32,19 +32,13 @@ describe('VideoToast', () => {
it('should open the video in a new tab when the gif is clicked', async () => {
const videoGif = screen.getByTestId('video-gif');
await userEvent.click(videoGif);
- expect(window.open).toHaveBeenCalledWith(
- 'https://videos.elastic.co/watch/BrDaDBAAvdygvemFKNAkBW',
- '_blank'
- );
+ expect(window.open).toHaveBeenCalledWith(VIDEO_PAGE, '_blank');
});
it('should open the video in a new tab when the "Watch overview video" button is clicked', async () => {
const watchVideoButton = screen.getByRole('button', { name: 'Watch overview video' });
await userEvent.click(watchVideoButton);
- expect(window.open).toHaveBeenCalledWith(
- 'https://videos.elastic.co/watch/BrDaDBAAvdygvemFKNAkBW',
- '_blank'
- );
+ expect(window.open).toHaveBeenCalledWith(VIDEO_PAGE, '_blank');
});
it('should call the onClose callback when the close button is clicked', async () => {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx
index b1b2bfe02a1eb..8431cf687ff0c 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx
@@ -18,10 +18,8 @@ import React, { useCallback } from 'react';
import * as i18n from './translations';
import theGif from './overview.gif';
-const VIDEO_CONTENT_WIDTH = 250;
-// TODO before removing assistantKnowledgeBaseByDefault feature flag
-// update the VIDEO_PAGE to the correct URL
-const VIDEO_PAGE = `https://videos.elastic.co/watch/BrDaDBAAvdygvemFKNAkBW`;
+const VIDEO_CONTENT_WIDTH = 330;
+export const VIDEO_PAGE = `https://ela.st/seckb`;
const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => {
const openVideoInNewTab = useCallback(() => {
diff --git a/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts
similarity index 87%
rename from x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts
rename to x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts
index 73906fa0a63e3..fc2610b804b69 100644
--- a/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts
+++ b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts
@@ -9,10 +9,10 @@ import { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server';
import { identity } from 'lodash';
import type { MethodKeysOf } from '@kbn/utility-types';
import { httpServerMock } from '@kbn/core/server/mocks';
-import { ActionsRequestHandlerContext } from '../../types';
-import { actionsClientMock } from '../../mocks';
-import { ActionsClientMock } from '../../actions_client/actions_client.mock';
-import { ConnectorType } from '../../application/connector/types';
+import { ActionsRequestHandlerContext } from '../types';
+import { actionsClientMock } from '../mocks';
+import { ActionsClientMock } from '../actions_client/actions_client.mock';
+import { ConnectorType } from '../application/connector/types';
export function mockHandlerArguments(
{
diff --git a/x-pack/plugins/actions/server/routes/connector/create/create.test.ts b/x-pack/plugins/actions/server/routes/connector/create/create.test.ts
index 0f97015bd4f01..7bf91629023c8 100644
--- a/x-pack/plugins/actions/server/routes/connector/create/create.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/create/create.test.ts
@@ -8,7 +8,7 @@
import { createConnectorRoute } from './create';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { omit } from 'lodash';
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
diff --git a/x-pack/plugins/actions/server/routes/connector/delete/delete.test.ts b/x-pack/plugins/actions/server/routes/connector/delete/delete.test.ts
index 9fb3f7f3a8ae5..82e6a6584a641 100644
--- a/x-pack/plugins/actions/server/routes/connector/delete/delete.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/delete/delete.test.ts
@@ -8,7 +8,7 @@
import { deleteConnectorRoute } from './delete';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { actionsClientMock } from '../../../mocks';
import { verifyAccessAndContext } from '../../verify_access_and_context';
diff --git a/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts b/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts
index a9ae5e881f141..a0f5bf0629aec 100644
--- a/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts
@@ -8,7 +8,7 @@
import { executeConnectorRoute } from './execute';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { asHttpRequestExecutionSource } from '../../../lib';
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
import { ActionTypeExecutorResult } from '../../../types';
diff --git a/x-pack/plugins/actions/server/routes/connector/get/get.test.ts b/x-pack/plugins/actions/server/routes/connector/get/get.test.ts
index 28293ae7947f2..cbf0cd86f9912 100644
--- a/x-pack/plugins/actions/server/routes/connector/get/get.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/get/get.test.ts
@@ -8,7 +8,7 @@
import { getConnectorRoute } from './get';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
import { verifyAccessAndContext } from '../../verify_access_and_context';
diff --git a/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts b/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts
index 0ab3b57e238cf..5328cd76e2c4d 100644
--- a/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/get_all/get_all.test.ts
@@ -8,7 +8,7 @@
import { getAllConnectorsRoute } from './get_all';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
diff --git a/x-pack/plugins/actions/server/routes/connector/get_all_system/get_all_system.test.ts b/x-pack/plugins/actions/server/routes/connector/get_all_system/get_all_system.test.ts
index 07221aacddde7..a82eeef55ddda 100644
--- a/x-pack/plugins/actions/server/routes/connector/get_all_system/get_all_system.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/get_all_system/get_all_system.test.ts
@@ -8,7 +8,7 @@
import { getAllConnectorsIncludingSystemRoute } from './get_all_system';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
diff --git a/x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts b/x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts
index e7370c7638a89..bf1ab91c5b6ab 100644
--- a/x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/list_types/list_types.test.ts
@@ -8,7 +8,7 @@
import { httpServiceMock } from '@kbn/core/server/mocks';
import { LicenseType } from '@kbn/licensing-plugin/server';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { listTypesRoute } from './list_types';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { actionsClientMock } from '../../../mocks';
diff --git a/x-pack/plugins/actions/server/routes/connector/list_types_system/list_types_system.test.ts b/x-pack/plugins/actions/server/routes/connector/list_types_system/list_types_system.test.ts
index 07d2d3adcd4f3..7398d020f5972 100644
--- a/x-pack/plugins/actions/server/routes/connector/list_types_system/list_types_system.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/list_types_system/list_types_system.test.ts
@@ -8,7 +8,7 @@
import { httpServiceMock } from '@kbn/core/server/mocks';
import { LicenseType } from '@kbn/licensing-plugin/server';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { listTypesWithSystemRoute } from './list_types_system';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { actionsClientMock } from '../../../mocks';
diff --git a/x-pack/plugins/actions/server/routes/connector/update/update.test.ts b/x-pack/plugins/actions/server/routes/connector/update/update.test.ts
index f48c87fca43c2..870882513c5ae 100644
--- a/x-pack/plugins/actions/server/routes/connector/update/update.test.ts
+++ b/x-pack/plugins/actions/server/routes/connector/update/update.test.ts
@@ -8,7 +8,7 @@
import { updateConnectorRoute } from './update';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../lib/license_state.mock';
-import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from '../../_mock_handler_arguments';
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
import { verifyAccessAndContext } from '../../verify_access_and_context';
import { updateConnectorBodySchema } from '../../../../common/routes/connector/apis/update';
diff --git a/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts b/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts
index 066d558bcfd59..026a53caebe48 100644
--- a/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts
+++ b/x-pack/plugins/actions/server/routes/get_global_execution_kpi.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
-import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from './_mock_handler_arguments';
import { actionsClientMock } from '../actions_client/actions_client.mock';
import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi';
import { verifyAccessAndContext } from './verify_access_and_context';
diff --git a/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts b/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts
index 4654885a49bcb..c31dedb52bb9d 100644
--- a/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts
+++ b/x-pack/plugins/actions/server/routes/get_global_execution_logs.test.ts
@@ -8,7 +8,7 @@
import { getGlobalExecutionLogRoute } from './get_global_execution_logs';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
-import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from './_mock_handler_arguments';
import { actionsClientMock } from '../actions_client/actions_client.mock';
import { IExecutionLogResult } from '../../common';
import { verifyAccessAndContext } from './verify_access_and_context';
diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts
index c1b7dd26aff15..e3474ebcaa0a6 100644
--- a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts
+++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts
@@ -8,7 +8,7 @@
import { getOAuthAccessToken } from './get_oauth_access_token';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
-import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from './_mock_handler_arguments';
import { verifyAccessAndContext } from './verify_access_and_context';
import { actionsConfigMock } from '../actions_config.mock';
import { actionsClientMock } from '../actions_client/actions_client.mock';
diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts
index 39efe6619176b..7e6d70148f4fb 100644
--- a/x-pack/plugins/actions/server/routes/index.ts
+++ b/x-pack/plugins/actions/server/routes/index.ts
@@ -19,7 +19,6 @@ import { executeConnectorRoute } from './connector/execute';
import { getConnectorRoute } from './connector/get';
import { updateConnectorRoute } from './connector/update';
import { getOAuthAccessToken } from './get_oauth_access_token';
-import { defineLegacyRoutes } from './legacy';
import { ActionsConfigurationUtilities } from '../actions_config';
import { getGlobalExecutionLogRoute } from './get_global_execution_logs';
import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi';
@@ -32,9 +31,7 @@ export interface RouteOptions {
}
export function defineRoutes(opts: RouteOptions) {
- const { router, licenseState, actionsConfigUtils, usageCounter } = opts;
-
- defineLegacyRoutes(router, licenseState, usageCounter);
+ const { router, licenseState, actionsConfigUtils } = opts;
createConnectorRoute(router, licenseState);
deleteConnectorRoute(router, licenseState);
diff --git a/x-pack/plugins/actions/server/routes/legacy/create.test.ts b/x-pack/plugins/actions/server/routes/legacy/create.test.ts
deleted file mode 100644
index 05993e44746f9..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/create.test.ts
+++ /dev/null
@@ -1,158 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { createActionRoute } from './create';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { actionsClientMock } from '../../actions_client/actions_client.mock';
-import { verifyAccessAndContext } from '../verify_access_and_context';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../verify_access_and_context', () => ({
- verifyAccessAndContext: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
- (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
-});
-
-describe('createActionRoute', () => {
- it('creates an action with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- createActionRoute(router, licenseState);
-
- const [config, handler] = router.post.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/action"`);
-
- const createResult = {
- id: '1',
- name: 'My name',
- actionTypeId: 'abc',
- config: { foo: true },
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- };
-
- const actionsClient = actionsClientMock.create();
- actionsClient.create.mockResolvedValueOnce(createResult);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- body: {
- name: 'My name',
- actionTypeId: 'abc',
- config: { foo: true },
- secrets: {},
- },
- },
- ['ok']
- );
-
- expect(await handler(context, req, res)).toEqual({ body: createResult });
-
- expect(actionsClient.create).toHaveBeenCalledTimes(1);
- expect(actionsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "action": Object {
- "actionTypeId": "abc",
- "config": Object {
- "foo": true,
- },
- "name": "My name",
- "secrets": Object {},
- },
- },
- ]
- `);
-
- expect(res.ok).toHaveBeenCalledWith({
- body: createResult,
- });
- });
-
- it('ensures the license allows creating actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- createActionRoute(router, licenseState);
-
- const [, handler] = router.post.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.create.mockResolvedValueOnce({
- id: '1',
- name: 'My name',
- actionTypeId: 'abc',
- config: { foo: true },
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- });
-
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {});
-
- await handler(context, req, res);
-
- expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
- });
-
- it('ensures the license check prevents creating actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => {
- throw new Error('OMG');
- });
-
- createActionRoute(router, licenseState);
-
- const [, handler] = router.post.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.create.mockResolvedValueOnce({
- id: '1',
- name: 'My name',
- actionTypeId: 'abc',
- config: { foo: true },
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- });
-
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {});
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- createActionRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.post.mock.calls[0];
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {});
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('create', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/create.ts b/x-pack/plugins/actions/server/routes/legacy/create.ts
deleted file mode 100644
index f667a9e003a77..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/create.ts
+++ /dev/null
@@ -1,67 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { schema } from '@kbn/config-schema';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { IRouter } from '@kbn/core/server';
-import { ActionsRequestHandlerContext } from '../../types';
-import { ILicenseState } from '../../lib';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { verifyAccessAndContext } from '../verify_access_and_context';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { connectorResponseSchemaV1 } from '../../../common/routes/connector/response';
-
-export const bodySchema = schema.object({
- name: schema.string({
- meta: { description: 'The display name for the connector.' },
- }),
- actionTypeId: schema.string({
- meta: { description: 'The connector type identifier.' },
- }),
- config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
- secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
-});
-
-export const createActionRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.post(
- {
- path: `${BASE_ACTION_API_PATH}/action`,
- options: {
- access: 'public',
- summary: `Create a connector`,
- tags: ['oas-tag:connectors'],
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- },
- validate: {
- request: {
- body: bodySchema,
- },
- response: {
- 200: {
- description: 'Indicates a successful call.',
- body: () => connectorResponseSchemaV1,
- },
- },
- },
- },
- router.handleLegacyErrors(
- verifyAccessAndContext(licenseState, async function (context, req, res) {
- const actionsClient = (await context.actions).getActionsClient();
- const action = req.body;
- trackLegacyRouteUsage('create', usageCounter);
- return res.ok({
- body: await actionsClient.create({ action }),
- });
- })
- )
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/legacy/delete.test.ts b/x-pack/plugins/actions/server/routes/legacy/delete.test.ts
deleted file mode 100644
index 2bfb5c7810e46..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/delete.test.ts
+++ /dev/null
@@ -1,138 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { deleteActionRoute } from './delete';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { verifyApiAccess } from '../../lib';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { actionsClientMock } from '../../mocks';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../../lib/verify_api_access', () => ({
- verifyApiAccess: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-describe('deleteActionRoute', () => {
- it('deletes an action with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- deleteActionRoute(router, licenseState);
-
- const [config, handler] = router.delete.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
-
- const actionsClient = actionsClientMock.create();
- actionsClient.delete.mockResolvedValueOnce({});
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: {
- id: '1',
- },
- },
- ['noContent']
- );
-
- expect(await handler(context, req, res)).toEqual(undefined);
-
- expect(actionsClient.delete).toHaveBeenCalledTimes(1);
- expect(actionsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "1",
- },
- ]
- `);
-
- expect(res.noContent).toHaveBeenCalled();
- });
-
- it('ensures the license allows deleting actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- deleteActionRoute(router, licenseState);
-
- const [, handler] = router.delete.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.delete.mockResolvedValueOnce({});
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: { id: '1' },
- }
- );
-
- await handler(context, req, res);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the license check prevents deleting actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- (verifyApiAccess as jest.Mock).mockImplementation(() => {
- throw new Error('OMG');
- });
-
- deleteActionRoute(router, licenseState);
-
- const [, handler] = router.delete.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.delete.mockResolvedValueOnce({});
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- id: '1',
- }
- );
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- deleteActionRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.delete.mock.calls[0];
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: {
- id: '1',
- },
- }
- );
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('delete', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/delete.ts b/x-pack/plugins/actions/server/routes/legacy/delete.ts
deleted file mode 100644
index c7e1e985cc6f0..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/delete.ts
+++ /dev/null
@@ -1,68 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { schema } from '@kbn/config-schema';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { IRouter } from '@kbn/core/server';
-import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../../lib';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { ActionsRequestHandlerContext } from '../../types';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-
-const paramSchema = schema.object({
- id: schema.string({
- meta: { description: 'An identifier for the connector.' },
- }),
-});
-
-export const deleteActionRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.delete(
- {
- path: `${BASE_ACTION_API_PATH}/action/{id}`,
- options: {
- access: 'public',
- summary: `Delete a connector`,
- description: 'WARNING: When you delete a connector, it cannot be recovered.',
- tags: ['oas-tag:connectors'],
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- },
- validate: {
- request: {
- params: paramSchema,
- },
- response: {
- 204: {
- description: 'Indicates a successful call.',
- },
- },
- },
- },
- router.handleLegacyErrors(async function (context, req, res) {
- verifyApiAccess(licenseState);
- if (!context.actions) {
- return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
- }
- const actionsClient = (await context.actions).getActionsClient();
- const { id } = req.params;
- trackLegacyRouteUsage('delete', usageCounter);
- try {
- await actionsClient.delete({ id });
- return res.noContent();
- } catch (e) {
- if (isErrorThatHandlesItsOwnResponse(e)) {
- return e.sendResponse(res);
- }
- throw e;
- }
- })
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
deleted file mode 100644
index c989731407650..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
+++ /dev/null
@@ -1,214 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { executeActionRoute } from './execute';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { verifyApiAccess, ActionTypeDisabledError, asHttpRequestExecutionSource } from '../../lib';
-import { actionsClientMock } from '../../actions_client/actions_client.mock';
-import { ActionTypeExecutorResult } from '../../types';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../../lib/verify_api_access', () => ({
- verifyApiAccess: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-describe('executeActionRoute', () => {
- it('executes an action with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- const actionsClient = actionsClientMock.create();
- actionsClient.execute.mockResolvedValueOnce({ status: 'ok', actionId: '1' });
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- body: {
- params: {
- someData: 'data',
- },
- },
- params: {
- id: '1',
- },
- },
- ['ok']
- );
-
- const executeResult = {
- actionId: '1',
- status: 'ok',
- };
-
- executeActionRoute(router, licenseState);
-
- const [config, handler] = router.post.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}/_execute"`);
-
- expect(await handler(context, req, res)).toEqual({ body: executeResult });
-
- expect(actionsClient.execute).toHaveBeenCalledWith({
- actionId: '1',
- params: {
- someData: 'data',
- },
- source: asHttpRequestExecutionSource(req),
- relatedSavedObjects: [],
- });
-
- expect(res.ok).toHaveBeenCalled();
- });
-
- it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- const actionsClient = actionsClientMock.create();
- actionsClient.execute.mockResolvedValueOnce(null as unknown as ActionTypeExecutorResult);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- body: {
- params: {},
- },
- params: {
- id: '1',
- },
- },
- ['noContent']
- );
-
- executeActionRoute(router, licenseState);
-
- const [, handler] = router.post.mock.calls[0];
-
- expect(await handler(context, req, res)).toEqual(undefined);
-
- expect(actionsClient.execute).toHaveBeenCalledWith({
- actionId: '1',
- params: {},
- source: asHttpRequestExecutionSource(req),
- relatedSavedObjects: [],
- });
-
- expect(res.ok).not.toHaveBeenCalled();
- expect(res.noContent).toHaveBeenCalled();
- });
-
- it('ensures the license allows action execution', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- const actionsClient = actionsClientMock.create();
- actionsClient.execute.mockResolvedValue({
- actionId: '1',
- status: 'ok',
- });
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- body: {},
- params: {},
- },
- ['ok']
- );
-
- executeActionRoute(router, licenseState);
-
- const [, handler] = router.post.mock.calls[0];
-
- await handler(context, req, res);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the license check prevents action execution', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- const actionsClient = actionsClientMock.create();
- actionsClient.execute.mockResolvedValue({
- actionId: '1',
- status: 'ok',
- });
-
- (verifyApiAccess as jest.Mock).mockImplementation(() => {
- throw new Error('OMG');
- });
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- body: {},
- params: {},
- },
- ['ok']
- );
-
- executeActionRoute(router, licenseState);
-
- const [, handler] = router.post.mock.calls[0];
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the action type gets validated for the license', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- const actionsClient = actionsClientMock.create();
- actionsClient.execute.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid'));
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- body: {},
- params: {},
- },
- ['ok', 'forbidden']
- );
-
- executeActionRoute(router, licenseState);
-
- const [, handler] = router.post.mock.calls[0];
-
- await handler(context, req, res);
-
- expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- executeActionRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.post.mock.calls[0];
- const [context, req, res] = mockHandlerArguments({ actionsClient }, { body: {}, params: {} });
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('execute', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts
deleted file mode 100644
index 71b04262075d4..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/execute.ts
+++ /dev/null
@@ -1,88 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { schema } from '@kbn/config-schema';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { IRouter } from '@kbn/core/server';
-import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../../lib';
-
-import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../../types';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { asHttpRequestExecutionSource } from '../../lib/action_execution_source';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { connectorResponseSchemaV1 } from '../../../common/routes/connector/response';
-
-const paramSchema = schema.object({
- id: schema.string({
- meta: { description: 'An identifier for the connector.' },
- }),
-});
-
-const bodySchema = schema.object({
- params: schema.recordOf(schema.string(), schema.any()),
-});
-
-export const executeActionRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.post(
- {
- path: `${BASE_ACTION_API_PATH}/action/{id}/_execute`,
- options: {
- access: 'public',
- summary: `Run a connector`,
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- tags: ['oas-tag:connectors'],
- },
- validate: {
- request: {
- body: bodySchema,
- params: paramSchema,
- },
- response: {
- 200: {
- description: 'Indicates a successful call.',
- body: () => connectorResponseSchemaV1,
- },
- },
- },
- },
- router.handleLegacyErrors(async function (context, req, res) {
- verifyApiAccess(licenseState);
-
- if (!context.actions) {
- return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
- }
-
- const actionsClient = (await context.actions).getActionsClient();
- const { params } = req.body;
- const { id } = req.params;
- trackLegacyRouteUsage('execute', usageCounter);
- try {
- const body: ActionTypeExecutorResult = await actionsClient.execute({
- params,
- actionId: id,
- source: asHttpRequestExecutionSource(req),
- relatedSavedObjects: [],
- });
- return body
- ? res.ok({
- body,
- })
- : res.noContent();
- } catch (e) {
- if (isErrorThatHandlesItsOwnResponse(e)) {
- return e.sendResponse(res);
- }
- throw e;
- }
- })
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/legacy/get.test.ts b/x-pack/plugins/actions/server/routes/legacy/get.test.ts
deleted file mode 100644
index 732c964fb8284..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/get.test.ts
+++ /dev/null
@@ -1,165 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getActionRoute } from './get';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { verifyApiAccess } from '../../lib';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { actionsClientMock } from '../../actions_client/actions_client.mock';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../../lib/verify_api_access', () => ({
- verifyApiAccess: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-describe('getActionRoute', () => {
- it('gets an action with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- getActionRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
-
- const getResult = {
- id: '1',
- actionTypeId: '2',
- name: 'action name',
- config: {},
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- };
-
- const actionsClient = actionsClientMock.create();
- actionsClient.get.mockResolvedValueOnce(getResult);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: { id: '1' },
- },
- ['ok']
- );
-
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Object {
- "actionTypeId": "2",
- "config": Object {},
- "id": "1",
- "isDeprecated": false,
- "isPreconfigured": false,
- "isSystemAction": false,
- "name": "action name",
- },
- }
- `);
-
- expect(actionsClient.get).toHaveBeenCalledTimes(1);
- expect(actionsClient.get.mock.calls[0][0].id).toEqual('1');
-
- expect(res.ok).toHaveBeenCalledWith({
- body: getResult,
- });
- });
-
- it('ensures the license allows getting actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- getActionRoute(router, licenseState);
-
- const [, handler] = router.get.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.get.mockResolvedValueOnce({
- id: '1',
- actionTypeId: '2',
- name: 'action name',
- config: {},
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- });
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: { id: '1' },
- },
- ['ok']
- );
-
- await handler(context, req, res);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the license check prevents getting actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- (verifyApiAccess as jest.Mock).mockImplementation(() => {
- throw new Error('OMG');
- });
-
- getActionRoute(router, licenseState);
-
- const [, handler] = router.get.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.get.mockResolvedValueOnce({
- id: '1',
- actionTypeId: '2',
- name: 'action name',
- config: {},
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- });
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: { id: '1' },
- },
- ['ok']
- );
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- getActionRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.get.mock.calls[0];
- const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: { id: '1' } });
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('get', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/get.ts b/x-pack/plugins/actions/server/routes/legacy/get.ts
deleted file mode 100644
index 571849ccaf478..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/get.ts
+++ /dev/null
@@ -1,63 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { schema } from '@kbn/config-schema';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { IRouter } from '@kbn/core/server';
-import { ILicenseState, verifyApiAccess } from '../../lib';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { ActionsRequestHandlerContext } from '../../types';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { connectorResponseSchemaV1 } from '../../../common/routes/connector/response';
-
-const paramSchema = schema.object({
- id: schema.string({
- meta: { description: 'An identifier for the connector.' },
- }),
-});
-
-export const getActionRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.get(
- {
- path: `${BASE_ACTION_API_PATH}/action/{id}`,
- options: {
- access: 'public',
- summary: `Get connector information`,
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- tags: ['oas-tag:connectors'],
- },
- validate: {
- request: {
- params: paramSchema,
- },
- response: {
- 200: {
- description: 'Indicates a successful call.',
- body: () => connectorResponseSchemaV1,
- },
- },
- },
- },
- router.handleLegacyErrors(async function (context, req, res) {
- verifyApiAccess(licenseState);
- if (!context.actions) {
- return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
- }
- const actionsClient = (await context.actions).getActionsClient();
- const { id } = req.params;
- trackLegacyRouteUsage('get', usageCounter);
- return res.ok({
- body: await actionsClient.get({ id }),
- });
- })
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts b/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts
deleted file mode 100644
index e8657e56259e1..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts
+++ /dev/null
@@ -1,116 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getAllActionRoute } from './get_all';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { verifyApiAccess } from '../../lib';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { actionsClientMock } from '../../actions_client/actions_client.mock';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../../lib/verify_api_access', () => ({
- verifyApiAccess: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-describe('getAllActionRoute', () => {
- it('get all actions with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- getAllActionRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
-
- const actionsClient = actionsClientMock.create();
- actionsClient.getAll.mockResolvedValueOnce([]);
-
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
-
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Array [],
- }
- `);
-
- expect(actionsClient.getAll).toHaveBeenCalledTimes(1);
-
- expect(res.ok).toHaveBeenCalledWith({
- body: [],
- });
- });
-
- it('ensures the license allows getting all actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- getAllActionRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
-
- const actionsClient = actionsClientMock.create();
- actionsClient.getAll.mockResolvedValueOnce([]);
-
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
-
- await handler(context, req, res);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the license check prevents getting all actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- (verifyApiAccess as jest.Mock).mockImplementation(() => {
- throw new Error('OMG');
- });
-
- getAllActionRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions"`);
-
- const actionsClient = actionsClientMock.create();
- actionsClient.getAll.mockResolvedValueOnce([]);
-
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- getAllActionRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.get.mock.calls[0];
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {});
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('getAll', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/get_all.ts b/x-pack/plugins/actions/server/routes/legacy/get_all.ts
deleted file mode 100644
index f0a17acb96691..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/get_all.ts
+++ /dev/null
@@ -1,45 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { IRouter } from '@kbn/core/server';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { ILicenseState, verifyApiAccess } from '../../lib';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { ActionsRequestHandlerContext } from '../../types';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-
-export const getAllActionRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.get(
- {
- path: `${BASE_ACTION_API_PATH}`,
- options: {
- access: 'public',
- summary: `Get all connectors`,
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- tags: ['oas-tag:connectors'],
- },
- validate: {},
- },
- router.handleLegacyErrors(async function (context, req, res) {
- verifyApiAccess(licenseState);
- if (!context.actions) {
- return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
- }
- const actionsClient = (await context.actions).getActionsClient();
- const result = await actionsClient.getAll();
- trackLegacyRouteUsage('getAll', usageCounter);
- return res.ok({
- body: result,
- });
- })
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/legacy/index.ts b/x-pack/plugins/actions/server/routes/legacy/index.ts
deleted file mode 100644
index 37ed5efbd99b9..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/index.ts
+++ /dev/null
@@ -1,32 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { IRouter } from '@kbn/core/server';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { ILicenseState } from '../../lib';
-import { ActionsRequestHandlerContext } from '../../types';
-import { createActionRoute } from './create';
-import { deleteActionRoute } from './delete';
-import { getAllActionRoute } from './get_all';
-import { getActionRoute } from './get';
-import { updateActionRoute } from './update';
-import { listActionTypesRoute } from './list_action_types';
-import { executeActionRoute } from './execute';
-
-export function defineLegacyRoutes(
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) {
- createActionRoute(router, licenseState, usageCounter);
- deleteActionRoute(router, licenseState, usageCounter);
- getActionRoute(router, licenseState, usageCounter);
- getAllActionRoute(router, licenseState, usageCounter);
- updateActionRoute(router, licenseState, usageCounter);
- listActionTypesRoute(router, licenseState, usageCounter);
- executeActionRoute(router, licenseState, usageCounter);
-}
diff --git a/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts
deleted file mode 100644
index ec57c4b9a99a9..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts
+++ /dev/null
@@ -1,178 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { listActionTypesRoute } from './list_action_types';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { verifyApiAccess } from '../../lib';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { LicenseType } from '@kbn/licensing-plugin/server';
-import { actionsClientMock } from '../../mocks';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../../lib/verify_api_access', () => ({
- verifyApiAccess: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-describe('listActionTypesRoute', () => {
- it('lists action types with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- listActionTypesRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
-
- const listTypes = [
- {
- id: '1',
- name: 'name',
- enabled: true,
- enabledInConfig: true,
- enabledInLicense: true,
- minimumLicenseRequired: 'gold' as LicenseType,
- supportedFeatureIds: ['alerting'],
- isSystemActionType: false,
- },
- ];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.listTypes.mockResolvedValueOnce(listTypes);
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
-
- expect(await handler(context, req, res)).toMatchInlineSnapshot(`
- Object {
- "body": Array [
- Object {
- "enabled": true,
- "enabledInConfig": true,
- "enabledInLicense": true,
- "id": "1",
- "isSystemActionType": false,
- "minimumLicenseRequired": "gold",
- "name": "name",
- "supportedFeatureIds": Array [
- "alerting",
- ],
- },
- ],
- }
- `);
-
- expect(res.ok).toHaveBeenCalledWith({
- body: listTypes,
- });
- });
-
- it('ensures the license allows listing action types', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- listActionTypesRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
-
- const listTypes = [
- {
- id: '1',
- name: 'name',
- enabled: true,
- enabledInConfig: true,
- enabledInLicense: true,
- minimumLicenseRequired: 'gold' as LicenseType,
- supportedFeatureIds: ['alerting'],
- isSystemActionType: false,
- },
- ];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.listTypes.mockResolvedValueOnce(listTypes);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: { id: '1' },
- },
- ['ok']
- );
-
- await handler(context, req, res);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the license check prevents listing action types', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- (verifyApiAccess as jest.Mock).mockImplementation(() => {
- throw new Error('OMG');
- });
-
- listActionTypesRoute(router, licenseState);
-
- const [config, handler] = router.get.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`);
-
- const listTypes = [
- {
- id: '1',
- name: 'name',
- enabled: true,
- enabledInConfig: true,
- enabledInLicense: true,
- minimumLicenseRequired: 'gold' as LicenseType,
- supportedFeatureIds: ['alerting'],
- isSystemActionType: false,
- },
- ];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.listTypes.mockResolvedValueOnce(listTypes);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: { id: '1' },
- },
- ['ok']
- );
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- listActionTypesRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.get.mock.calls[0];
- const [context, req, res] = mockHandlerArguments({ actionsClient }, {});
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('listActionTypes', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts b/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts
deleted file mode 100644
index cc3e9c23f240d..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts
+++ /dev/null
@@ -1,48 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { IRouter } from '@kbn/core/server';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { ILicenseState, verifyApiAccess } from '../../lib';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { ActionsRequestHandlerContext } from '../../types';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-
-/**
- * Return all available action types
- * expect system action types
- */
-export const listActionTypesRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.get(
- {
- path: `${BASE_ACTION_API_PATH}/list_action_types`,
- options: {
- access: 'public',
- summary: `Get connector types`,
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- tags: ['oas-tag:connectors'],
- },
- validate: {},
- },
- router.handleLegacyErrors(async function (context, req, res) {
- verifyApiAccess(licenseState);
- if (!context.actions) {
- return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
- }
- const actionsClient = (await context.actions).getActionsClient();
- trackLegacyRouteUsage('listActionTypes', usageCounter);
- return res.ok({
- body: await actionsClient.listTypes(),
- });
- })
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/legacy/update.test.ts b/x-pack/plugins/actions/server/routes/legacy/update.test.ts
deleted file mode 100644
index 493d1c873690e..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/update.test.ts
+++ /dev/null
@@ -1,215 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { updateActionRoute } from './update';
-import { httpServiceMock } from '@kbn/core/server/mocks';
-import { licenseStateMock } from '../../lib/license_state.mock';
-import { verifyApiAccess, ActionTypeDisabledError } from '../../lib';
-import { mockHandlerArguments } from './_mock_handler_arguments';
-import { actionsClientMock } from '../../actions_client/actions_client.mock';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-jest.mock('../../lib/verify_api_access', () => ({
- verifyApiAccess: jest.fn(),
-}));
-
-jest.mock('../../lib/track_legacy_route_usage', () => ({
- trackLegacyRouteUsage: jest.fn(),
-}));
-
-const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
-const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
-
-beforeEach(() => {
- jest.resetAllMocks();
-});
-
-describe('updateActionRoute', () => {
- it('updates an action with proper parameters', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- updateActionRoute(router, licenseState);
-
- const [config, handler] = router.put.mock.calls[0];
-
- expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`);
-
- const updateResult = {
- id: '1',
- actionTypeId: 'my-action-type-id',
- name: 'My name',
- config: { foo: true },
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- };
-
- const actionsClient = actionsClientMock.create();
- actionsClient.update.mockResolvedValueOnce(updateResult);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: {
- id: '1',
- },
- body: {
- name: 'My name',
- config: { foo: true },
- secrets: { key: 'i8oh34yf9783y39' },
- },
- },
- ['ok']
- );
-
- expect(await handler(context, req, res)).toEqual({ body: updateResult });
-
- expect(actionsClient.update).toHaveBeenCalledTimes(1);
- expect(actionsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "action": Object {
- "config": Object {
- "foo": true,
- },
- "name": "My name",
- "secrets": Object {
- "key": "i8oh34yf9783y39",
- },
- },
- "id": "1",
- },
- ]
- `);
-
- expect(res.ok).toHaveBeenCalled();
- });
-
- it('ensures the license allows deleting actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- updateActionRoute(router, licenseState);
-
- const [, handler] = router.put.mock.calls[0];
-
- const updateResult = {
- id: '1',
- actionTypeId: 'my-action-type-id',
- name: 'My name',
- config: { foo: true },
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- };
-
- const actionsClient = actionsClientMock.create();
- actionsClient.update.mockResolvedValueOnce(updateResult);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: {
- id: '1',
- },
- body: {
- name: 'My name',
- config: { foo: true },
- secrets: { key: 'i8oh34yf9783y39' },
- },
- },
- ['ok']
- );
-
- await handler(context, req, res);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the license check prevents deleting actions', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- (verifyApiAccess as jest.Mock).mockImplementation(() => {
- throw new Error('OMG');
- });
-
- updateActionRoute(router, licenseState);
-
- const [, handler] = router.put.mock.calls[0];
-
- const updateResult = {
- id: '1',
- actionTypeId: 'my-action-type-id',
- name: 'My name',
- config: { foo: true },
- isPreconfigured: false,
- isDeprecated: false,
- isSystemAction: false,
- };
-
- const actionsClient = actionsClientMock.create();
- actionsClient.update.mockResolvedValueOnce(updateResult);
-
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- {
- params: {
- id: '1',
- },
- body: {
- name: 'My name',
- config: { foo: true },
- secrets: { key: 'i8oh34yf9783y39' },
- },
- },
- ['ok']
- );
-
- await expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
-
- expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
- });
-
- it('ensures the action type gets validated for the license', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
-
- updateActionRoute(router, licenseState);
-
- const [, handler] = router.put.mock.calls[0];
-
- const actionsClient = actionsClientMock.create();
- actionsClient.update.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid'));
-
- const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [
- 'ok',
- 'forbidden',
- ]);
-
- await handler(context, req, res);
-
- expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
- });
-
- it('should track every call', async () => {
- const licenseState = licenseStateMock.create();
- const router = httpServiceMock.createRouter();
- const actionsClient = actionsClientMock.create();
-
- updateActionRoute(router, licenseState, mockUsageCounter);
- const [, handler] = router.put.mock.calls[0];
- const [context, req, res] = mockHandlerArguments(
- { actionsClient },
- { params: { id: '1' }, body: {} }
- );
- await handler(context, req, res);
- expect(trackLegacyRouteUsage).toHaveBeenCalledWith('update', mockUsageCounter);
- });
-});
diff --git a/x-pack/plugins/actions/server/routes/legacy/update.ts b/x-pack/plugins/actions/server/routes/legacy/update.ts
deleted file mode 100644
index 0bf1ec7ece55d..0000000000000
--- a/x-pack/plugins/actions/server/routes/legacy/update.ts
+++ /dev/null
@@ -1,82 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { schema } from '@kbn/config-schema';
-import { UsageCounter } from '@kbn/usage-collection-plugin/server';
-import { IRouter } from '@kbn/core/server';
-import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../../lib';
-import { BASE_ACTION_API_PATH } from '../../../common';
-import { ActionsRequestHandlerContext } from '../../types';
-import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
-import { connectorResponseSchemaV1 } from '../../../common/routes/connector/response';
-
-const paramSchema = schema.object({
- id: schema.string({
- meta: { description: 'An identifier for the connector.' },
- }),
-});
-
-const bodySchema = schema.object({
- name: schema.string(),
- config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
- secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
-});
-
-export const updateActionRoute = (
- router: IRouter,
- licenseState: ILicenseState,
- usageCounter?: UsageCounter
-) => {
- router.put(
- {
- path: `${BASE_ACTION_API_PATH}/action/{id}`,
- options: {
- access: 'public',
- summary: `Update a connector`,
- // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
- deprecated: true,
- tags: ['oas-tag:connectors'],
- },
- validate: {
- request: {
- body: bodySchema,
- params: paramSchema,
- },
- response: {
- 200: {
- description: 'Indicates a successful call.',
- body: () => connectorResponseSchemaV1,
- },
- },
- },
- },
- router.handleLegacyErrors(async function (context, req, res) {
- verifyApiAccess(licenseState);
- if (!context.actions) {
- return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
- }
- const actionsClient = (await context.actions).getActionsClient();
- const { id } = req.params;
- const { name, config, secrets } = req.body;
- trackLegacyRouteUsage('update', usageCounter);
-
- try {
- return res.ok({
- body: await actionsClient.update({
- id,
- action: { name, config, secrets },
- }),
- });
- } catch (e) {
- if (isErrorThatHandlesItsOwnResponse(e)) {
- return e.sendResponse(res);
- }
- throw e;
- }
- })
- );
-};
diff --git a/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts b/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts
index e079634fbfeff..7c1088e8c1d9e 100644
--- a/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts
+++ b/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts
@@ -7,7 +7,7 @@
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess, ActionTypeDisabledError } from '../lib';
-import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
+import { mockHandlerArguments } from './_mock_handler_arguments';
import { actionsClientMock } from '../actions_client/actions_client.mock';
import { verifyAccessAndContext } from './verify_access_and_context';
diff --git a/x-pack/plugins/alerting/server/manual_tests/action_param_templates.sh b/x-pack/plugins/alerting/server/manual_tests/action_param_templates.sh
index 0b72c5e57f5d7..5a63a4a1ecf51 100644
--- a/x-pack/plugins/alerting/server/manual_tests/action_param_templates.sh
+++ b/x-pack/plugins/alerting/server/manual_tests/action_param_templates.sh
@@ -24,10 +24,10 @@ KIBANA_URL=https://elastic:changeme@localhost:5601
# create email action
ACTION_ID_EMAIL=`curl -X POST --insecure --silent \
- $KIBANA_URL/api/actions/action \
+ $KIBANA_URL/api/actions/connector \
-H "kbn-xsrf: foo" -H "content-type: application/json" \
-d '{
- "actionTypeId": ".email",
+ "connector_type_id": ".email",
"name": "email for action_param_templates test",
"config": {
"from": "team-alerting@example.com",
@@ -41,10 +41,10 @@ echo "email action id: $ACTION_ID_EMAIL"
# create slack action
ACTION_ID_SLACK=`curl -X POST --insecure --silent \
- $KIBANA_URL/api/actions/action \
+ $KIBANA_URL/api/actions/connector \
-H "kbn-xsrf: foo" -H "content-type: application/json" \
-d "{
- \"actionTypeId\": \".slack\",
+ \"connector_type_id\": \".slack\",
\"name\": \"slack for action_param_templates test\",
\"config\": {
},
@@ -56,10 +56,10 @@ echo "slack action id: $ACTION_ID_SLACK"
# create webhook action
ACTION_ID_WEBHOOK=`curl -X POST --insecure --silent \
- $KIBANA_URL/api/actions/action \
+ $KIBANA_URL/api/actions/connector \
-H "kbn-xsrf: foo" -H "content-type: application/json" \
-d "{
- \"actionTypeId\": \".webhook\",
+ \"connector_type_id\": \".webhook\",
\"name\": \"webhook for action_param_templates test\",
\"config\": {
\"url\": \"$SLACK_WEBHOOKURL\",
@@ -108,7 +108,7 @@ ALERT_ID=`curl -X POST --insecure --silent \
}
],
\"params\": {
- \"index\": [\".kibana\"],
+ \"index\": [\".kibana\"],
\"timeField\": \"updated_at\",
\"aggType\": \"count\",
\"groupBy\": \"all\",
diff --git a/x-pack/plugins/cases/common/schema/index.test.ts b/x-pack/plugins/cases/common/schema/index.test.ts
index ae1146b594dbb..64eb2ad393fcb 100644
--- a/x-pack/plugins/cases/common/schema/index.test.ts
+++ b/x-pack/plugins/cases/common/schema/index.test.ts
@@ -13,6 +13,7 @@ import {
limitedStringSchema,
NonEmptyString,
paginationSchema,
+ limitedNumberAsIntegerSchema,
} from '.';
import { MAX_DOCS_PER_PAGE } from '../constants';
@@ -319,4 +320,69 @@ describe('schema', () => {
`);
});
});
+
+ describe('limitedNumberAsIntegerSchema', () => {
+ it('works correctly the number is safe integer', () => {
+ expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1)))
+ .toMatchInlineSnapshot(`
+ Array [
+ "No errors!",
+ ]
+ `);
+ });
+
+ it('fails when given a number that is lower than the minimum', () => {
+ expect(
+ PathReporter.report(
+ limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(Number.MIN_SAFE_INTEGER - 1)
+ )
+ ).toMatchInlineSnapshot(`
+ Array [
+ "The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
+ ]
+ `);
+ });
+
+ it('fails when given a number that is higher than the maximum', () => {
+ expect(
+ PathReporter.report(
+ limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(Number.MAX_SAFE_INTEGER + 1)
+ )
+ ).toMatchInlineSnapshot(`
+ Array [
+ "The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
+ ]
+ `);
+ });
+
+ it('fails when given a null instead of a number', () => {
+ expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(null)))
+ .toMatchInlineSnapshot(`
+ Array [
+ "Invalid value null supplied to : LimitedNumberAsInteger",
+ ]
+ `);
+ });
+
+ it('fails when given a string instead of a number', () => {
+ expect(
+ PathReporter.report(
+ limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode('some string')
+ )
+ ).toMatchInlineSnapshot(`
+ Array [
+ "Invalid value \\"some string\\" supplied to : LimitedNumberAsInteger",
+ ]
+ `);
+ });
+
+ it('fails when given a float number instead of an safe integer number', () => {
+ expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1.2)))
+ .toMatchInlineSnapshot(`
+ Array [
+ "The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
+ ]
+ `);
+ });
+ });
});
diff --git a/x-pack/plugins/cases/common/schema/index.ts b/x-pack/plugins/cases/common/schema/index.ts
index b38d499c8c04c..0bcbdcfb2c480 100644
--- a/x-pack/plugins/cases/common/schema/index.ts
+++ b/x-pack/plugins/cases/common/schema/index.ts
@@ -154,6 +154,24 @@ export const limitedNumberSchema = ({ fieldName, min, max }: LimitedSchemaType)
rt.identity
);
+export const limitedNumberAsIntegerSchema = ({ fieldName }: { fieldName: string }) =>
+ new rt.Type(
+ 'LimitedNumberAsInteger',
+ rt.number.is,
+ (input, context) =>
+ either.chain(rt.number.validate(input, context), (s) => {
+ if (!Number.isSafeInteger(s)) {
+ return rt.failure(
+ input,
+ context,
+ `The ${fieldName} field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
+ );
+ }
+ return rt.success(s);
+ }),
+ rt.identity
+ );
+
export interface RegexStringSchemaType {
codec: rt.Type;
pattern: string;
diff --git a/x-pack/plugins/cases/common/types/api/case/v1.test.ts b/x-pack/plugins/cases/common/types/api/case/v1.test.ts
index a509bdee36525..baf9626d3562e 100644
--- a/x-pack/plugins/cases/common/types/api/case/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/api/case/v1.test.ts
@@ -114,10 +114,15 @@ const basicCase: Case = {
value: true,
},
{
- key: 'second_custom_field_key',
+ key: 'third_custom_field_key',
type: CustomFieldTypes.TEXT,
value: 'www.example.com',
},
+ {
+ key: 'fourth_custom_field_key',
+ type: CustomFieldTypes.NUMBER,
+ value: 3,
+ },
],
};
@@ -149,6 +154,11 @@ describe('CasePostRequestRt', () => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
+ {
+ key: 'third_custom_field_key',
+ type: CustomFieldTypes.NUMBER,
+ value: 3,
+ },
],
};
@@ -322,6 +332,44 @@ describe('CasePostRequestRt', () => {
);
});
+ it(`throws an error when a number customFields is more than ${Number.MAX_SAFE_INTEGER}`, () => {
+ expect(
+ PathReporter.report(
+ CasePostRequestRt.decode({
+ ...defaultRequest,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: CustomFieldTypes.NUMBER,
+ value: Number.MAX_SAFE_INTEGER + 1,
+ },
+ ],
+ })
+ )
+ ).toContain(
+ `The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
+ );
+ });
+
+ it(`throws an error when a number customFields is less than ${Number.MIN_SAFE_INTEGER}`, () => {
+ expect(
+ PathReporter.report(
+ CasePostRequestRt.decode({
+ ...defaultRequest,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: CustomFieldTypes.NUMBER,
+ value: Number.MIN_SAFE_INTEGER - 1,
+ },
+ ],
+ })
+ )
+ ).toContain(
+ `The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
+ );
+ });
+
it('throws an error when a text customField is an empty string', () => {
expect(
PathReporter.report(
@@ -665,6 +713,11 @@ describe('CasePatchRequestRt', () => {
type: 'toggle',
value: true,
},
+ {
+ key: 'third_custom_field_key',
+ type: 'number',
+ value: 123,
+ },
],
};
diff --git a/x-pack/plugins/cases/common/types/api/case/v1.ts b/x-pack/plugins/cases/common/types/api/case/v1.ts
index 7a45f92fa4668..f66df68169e5b 100644
--- a/x-pack/plugins/cases/common/types/api/case/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/case/v1.ts
@@ -29,7 +29,11 @@ import {
NonEmptyString,
paginationSchema,
} from '../../../schema';
-import { CaseCustomFieldToggleRt, CustomFieldTextTypeRt } from '../../domain';
+import {
+ CaseCustomFieldToggleRt,
+ CustomFieldTextTypeRt,
+ CustomFieldNumberTypeRt,
+} from '../../domain';
import {
CaseRt,
CaseSettingsRt,
@@ -41,7 +45,10 @@ import {
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseUserProfileRt, UserRt } from '../../domain/user/v1';
import { CasesStatusResponseRt } from '../stats/v1';
-import { CaseCustomFieldTextWithValidationValueRt } from '../custom_field/v1';
+import {
+ CaseCustomFieldTextWithValidationValueRt,
+ CaseCustomFieldNumberWithValidationValueRt,
+} from '../custom_field/v1';
const CaseCustomFieldTextWithValidationRt = rt.strict({
key: rt.string,
@@ -49,7 +56,17 @@ const CaseCustomFieldTextWithValidationRt = rt.strict({
value: rt.union([CaseCustomFieldTextWithValidationValueRt('value'), rt.null]),
});
-const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]);
+const CaseCustomFieldNumberWithValidationRt = rt.strict({
+ key: rt.string,
+ type: CustomFieldNumberTypeRt,
+ value: rt.union([CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }), rt.null]),
+});
+
+const CustomFieldRt = rt.union([
+ CaseCustomFieldTextWithValidationRt,
+ CaseCustomFieldToggleRt,
+ CaseCustomFieldNumberWithValidationRt,
+]);
export const CaseRequestCustomFieldsRt = limitedArraySchema({
codec: CustomFieldRt,
diff --git a/x-pack/plugins/cases/common/types/api/configure/v1.test.ts b/x-pack/plugins/cases/common/types/api/configure/v1.test.ts
index c16dfbc60eaf7..64baf7b2e46f4 100644
--- a/x-pack/plugins/cases/common/types/api/configure/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/api/configure/v1.test.ts
@@ -36,6 +36,7 @@ import {
CustomFieldConfigurationWithoutTypeRt,
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
+ NumberCustomFieldConfigurationRt,
TemplateConfigurationRt,
} from './v1';
@@ -79,6 +80,12 @@ describe('configure', () => {
type: CustomFieldTypes.TOGGLE,
required: false,
},
+ {
+ key: 'number_custom_field',
+ label: 'Number custom field',
+ type: CustomFieldTypes.NUMBER,
+ required: false,
+ },
],
};
const query = ConfigurationRequestRt.decode(request);
@@ -512,6 +519,93 @@ describe('configure', () => {
});
});
+ describe('NumberCustomFieldConfigurationRt', () => {
+ const defaultRequest = {
+ key: 'my_number_custom_field',
+ label: 'Number Custom Field',
+ type: CustomFieldTypes.NUMBER,
+ required: true,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = NumberCustomFieldConfigurationRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('has expected attributes in request with defaultValue', () => {
+ const query = NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ defaultValue: 1,
+ });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, defaultValue: 1 },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('defaultValue fails if the type is string', () => {
+ expect(
+ PathReporter.report(
+ NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ defaultValue: 'string',
+ })
+ )[0]
+ ).toContain('Invalid value "string" supplied');
+ });
+
+ it('defaultValue fails if the type is boolean', () => {
+ expect(
+ PathReporter.report(
+ NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ defaultValue: false,
+ })
+ )[0]
+ ).toContain('Invalid value false supplied');
+ });
+
+ it(`throws an error if the default value is more than ${Number.MAX_SAFE_INTEGER}`, () => {
+ expect(
+ PathReporter.report(
+ NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ defaultValue: Number.MAX_SAFE_INTEGER + 1,
+ })
+ )[0]
+ ).toContain(
+ 'The defaultValue field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ );
+ });
+
+ it(`throws an error if the default value is less than ${Number.MIN_SAFE_INTEGER}`, () => {
+ expect(
+ PathReporter.report(
+ NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ defaultValue: Number.MIN_SAFE_INTEGER - 1,
+ })
+ )[0]
+ ).toContain(
+ 'The defaultValue field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ );
+ });
+ });
+
describe('TemplateConfigurationRt', () => {
const defaultRequest = {
key: 'template_key_1',
diff --git a/x-pack/plugins/cases/common/types/api/configure/v1.ts b/x-pack/plugins/cases/common/types/api/configure/v1.ts
index bd2e1f5c11af0..52843da1ac1ad 100644
--- a/x-pack/plugins/cases/common/types/api/configure/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/configure/v1.ts
@@ -18,12 +18,19 @@ import {
MAX_TEMPLATE_TAG_LENGTH,
} from '../../../constants';
import { limitedArraySchema, limitedStringSchema, regexStringRt } from '../../../schema';
-import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../../domain';
+import {
+ CustomFieldTextTypeRt,
+ CustomFieldToggleTypeRt,
+ CustomFieldNumberTypeRt,
+} from '../../domain';
import type { Configurations, Configuration } from '../../domain/configure/v1';
import { ConfigurationBasicWithoutOwnerRt, ClosureTypeRt } from '../../domain/configure/v1';
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseBaseOptionalFieldsRequestRt } from '../case/v1';
-import { CaseCustomFieldTextWithValidationValueRt } from '../custom_field/v1';
+import {
+ CaseCustomFieldTextWithValidationValueRt,
+ CaseCustomFieldNumberWithValidationValueRt,
+} from '../custom_field/v1';
export const CustomFieldConfigurationWithoutTypeRt = rt.strict({
/**
@@ -64,8 +71,25 @@ export const ToggleCustomFieldConfigurationRt = rt.intersection([
),
]);
+export const NumberCustomFieldConfigurationRt = rt.intersection([
+ rt.strict({ type: CustomFieldNumberTypeRt }),
+ CustomFieldConfigurationWithoutTypeRt,
+ rt.exact(
+ rt.partial({
+ defaultValue: rt.union([
+ CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'defaultValue' }),
+ rt.null,
+ ]),
+ })
+ ),
+]);
+
export const CustomFieldsConfigurationRt = limitedArraySchema({
- codec: rt.union([TextCustomFieldConfigurationRt, ToggleCustomFieldConfigurationRt]),
+ codec: rt.union([
+ TextCustomFieldConfigurationRt,
+ ToggleCustomFieldConfigurationRt,
+ NumberCustomFieldConfigurationRt,
+ ]),
min: 0,
max: MAX_CUSTOM_FIELDS_PER_CASE,
fieldName: 'customFields',
diff --git a/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts b/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts
index 83d9a437c998d..d17c936ff4463 100644
--- a/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/api/custom_field/v1.test.ts
@@ -7,7 +7,11 @@
import { PathReporter } from 'io-ts/lib/PathReporter';
import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants';
-import { CaseCustomFieldTextWithValidationValueRt, CustomFieldPutRequestRt } from './v1';
+import {
+ CaseCustomFieldTextWithValidationValueRt,
+ CustomFieldPutRequestRt,
+ CaseCustomFieldNumberWithValidationValueRt,
+} from './v1';
describe('Custom Fields', () => {
describe('CaseCustomFieldTextWithValidationValueRt', () => {
@@ -100,4 +104,34 @@ describe('Custom Fields', () => {
).toContain('The value field cannot be an empty string.');
});
});
+
+ describe('CaseCustomFieldNumberWithValidationValueRt', () => {
+ const numberCustomFieldValueType = CaseCustomFieldNumberWithValidationValueRt({
+ fieldName: 'value',
+ });
+ it('should decode number correctly', () => {
+ const query = numberCustomFieldValueType.decode(123);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: 123,
+ });
+ });
+
+ it('should not be more than Number.MAX_SAFE_INTEGER', () => {
+ expect(
+ PathReporter.report(numberCustomFieldValueType.decode(Number.MAX_SAFE_INTEGER + 1))[0]
+ ).toContain(
+ 'The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ );
+ });
+
+ it('should not be less than Number.MIN_SAFE_INTEGER', () => {
+ expect(
+ PathReporter.report(numberCustomFieldValueType.decode(Number.MIN_SAFE_INTEGER - 1))[0]
+ ).toContain(
+ 'The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/cases/common/types/api/custom_field/v1.ts b/x-pack/plugins/cases/common/types/api/custom_field/v1.ts
index fb59f187991b3..c3e618278adbe 100644
--- a/x-pack/plugins/cases/common/types/api/custom_field/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/custom_field/v1.ts
@@ -7,7 +7,7 @@
import * as rt from 'io-ts';
import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants';
-import { limitedStringSchema } from '../../../schema';
+import { limitedStringSchema, limitedNumberAsIntegerSchema } from '../../../schema';
export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) =>
limitedStringSchema({
@@ -16,12 +16,22 @@ export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) =>
max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH,
});
+export const CaseCustomFieldNumberWithValidationValueRt = ({ fieldName }: { fieldName: string }) =>
+ limitedNumberAsIntegerSchema({
+ fieldName,
+ });
+
/**
* Update custom_field
*/
export const CustomFieldPutRequestRt = rt.strict({
- value: rt.union([rt.boolean, rt.null, CaseCustomFieldTextWithValidationValueRt('value')]),
+ value: rt.union([
+ rt.boolean,
+ rt.null,
+ CaseCustomFieldTextWithValidationValueRt('value'),
+ CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }),
+ ]),
caseVersion: rt.string,
});
diff --git a/x-pack/plugins/cases/common/types/domain/case/v1.test.ts b/x-pack/plugins/cases/common/types/domain/case/v1.test.ts
index 267e08d205f15..b0a6f96bcacd0 100644
--- a/x-pack/plugins/cases/common/types/domain/case/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/domain/case/v1.test.ts
@@ -85,6 +85,11 @@ const basicCase = {
type: 'toggle',
value: true,
},
+ {
+ key: 'third_custom_field_key',
+ type: 'number',
+ value: 0,
+ },
],
};
@@ -193,6 +198,11 @@ describe('CaseAttributesRt', () => {
type: 'toggle',
value: true,
},
+ {
+ key: 'third_custom_field_key',
+ type: 'number',
+ value: 0,
+ },
],
};
diff --git a/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts b/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
index 13637fb4d8c68..59682de1e7c7a 100644
--- a/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
@@ -16,6 +16,7 @@ import {
TemplateConfigurationRt,
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
+ NumberCustomFieldConfigurationRt,
} from './v1';
describe('configure', () => {
@@ -47,6 +48,13 @@ describe('configure', () => {
required: false,
};
+ const numberCustomField = {
+ key: 'number_custom_field',
+ label: 'Number custom field',
+ type: CustomFieldTypes.NUMBER,
+ required: false,
+ };
+
const templateWithAllCaseFields = {
key: 'template_sample_1',
name: 'Sample template 1',
@@ -98,7 +106,7 @@ describe('configure', () => {
const defaultRequest = {
connector: resilient,
closure_type: 'close-by-user',
- customFields: [textCustomField, toggleCustomField],
+ customFields: [textCustomField, toggleCustomField, numberCustomField],
templates: [],
owner: 'cases',
created_at: '2020-02-19T23:06:33.798Z',
@@ -122,7 +130,7 @@ describe('configure', () => {
_tag: 'Right',
right: {
...defaultRequest,
- customFields: [textCustomField, toggleCustomField],
+ customFields: [textCustomField, toggleCustomField, numberCustomField],
},
});
});
@@ -134,7 +142,7 @@ describe('configure', () => {
_tag: 'Right',
right: {
...defaultRequest,
- customFields: [textCustomField, toggleCustomField],
+ customFields: [textCustomField, toggleCustomField, numberCustomField],
},
});
});
@@ -142,14 +150,14 @@ describe('configure', () => {
it('removes foo:bar attributes from custom fields', () => {
const query = ConfigurationAttributesRt.decode({
...defaultRequest,
- customFields: [{ ...textCustomField, foo: 'bar' }, toggleCustomField],
+ customFields: [{ ...textCustomField, foo: 'bar' }, toggleCustomField, numberCustomField],
});
expect(query).toStrictEqual({
_tag: 'Right',
right: {
...defaultRequest,
- customFields: [textCustomField, toggleCustomField],
+ customFields: [textCustomField, toggleCustomField, numberCustomField],
},
});
});
@@ -351,6 +359,62 @@ describe('configure', () => {
});
});
+ describe('NumberCustomFieldConfigurationRt', () => {
+ const defaultRequest = {
+ key: 'my_number_custom_field',
+ label: 'Number Custom Field',
+ type: CustomFieldTypes.NUMBER,
+ required: false,
+ };
+
+ it('has expected attributes in request with required: false', () => {
+ const query = NumberCustomFieldConfigurationRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('has expected attributes in request with defaultValue and required: true', () => {
+ const query = NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ required: true,
+ defaultValue: 0,
+ });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: {
+ ...defaultRequest,
+ required: true,
+ defaultValue: 0,
+ },
+ });
+ });
+
+ it('defaultValue fails if the type is not number', () => {
+ expect(
+ PathReporter.report(
+ NumberCustomFieldConfigurationRt.decode({
+ ...defaultRequest,
+ required: true,
+ defaultValue: 'foobar',
+ })
+ )[0]
+ ).toContain('Invalid value "foobar" supplied');
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+ });
+
describe('TemplateConfigurationRt', () => {
const defaultRequest = templateWithAllCaseFields;
diff --git a/x-pack/plugins/cases/common/types/domain/configure/v1.ts b/x-pack/plugins/cases/common/types/domain/configure/v1.ts
index 1e4e30c95e381..17760922d2cda 100644
--- a/x-pack/plugins/cases/common/types/domain/configure/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/configure/v1.ts
@@ -8,7 +8,11 @@
import * as rt from 'io-ts';
import { CaseConnectorRt, ConnectorMappingsRt } from '../connector/v1';
import { UserRt } from '../user/v1';
-import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../custom_field/v1';
+import {
+ CustomFieldTextTypeRt,
+ CustomFieldToggleTypeRt,
+ CustomFieldNumberTypeRt,
+} from '../custom_field/v1';
import { CaseBaseOptionalFieldsRt } from '../case/v1';
export const ClosureTypeRt = rt.union([
@@ -51,9 +55,20 @@ export const ToggleCustomFieldConfigurationRt = rt.intersection([
),
]);
+export const NumberCustomFieldConfigurationRt = rt.intersection([
+ rt.strict({ type: CustomFieldNumberTypeRt }),
+ CustomFieldConfigurationWithoutTypeRt,
+ rt.exact(
+ rt.partial({
+ defaultValue: rt.union([rt.number, rt.null]),
+ })
+ ),
+]);
+
export const CustomFieldConfigurationRt = rt.union([
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
+ NumberCustomFieldConfigurationRt,
]);
export const CustomFieldsConfigurationRt = rt.array(CustomFieldConfigurationRt);
diff --git a/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts b/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts
index ea57d3e3201c1..5513325d30fb0 100644
--- a/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts
@@ -42,6 +42,22 @@ describe('CaseCustomFieldRt', () => {
value: null,
},
],
+ [
+ 'type number value number',
+ {
+ key: 'number_custom_field_1',
+ type: 'number',
+ value: 1,
+ },
+ ],
+ [
+ 'type number value null',
+ {
+ key: 'number_custom_field_2',
+ type: 'number',
+ value: null,
+ },
+ ],
])(`has expected attributes for customField with %s`, (_, customField) => {
const query = CaseCustomFieldRt.decode(customField);
@@ -70,4 +86,14 @@ describe('CaseCustomFieldRt', () => {
expect(PathReporter.report(query)[0]).toContain('Invalid value "hello" supplied');
});
+
+ it('fails if number type but value is a string', () => {
+ const query = CaseCustomFieldRt.decode({
+ key: 'list_custom_field_1',
+ type: 'number',
+ value: 'hi',
+ });
+
+ expect(PathReporter.report(query)[0]).toContain('Invalid value "hi" supplied');
+ });
});
diff --git a/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts b/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts
index 4878fea326b04..d0f9404f8f113 100644
--- a/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts
@@ -9,10 +9,12 @@ import * as rt from 'io-ts';
export enum CustomFieldTypes {
TEXT = 'text',
TOGGLE = 'toggle',
+ NUMBER = 'number',
}
export const CustomFieldTextTypeRt = rt.literal(CustomFieldTypes.TEXT);
export const CustomFieldToggleTypeRt = rt.literal(CustomFieldTypes.TOGGLE);
+export const CustomFieldNumberTypeRt = rt.literal(CustomFieldTypes.NUMBER);
const CaseCustomFieldTextRt = rt.strict({
key: rt.string,
@@ -26,10 +28,21 @@ export const CaseCustomFieldToggleRt = rt.strict({
value: rt.union([rt.boolean, rt.null]),
});
-export const CaseCustomFieldRt = rt.union([CaseCustomFieldTextRt, CaseCustomFieldToggleRt]);
+export const CaseCustomFieldNumberRt = rt.strict({
+ key: rt.string,
+ type: CustomFieldNumberTypeRt,
+ value: rt.union([rt.number, rt.null]),
+});
+
+export const CaseCustomFieldRt = rt.union([
+ CaseCustomFieldTextRt,
+ CaseCustomFieldToggleRt,
+ CaseCustomFieldNumberRt,
+]);
export const CaseCustomFieldsRt = rt.array(CaseCustomFieldRt);
export type CaseCustomFields = rt.TypeOf;
export type CaseCustomField = rt.TypeOf;
export type CaseCustomFieldToggle = rt.TypeOf;
export type CaseCustomFieldText = rt.TypeOf;
+export type CaseCustomFieldNumber = rt.TypeOf;
diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts
index 2e11c3a64caae..7fa5b54db00ec 100644
--- a/x-pack/plugins/cases/public/common/translations.ts
+++ b/x-pack/plugins/cases/public/common/translations.ts
@@ -300,6 +300,12 @@ export const MAX_LENGTH_ERROR = (field: string, length: number) =>
'The length of the {field} is too long. The maximum length is {length} characters.',
});
+export const SAFE_INTEGER_NUMBER_ERROR = (field: string) =>
+ i18n.translate('xpack.cases.customFields.safeIntegerNumberError', {
+ values: { field },
+ defaultMessage: `The value of the {field} should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`,
+ });
+
export const MAX_TAGS_ERROR = (length: number) =>
i18n.translate('xpack.cases.createCase.maxTagsError', {
values: { length },
diff --git a/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx b/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx
index 7746c5f0a4f1b..84f2c9f3726d6 100644
--- a/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/columns_popover.test.tsx
@@ -14,7 +14,8 @@ import type { AppMockRenderer } from '../../common/mock';
import { createAppMockRenderer } from '../../common/mock';
import { ColumnsPopover } from './columns_popover';
-describe('ColumnsPopover', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/174682
+describe.skip('ColumnsPopover', () => {
let appMockRenderer: AppMockRenderer;
beforeEach(() => {
diff --git a/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.test.tsx
index f11e5826ca91c..9a96b0a342771 100644
--- a/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_form_fields/custom_fields.test.tsx
@@ -78,7 +78,7 @@ describe.skip('CustomFields', () => {
);
- expect(await screen.findAllByTestId('form-optional-field-label')).toHaveLength(2);
+ expect(await screen.findAllByTestId('form-optional-field-label')).toHaveLength(4);
});
it('should not set default value when in edit mode', async () => {
@@ -115,12 +115,14 @@ describe.skip('CustomFields', () => {
const customFields = customFieldsWrapper.querySelectorAll('.euiFormRow');
- expect(customFields).toHaveLength(4);
+ expect(customFields).toHaveLength(6);
expect(customFields[0]).toHaveTextContent('My test label 1');
expect(customFields[1]).toHaveTextContent('My test label 2');
expect(customFields[2]).toHaveTextContent('My test label 3');
expect(customFields[3]).toHaveTextContent('My test label 4');
+ expect(customFields[4]).toHaveTextContent('My test label 5');
+ expect(customFields[5]).toHaveTextContent('My test label 6');
});
it('should update the custom fields', async () => {
@@ -132,6 +134,7 @@ describe.skip('CustomFields', () => {
const textField = customFieldsConfigurationMock[2];
const toggleField = customFieldsConfigurationMock[3];
+ const numberField = customFieldsConfigurationMock[5];
await userEvent.type(
await screen.findByTestId(`${textField.key}-${textField.type}-create-custom-field`),
@@ -140,6 +143,10 @@ describe.skip('CustomFields', () => {
await userEvent.click(
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
+ await userEvent.type(
+ await screen.findByTestId(`${numberField.key}-${numberField.type}-create-custom-field`),
+ '4'
+ );
await userEvent.click(await screen.findByText('Submit'));
@@ -152,6 +159,8 @@ describe.skip('CustomFields', () => {
[customFieldsConfigurationMock[1].key]: customFieldsConfigurationMock[1].defaultValue,
[textField.key]: 'hello',
[toggleField.key]: true,
+ [customFieldsConfigurationMock[4].key]: customFieldsConfigurationMock[4].defaultValue,
+ [numberField.key]: '4',
},
},
true
diff --git a/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx b/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
index ac162e41a47e4..438b0a24841e9 100644
--- a/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_form_fields/index.test.tsx
@@ -206,6 +206,7 @@ describe('CaseFormFields', () => {
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];
+ const numberField = customFieldsConfigurationMock[4];
const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
@@ -219,6 +220,13 @@ describe('CaseFormFields', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
+ const numberCustomField = await screen.findByTestId(
+ `${numberField.key}-${numberField.type}-create-custom-field`
+ );
+
+ await user.clear(numberCustomField);
+ await user.paste('4321');
+
await user.click(await screen.findByText('Submit'));
await waitFor(() => {
@@ -230,6 +238,7 @@ describe('CaseFormFields', () => {
test_key_1: 'My text test value 1',
test_key_2: false,
test_key_4: false,
+ test_key_5: '4321',
},
},
true
@@ -268,6 +277,7 @@ describe('CaseFormFields', () => {
test_key_1: 'Test custom filed value',
test_key_2: true,
test_key_4: false,
+ test_key_5: 123,
},
},
true
diff --git a/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx
index 3b9d762137abb..67d8f8fd05764 100644
--- a/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx
@@ -89,7 +89,7 @@ describe('Case View Page files tab', () => {
exact: false,
});
- expect(customFields.length).toBe(4);
+ expect(customFields.length).toBe(6);
expect(await within(customFields[0]).findByRole('heading')).toHaveTextContent(
'My test label 1'
@@ -103,6 +103,12 @@ describe('Case View Page files tab', () => {
expect(await within(customFields[3]).findByRole('heading')).toHaveTextContent(
'My test label 4'
);
+ expect(await within(customFields[4]).findByRole('heading')).toHaveTextContent(
+ 'My test label 5'
+ );
+ expect(await within(customFields[5]).findByRole('heading')).toHaveTextContent(
+ 'My test label 6'
+ );
});
it('pass the permissions to custom fields correctly', async () => {
diff --git a/x-pack/plugins/cases/public/components/configure_cases/flyout.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/flyout.test.tsx
index b3c782f83fb50..8b42dd7df6f0d 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/flyout.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/flyout.test.tsx
@@ -612,6 +612,16 @@ describe('CommonFlyout ', () => {
type: 'toggle',
value: false,
},
+ {
+ key: 'test_key_5',
+ type: 'number',
+ value: 123,
+ },
+ {
+ key: 'test_key_6',
+ type: 'number',
+ value: null,
+ },
],
},
});
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
index e058d982e7367..7a29c959d2525 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
@@ -715,6 +715,8 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
+ { ...customFieldsConfigurationMock[4] },
+ { ...customFieldsConfigurationMock[5] },
],
templates: [],
id: '',
@@ -774,6 +776,8 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
+ { ...customFieldsConfigurationMock[4] },
+ { ...customFieldsConfigurationMock[5] },
],
templates: [
{
@@ -867,6 +871,16 @@ describe('ConfigureCases', () => {
type: customFieldsConfigurationMock[3].type,
value: false,
},
+ {
+ key: customFieldsConfigurationMock[4].key,
+ type: customFieldsConfigurationMock[4].type,
+ value: customFieldsConfigurationMock[4].defaultValue,
+ },
+ {
+ key: customFieldsConfigurationMock[5].key,
+ type: customFieldsConfigurationMock[5].type,
+ value: null,
+ },
{
key: expect.anything(),
type: CustomFieldTypes.TEXT as const,
@@ -930,6 +944,8 @@ describe('ConfigureCases', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
+ { ...customFieldsConfigurationMock[4] },
+ { ...customFieldsConfigurationMock[5] },
],
templates: [],
id: '',
@@ -1107,6 +1123,16 @@ describe('ConfigureCases', () => {
type: customFieldsConfigurationMock[3].type,
value: false, // when no default value for toggle, we set it to false
},
+ {
+ key: customFieldsConfigurationMock[4].key,
+ type: customFieldsConfigurationMock[4].type,
+ value: customFieldsConfigurationMock[4].defaultValue,
+ },
+ {
+ key: customFieldsConfigurationMock[5].key,
+ type: customFieldsConfigurationMock[5].type,
+ value: null,
+ },
],
},
},
diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx
index 0f28e6f9db1c2..252726ef559c9 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx
@@ -517,6 +517,7 @@ describe('Create case', () => {
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];
+ const numberField = customFieldsConfigurationMock[4];
expect(await screen.findByTestId('caseCustomFields')).toBeInTheDocument();
@@ -532,6 +533,14 @@ describe('Create case', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
+ const numberCustomField = await screen.findByTestId(
+ `${numberField.key}-${numberField.type}-create-custom-field`
+ );
+
+ await user.clear(numberCustomField);
+ await user.click(numberCustomField);
+ await user.paste('678');
+
await user.click(await screen.findByTestId('create-case-submit'));
await waitFor(() => expect(postCase).toHaveBeenCalled());
@@ -544,6 +553,8 @@ describe('Create case', () => {
{ ...customFieldsMock[1], value: false }, // toggled the default
customFieldsMock[2],
{ ...customFieldsMock[3], value: false },
+ { ...customFieldsMock[4], value: 678 },
+ customFieldsMock[5],
{
key: 'my_custom_field_key',
type: CustomFieldTypes.TEXT,
diff --git a/x-pack/plugins/cases/public/components/custom_fields/builder.tsx b/x-pack/plugins/cases/public/components/custom_fields/builder.tsx
index d2ee25d08bfa6..4baf050fd0f52 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/builder.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/builder.tsx
@@ -9,8 +9,10 @@ import type { CustomFieldBuilderMap } from './types';
import { CustomFieldTypes } from '../../../common/types/domain';
import { configureTextCustomFieldFactory } from './text/configure_text_field';
import { configureToggleCustomFieldFactory } from './toggle/configure_toggle_field';
+import { configureNumberCustomFieldFactory } from './number/configure_number_field';
export const builderMap = Object.freeze({
[CustomFieldTypes.TEXT]: configureTextCustomFieldFactory,
[CustomFieldTypes.TOGGLE]: configureToggleCustomFieldFactory,
+ [CustomFieldTypes.NUMBER]: configureNumberCustomFieldFactory,
} as const) as CustomFieldBuilderMap;
diff --git a/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.test.tsx
index eaaa0e28747ea..0f87c04bc9ad3 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/custom_fields/custom_fields_list/index.test.tsx
@@ -59,13 +59,20 @@ describe('CustomFieldsList', () => {
)
).toBeInTheDocument();
expect((await screen.findAllByText('Text')).length).toBe(2);
- expect((await screen.findAllByText('Required')).length).toBe(2);
+ expect((await screen.findAllByText('Required')).length).toBe(3);
expect(
await screen.findByTestId(
`custom-field-${customFieldsConfigurationMock[1].key}-${customFieldsConfigurationMock[1].type}`
)
).toBeInTheDocument();
expect((await screen.findAllByText('Toggle')).length).toBe(2);
+
+ expect(
+ await screen.findByTestId(
+ `custom-field-${customFieldsConfigurationMock[4].key}-${customFieldsConfigurationMock[4].type}`
+ )
+ ).toBeInTheDocument();
+ expect((await screen.findAllByText('Number')).length).toBe(2);
});
it('shows single CustomFieldsList correctly', async () => {
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/config.ts b/x-pack/plugins/cases/public/components/custom_fields/number/config.ts
new file mode 100644
index 0000000000000..b73bc033883a8
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/config.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FieldConfig } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
+import { REQUIRED_FIELD, SAFE_INTEGER_NUMBER_ERROR } from '../translations';
+
+const { emptyField } = fieldValidators;
+
+export const getNumberFieldConfig = ({
+ required,
+ label,
+ defaultValue,
+}: {
+ required: boolean;
+ label: string;
+ defaultValue?: number;
+}): FieldConfig => {
+ const validators = [];
+
+ if (required) {
+ validators.push({
+ validator: emptyField(REQUIRED_FIELD(label)),
+ });
+ }
+
+ return {
+ ...(defaultValue && { defaultValue }),
+ validations: [
+ ...validators,
+ {
+ validator: ({ value }) => {
+ if (value == null) {
+ return;
+ }
+ const numericValue = Number(value);
+
+ if (!Number.isSafeInteger(numericValue)) {
+ return { message: SAFE_INTEGER_NUMBER_ERROR(label) };
+ }
+ },
+ },
+ ],
+ };
+};
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/configure.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/configure.test.tsx
new file mode 100644
index 0000000000000..f96e47ce30918
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/configure.test.tsx
@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { FormTestComponent } from '../../../common/test_utils';
+import * as i18n from '../translations';
+import { Configure } from './configure';
+
+describe('Configure ', () => {
+ const onSubmit = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', async () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText(i18n.FIELD_OPTION_REQUIRED)).toBeInTheDocument();
+ });
+
+ it('updates field options without default value correctly when not required', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toBeCalledWith({}, true);
+ });
+ });
+
+ it('updates field options with default value correctly when not required', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(await screen.findByTestId('number-custom-field-default-value'));
+ await userEvent.paste('123');
+ await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toBeCalledWith({ defaultValue: '123' }, true);
+ });
+ });
+
+ it('updates field options with default value correctly when required', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(await screen.findByTestId('number-custom-field-required'));
+ await userEvent.click(await screen.findByTestId('number-custom-field-default-value'));
+ await userEvent.paste('123');
+ await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toBeCalledWith(
+ {
+ required: true,
+ defaultValue: '123',
+ },
+ true
+ );
+ });
+ });
+
+ it('updates field options without default value correctly when required', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(await screen.findByTestId('number-custom-field-required'));
+ await userEvent.click(await screen.findByTestId('form-test-component-submit-button'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toBeCalledWith(
+ {
+ required: true,
+ },
+ true
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/configure.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/configure.tsx
new file mode 100644
index 0000000000000..db1fcffd0be0b
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/configure.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { CheckBoxField, NumericField } from '@kbn/es-ui-shared-plugin/static/forms/components';
+import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
+import type { CustomFieldType } from '../types';
+import { getNumberFieldConfig } from './config';
+import * as i18n from '../translations';
+
+const ConfigureComponent: CustomFieldType['Configure'] = () => {
+ const config = getNumberFieldConfig({
+ required: false,
+ label: i18n.DEFAULT_VALUE.toLocaleLowerCase(),
+ });
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+ConfigureComponent.displayName = 'Configure';
+
+export const Configure = React.memo(ConfigureComponent);
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/configure_number_field.test.ts b/x-pack/plugins/cases/public/components/custom_fields/number/configure_number_field.test.ts
new file mode 100644
index 0000000000000..aee9a4439792d
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/configure_number_field.test.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { configureNumberCustomFieldFactory } from './configure_number_field';
+
+describe('configureTextCustomFieldFactory ', () => {
+ const builder = configureNumberCustomFieldFactory();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', async () => {
+ expect(builder).toEqual({
+ id: 'number',
+ label: 'Number',
+ getEuiTableColumn: expect.any(Function),
+ build: expect.any(Function),
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/configure_number_field.ts b/x-pack/plugins/cases/public/components/custom_fields/number/configure_number_field.ts
new file mode 100644
index 0000000000000..428559f5f83c0
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/configure_number_field.ts
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type { CustomFieldFactory } from '../types';
+import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
+
+import { CustomFieldTypes } from '../../../../common/types/domain';
+import * as i18n from '../translations';
+import { getEuiTableColumn } from './get_eui_table_column';
+import { Edit } from './edit';
+import { View } from './view';
+import { Configure } from './configure';
+import { Create } from './create';
+
+export const configureNumberCustomFieldFactory: CustomFieldFactory = () => ({
+ id: CustomFieldTypes.NUMBER,
+ label: i18n.NUMBER_LABEL,
+ getEuiTableColumn,
+ build: () => ({
+ Configure,
+ Edit,
+ View,
+ Create,
+ }),
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/create.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/create.test.tsx
new file mode 100644
index 0000000000000..2a8a515df01ee
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/create.test.tsx
@@ -0,0 +1,225 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { FormTestComponent } from '../../../common/test_utils';
+import { Create } from './create';
+import { customFieldsConfigurationMock } from '../../../containers/mock';
+
+describe('Create ', () => {
+ const onSubmit = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ // required number custom field with a default value
+ const customFieldConfiguration = customFieldsConfigurationMock[4];
+
+ it('renders correctly with default value and required', async () => {
+ render(
+
+
+
+ );
+
+ expect(await screen.findByText(customFieldConfiguration.label)).toBeInTheDocument();
+
+ expect(
+ await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
+ ).toHaveValue(customFieldConfiguration.defaultValue as number);
+ });
+
+ it('renders correctly without default value and not required', async () => {
+ const optionalField = customFieldsConfigurationMock[5]; // optional number custom field
+
+ render(
+
+
+
+ );
+
+ expect(await screen.findByText(optionalField.label)).toBeInTheDocument();
+ expect(
+ await screen.findByTestId(`${optionalField.key}-number-create-custom-field`)
+ ).toHaveValue(null);
+ });
+
+ it('does not render default value when setDefaultValue is false', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
+ ).toHaveValue(null);
+ });
+
+ it('renders loading state correctly', async () => {
+ render(
+
+
+
+ );
+
+ expect(await screen.findByRole('progressbar')).toBeInTheDocument();
+ });
+
+ it('disables the text when loading', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
+ ).toHaveAttribute('disabled');
+ });
+
+ it('updates the value correctly', async () => {
+ render(
+
+
+
+ );
+
+ const numberCustomField = await screen.findByTestId(
+ `${customFieldConfiguration.key}-number-create-custom-field`
+ );
+
+ await userEvent.clear(numberCustomField);
+ await userEvent.click(numberCustomField);
+ await userEvent.paste('1234');
+ await userEvent.click(await screen.findByText('Submit'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toHaveBeenCalledWith(
+ {
+ customFields: {
+ [customFieldConfiguration.key]: '1234',
+ },
+ },
+ true
+ );
+ });
+ });
+
+ it('shows error when number is too big', async () => {
+ render(
+
+
+
+ );
+
+ const numberCustomField = await screen.findByTestId(
+ `${customFieldConfiguration.key}-number-create-custom-field`
+ );
+
+ await userEvent.clear(numberCustomField);
+ await userEvent.click(numberCustomField);
+ await userEvent.paste(`${Number.MAX_SAFE_INTEGER + 1}`);
+
+ await userEvent.click(await screen.findByText('Submit'));
+
+ expect(
+ await screen.findByText(
+ 'The value of the My test label 5 should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ )
+ ).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith({}, false);
+ });
+ });
+
+ it('shows error when number is too small', async () => {
+ render(
+
+
+
+ );
+
+ const numberCustomField = await screen.findByTestId(
+ `${customFieldConfiguration.key}-number-create-custom-field`
+ );
+
+ await userEvent.clear(numberCustomField);
+ await userEvent.click(numberCustomField);
+ await userEvent.paste(`${Number.MIN_SAFE_INTEGER - 1}`);
+
+ await userEvent.click(await screen.findByText('Submit'));
+
+ expect(
+ await screen.findByText(
+ 'The value of the My test label 5 should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ )
+ ).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith({}, false);
+ });
+ });
+
+ it('shows error when number is required but is empty', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.clear(
+ await screen.findByTestId(`${customFieldConfiguration.key}-number-create-custom-field`)
+ );
+ await userEvent.click(await screen.findByText('Submit'));
+
+ expect(
+ await screen.findByText(`${customFieldConfiguration.label} is required.`)
+ ).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith({}, false);
+ });
+ });
+
+ it('does not show error when number is not required but is empty', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(await screen.findByText('Submit'));
+
+ await waitFor(() => {
+ expect(onSubmit).toHaveBeenCalledWith({}, true);
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/create.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/create.tsx
new file mode 100644
index 0000000000000..bc01145fd5d46
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/create.tsx
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { NumericField } from '@kbn/es-ui-shared-plugin/static/forms/components';
+import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
+import type { CustomFieldType } from '../types';
+import { getNumberFieldConfig } from './config';
+import { OptionalFieldLabel } from '../../optional_field_label';
+
+const CreateComponent: CustomFieldType['Create'] = ({
+ customFieldConfiguration,
+ isLoading,
+ setAsOptional,
+ setDefaultValue = true,
+}) => {
+ const { key, label, required, defaultValue } = customFieldConfiguration;
+ const config = getNumberFieldConfig({
+ required: setAsOptional ? false : required,
+ label,
+ ...(defaultValue &&
+ setDefaultValue &&
+ !isNaN(Number(defaultValue)) && { defaultValue: Number(defaultValue) }),
+ });
+
+ return (
+
+ );
+};
+
+CreateComponent.displayName = 'Create';
+
+export const Create = React.memo(CreateComponent);
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/edit.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/edit.test.tsx
new file mode 100644
index 0000000000000..fb19bdb553d41
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/edit.test.tsx
@@ -0,0 +1,475 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { FormTestComponent } from '../../../common/test_utils';
+import { Edit } from './edit';
+import { customFieldsMock, customFieldsConfigurationMock } from '../../../containers/mock';
+import userEvent from '@testing-library/user-event';
+import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
+import { POPULATED_WITH_DEFAULT } from '../translations';
+
+describe('Edit ', () => {
+ const onSubmit = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const customField = customFieldsMock[4] as CaseCustomFieldNumber;
+ const customFieldConfiguration = customFieldsConfigurationMock[4];
+
+ it('renders correctly', async () => {
+ render(
+
+
+
+ );
+
+ expect(await screen.findByTestId('case-number-custom-field-test_key_5')).toBeInTheDocument();
+ expect(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ ).toBeInTheDocument();
+ expect(await screen.findByText(customFieldConfiguration.label)).toBeInTheDocument();
+ expect(await screen.findByText('1234')).toBeInTheDocument();
+ });
+
+ it('does not shows the edit button if the user does not have permissions', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ screen.queryByTestId('case-number-custom-field-edit-button-test_key_1')
+ ).not.toBeInTheDocument();
+ });
+
+ it('does not shows the edit button when loading', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ screen.queryByTestId('case-number-custom-field-edit-button-test_key_1')
+ ).not.toBeInTheDocument();
+ });
+
+ it('shows the loading spinner when loading', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ await screen.findByTestId('case-number-custom-field-loading-test_key_5')
+ ).toBeInTheDocument();
+ });
+
+ it('shows the no value number if the custom field is undefined', async () => {
+ render(
+
+
+
+ );
+
+ expect(await screen.findByText('No value is added')).toBeInTheDocument();
+ });
+
+ it('uses the required value correctly if a required field is empty', async () => {
+ render(
+
+
+
+ );
+
+ expect(await screen.findByText('No value is added')).toBeInTheDocument();
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+
+ expect(
+ await screen.findByTestId(
+ `case-number-custom-field-form-field-${customFieldConfiguration.key}`
+ )
+ ).toHaveValue(customFieldConfiguration.defaultValue as number);
+ expect(
+ await screen.findByText('This field is populated with the default value.')
+ ).toBeInTheDocument();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ );
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith({
+ ...customField,
+ value: customFieldConfiguration.defaultValue,
+ });
+ });
+ });
+
+ it('does not show the value when the custom field is undefined', async () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('number-custom-field-view-test_key_5')).not.toBeInTheDocument();
+ });
+
+ it('does not show the value when the value is null', async () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('number-custom-field-view-test_key_5')).not.toBeInTheDocument();
+ });
+
+ it('does not show the form when the user does not have permissions', async () => {
+ render(
+
+
+
+ );
+
+ expect(
+ screen.queryByTestId('case-number-custom-field-form-field-test_key_5')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('case-number-custom-field-submit-button-test_key_5')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('case-number-custom-field-cancel-button-test_key_5')
+ ).not.toBeInTheDocument();
+ });
+
+ it('calls onSubmit when changing value', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+ await userEvent.paste('12345');
+
+ expect(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ ).not.toBeDisabled();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ );
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith({
+ ...customField,
+ value: 123412345,
+ });
+ });
+ });
+
+ it('calls onSubmit with defaultValue if no initialValue exists', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+
+ expect(await screen.findByText(POPULATED_WITH_DEFAULT)).toBeInTheDocument();
+ expect(await screen.findByTestId('case-number-custom-field-form-field-test_key_5')).toHaveValue(
+ customFieldConfiguration.defaultValue as number
+ );
+ expect(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ ).not.toBeDisabled();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ );
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith({
+ ...customField,
+ value: customFieldConfiguration.defaultValue,
+ });
+ });
+ });
+
+ it('sets the value to null if the number field is empty', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ await userEvent.clear(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+
+ expect(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ ).not.toBeDisabled();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ );
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith({
+ ...customField,
+ value: null,
+ });
+ });
+ });
+
+ it('hides the form when clicking the cancel button', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+
+ expect(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ ).toBeInTheDocument();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-cancel-button-test_key_5')
+ );
+
+ expect(
+ screen.queryByTestId('case-number-custom-field-form-field-test_key_5')
+ ).not.toBeInTheDocument();
+ });
+
+ it('reset to initial value when canceling', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+ await userEvent.paste('321');
+
+ expect(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ ).not.toBeDisabled();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-cancel-button-test_key_5')
+ );
+
+ expect(
+ screen.queryByTestId('case-number-custom-field-form-field-test_key_5')
+ ).not.toBeInTheDocument();
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ expect(await screen.findByTestId('case-number-custom-field-form-field-test_key_5')).toHaveValue(
+ 1234
+ );
+ });
+
+ it('shows validation error if the field is required', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ await userEvent.clear(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+
+ expect(await screen.findByText('My test label 5 is required.')).toBeInTheDocument();
+ });
+
+ it('does not shows a validation error if the field is not required', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ await userEvent.clear(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+
+ expect(
+ await screen.findByTestId('case-number-custom-field-submit-button-test_key_5')
+ ).not.toBeDisabled();
+
+ expect(screen.queryByText('My test label 1 is required.')).not.toBeInTheDocument();
+ });
+
+ it('shows validation error if the number is too big', async () => {
+ render(
+
+
+
+ );
+
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-edit-button-test_key_5')
+ );
+ await userEvent.clear(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+ await userEvent.click(
+ await screen.findByTestId('case-number-custom-field-form-field-test_key_5')
+ );
+ await userEvent.paste(`${2 ** 53 + 1}`);
+
+ expect(
+ await screen.findByText(
+ 'The value of the My test label 5 should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
+ )
+ ).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/edit.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/edit.tsx
new file mode 100644
index 0000000000000..3ebb65a9dab8e
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/edit.tsx
@@ -0,0 +1,246 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiLoadingSpinner,
+ EuiText,
+} from '@elastic/eui';
+import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import {
+ useForm,
+ UseField,
+ Form,
+ useFormData,
+} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
+import { NumericField } from '@kbn/es-ui-shared-plugin/static/forms/components';
+import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
+import { CustomFieldTypes } from '../../../../common/types/domain';
+import type { CasesConfigurationUICustomField } from '../../../../common/ui';
+import type { CustomFieldType } from '../types';
+import { View } from './view';
+import {
+ CANCEL,
+ EDIT_CUSTOM_FIELDS_ARIA_LABEL,
+ NO_CUSTOM_FIELD_SET,
+ SAVE,
+ POPULATED_WITH_DEFAULT,
+} from '../translations';
+import { getNumberFieldConfig } from './config';
+
+const isEmpty = (value: number | null | undefined) => {
+ return value == null;
+};
+
+interface FormState {
+ value: number | null;
+ isValid?: boolean;
+ submit: FormHook<{ value: number | null }>['submit'];
+}
+
+interface FormWrapper {
+ initialValue: number | null;
+ isLoading: boolean;
+ customFieldConfiguration: CasesConfigurationUICustomField;
+ onChange: (state: FormState) => void;
+}
+
+const FormWrapperComponent: React.FC = ({
+ initialValue,
+ customFieldConfiguration,
+ isLoading,
+ onChange,
+}) => {
+ const { form } = useForm<{ value: number | null }>({
+ defaultValue: {
+ value:
+ customFieldConfiguration?.defaultValue != null && isEmpty(initialValue)
+ ? Number(customFieldConfiguration.defaultValue)
+ : initialValue,
+ },
+ });
+
+ const [{ value }] = useFormData({ form });
+ const { submit, isValid } = form;
+ const formFieldConfig = getNumberFieldConfig({
+ required: customFieldConfiguration.required,
+ label: customFieldConfiguration.label,
+ });
+ const populatedWithDefault =
+ value === customFieldConfiguration?.defaultValue && isEmpty(initialValue);
+
+ useEffect(() => {
+ onChange({
+ value,
+ isValid,
+ submit,
+ });
+ }, [isValid, onChange, submit, value]);
+
+ return (
+
+ );
+};
+
+FormWrapperComponent.displayName = 'FormWrapper';
+
+const EditComponent: CustomFieldType['Edit'] = ({
+ customField,
+ customFieldConfiguration,
+ onSubmit,
+ isLoading,
+ canUpdate,
+}) => {
+ const initialValue = customField?.value ?? null;
+ const [isEdit, setIsEdit] = useState(false);
+ const [formState, setFormState] = useState({
+ isValid: undefined,
+ submit: async () => ({ isValid: false, data: { value: null } }),
+ value: initialValue,
+ });
+
+ const onEdit = useCallback(() => {
+ setIsEdit(true);
+ }, []);
+
+ const onCancel = useCallback(() => {
+ setIsEdit(false);
+ }, []);
+
+ const onSubmitCustomField = useCallback(async () => {
+ const { isValid, data } = await formState.submit();
+
+ if (isValid) {
+ onSubmit({
+ ...customField,
+ key: customField?.key ?? customFieldConfiguration.key,
+ type: CustomFieldTypes.NUMBER,
+ value: data.value ? Number(data.value) : null,
+ });
+ }
+ setIsEdit(false);
+ }, [customField, customFieldConfiguration.key, formState, onSubmit]);
+
+ const title = customFieldConfiguration.label;
+
+ const isNumberFieldValid =
+ formState.isValid ||
+ (formState.value === customFieldConfiguration.defaultValue && isEmpty(initialValue));
+
+ const isCustomFieldValueDefined = !isEmpty(customField?.value);
+
+ return (
+ <>
+
+
+
+ {title}
+
+
+ {isLoading && (
+
+ )}
+ {!isLoading && canUpdate && (
+
+
+
+ )}
+
+
+
+ {!isCustomFieldValueDefined && !isEdit && (
+ {NO_CUSTOM_FIELD_SET}
+ )}
+ {!isEdit && isCustomFieldValueDefined && (
+
+
+
+ )}
+ {isEdit && canUpdate && (
+
+
+
+
+
+
+
+
+ {SAVE}
+
+
+
+
+ {CANCEL}
+
+
+
+
+
+ )}
+
+ >
+ );
+};
+
+EditComponent.displayName = 'Edit';
+
+export const Edit = React.memo(EditComponent);
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/get_eui_table_column.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/get_eui_table_column.test.tsx
new file mode 100644
index 0000000000000..73e94f9335705
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/get_eui_table_column.test.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+
+import { screen } from '@testing-library/react';
+
+import { CustomFieldTypes } from '../../../../common/types/domain';
+import type { AppMockRenderer } from '../../../common/mock';
+import { createAppMockRenderer } from '../../../common/mock';
+import { getEuiTableColumn } from './get_eui_table_column';
+
+describe('getEuiTableColumn ', () => {
+ let appMockRender: AppMockRenderer;
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+
+ jest.clearAllMocks();
+ });
+
+ it('returns a name and a render function', async () => {
+ const label = 'MockLabel';
+
+ expect(getEuiTableColumn({ label })).toEqual({
+ name: label,
+ render: expect.any(Function),
+ width: '150px',
+ 'data-test-subj': 'number-custom-field-column',
+ });
+ });
+
+ it('render function renders a number column correctly', async () => {
+ const key = 'test_key_1';
+ const value = 1234567;
+ const column = getEuiTableColumn({ label: 'MockLabel' });
+
+ appMockRender.render({column.render({ key, type: CustomFieldTypes.NUMBER, value })}
);
+
+ expect(screen.getByTestId(`number-custom-field-column-view-${key}`)).toBeInTheDocument();
+ expect(screen.getByTestId(`number-custom-field-column-view-${key}`)).toHaveTextContent(
+ String(value)
+ );
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/get_eui_table_column.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/get_eui_table_column.tsx
new file mode 100644
index 0000000000000..a5b68364b9758
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/get_eui_table_column.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import type { CaseCustomField } from '../../../../common/types/domain';
+import type { CustomFieldEuiTableColumn } from '../types';
+
+export const getEuiTableColumn = ({ label }: { label: string }): CustomFieldEuiTableColumn => ({
+ name: label,
+ width: '150px',
+ render: (customField: CaseCustomField) => {
+ return (
+
+ {customField.value}
+
+ );
+ },
+ 'data-test-subj': 'number-custom-field-column',
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/view.test.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/view.test.tsx
new file mode 100644
index 0000000000000..cdcc3cdacf534
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/view.test.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { CustomFieldTypes } from '../../../../common/types/domain';
+import { View } from './view';
+
+describe('View ', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const customField = {
+ type: CustomFieldTypes.NUMBER as const,
+ key: 'test_key_1',
+ value: 123 as number,
+ };
+
+ it('renders correctly', async () => {
+ render();
+
+ expect(screen.getByText('123')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/number/view.tsx b/x-pack/plugins/cases/public/components/custom_fields/number/view.tsx
new file mode 100644
index 0000000000000..542ea92def998
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/custom_fields/number/view.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiText } from '@elastic/eui';
+import type { CaseCustomFieldNumber } from '../../../../common/types/domain';
+import type { CustomFieldType } from '../types';
+
+const ViewComponent: CustomFieldType['View'] = ({ customField }) => {
+ const value = customField?.value ?? '-';
+
+ return (
+
+ {value}
+
+ );
+};
+
+ViewComponent.displayName = 'View';
+
+export const View = React.memo(ViewComponent);
diff --git a/x-pack/plugins/cases/public/components/custom_fields/text/configure_text_field.ts b/x-pack/plugins/cases/public/components/custom_fields/text/configure_text_field.ts
index c0f50820d45f3..0f1595135f9b8 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/text/configure_text_field.ts
+++ b/x-pack/plugins/cases/public/components/custom_fields/text/configure_text_field.ts
@@ -25,5 +25,6 @@ export const configureTextCustomFieldFactory: CustomFieldFactory (value == null ? '' : String(value)),
+ convertNullToEmpty: (value: string | number | boolean | null) =>
+ value == null ? '' : String(value),
});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/translations.ts b/x-pack/plugins/cases/public/components/custom_fields/translations.ts
index 5f1a91765193f..22bafbb80f92f 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/translations.ts
+++ b/x-pack/plugins/cases/public/components/custom_fields/translations.ts
@@ -51,6 +51,10 @@ export const TOGGLE_LABEL = i18n.translate('xpack.cases.customFields.toggleLabel
defaultMessage: 'Toggle',
});
+export const NUMBER_LABEL = i18n.translate('xpack.cases.customFields.textLabel', {
+ defaultMessage: 'Number',
+});
+
export const FIELD_TYPE = i18n.translate('xpack.cases.customFields.fieldType', {
defaultMessage: 'Field type',
});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/types.ts b/x-pack/plugins/cases/public/components/custom_fields/types.ts
index 70caeabd8edd2..ca63caef38748 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/types.ts
+++ b/x-pack/plugins/cases/public/components/custom_fields/types.ts
@@ -55,7 +55,7 @@ export type CustomFieldFactory = () => {
build: () => CustomFieldType;
filterOptions?: CustomFieldFactoryFilterOption[];
getDefaultValue?: () => string | boolean | null;
- convertNullToEmpty?: (value: string | boolean | null) => string;
+ convertNullToEmpty?: (value: string | number | boolean | null) => string;
};
export type CustomFieldBuilderMap = {
diff --git a/x-pack/plugins/cases/public/components/custom_fields/utils.test.ts b/x-pack/plugins/cases/public/components/custom_fields/utils.test.ts
index 5a21319645836..61a77fc941451 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/utils.test.ts
+++ b/x-pack/plugins/cases/public/components/custom_fields/utils.test.ts
@@ -97,5 +97,40 @@ describe('utils ', () => {
}
`);
});
+
+ it('serializes the data correctly if the default value is integer number', async () => {
+ const customField = {
+ key: 'my_test_key',
+ type: CustomFieldTypes.NUMBER,
+ required: true,
+ defaultValue: 1,
+ } as CustomFieldConfiguration;
+
+ expect(customFieldSerializer(customField)).toMatchInlineSnapshot(`
+ Object {
+ "defaultValue": 1,
+ "key": "my_test_key",
+ "required": true,
+ "type": "number",
+ }
+ `);
+ });
+
+ it('serializes the data correctly if the default value is float number', async () => {
+ const customField = {
+ key: 'my_test_key',
+ type: CustomFieldTypes.NUMBER,
+ required: true,
+ defaultValue: 1.5,
+ } as CustomFieldConfiguration;
+
+ expect(customFieldSerializer(customField)).toMatchInlineSnapshot(`
+ Object {
+ "key": "my_test_key",
+ "required": true,
+ "type": "number",
+ }
+ `);
+ });
});
});
diff --git a/x-pack/plugins/cases/public/components/custom_fields/utils.ts b/x-pack/plugins/cases/public/components/custom_fields/utils.ts
index 3842b75b5a7ea..96438a9337265 100644
--- a/x-pack/plugins/cases/public/components/custom_fields/utils.ts
+++ b/x-pack/plugins/cases/public/components/custom_fields/utils.ts
@@ -8,6 +8,7 @@
import { isEmptyString } from '@kbn/es-ui-shared-plugin/static/validators/string';
import { isString } from 'lodash';
import type { CustomFieldConfiguration } from '../../../common/types/domain';
+import { CustomFieldTypes } from '../../../common/types/domain';
export const customFieldSerializer = (
field: CustomFieldConfiguration
@@ -18,5 +19,13 @@ export const customFieldSerializer = (
return otherProperties;
}
+ if (field.type === CustomFieldTypes.NUMBER) {
+ if (defaultValue !== null && Number.isSafeInteger(Number(defaultValue))) {
+ return { ...field, defaultValue: Number(defaultValue) };
+ } else {
+ return otherProperties;
+ }
+ }
+
return field;
};
diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editable_markdown_renderer.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editable_markdown_renderer.test.tsx
index 6e56126708034..81758ea1076c7 100644
--- a/x-pack/plugins/cases/public/components/markdown_editor/editable_markdown_renderer.test.tsx
+++ b/x-pack/plugins/cases/public/components/markdown_editor/editable_markdown_renderer.test.tsx
@@ -61,7 +61,8 @@ const defaultProps = {
editorRef,
};
-describe('EditableMarkdown', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/171177
+describe.skip('EditableMarkdown', () => {
let appMockRender: AppMockRenderer;
beforeEach(() => {
diff --git a/x-pack/plugins/cases/public/components/templates/form.test.tsx b/x-pack/plugins/cases/public/components/templates/form.test.tsx
index bf5f66aaa3e21..349457c2be98f 100644
--- a/x-pack/plugins/cases/public/components/templates/form.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form.test.tsx
@@ -589,11 +589,14 @@ describe('TemplateForm', () => {
expect(
await within(customFieldsElement).findAllByTestId('form-optional-field-label')
).toHaveLength(
- customFieldsConfigurationMock.filter((field) => field.type === CustomFieldTypes.TEXT).length
+ customFieldsConfigurationMock.filter(
+ (field) => field.type === CustomFieldTypes.TEXT || field.type === CustomFieldTypes.NUMBER
+ ).length
);
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[3];
+ const numberField = customFieldsConfigurationMock[4];
const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
@@ -608,6 +611,15 @@ describe('TemplateForm', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
+ const numberCustomField = await screen.findByTestId(
+ `${numberField.key}-${numberField.type}-create-custom-field`
+ );
+
+ await user.clear(numberCustomField);
+
+ await user.click(numberCustomField);
+ await user.paste('765');
+
const submitSpy = jest.spyOn(formState!, 'submit');
await user.click(screen.getByText('testSubmit'));
@@ -644,6 +656,16 @@ describe('TemplateForm', () => {
type: 'toggle',
value: true,
},
+ {
+ key: 'test_key_5',
+ type: 'number',
+ value: 1234,
+ },
+ {
+ key: 'test_key_6',
+ type: 'number',
+ value: true,
+ },
],
settings: {
syncAlerts: true,
diff --git a/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx b/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
index 75cfa58e8d5f8..48c6f956ccc7c 100644
--- a/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
+++ b/x-pack/plugins/cases/public/components/templates/form_fields.test.tsx
@@ -311,6 +311,7 @@ describe('form fields', () => {
const textField = customFieldsConfigurationMock[0];
const toggleField = customFieldsConfigurationMock[1];
+ const numberField = customFieldsConfigurationMock[4];
const textCustomField = await screen.findByTestId(
`${textField.key}-${textField.type}-create-custom-field`
@@ -324,6 +325,14 @@ describe('form fields', () => {
await screen.findByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
);
+ const numberCustomField = await screen.findByTestId(
+ `${numberField.key}-${numberField.type}-create-custom-field`
+ );
+
+ await userEvent.clear(numberCustomField);
+ await userEvent.click(numberCustomField);
+ await userEvent.paste('987');
+
await userEvent.click(screen.getByText('Submit'));
await waitFor(() => {
@@ -336,6 +345,7 @@ describe('form fields', () => {
test_key_1: 'My text test value 1',
test_key_2: false,
test_key_4: false,
+ test_key_5: '987',
},
syncAlerts: true,
templateTags: [],
diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts
index 005f15b78b3d7..f10590cc9a358 100644
--- a/x-pack/plugins/cases/public/components/utils.test.ts
+++ b/x-pack/plugins/cases/public/components/utils.test.ts
@@ -523,19 +523,46 @@ describe('Utils', () => {
});
it('returns the string when the value is a non-empty string', async () => {
- expect(convertCustomFieldValue('my text value')).toMatchInlineSnapshot(`"my text value"`);
+ expect(
+ convertCustomFieldValue({ value: 'my text value', type: CustomFieldTypes.TEXT })
+ ).toMatchInlineSnapshot(`"my text value"`);
});
it('returns null when value is empty string', async () => {
- expect(convertCustomFieldValue('')).toMatchInlineSnapshot('null');
+ expect(
+ convertCustomFieldValue({ value: '', type: CustomFieldTypes.TEXT })
+ ).toMatchInlineSnapshot('null');
});
it('returns value as it is when value is true', async () => {
- expect(convertCustomFieldValue(true)).toMatchInlineSnapshot('true');
+ expect(
+ convertCustomFieldValue({ value: true, type: CustomFieldTypes.TOGGLE })
+ ).toMatchInlineSnapshot('true');
});
it('returns value as it is when value is false', async () => {
- expect(convertCustomFieldValue(false)).toMatchInlineSnapshot('false');
+ expect(
+ convertCustomFieldValue({ value: false, type: CustomFieldTypes.TOGGLE })
+ ).toMatchInlineSnapshot('false');
+ });
+ it('returns value as integer number when value is integer string and type is number', () => {
+ expect(convertCustomFieldValue({ value: '123', type: CustomFieldTypes.NUMBER })).toEqual(123);
+ });
+
+ it('returns value as null when value is float string and type is number', () => {
+ expect(convertCustomFieldValue({ value: '0.5', type: CustomFieldTypes.NUMBER })).toEqual(
+ null
+ );
+ });
+
+ it('returns value as null when value is null and type is number', () => {
+ expect(convertCustomFieldValue({ value: null, type: CustomFieldTypes.NUMBER })).toEqual(null);
+ });
+
+ it('returns value as null when value is characters string and type is number', () => {
+ expect(convertCustomFieldValue({ value: 'fdgdg', type: CustomFieldTypes.NUMBER })).toEqual(
+ null
+ );
});
});
@@ -575,6 +602,16 @@ describe('Utils', () => {
"type": "toggle",
"value": null,
},
+ Object {
+ "key": "test_key_5",
+ "type": "number",
+ "value": 1234,
+ },
+ Object {
+ "key": "test_key_6",
+ "type": "number",
+ "value": null,
+ },
Object {
"key": "my_test_key",
"type": "text",
@@ -598,6 +635,8 @@ describe('Utils', () => {
{ ...customFieldsMock[1] },
{ ...customFieldsMock[2] },
{ ...customFieldsMock[3] },
+ { ...customFieldsMock[4] },
+ { ...customFieldsMock[5] },
],
`
Array [
@@ -626,6 +665,16 @@ describe('Utils', () => {
"type": "toggle",
"value": null,
},
+ Object {
+ "key": "test_key_5",
+ "type": "number",
+ "value": 1234,
+ },
+ Object {
+ "key": "test_key_6",
+ "type": "number",
+ "value": null,
+ },
]
`
);
@@ -669,6 +718,19 @@ describe('Utils', () => {
"required": false,
"type": "toggle",
},
+ Object {
+ "defaultValue": 123,
+ "key": "test_key_5",
+ "label": "My test label 5",
+ "required": true,
+ "type": "number",
+ },
+ Object {
+ "key": "test_key_6",
+ "label": "My test label 6",
+ "required": false,
+ "type": "number",
+ },
Object {
"key": "my_test_key",
"label": "my_test_label",
@@ -693,6 +755,8 @@ describe('Utils', () => {
{ ...customFieldsConfigurationMock[1] },
{ ...customFieldsConfigurationMock[2] },
{ ...customFieldsConfigurationMock[3] },
+ { ...customFieldsConfigurationMock[4] },
+ { ...customFieldsConfigurationMock[5] },
],
`
Array [
@@ -722,6 +786,19 @@ describe('Utils', () => {
"required": false,
"type": "toggle",
},
+ Object {
+ "defaultValue": 123,
+ "key": "test_key_5",
+ "label": "My test label 5",
+ "required": true,
+ "type": "number",
+ },
+ Object {
+ "key": "test_key_6",
+ "label": "My test label 6",
+ "required": false,
+ "type": "number",
+ },
]
`
);
diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts
index 7e1aa54554f50..bcc6be9a7ae9e 100644
--- a/x-pack/plugins/cases/public/components/utils.ts
+++ b/x-pack/plugins/cases/public/components/utils.ts
@@ -13,7 +13,7 @@ import type {
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import type { ConnectorTypeFields } from '../../common/types/domain';
-import { ConnectorTypes } from '../../common/types/domain';
+import { ConnectorTypes, CustomFieldTypes } from '../../common/types/domain';
import type { CasesPublicStartDependencies } from '../types';
import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator';
import type { CaseActionConnector } from './types';
@@ -234,11 +234,25 @@ export const parseCaseUsers = ({
return { userProfiles, reporterAsArray };
};
-export const convertCustomFieldValue = (value: string | boolean) => {
+export const convertCustomFieldValue = ({
+ value,
+ type,
+}: {
+ value: string | number | boolean | null;
+ type: CustomFieldTypes;
+}) => {
if (typeof value === 'string' && isEmpty(value)) {
return null;
}
+ if (type === CustomFieldTypes.NUMBER) {
+ if (value !== null && Number.isSafeInteger(Number(value))) {
+ return Number(value);
+ } else {
+ return null;
+ }
+ }
+
return value;
};
@@ -288,7 +302,7 @@ export const customFieldsFormDeserializer = (
};
export const customFieldsFormSerializer = (
- customFields: Record,
+ customFields: Record,
selectedCustomFieldsConfiguration: CasesConfigurationUI['customFields']
): CaseUI['customFields'] => {
const transformedCustomFields: CaseUI['customFields'] = [];
@@ -303,7 +317,7 @@ export const customFieldsFormSerializer = (
transformedCustomFields.push({
key: configCustomField.key,
type: configCustomField.type,
- value: convertCustomFieldValue(value),
+ value: convertCustomFieldValue({ value, type: configCustomField.type }),
} as CaseUICustomField);
}
}
diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts
index 8d2feca6b9be0..c3cee2d60d2b0 100644
--- a/x-pack/plugins/cases/public/containers/mock.ts
+++ b/x-pack/plugins/cases/public/containers/mock.ts
@@ -1158,6 +1158,8 @@ export const customFieldsMock: CaseUICustomField[] = [
{ type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: true },
{ type: CustomFieldTypes.TEXT, key: 'test_key_3', value: null },
{ type: CustomFieldTypes.TOGGLE, key: 'test_key_4', value: null },
+ { type: CustomFieldTypes.NUMBER, key: 'test_key_5', value: 1234 },
+ { type: CustomFieldTypes.NUMBER, key: 'test_key_6', value: null },
];
export const customFieldsConfigurationMock: CasesConfigurationUICustomField[] = [
@@ -1177,6 +1179,19 @@ export const customFieldsConfigurationMock: CasesConfigurationUICustomField[] =
},
{ type: CustomFieldTypes.TEXT, key: 'test_key_3', label: 'My test label 3', required: false },
{ type: CustomFieldTypes.TOGGLE, key: 'test_key_4', label: 'My test label 4', required: false },
+ {
+ type: CustomFieldTypes.NUMBER,
+ key: 'test_key_5',
+ label: 'My test label 5',
+ required: true,
+ defaultValue: 123,
+ },
+ {
+ type: CustomFieldTypes.NUMBER,
+ key: 'test_key_6',
+ label: 'My test label 6',
+ required: false,
+ },
];
export const templatesConfigurationMock: CasesConfigurationUITemplate[] = [
diff --git a/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx b/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx
index 5d2969f6e6d44..f1d0b87ff07e8 100644
--- a/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx
+++ b/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx
@@ -16,7 +16,7 @@ import * as i18n from './translations';
interface ReplaceCustomField {
caseId: string;
customFieldId: string;
- customFieldValue: string | boolean | null;
+ customFieldValue: string | number | boolean | null;
caseVersion: string;
}
diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts
index eb7aaea6d6938..680887b82c653 100644
--- a/x-pack/plugins/cases/server/client/utils.test.ts
+++ b/x-pack/plugins/cases/server/client/utils.test.ts
@@ -906,7 +906,7 @@ describe('utils', () => {
...customFieldsConfiguration,
{
key: 'fourth_key',
- type: 'number',
+ type: 'symbol',
label: 'Number field',
required: true,
},
diff --git a/x-pack/plugins/cases/server/common/types/configure.ts b/x-pack/plugins/cases/server/common/types/configure.ts
index faf2517fbe173..27e66ba76eb02 100644
--- a/x-pack/plugins/cases/server/common/types/configure.ts
+++ b/x-pack/plugins/cases/server/common/types/configure.ts
@@ -39,7 +39,7 @@ type PersistedCustomFieldsConfiguration = Array<{
type: string;
label: string;
required: boolean;
- defaultValue?: string | boolean | null;
+ defaultValue?: string | number | boolean | null;
}>;
type PersistedTemplatesConfiguration = Array<{
diff --git a/x-pack/plugins/cases/server/connectors/cases/constants.ts b/x-pack/plugins/cases/server/connectors/cases/constants.ts
index fafd1a3e0eaeb..f1d0e548e1f3a 100644
--- a/x-pack/plugins/cases/server/connectors/cases/constants.ts
+++ b/x-pack/plugins/cases/server/connectors/cases/constants.ts
@@ -12,8 +12,11 @@ export const MAX_OPEN_CASES = 10;
export const DEFAULT_MAX_OPEN_CASES = 5;
export const INITIAL_ORACLE_RECORD_COUNTER = 1;
-export const VALUES_FOR_CUSTOM_FIELDS_MISSING_DEFAULTS: Record =
- {
- [CustomFieldTypes.TEXT]: 'N/A',
- [CustomFieldTypes.TOGGLE]: false,
- };
+export const VALUES_FOR_CUSTOM_FIELDS_MISSING_DEFAULTS: Record<
+ CustomFieldTypes,
+ string | boolean | number
+> = {
+ [CustomFieldTypes.TEXT]: 'N/A',
+ [CustomFieldTypes.TOGGLE]: false,
+ [CustomFieldTypes.NUMBER]: 0,
+};
diff --git a/x-pack/plugins/cases/server/custom_fields/factory.ts b/x-pack/plugins/cases/server/custom_fields/factory.ts
index d9e1bc86671fe..3b42dcfd6eddb 100644
--- a/x-pack/plugins/cases/server/custom_fields/factory.ts
+++ b/x-pack/plugins/cases/server/custom_fields/factory.ts
@@ -9,10 +9,12 @@ import { CustomFieldTypes } from '../../common/types/domain';
import type { ICasesCustomField, CasesCustomFieldsMap } from './types';
import { getCasesTextCustomField } from './text';
import { getCasesToggleCustomField } from './toggle';
+import { getCasesNumberCustomField } from './number';
const mapping: Record = {
[CustomFieldTypes.TEXT]: getCasesTextCustomField(),
[CustomFieldTypes.TOGGLE]: getCasesToggleCustomField(),
+ [CustomFieldTypes.NUMBER]: getCasesNumberCustomField(),
};
export const casesCustomFields: CasesCustomFieldsMap = {
diff --git a/x-pack/plugins/cases/server/custom_fields/number.ts b/x-pack/plugins/cases/server/custom_fields/number.ts
new file mode 100644
index 0000000000000..f036a01cbe1b8
--- /dev/null
+++ b/x-pack/plugins/cases/server/custom_fields/number.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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Boom from '@hapi/boom';
+
+export const getCasesNumberCustomField = () => ({
+ isFilterable: false,
+ isSortable: false,
+ savedObjectMappingType: 'long',
+ validateFilteringValues: (values: Array) => {
+ values.forEach((value) => {
+ if (value !== null && !Number.isSafeInteger(value)) {
+ throw Boom.badRequest('Unsupported filtering value for custom field of type number.');
+ }
+ });
+ },
+});
diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts
index 4da0244b2ec5e..67dfa03dc3705 100644
--- a/x-pack/plugins/enterprise_search/common/constants.ts
+++ b/x-pack/plugins/enterprise_search/common/constants.ts
@@ -218,50 +218,6 @@ export const CREATE_CONNECTOR_PLUGIN = {
--index-language en
--from-file config.yml
`,
- CONSOLE_SNIPPET: dedent`# Create an index
-PUT /my-index-000001
-{
- "settings": {
- "index": {
- "number_of_shards": 3,
- "number_of_replicas": 2
- }
- }
-}
-
-# Create an API key
-POST /_security/api_key
-{
- "name": "my-api-key",
- "expiration": "1d",
- "role_descriptors":
- {
- "role-a": {
- "cluster": ["all"],
- "indices": [
- {
- "names": ["index-a*"],
- "privileges": ["read"]
- }
- ]
- },
- "role-b": {
- "cluster": ["all"],
- "indices": [
- {
- "names": ["index-b*"],
- "privileges": ["all"]
- }]
- }
- }, "metadata":
- { "application": "my-application",
- "environment": {
- "level": 1,
- "trusted": true,
- "tags": ["dev", "staging"]
- }
- }
- }`,
};
export const LICENSED_SUPPORT_URL = 'https://support.elastic.co';
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx
index 13273266a2068..825f47920d256 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/components/manual_configuration.tsx
@@ -6,16 +6,27 @@
*/
import React, { useState } from 'react';
+import { css } from '@emotion/react';
+import dedent from 'dedent';
+
+import { useValues } from 'kea';
+
import {
EuiButtonIcon,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiPopover,
useGeneratedHtmlId,
+ useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { NATIVE_CONNECTOR_DEFINITIONS, NativeConnector } from '@kbn/search-connectors';
+import { TryInConsoleButton } from '@kbn/try-in-console';
+import { KibanaDeps } from '../../../../../../../common/types';
+import { NewConnectorLogic } from '../../../new_index/method_connector/new_connector_logic';
import { SelfManagePreference } from '../create_connector';
import { ManualConfigurationFlyout } from './manual_configuration_flyout';
@@ -25,10 +36,18 @@ export interface ManualConfigurationProps {
selfManagePreference: SelfManagePreference;
}
+interface ConnectorConfiguration {
+ [key: string]: {
+ value: string;
+ };
+}
+
export const ManualConfiguration: React.FC = ({
isDisabled,
selfManagePreference,
}) => {
+ const { euiTheme } = useEuiTheme();
+ const { services } = useKibana();
const [isPopoverOpen, setPopover] = useState(false);
const splitButtonPopoverId = useGeneratedHtmlId({
prefix: 'splitButtonPopover',
@@ -40,9 +59,104 @@ export const ManualConfiguration: React.FC = ({
const closePopover = () => {
setPopover(false);
};
-
+ const { selectedConnector, rawName } = useValues(NewConnectorLogic);
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const [flyoutContent, setFlyoutContent] = useState<'manual_config' | 'client'>();
+ const getCodeSnippet = (): string => {
+ const connectorInfo: NativeConnector | undefined = selectedConnector?.serviceType
+ ? NATIVE_CONNECTOR_DEFINITIONS[selectedConnector.serviceType]
+ : undefined;
+ if (!connectorInfo) {
+ return '';
+ }
+
+ const dynamicConfigValues = Object.entries(
+ connectorInfo.configuration as ConnectorConfiguration
+ )
+ .map(([key, config]) => {
+ const defaultValue = config ? JSON.stringify(config.value) : null;
+ return ` "${key}": ${defaultValue}`;
+ })
+ .join(',\n');
+ const CONSOLE_SNIPPET = dedent` # Example of how to create a ${connectorInfo?.name} connector using the API
+# This also creates related resources like an index and an API key.
+# This is an alternative to using the UI creation flow.
+
+# 1. Create an index
+PUT connector-${rawName}
+{
+ "settings": {
+ "index": {
+ "number_of_shards": 3,
+ "number_of_replicas": 2
+ }
+ }
+}
+# 2. Create a connector
+PUT _connector/${rawName}
+{
+ "name": "My ${connectorInfo?.name} connector",
+ "index_name": "connector-${rawName}",
+ "service_type": "${selectedConnector?.serviceType}"
+}
+# 3. Create an API key
+POST /_security/api_key
+{
+ "name": "${rawName}-api-key",
+ "role_descriptors": {
+ "${selectedConnector?.serviceType}-api-key-role": {
+ "cluster": [
+ "monitor",
+ "manage_connector"
+ ],
+ "indices": [
+ {
+ "names": [
+ "connector-${rawName}",
+ ".search-acl-filter-connector-${rawName}",
+ ".elastic-connectors*"
+ ],
+ "privileges": [
+ "all"
+ ],
+ "allow_restricted_indices": false
+ }
+ ]
+ }
+ }
+}
+
+# 🔧 Configure your connector
+# NOTE: Configuration keys differ per service type.
+PUT _connector/${rawName}/_configuration
+{
+ "values": {
+${dynamicConfigValues}
+ }
+}
+
+# 🔌 Verify your connector is connected
+GET _connector/${rawName}
+
+# 🔄 Sync data
+POST _connector/_sync_job
+{
+ "id": "${rawName}",
+ "job_type": "full"
+}
+
+# ⏳ Check sync status
+GET _connector/_sync_job?connector_id=${rawName}&size=1
+
+# Once the job completes, the status should return completed
+# 🎉 Verify that data is present in the index with the following API call
+GET connector-${rawName}/_count
+
+# 🔎 Elasticsearch stores data in documents, which are JSON objects. List the individual documents with the following API call
+GET connector-${rawName}/_search
+`;
+ return CONSOLE_SNIPPET;
+ };
const items = [
= ({
{ defaultMessage: 'Manual configuration' }
)}
,
+ {
+ closePopover();
+ }}
+ css={css`
+ .euiLink {
+ color: ${euiTheme.colors.text};
+ font-weight: ${euiTheme.font.weight.regular};
+ }
+ `}
+ >
+
+ ,
= ({ title, setCurrentStep }) => {
const { connector } = useValues(ConnectorViewLogic);
const { updateConnectorConfiguration } = useActions(ConnectorViewLogic);
+ const { setFormDirty } = useActions(NewConnectorLogic);
const { status } = useValues(ConnectorConfigurationApiLogic);
const isSyncing = false;
@@ -109,7 +111,10 @@ export const ConfigurationStep: React.FC = ({ title, set
setCurrentStep('finish')}
+ onClick={() => {
+ setFormDirty(false);
+ setCurrentStep('finish');
+ }}
fill
>
{Constants.NEXT_BUTTON_LABEL}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx
index e8cef81662096..a4ed43e2a8fcd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connectors/create_connector/create_connector.tsx
@@ -28,6 +28,11 @@ import {
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
import { i18n } from '@kbn/i18n';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
+
+import { HttpLogic } from '../../../../shared/http';
+import { KibanaLogic } from '../../../../shared/kibana';
import { AddConnectorApiLogic } from '../../../api/connector/add_connector_api_logic';
import { EnterpriseSearchContentPageTemplate } from '../../layout';
@@ -47,11 +52,16 @@ import { StartStep } from './start_step';
export type ConnectorCreationSteps = 'start' | 'deployment' | 'configure' | 'finish';
export type SelfManagePreference = 'native' | 'selfManaged';
export const CreateConnector: React.FC = () => {
+ const { overlays } = useKibana().services;
+
+ const { http } = useValues(HttpLogic);
+ const { application, history } = useValues(KibanaLogic);
+
const { error } = useValues(AddConnectorApiLogic);
const { euiTheme } = useEuiTheme();
const [selfManagePreference, setSelfManagePreference] = useState('native');
- const { selectedConnector, currentStep } = useValues(NewConnectorLogic);
+ const { selectedConnector, currentStep, isFormDirty } = useValues(NewConnectorLogic);
const { setCurrentStep } = useActions(NewConnectorLogic);
const stepStates = generateStepState(currentStep);
@@ -137,6 +147,33 @@ export const CreateConnector: React.FC = () => {
),
};
+ useUnsavedChangesPrompt({
+ cancelButtonText: i18n.translate(
+ 'xpack.enterpriseSearch.createConnector.unsavedPrompt.cancel',
+ {
+ defaultMessage: 'Continue setup',
+ }
+ ),
+ confirmButtonText: i18n.translate(
+ 'xpack.enterpriseSearch.createConnector.unsavedPrompt.confirm',
+ {
+ defaultMessage: 'Leave the page',
+ }
+ ),
+ hasUnsavedChanges: isFormDirty,
+ history,
+ http,
+ messageText: i18n.translate('xpack.enterpriseSearch.createConnector.unsavedPrompt.body', {
+ defaultMessage:
+ 'Your connector is created but missing some details. You can complete the setup later in the connector configuration page, but this guided flow offers more help.',
+ }),
+ navigateToUrl: application.navigateToUrl,
+ openConfirm: overlays?.openConfirm ?? (() => Promise.resolve(false)),
+ titleText: i18n.translate('xpack.enterpriseSearch.createConnector.unsavedPrompt.title', {
+ defaultMessage: 'Your connector is not fully configured',
+ }),
+ });
+
return (
= ({
isGenerateLoading,
isCreateLoading,
} = useValues(NewConnectorLogic);
- const { setRawName, createConnector, generateConnectorName } = useActions(NewConnectorLogic);
+ const { setRawName, createConnector, generateConnectorName, setFormDirty } =
+ useActions(NewConnectorLogic);
const { connector } = useValues(ConnectorViewLogic);
const handleNameChange = (e: ChangeEvent) => {
@@ -236,6 +237,7 @@ export const StartStep: React.FC = ({
createConnector({
isSelfManaged: true,
});
+ setFormDirty(true);
setCurrentStep('deployment');
}
}}
@@ -294,7 +296,9 @@ export const StartStep: React.FC = ({
setCurrentStep('configure')}
+ onClick={() => {
+ setCurrentStep('configure');
+ }}
>
{Constants.NEXT_BUTTON_LABEL}
@@ -310,6 +314,7 @@ export const StartStep: React.FC = ({
iconType="sparkles"
isLoading={isGenerateLoading || isCreateLoading}
onClick={() => {
+ setFormDirty(true);
createConnector({
isSelfManaged: false,
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts
index 796a2a64ab56c..0d21db6e03baf 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector/new_connector_logic.ts
@@ -56,6 +56,7 @@ export interface NewConnectorValues {
| undefined;
generatedNameData: GenerateConnectorNamesApiResponse | undefined;
isCreateLoading: boolean;
+ isFormDirty: boolean;
isGenerateLoading: boolean;
rawName: string;
selectedConnector: ConnectorDefinition | null;
@@ -85,6 +86,7 @@ type NewConnectorActions = {
createConnectorApi: AddConnectorApiLogicActions['makeRequest'];
fetchConnector: ConnectorViewActions['fetchConnector'];
setCurrentStep(step: ConnectorCreationSteps): { step: ConnectorCreationSteps };
+ setFormDirty: (isDirty: boolean) => { isDirty: boolean };
setRawName(rawName: string): { rawName: string };
setSelectedConnector(connector: ConnectorDefinition | null): {
connector: ConnectorDefinition | null;
@@ -103,6 +105,7 @@ export const NewConnectorLogic = kea ({ step }),
+ setFormDirty: (isDirty) => ({ isDirty }),
setRawName: (rawName) => ({ rawName }),
setSelectedConnector: (connector) => ({ connector }),
},
@@ -214,6 +217,13 @@ export const NewConnectorLogic = kea step,
},
],
+ isFormDirty: [
+ false, // Initial state (form is not dirty)
+ {
+ // @ts-expect-error upgrade typescript v5.1.6
+ setFormDirty: (_, { isDirty }) => isDirty,
+ },
+ ],
rawName: [
'',
{
diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json
index fa0751078c0f7..7b7556729a76c 100644
--- a/x-pack/plugins/enterprise_search/tsconfig.json
+++ b/x-pack/plugins/enterprise_search/tsconfig.json
@@ -82,6 +82,7 @@
"@kbn/navigation-plugin",
"@kbn/security-plugin-types-common",
"@kbn/core-security-server",
- "@kbn/core-security-server-mocks"
+ "@kbn/core-security-server-mocks",
+ "@kbn/unsaved-changes-prompt"
]
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx
index f4f519b0a9c95..88dd00546e51f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx
@@ -137,13 +137,22 @@ export const AgentPolicyActionMenu = memo<{
const copyPolicyItem = (
{
setIsContextMenuOpen(false);
copyAgentPolicyPrompt(agentPolicy, onCopySuccess);
}}
key="copyPolicy"
+ toolTipContent={
+ hasManagedPackagePolicy ? (
+
+ ) : undefined
+ }
>