element because shallow() would fail
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx
index b8f63ef697e78c..1499f99f828248 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { isEqual } from 'lodash';
import React, { Fragment, FC, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
@@ -27,7 +28,9 @@ import {
EuiSwitch,
} from '@elastic/eui';
-import { dictionaryToArray } from '../../../../../../common/types/common';
+import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode';
+import { TransformPivotConfig } from '../../../../common';
+import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common';
import { DropDown } from '../aggregation_dropdown';
import { AggListForm } from '../aggregation_list';
import { GroupByListForm } from '../group_by_list';
@@ -43,10 +46,12 @@ import {
} from '../../../../lib/kibana';
import {
- AggName,
- DropDownLabel,
getPivotQuery,
getPreviewRequestBody,
+ isMatchAllQuery,
+ matchAllQuery,
+ AggName,
+ DropDownLabel,
PivotAggDict,
PivotAggsConfig,
PivotAggsConfigDict,
@@ -55,6 +60,7 @@ import {
PivotGroupByConfigDict,
PivotSupportedGroupByAggs,
PIVOT_SUPPORTED_AGGS,
+ PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from '../../../../common';
import { getPivotDropdownOptions } from './common';
@@ -89,6 +95,58 @@ export function getDefaultStepDefineState(
valid: false,
};
}
+
+export function applyTransformConfigToDefineState(
+ state: StepDefineExposedState,
+ transformConfig?: TransformPivotConfig
+): StepDefineExposedState {
+ // apply the transform configuration to wizard DEFINE state
+ if (transformConfig !== undefined) {
+ // transform aggregations config to wizard state
+ state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => {
+ const aggConfig = transformConfig.pivot.aggregations[aggName] as Dictionary
;
+ const agg = Object.keys(aggConfig)[0];
+ aggList[aggName] = {
+ ...aggConfig[agg],
+ agg: agg as PIVOT_SUPPORTED_AGGS,
+ aggName,
+ dropDownName: aggName,
+ } as PivotAggsConfig;
+ return aggList;
+ }, {} as PivotAggsConfigDict);
+
+ // transform group by config to wizard state
+ state.groupByList = Object.keys(transformConfig.pivot.group_by).reduce(
+ (groupByList, groupByName) => {
+ const groupByConfig = transformConfig.pivot.group_by[groupByName] as Dictionary;
+ const groupBy = Object.keys(groupByConfig)[0];
+ groupByList[groupByName] = {
+ agg: groupBy as PIVOT_SUPPORTED_GROUP_BY_AGGS,
+ aggName: groupByName,
+ dropDownName: groupByName,
+ ...groupByConfig[groupBy],
+ } as PivotGroupByConfig;
+ return groupByList;
+ },
+ {} as PivotGroupByConfigDict
+ );
+
+ // only apply the query from the transform config to wizard state if it's not the default query
+ const query = transformConfig.source.query;
+ if (query !== undefined && !isEqual(query, matchAllQuery)) {
+ state.isAdvancedSourceEditorEnabled = true;
+ state.searchString = '';
+ state.searchQuery = query;
+ state.sourceConfigUpdated = true;
+ }
+
+ // applying a transform config to wizard state will always result in a valid configuration
+ state.valid = true;
+ }
+
+ return state;
+}
+
export function isAggNameConflict(
aggName: AggName,
aggList: PivotAggsConfigDict,
@@ -208,10 +266,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
const searchHandler = (d: Record) => {
const { filterQuery, queryString } = d;
const newSearch = queryString === emptySearch ? defaultSearch : queryString;
- const newSearchQuery =
- filterQuery.match_all && Object.keys(filterQuery.match_all).length === 0
- ? defaultSearch
- : filterQuery;
+ const newSearchQuery = isMatchAllQuery(filterQuery) ? defaultSearch : filterQuery;
setSearchString(newSearch);
setSearchQuery(newSearchQuery);
};
@@ -329,7 +384,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
const [advancedEditorConfigLastApplied, setAdvancedEditorConfigLastApplied] = useState(
stringifiedPivotConfig
);
- const [advancedEditorConfig, setAdvancedEditorConfig] = useState(stringifiedPivotConfig);
+
+ const {
+ convertToJson,
+ setXJson: setAdvancedEditorConfig,
+ xJson: advancedEditorConfig,
+ } = useXJsonMode(stringifiedPivotConfig);
+
// source config
const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2);
const [
@@ -353,7 +414,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
};
const applyAdvancedPivotEditorChanges = () => {
- const pivotConfig = JSON.parse(advancedEditorConfig);
+ const pivotConfig = JSON.parse(convertToJson(advancedEditorConfig));
const newGroupByList: PivotGroupByConfigDict = {};
if (pivotConfig !== undefined && pivotConfig.group_by !== undefined) {
@@ -363,10 +424,10 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
const aggConfigKeys = Object.keys(aggConfig);
const agg = aggConfigKeys[0] as PivotSupportedGroupByAggs;
newGroupByList[aggName] = {
+ ...aggConfig[agg],
agg,
aggName,
dropDownName: '',
- ...aggConfig[agg],
};
});
}
@@ -380,18 +441,16 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
const aggConfigKeys = Object.keys(aggConfig);
const agg = aggConfigKeys[0] as PIVOT_SUPPORTED_AGGS;
newAggList[aggName] = {
+ ...aggConfig[agg],
agg,
aggName,
dropDownName: '',
- ...aggConfig[agg],
};
});
}
setAggList(newAggList);
- const prettyPivotConfig = JSON.stringify(pivotConfig, null, 2);
- setAdvancedEditorConfig(prettyPivotConfig);
- setAdvancedEditorConfigLastApplied(prettyPivotConfig);
+ setAdvancedEditorConfigLastApplied(advancedEditorConfig);
setAdvancedPivotEditorApplyButtonEnabled(false);
};
@@ -459,13 +518,11 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
pivotAggsArr
);
- const stringifiedPivotConfigUpdate = JSON.stringify(previewRequestUpdate.pivot, null, 2);
const stringifiedSourceConfigUpdate = JSON.stringify(
previewRequestUpdate.source.query,
null,
2
);
- setAdvancedEditorConfig(stringifiedPivotConfigUpdate);
setAdvancedEditorSourceConfig(stringifiedSourceConfigUpdate);
onChange({
@@ -730,7 +787,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
>
{
@@ -745,7 +802,7 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
// Try to parse the string passed on from the editor.
// If parsing fails, the "Apply"-Button will be disabled
try {
- JSON.parse(d);
+ JSON.parse(convertToJson(d));
setAdvancedPivotEditorApplyButtonEnabled(true);
} catch (e) {
setAdvancedPivotEditorApplyButtonEnabled(false);
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx
index 2d9895e8ddcf18..aae366e6008d55 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx
@@ -26,6 +26,8 @@ jest.mock('react', () => {
return { ...r, memo: (x: any) => x };
});
+jest.mock('../../../../../shared_imports');
+
describe('Transform: ', () => {
test('Minimal initialization', () => {
const groupBy: PivotGroupByConfig = {
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts
index e454ea32d76edc..5cbdf4500e3c3f 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts
@@ -4,5 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { StepDetailsForm, getDefaultStepDetailsState } from './step_details_form';
+export {
+ applyTransformConfigToDetailsState,
+ getDefaultStepDetailsState,
+ StepDetailsForm,
+} from './step_details_form';
export { StepDetailsSummary } from './step_details_summary';
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
index a01481fde343c0..220923f88ed363 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
@@ -49,6 +49,22 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState {
};
}
+export function applyTransformConfigToDetailsState(
+ state: StepDetailsExposedState,
+ transformConfig?: TransformPivotConfig
+): StepDetailsExposedState {
+ // apply the transform configuration to wizard DETAILS state
+ if (transformConfig !== undefined) {
+ const time = transformConfig.sync?.time;
+ if (time !== undefined) {
+ state.continuousModeDateField = time.field;
+ state.continuousModeDelay = time.delay;
+ state.isContinuousModeEnabled = true;
+ }
+ }
+ return state;
+}
+
interface Props {
overrides?: StepDetailsExposedState;
onChange(s: StepDetailsExposedState): void;
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx
index 109cf81da6caaf..f1861755d97424 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx
@@ -12,16 +12,22 @@ import { EuiSteps, EuiStepStatus } from '@elastic/eui';
import { useKibanaContext } from '../../../../lib/kibana';
-import { getCreateRequestBody } from '../../../../common';
+import { getCreateRequestBody, TransformPivotConfig } from '../../../../common';
import {
+ applyTransformConfigToDefineState,
+ getDefaultStepDefineState,
StepDefineExposedState,
StepDefineForm,
StepDefineSummary,
- getDefaultStepDefineState,
} from '../step_define';
import { getDefaultStepCreateState, StepCreateForm, StepCreateSummary } from '../step_create';
-import { getDefaultStepDetailsState, StepDetailsForm, StepDetailsSummary } from '../step_details';
+import {
+ applyTransformConfigToDetailsState,
+ getDefaultStepDetailsState,
+ StepDetailsForm,
+ StepDetailsSummary,
+} from '../step_details';
import { WizardNav } from '../wizard_nav';
enum KBN_MANAGEMENT_PAGE_CLASSNAME {
@@ -67,17 +73,25 @@ const StepDefine: FC = ({
);
};
-export const Wizard: FC = React.memo(() => {
+interface WizardProps {
+ cloneConfig?: TransformPivotConfig;
+}
+
+export const Wizard: FC = React.memo(({ cloneConfig }) => {
const kibanaContext = useKibanaContext();
// The current WIZARD_STEP
const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE);
// The DEFINE state
- const [stepDefineState, setStepDefineState] = useState(getDefaultStepDefineState(kibanaContext));
+ const [stepDefineState, setStepDefineState] = useState(
+ applyTransformConfigToDefineState(getDefaultStepDefineState(kibanaContext), cloneConfig)
+ );
// The DETAILS state
- const [stepDetailsState, setStepDetailsState] = useState(getDefaultStepDetailsState());
+ const [stepDetailsState, setStepDetailsState] = useState(
+ applyTransformConfigToDetailsState(getDefaultStepDetailsState(), cloneConfig)
+ );
const stepDetails =
currentStep === WIZARD_STEPS.DETAILS ? (
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx
index 673e60de545726..288630333615a1 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx
@@ -11,6 +11,8 @@ import { CreateTransformButton } from './create_transform_button';
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List ', () => {
test('Minimal initialization', () => {
const wrapper = shallow();
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx
new file mode 100644
index 00000000000000..40098ac7ef72ad
--- /dev/null
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useContext } from 'react';
+import { useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
+
+import {
+ createCapabilityFailureMessage,
+ AuthorizationContext,
+} from '../../../../lib/authorization';
+
+import { CLIENT_BASE_PATH, SECTION_SLUG } from '../../../../constants';
+
+interface CloneActionProps {
+ itemId: string;
+}
+
+export const CloneAction: FC = ({ itemId }) => {
+ const history = useHistory();
+
+ const { canCreateTransform } = useContext(AuthorizationContext).capabilities;
+
+ const buttonCloneText = i18n.translate('xpack.transform.transformList.cloneActionName', {
+ defaultMessage: 'Clone',
+ });
+
+ function clickHandler() {
+ history.push(`${CLIENT_BASE_PATH}/${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`);
+ }
+
+ const cloneButton = (
+
+ {buttonCloneText}
+
+ );
+
+ if (!canCreateTransform) {
+ const content = createCapabilityFailureMessage('canStartStopTransform');
+
+ return (
+
+ {cloneButton}
+
+ );
+ }
+
+ return <>{cloneButton}>;
+};
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx
index 979da13b1f83a7..4795a2eb7d7bc2 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx
@@ -17,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List Actions ', () => {
test('Minimal initialization', () => {
const Providers = getAppProviders(createPublicShim());
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx
index 71a2eff39506d4..5f4d4a71c71eba 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx
@@ -17,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List Actions ', () => {
test('Minimal initialization', () => {
const Providers = getAppProviders(createPublicShim());
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx
index c3b67f7661a1af..f6bb1c8b60667a 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx
@@ -17,6 +17,8 @@ import transformListRow from '../../../../common/__mocks__/transform_list_row.js
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List Actions ', () => {
test('Minimal initialization', () => {
const Providers = getAppProviders(createPublicShim());
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx
index 3d847890b2bd52..12e1ba5528c43a 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx
@@ -8,13 +8,16 @@ import { getActions } from './actions';
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List Actions', () => {
test('getActions()', () => {
const actions = getActions({ forceDisable: false });
- expect(actions).toHaveLength(2);
+ expect(actions).toHaveLength(3);
expect(actions[0].isPrimary).toBeTruthy();
expect(typeof actions[0].render).toBe('function');
expect(typeof actions[1].render).toBe('function');
+ expect(typeof actions[2].render).toBe('function');
});
});
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx
index 1773405e36e396..3e3829973e328e 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import { TransformListRow, TRANSFORM_STATE } from '../../../../common';
+import { CloneAction } from './action_clone';
import { StartAction } from './action_start';
import { StopAction } from './action_stop';
import { DeleteAction } from './action_delete';
@@ -21,6 +22,11 @@ export const getActions = ({ forceDisable }: { forceDisable: boolean }) => {
return ;
},
},
+ {
+ render: (item: TransformListRow) => {
+ return ;
+ },
+ },
{
render: (item: TransformListRow) => {
return ;
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx
index f16130bfe618be..42f04ed101ad6b 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx
@@ -8,6 +8,8 @@ import { getColumns } from './columns';
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Job List Columns', () => {
test('getColumns()', () => {
const columns = getColumns([], () => {}, []);
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx
index 4f992707cbf1aa..7fcaf5e6048f6b 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx
@@ -13,6 +13,8 @@ import { ExpandedRow } from './expanded_row';
import transformListRow from '../../../../common/__mocks__/transform_list_row.json';
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List ', () => {
// Set timezone to US/Eastern for consistent test results.
beforeEach(() => {
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx
index 303de6b86ac530..e1a19ddd3c742e 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx
@@ -12,6 +12,8 @@ import { TransformList } from './transform_list';
jest.mock('ui/new_platform');
+jest.mock('../../../../../shared_imports');
+
describe('Transform: Transform List ', () => {
test('Minimal initialization', () => {
const wrapper = shallow(
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx
index c94f5c1d57d49d..f68670f0b38b2b 100644
--- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx
+++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx
@@ -14,6 +14,8 @@ jest.mock('ui/timefilter', () => {
return {};
});
+jest.mock('../../../shared_imports');
+
describe('Transform: ', () => {
test('Minimal initialization', () => {
const wrapper = shallow();
diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts
index 0e0b174f28f995..5a2f698b351544 100644
--- a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts
+++ b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts
@@ -10,6 +10,7 @@ import { linkToHome } from './links';
export enum BREADCRUMB_SECTION {
MANAGEMENT = 'management',
HOME = 'home',
+ CLONE_TRANSFORM = 'cloneTransform',
CREATE_TRANSFORM = 'createTransform',
}
@@ -27,6 +28,7 @@ class BreadcrumbService {
private breadcrumbs: Breadcrumbs = {
management: [],
home: [],
+ cloneTransform: [],
createTransform: [],
};
@@ -42,6 +44,12 @@ class BreadcrumbService {
href: linkToHome(),
},
];
+ this.breadcrumbs.cloneTransform = [
+ ...this.breadcrumbs.home,
+ {
+ text: textService.breadcrumbs.cloneTransform,
+ },
+ ];
this.breadcrumbs.createTransform = [
...this.breadcrumbs.home,
{
diff --git a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts b/x-pack/legacy/plugins/transform/public/app/services/text/text.ts
index df1b07e171c626..af4aea7e8db4e6 100644
--- a/x-pack/legacy/plugins/transform/public/app/services/text/text.ts
+++ b/x-pack/legacy/plugins/transform/public/app/services/text/text.ts
@@ -14,6 +14,9 @@ class TextService {
home: i18n.translate('xpack.transform.home.breadcrumbTitle', {
defaultMessage: 'Transforms',
}),
+ cloneTransform: i18n.translate('xpack.transform.cloneTransform.breadcrumbTitle', {
+ defaultMessage: 'Clone transform',
+ }),
createTransform: i18n.translate('xpack.transform.createTransform.breadcrumbTitle', {
defaultMessage: 'Create transform',
}),
diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts
index 74e0c9a3878dbc..248eb00c67dff6 100644
--- a/x-pack/legacy/plugins/transform/public/shared_imports.ts
+++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts
@@ -4,6 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export { XJsonMode } from '../../../../plugins/es_ui_shared/console_lang/ace/modes/x_json';
+export {
+ collapseLiteralStrings,
+ expandLiteralStrings,
+} from '../../../../../src/plugins/es_ui_shared/console_lang/lib';
+
export {
SendRequestConfig,
SendRequestResponse,
diff --git a/x-pack/package.json b/x-pack/package.json
index 9d6b5d76a58e7a..551e466893f931 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -262,7 +262,7 @@
"json-stable-stringify": "^1.0.1",
"jsonwebtoken": "^8.5.1",
"jsts": "^1.6.2",
- "lodash": "npm:@elastic/lodash@3.10.1-kibana3",
+ "lodash": "npm:@elastic/lodash@3.10.1-kibana4",
"lodash.keyby": "^4.6.0",
"lodash.mean": "^4.1.0",
"lodash.topath": "^4.5.2",
diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md
index 32ca804198ebd4..6b04b3ba500b08 100644
--- a/x-pack/plugins/alerting/README.md
+++ b/x-pack/plugins/alerting/README.md
@@ -249,6 +249,7 @@ Payload:
|tags|A list of keywords to reference and search in the future.|string[]|
|alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string|
|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object|
+|throttle|A Duration specifying how often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications over this period.|string|
|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object|
|actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array|
@@ -299,6 +300,7 @@ Payload:
|Property|Description|Type|
|---|---|---|
|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object|
+|throttle|A Duration specifying how often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications over this period.|string|
|name|A name to reference and search in the future.|string|
|tags|A list of keywords to reference and search in the future.|string[]|
|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object|
diff --git a/x-pack/plugins/drilldowns/README.md b/x-pack/plugins/drilldowns/README.md
new file mode 100644
index 00000000000000..701b6082d4985f
--- /dev/null
+++ b/x-pack/plugins/drilldowns/README.md
@@ -0,0 +1,3 @@
+# Drilldowns
+
+Provides functionality to navigate between Kibana apps with context information.
diff --git a/x-pack/plugins/drilldowns/kibana.json b/x-pack/plugins/drilldowns/kibana.json
new file mode 100644
index 00000000000000..b951c7dc1fc875
--- /dev/null
+++ b/x-pack/plugins/drilldowns/kibana.json
@@ -0,0 +1,10 @@
+{
+ "id": "drilldowns",
+ "version": "kibana",
+ "server": false,
+ "ui": true,
+ "requiredPlugins": [
+ "uiActions",
+ "embeddable"
+ ]
+}
diff --git a/x-pack/plugins/drilldowns/public/actions/index.ts b/x-pack/plugins/drilldowns/public/actions/index.ts
new file mode 100644
index 00000000000000..c0ca7fac22049b
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/actions/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './open_flyout_add_drilldown';
diff --git a/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx
new file mode 100644
index 00000000000000..06f134b10a4b7d
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/actions/open_flyout_add_drilldown/index.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { CoreStart } from 'src/core/public';
+import { Action } from '../../../../../../src/plugins/ui_actions/public';
+import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
+import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
+import { FormCreateDrilldown } from '../../components/form_create_drilldown';
+
+export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN';
+
+interface ActionContext {
+ embeddable: IEmbeddable;
+}
+
+export interface OpenFlyoutAddDrilldownParams {
+ overlays: () => Promise;
+}
+
+export class OpenFlyoutAddDrilldown implements Action {
+ public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN;
+ public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN;
+ public order = 100;
+
+ constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {}
+
+ public getDisplayName() {
+ return i18n.translate('xpack.drilldowns.panel.openFlyoutAddDrilldown.displayName', {
+ defaultMessage: 'Add drilldown',
+ });
+ }
+
+ public getIconType() {
+ return 'empty';
+ }
+
+ public async isCompatible({ embeddable }: ActionContext) {
+ return true;
+ }
+
+ public async execute({ embeddable }: ActionContext) {
+ const overlays = await this.params.overlays();
+ overlays.openFlyout(toMountPoint());
+ }
+}
diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/fields_browsers.ts b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx
similarity index 50%
rename from x-pack/legacy/plugins/siem/cypress/tasks/hosts/fields_browsers.ts
rename to x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx
index ae3ed2010a0aed..afa82f5e74c16b 100644
--- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/fields_browsers.ts
+++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/__examples__/drilldown_hello_bar.examples.tsx
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers';
-import { KQL_SEARCH_BAR } from '../../screens/hosts/main';
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { DrilldownHelloBar } from '..';
-export const closeFieldsBrowser = () => {
- cy.get(KQL_SEARCH_BAR, { timeout: DEFAULT_TIMEOUT }).click();
-};
+storiesOf('components/DrilldownHelloBar', module).add('default', () => {
+ return ;
+});
diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.tsx
new file mode 100644
index 00000000000000..895a100df3ac50
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/index.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+export interface DrilldownHelloBarProps {
+ docsLink?: string;
+}
+
+export const DrilldownHelloBar: React.FC = ({ docsLink }) => {
+ return (
+
+
+ Drilldowns provide the ability to define a new behavior when interacting with a panel. You
+ can add multiple options or simply override the default filtering behavior.
+
+
View docs
+
+
+ );
+};
diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx
new file mode 100644
index 00000000000000..dfdd9627ab5cd4
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/__examples__/drilldown_picker.examples.tsx
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { DrilldownPicker } from '..';
+
+storiesOf('components/DrilldownPicker', module).add('default', () => {
+ return ;
+});
diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx
new file mode 100644
index 00000000000000..3748fc666c81c5
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+// eslint-disable-next-line
+export interface DrilldownPickerProps {}
+
+export const DrilldownPicker: React.FC = () => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx
new file mode 100644
index 00000000000000..34f6932b41dacf
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/__examples__/form_create_drilldown.examples.tsx
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as React from 'react';
+import { storiesOf } from '@storybook/react';
+import { FormCreateDrilldown } from '..';
+
+storiesOf('components/FormCreateDrilldown', module).add('default', () => {
+ return ;
+});
diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts
new file mode 100644
index 00000000000000..922131ba4b9012
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const txtNameOfDrilldown = i18n.translate(
+ 'xpack.drilldowns.components.form_create_drilldown.nameOfDrilldown',
+ {
+ defaultMessage: 'Name of drilldown',
+ }
+);
+
+export const txtUntitledDrilldown = i18n.translate(
+ 'xpack.drilldowns.components.form_create_drilldown.untitledDrilldown',
+ {
+ defaultMessage: 'Untitled drilldown',
+ }
+);
+
+export const txtDrilldownAction = i18n.translate(
+ 'xpack.drilldowns.components.form_create_drilldown.drilldownAction',
+ {
+ defaultMessage: 'Drilldown action',
+ }
+);
diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx
new file mode 100644
index 00000000000000..40cd4cf2b210b8
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui';
+import { DrilldownHelloBar } from '../drilldown_hello_bar';
+import { txtNameOfDrilldown, txtUntitledDrilldown, txtDrilldownAction } from './i18n';
+import { DrilldownPicker } from '../drilldown_picker';
+
+// eslint-disable-next-line
+export interface FormCreateDrilldownProps {}
+
+export const FormCreateDrilldown: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/drilldowns/public/index.ts b/x-pack/plugins/drilldowns/public/index.ts
new file mode 100644
index 00000000000000..63e7a122354620
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { DrilldownsPlugin } from './plugin';
+
+export {
+ DrilldownsSetupContract,
+ DrilldownsSetupDependencies,
+ DrilldownsStartContract,
+ DrilldownsStartDependencies,
+} from './plugin';
+
+export function plugin() {
+ return new DrilldownsPlugin();
+}
diff --git a/x-pack/plugins/drilldowns/public/mocks.ts b/x-pack/plugins/drilldowns/public/mocks.ts
new file mode 100644
index 00000000000000..bfade1674072ac
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/mocks.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { DrilldownsSetupContract, DrilldownsStartContract } from '.';
+
+export type Setup = jest.Mocked;
+export type Start = jest.Mocked;
+
+const createSetupContract = (): Setup => {
+ const setupContract: Setup = {
+ registerDrilldown: jest.fn(),
+ };
+ return setupContract;
+};
+
+const createStartContract = (): Start => {
+ const startContract: Start = {};
+
+ return startContract;
+};
+
+export const bfetchPluginMock = {
+ createSetupContract,
+ createStartContract,
+};
diff --git a/x-pack/plugins/drilldowns/public/plugin.ts b/x-pack/plugins/drilldowns/public/plugin.ts
new file mode 100644
index 00000000000000..6c8555fa55a119
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/plugin.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreStart, CoreSetup, Plugin } from 'src/core/public';
+import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public';
+import { DrilldownService } from './service';
+
+export interface DrilldownsSetupDependencies {
+ uiActions: UiActionsSetup;
+}
+
+export interface DrilldownsStartDependencies {
+ uiActions: UiActionsStart;
+}
+
+export type DrilldownsSetupContract = Pick;
+
+// eslint-disable-next-line
+export interface DrilldownsStartContract {}
+
+export class DrilldownsPlugin
+ implements
+ Plugin<
+ DrilldownsSetupContract,
+ DrilldownsStartContract,
+ DrilldownsSetupDependencies,
+ DrilldownsStartDependencies
+ > {
+ private readonly service = new DrilldownService();
+
+ public setup(core: CoreSetup, plugins: DrilldownsSetupDependencies): DrilldownsSetupContract {
+ this.service.bootstrap(core, plugins);
+
+ return this.service;
+ }
+
+ public start(core: CoreStart, plugins: DrilldownsStartDependencies): DrilldownsStartContract {
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts
new file mode 100644
index 00000000000000..f22f4521816480
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/service/drilldown_service.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CoreSetup } from 'src/core/public';
+import { OpenFlyoutAddDrilldown } from '../actions/open_flyout_add_drilldown';
+import { DrilldownsSetupDependencies } from '../plugin';
+
+export class DrilldownService {
+ bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) {
+ const actionOpenFlyoutAddDrilldown = new OpenFlyoutAddDrilldown({
+ overlays: async () => (await core.getStartServices())[0].overlays,
+ });
+
+ uiActions.registerAction(actionOpenFlyoutAddDrilldown);
+ uiActions.attachAction('CONTEXT_MENU_TRIGGER', actionOpenFlyoutAddDrilldown.id);
+ }
+
+ /**
+ * Convenience method to register a drilldown. (It should set-up all the
+ * necessary triggers and actions.)
+ */
+ registerDrilldown = (): void => {
+ throw new Error('not implemented');
+ };
+}
diff --git a/x-pack/plugins/drilldowns/public/service/index.ts b/x-pack/plugins/drilldowns/public/service/index.ts
new file mode 100644
index 00000000000000..44472b18a53172
--- /dev/null
+++ b/x-pack/plugins/drilldowns/public/service/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './drilldown_service';
diff --git a/x-pack/plugins/drilldowns/scripts/storybook.js b/x-pack/plugins/drilldowns/scripts/storybook.js
new file mode 100644
index 00000000000000..9b0f57746e5849
--- /dev/null
+++ b/x-pack/plugins/drilldowns/scripts/storybook.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { join } from 'path';
+
+// eslint-disable-next-line
+require('@kbn/storybook').runStorybookCli({
+ name: 'drilldowns',
+ storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.examples.tsx')],
+});
diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts
index f0fd9dc610e4e1..78be98b7805ba5 100644
--- a/x-pack/plugins/endpoint/common/types.ts
+++ b/x-pack/plugins/endpoint/common/types.ts
@@ -71,7 +71,7 @@ export interface EndpointResultList {
}
export interface AlertData {
- '@timestamp': Date;
+ '@timestamp': string;
agent: {
id: string;
version: string;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
index 8530d6206d3987..c6c032c2735435 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
@@ -8,16 +8,14 @@ import * as React from 'react';
import ReactDOM from 'react-dom';
import { CoreStart, AppMountParameters } from 'kibana/public';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
-import { Route, Switch, BrowserRouter, useLocation } from 'react-router-dom';
-import { Provider, useDispatch } from 'react-redux';
+import { Route, Switch, BrowserRouter } from 'react-router-dom';
+import { Provider } from 'react-redux';
import { Store } from 'redux';
-import { memo } from 'react';
+import { RouteCapture } from './view/route_capture';
import { appStoreFactory } from './store';
import { AlertIndex } from './view/alerts';
import { ManagementList } from './view/managing';
import { PolicyList } from './view/policy';
-import { AppAction } from './store/action';
-import { EndpointAppLocation } from './types';
/**
* This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
@@ -33,13 +31,6 @@ export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMou
};
}
-const RouteCapture = memo(({ children }) => {
- const location: EndpointAppLocation = useLocation();
- const dispatch: (action: AppAction) => unknown = useDispatch();
- dispatch({ type: 'userChangedUrl', payload: location });
- return <>{children}>;
-});
-
interface RouterProps {
basename: string;
store: Store;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts
index 6ba7a34ae81d1b..0aeeb6881ad96e 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list.test.ts
@@ -14,6 +14,7 @@ import { coreMock } from 'src/core/public/mocks';
import { AlertResultList } from '../../../../../common/types';
import { isOnAlertPage } from './selectors';
import { createBrowserHistory } from 'history';
+import { mockAlertResultList } from './mock_alert_result_list';
describe('alert list tests', () => {
let store: Store;
@@ -28,37 +29,7 @@ describe('alert list tests', () => {
describe('when the user navigates to the alert list page', () => {
beforeEach(() => {
coreStart.http.get.mockImplementation(async () => {
- const response: AlertResultList = {
- alerts: [
- {
- '@timestamp': new Date(1542341895000),
- agent: {
- id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f',
- version: '3.0.0',
- },
- event: {
- action: 'open',
- },
- file_classification: {
- malware_classification: {
- score: 3,
- },
- },
- host: {
- hostname: 'HD-c15-bc09190a',
- ip: '10.179.244.14',
- os: {
- name: 'Windows',
- },
- },
- thread: {},
- },
- ],
- total: 1,
- request_page_size: 10,
- request_page_index: 0,
- result_from_index: 0,
- };
+ const response: AlertResultList = mockAlertResultList();
return response;
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts
index 77708a3c77e2b7..5c257c3d65fdc5 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/alert_list_pagination.test.ts
@@ -7,37 +7,47 @@
import { Store, createStore, applyMiddleware } from 'redux';
import { History } from 'history';
import { alertListReducer } from './reducer';
-import { AlertListState } from '../../types';
+import { AlertListState, AlertingIndexUIQueryParams } from '../../types';
import { alertMiddlewareFactory } from './middleware';
import { AppAction } from '../action';
import { coreMock } from 'src/core/public/mocks';
import { createBrowserHistory } from 'history';
-import {
- urlFromNewPageSizeParam,
- paginationDataFromUrl,
- urlFromNewPageIndexParam,
-} from './selectors';
+import { uiQueryParams } from './selectors';
+import { urlFromQueryParams } from '../../view/alerts/url_from_query_params';
describe('alert list pagination', () => {
let store: Store;
let coreStart: ReturnType;
let history: History;
+ let queryParams: () => AlertingIndexUIQueryParams;
+ /**
+ * Update the history with a new `AlertingIndexUIQueryParams`
+ */
+ let historyPush: (params: AlertingIndexUIQueryParams) => void;
beforeEach(() => {
coreStart = coreMock.createStart();
history = createBrowserHistory();
+
const middleware = alertMiddlewareFactory(coreStart);
store = createStore(alertListReducer, applyMiddleware(middleware));
+
+ history.listen(location => {
+ store.dispatch({ type: 'userChangedUrl', payload: location });
+ });
+
+ queryParams = () => uiQueryParams(store.getState());
+
+ historyPush = (nextQueryParams: AlertingIndexUIQueryParams): void => {
+ return history.push(urlFromQueryParams(nextQueryParams));
+ };
});
describe('when the user navigates to the alert list page', () => {
describe('when a new page size is passed', () => {
beforeEach(() => {
- const urlPageSizeSelector = urlFromNewPageSizeParam(store.getState());
- history.push(urlPageSizeSelector(1));
- store.dispatch({ type: 'userChangedUrl', payload: history.location });
+ historyPush({ ...queryParams(), page_size: '1' });
});
it('should modify the url correctly', () => {
- const actualPaginationQuery = paginationDataFromUrl(store.getState());
- expect(actualPaginationQuery).toMatchInlineSnapshot(`
+ expect(queryParams()).toMatchInlineSnapshot(`
Object {
"page_size": "1",
}
@@ -46,13 +56,10 @@ describe('alert list pagination', () => {
describe('and then a new page index is passed', () => {
beforeEach(() => {
- const urlPageIndexSelector = urlFromNewPageIndexParam(store.getState());
- history.push(urlPageIndexSelector(1));
- store.dispatch({ type: 'userChangedUrl', payload: history.location });
+ historyPush({ ...queryParams(), page_index: '1' });
});
it('should modify the url in the correct order', () => {
- const actualPaginationQuery = paginationDataFromUrl(store.getState());
- expect(actualPaginationQuery).toMatchInlineSnapshot(`
+ expect(queryParams()).toMatchInlineSnapshot(`
Object {
"page_index": "1",
"page_size": "1",
@@ -64,35 +71,15 @@ describe('alert list pagination', () => {
describe('when a new page index is passed', () => {
beforeEach(() => {
- const urlPageIndexSelector = urlFromNewPageIndexParam(store.getState());
- history.push(urlPageIndexSelector(1));
- store.dispatch({ type: 'userChangedUrl', payload: history.location });
+ historyPush({ ...queryParams(), page_index: '1' });
});
it('should modify the url correctly', () => {
- const actualPaginationQuery = paginationDataFromUrl(store.getState());
- expect(actualPaginationQuery).toMatchInlineSnapshot(`
+ expect(queryParams()).toMatchInlineSnapshot(`
Object {
"page_index": "1",
}
`);
});
-
- describe('and then a new page size is passed', () => {
- beforeEach(() => {
- const urlPageSizeSelector = urlFromNewPageSizeParam(store.getState());
- history.push(urlPageSizeSelector(1));
- store.dispatch({ type: 'userChangedUrl', payload: history.location });
- });
- it('should modify the url correctly and reset index to `0`', () => {
- const actualPaginationQuery = paginationDataFromUrl(store.getState());
- expect(actualPaginationQuery).toMatchInlineSnapshot(`
- Object {
- "page_index": "0",
- "page_size": "1",
- }
- `);
- });
- });
});
});
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts
index 059507c8f06581..76a6867418bd86 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts
@@ -4,11 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { HttpFetchQuery } from 'kibana/public';
import { AlertResultList } from '../../../../../common/types';
import { AppAction } from '../action';
import { MiddlewareFactory, AlertListState } from '../../types';
-import { isOnAlertPage, paginationDataFromUrl } from './selectors';
+import { isOnAlertPage, apiQueryParams } from './selectors';
export const alertMiddlewareFactory: MiddlewareFactory = coreStart => {
return api => next => async (action: AppAction) => {
@@ -16,7 +15,7 @@ export const alertMiddlewareFactory: MiddlewareFactory = coreSta
const state = api.getState();
if (action.type === 'userChangedUrl' && isOnAlertPage(state)) {
const response: AlertResultList = await coreStart.http.get(`/api/endpoint/alerts`, {
- query: paginationDataFromUrl(state) as HttpFetchQuery,
+ query: apiQueryParams(state),
});
api.dispatch({ type: 'serverReturnedAlertsData', payload: response });
}
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts
new file mode 100644
index 00000000000000..338a1077b58a29
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/mock_alert_result_list.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AlertResultList } from '../../../../../common/types';
+
+export const mockAlertResultList: (options?: {
+ total?: number;
+ request_page_size?: number;
+ request_page_index?: number;
+}) => AlertResultList = (options = {}) => {
+ const {
+ total = 1,
+ request_page_size: requestPageSize = 10,
+ request_page_index: requestPageIndex = 0,
+ } = options;
+
+ // Skip any that are before the page we're on
+ const numberToSkip = requestPageSize * requestPageIndex;
+
+ // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0
+ const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0);
+
+ const alerts = [];
+ for (let index = 0; index < actualCountToReturn; index++) {
+ alerts.push({
+ '@timestamp': new Date(1542341895000).toString(),
+ agent: {
+ id: 'ced9c68e-b94a-4d66-bb4c-6106514f0a2f',
+ version: '3.0.0',
+ },
+ event: {
+ action: 'open',
+ },
+ file_classification: {
+ malware_classification: {
+ score: 3,
+ },
+ },
+ host: {
+ hostname: 'HD-c15-bc09190a',
+ ip: '10.179.244.14',
+ os: {
+ name: 'Windows',
+ },
+ },
+ thread: {},
+ });
+ }
+ const mock: AlertResultList = {
+ alerts,
+ total,
+ request_page_size: requestPageSize,
+ request_page_index: requestPageIndex,
+ result_from_index: 0,
+ };
+ return mock;
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts
index 6ad053888729c3..3a0461e06538fc 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts
@@ -4,9 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import qs from 'querystring';
-import { AlertListState } from '../../types';
+import querystring from 'querystring';
+import {
+ createSelector,
+ createStructuredSelector as createStructuredSelectorWithBadType,
+} from 'reselect';
+import { Immutable } from '../../../../../common/types';
+import {
+ AlertListState,
+ AlertingIndexUIQueryParams,
+ AlertsAPIQueryParams,
+ CreateStructuredSelector,
+} from '../../types';
+const createStructuredSelector: CreateStructuredSelector = createStructuredSelectorWithBadType;
/**
* Returns the Alert Data array from state
*/
@@ -15,14 +26,12 @@ export const alertListData = (state: AlertListState) => state.alerts;
/**
* Returns the alert list pagination data from state
*/
-export const alertListPagination = (state: AlertListState) => {
- return {
- pageIndex: state.request_page_index,
- pageSize: state.request_page_size,
- resultFromIndex: state.result_from_index,
- total: state.total,
- };
-};
+export const alertListPagination = createStructuredSelector({
+ pageIndex: (state: AlertListState) => state.request_page_index,
+ pageSize: (state: AlertListState) => state.request_page_size,
+ resultFromIndex: (state: AlertListState) => state.result_from_index,
+ total: (state: AlertListState) => state.total,
+});
/**
* Returns a boolean based on whether or not the user is on the alerts page
@@ -32,48 +41,55 @@ export const isOnAlertPage = (state: AlertListState): boolean => {
};
/**
- * Returns the query object received from parsing the URL query params
- */
-export const paginationDataFromUrl = (state: AlertListState): qs.ParsedUrlQuery => {
- if (state.location) {
- // Removes the `?` from the beginning of query string if it exists
- const query = qs.parse(state.location.search.slice(1));
- return {
- ...(query.page_size ? { page_size: query.page_size } : {}),
- ...(query.page_index ? { page_index: query.page_index } : {}),
- };
- } else {
- return {};
- }
-};
-
-/**
- * Returns a function that takes in a new page size and returns a new query param string
+ * Returns the query object received from parsing the browsers URL query params.
+ * Used to calculate urls for links and such.
*/
-export const urlFromNewPageSizeParam: (
+export const uiQueryParams: (
state: AlertListState
-) => (newPageSize: number) => string = state => {
- return newPageSize => {
- const urlPaginationData = paginationDataFromUrl(state);
- urlPaginationData.page_size = newPageSize.toString();
+) => Immutable = createSelector(
+ (state: AlertListState) => state.location,
+ (location: AlertListState['location']) => {
+ const data: AlertingIndexUIQueryParams = {};
+ if (location) {
+ // Removes the `?` from the beginning of query string if it exists
+ const query = querystring.parse(location.search.slice(1));
- // Only set the url back to page zero if the user has changed the page index already
- if (urlPaginationData.page_index !== undefined) {
- urlPaginationData.page_index = '0';
+ /**
+ * Build an AlertingIndexUIQueryParams object with keys from the query.
+ * If more than one value exists for a key, use the last.
+ */
+ const keys: Array = [
+ 'page_size',
+ 'page_index',
+ 'selected_alert',
+ ];
+ for (const key of keys) {
+ const value = query[key];
+ if (typeof value === 'string') {
+ data[key] = value;
+ } else if (Array.isArray(value)) {
+ data[key] = value[value.length - 1];
+ }
+ }
}
- return '?' + qs.stringify(urlPaginationData);
- };
-};
+ return data;
+ }
+);
/**
- * Returns a function that takes in a new page index and returns a new query param string
+ * query params to use when requesting alert data.
*/
-export const urlFromNewPageIndexParam: (
+export const apiQueryParams: (
state: AlertListState
-) => (newPageIndex: number) => string = state => {
- return newPageIndex => {
- const urlPaginationData = paginationDataFromUrl(state);
- urlPaginationData.page_index = newPageIndex.toString();
- return '?' + qs.stringify(urlPaginationData);
- };
-};
+) => Immutable = createSelector(
+ uiQueryParams,
+ ({ page_size, page_index }) => ({
+ page_size,
+ page_index,
+ })
+);
+
+export const hasSelectedAlert: (state: AlertListState) => boolean = createSelector(
+ uiQueryParams,
+ ({ selected_alert: selectedAlert }) => selectedAlert !== undefined
+);
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts
index 3aeeeaf1c09e26..b95ff7cb2d45ca 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts
@@ -48,25 +48,36 @@ export const substateMiddlewareFactory = (
};
};
-export const appStoreFactory = (coreStart: CoreStart): Store => {
+export const appStoreFactory: (
+ /**
+ * Allow middleware to communicate with Kibana core.
+ */
+ coreStart: CoreStart,
+ /**
+ * Create the store without any middleware. This is useful for testing the store w/o side effects.
+ */
+ disableMiddleware?: boolean
+) => Store = (coreStart, disableMiddleware = false) => {
const store = createStore(
appReducer,
- composeWithReduxDevTools(
- applyMiddleware(
- substateMiddlewareFactory(
- globalState => globalState.managementList,
- managementMiddlewareFactory(coreStart)
- ),
- substateMiddlewareFactory(
- globalState => globalState.policyList,
- policyListMiddlewareFactory(coreStart)
- ),
- substateMiddlewareFactory(
- globalState => globalState.alertList,
- alertMiddlewareFactory(coreStart)
+ disableMiddleware
+ ? undefined
+ : composeWithReduxDevTools(
+ applyMiddleware(
+ substateMiddlewareFactory(
+ globalState => globalState.managementList,
+ managementMiddlewareFactory(coreStart)
+ ),
+ substateMiddlewareFactory(
+ globalState => globalState.policyList,
+ policyListMiddlewareFactory(coreStart)
+ ),
+ substateMiddlewareFactory(
+ globalState => globalState.alertList,
+ alertMiddlewareFactory(coreStart)
+ )
+ )
)
- )
- )
);
return store;
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts
index 250cbc6e312ed2..9fb12b77e7252f 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts
@@ -72,7 +72,7 @@ describe('endpoint list saga', () => {
expect(fakeHttpServices.post).not.toHaveBeenCalled();
dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' });
await sleep();
- expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints', {
+ expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: 0 }, { page_size: 10 }],
}),
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts
index ae756caf5aa353..754a855c171ad4 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts
@@ -18,7 +18,7 @@ export const managementMiddlewareFactory: MiddlewareFactory