+
+ ) : (
+ bodyHeader
+ )}
+
+
+ )}
+ {children}
+
+
+
+ );
+});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts
new file mode 100644
index 0000000000000..5fbfa5e0e58a8
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/hooks/use_navigate_to_app_event_handler.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MouseEventHandler, useCallback } from 'react';
+import { ApplicationStart } from 'kibana/public';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+
+type NavigateToAppHandlerProps = Parameters;
+type EventHandlerCallback = MouseEventHandler;
+
+/**
+ * Provides an event handlers that can be used with (for example) `onClick` to prevent the
+ * event's default behaviour and instead use Kibana's `navigateToApp()` to send user to a
+ * different app. Use of `navigateToApp()` prevents a full browser refresh for apps that have
+ * been converted to the New Platform.
+ *
+ * @param appId
+ * @param [options]
+ *
+ * @example
+ *
+ * const handleOnClick = useNavigateToAppEventHandler('ingestManager', {path: '#/configs'})
+ * return See configs
+ */
+export const useNavigateToAppEventHandler = (
+ /** the app id - normally the value of the `id` in that plugin's `kibana.json` */
+ appId: NavigateToAppHandlerProps[0],
+
+ /** Options, some of which are passed along to the app route */
+ options?: NavigateToAppHandlerProps[1] & {
+ onClick?: EventHandlerCallback;
+ }
+): EventHandlerCallback => {
+ const { services } = useKibana();
+ const { path, state, onClick } = options || {};
+ return useCallback(
+ ev => {
+ try {
+ if (onClick) {
+ onClick(ev);
+ }
+ } catch (error) {
+ ev.preventDefault();
+ throw error;
+ }
+
+ if (ev.defaultPrevented) {
+ return;
+ }
+
+ if (ev.button !== 0) {
+ return;
+ }
+
+ if (
+ ev.currentTarget instanceof HTMLAnchorElement &&
+ ev.currentTarget.target !== '' &&
+ ev.currentTarget.target !== '_self'
+ ) {
+ return;
+ }
+
+ if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
+ return;
+ }
+
+ ev.preventDefault();
+ services.application.navigateToApp(appId, { path, state });
+ },
+ [appId, onClick, path, services.application, state]
+ );
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
index 884646369b4b1..fa9055e0d9bbd 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx
@@ -6,9 +6,9 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
-import { CoreStart, AppMountParameters } from 'kibana/public';
+import { CoreStart, AppMountParameters, ScopedHistory } from 'kibana/public';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
-import { Route, Switch, BrowserRouter } from 'react-router-dom';
+import { Route, Switch, Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import { useObservable } from 'react-use';
@@ -29,12 +29,11 @@ import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_compon
export function renderApp(
coreStart: CoreStart,
depsStart: EndpointPluginStartDependencies,
- { appBasePath, element }: AppMountParameters
+ { element, history }: AppMountParameters
) {
- coreStart.http.get('/api/endpoint/hello-world');
const store = appStoreFactory({ coreStart, depsStart });
ReactDOM.render(
- ,
+ ,
element
);
return () => {
@@ -43,7 +42,7 @@ export function renderApp(
}
interface RouterProps {
- basename: string;
+ history: ScopedHistory;
store: Store;
coreStart: CoreStart;
depsStart: EndpointPluginStartDependencies;
@@ -51,7 +50,7 @@ interface RouterProps {
const AppRoot: React.FunctionComponent = React.memo(
({
- basename,
+ history,
store,
coreStart: { http, notifications, uiSettings, application },
depsStart: { data },
@@ -63,9 +62,9 @@ const AppRoot: React.FunctionComponent = React.memo(
-
+
-
+ = React.memo(
/>
-
+
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
index 7317bd5e03ed9..a64b3293ec6cd 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
@@ -6,11 +6,6 @@
import React from 'react';
import {
- EuiTitle,
- EuiPage,
- EuiPageBody,
- EuiPageHeader,
- EuiPageHeaderSection,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
@@ -19,64 +14,49 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { usePolicyDetailsSelector } from './policy_hooks';
import { policyDetails } from '../../store/policy_details/selectors';
import { WindowsEventing } from './policy_forms/eventing/windows';
+import { PageView } from '../../components/page_view';
export const PolicyDetails = React.memo(() => {
const policyItem = usePolicyDetailsSelector(policyDetails);
- function policyName() {
- if (policyItem) {
- return {policyItem.name};
- } else {
- return (
-
-
-
- );
- }
- }
+ const headerLeftContent =
+ policyItem?.name ??
+ i18n.translate('xpack.endpoint.policyDetails.notFound', {
+ defaultMessage: 'Policy Not Found',
+ });
+
+ const headerRightContent = (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
return (
-
-
-
-
-
-
{policyName()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
index f949efa46a2bd..7af302de8576e 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
@@ -4,20 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { SyntheticEvent, useCallback, useEffect, useMemo } from 'react';
-import {
- EuiPage,
- EuiPageBody,
- EuiPageContent,
- EuiPageContentBody,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
- EuiTitle,
- EuiBasicTable,
- EuiText,
- EuiTableFieldDataColumnType,
- EuiLink,
-} from '@elastic/eui';
+import React, { useCallback, useEffect, useMemo } from 'react';
+import { EuiBasicTable, EuiText, EuiTableFieldDataColumnType, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useDispatch } from 'react-redux';
@@ -35,6 +23,8 @@ import { usePolicyListSelector } from './policy_hooks';
import { PolicyListAction } from '../../store/policy_list';
import { PolicyData } from '../../types';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import { PageView } from '../../components/page_view';
+import { LinkToApp } from '../../components/link_to_app';
interface TableChangeCallbackArguments {
page: { index: number; size: number };
@@ -145,18 +135,13 @@ export const PolicyList = React.memo(() => {
}),
render(version: string) {
return (
- // eslint-disable-next-line @elastic/eui/href-or-on-click
- {
- ev.preventDefault();
- services.application.navigateToApp('ingestManager', {
- path: `#/configs/${version}`,
- });
- }}
>
{version}
-
+
);
},
},
@@ -165,42 +150,29 @@ export const PolicyList = React.memo(() => {
);
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ }
+ >
+
+
);
});
diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts
index 2759db26bb6c8..ee5bbe71ae8aa 100644
--- a/x-pack/plugins/endpoint/public/plugin.ts
+++ b/x-pack/plugins/endpoint/public/plugin.ts
@@ -47,6 +47,7 @@ export class EndpointPlugin
title: i18n.translate('xpack.endpoint.pluginTitle', {
defaultMessage: 'Endpoint',
}),
+ euiIconType: 'securityApp',
async mount(params: AppMountParameters) {
const [coreStart, depsStart] = await core.getStartServices();
const { renderApp } = await import('./applications/endpoint');
diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts
index 4b4afd8088744..6d2e9e510551a 100644
--- a/x-pack/plugins/endpoint/server/plugin.ts
+++ b/x-pack/plugins/endpoint/server/plugin.ts
@@ -9,7 +9,6 @@ import { PluginSetupContract as FeaturesPluginSetupContract } from '../../featur
import { createConfig$, EndpointConfigType } from './config';
import { EndpointAppContext } from './types';
-import { addRoutes } from './routes';
import { registerEndpointRoutes } from './routes/metadata';
import { registerAlertRoutes } from './routes/alerts';
import { registerResolverRoutes } from './routes/resolver';
@@ -71,7 +70,6 @@ export class EndpointPlugin
},
} as EndpointAppContext;
const router = core.http.createRouter();
- addRoutes(router);
registerEndpointRoutes(router, endpointContext);
registerResolverRoutes(router, endpointContext);
registerAlertRoutes(router, endpointContext);
diff --git a/x-pack/plugins/endpoint/server/routes/index.ts b/x-pack/plugins/endpoint/server/routes/index.ts
deleted file mode 100644
index 8b0476ea7b229..0000000000000
--- a/x-pack/plugins/endpoint/server/routes/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { IRouter } from 'kibana/server';
-
-export function addRoutes(router: IRouter) {
- router.get(
- {
- path: '/api/endpoint/hello-world',
- validate: false,
- options: {
- tags: ['access:resolver'],
- },
- },
- async function greetingIndex(_context, _request, response) {
- return response.ok({
- body: { hello: 'world' },
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- }
- );
-}
diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
index bf3d642307d8c..c543046031e9f 100644
--- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
+++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts
@@ -31,7 +31,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
basePath: '/s/custom_space',
});
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
- expect(navLinks).to.contain('EEndpoint');
+ expect(navLinks).to.contain('Endpoint');
});
it(`endpoint app shows 'Hello World'`, async () => {
@@ -69,7 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
basePath: '/s/custom_space',
});
const navLinks = (await appsMenu.readLinks()).map(link => link.text);
- expect(navLinks).not.to.contain('EEndpoint');
+ expect(navLinks).not.to.contain('Endpoint');
});
});
});
diff --git a/x-pack/test/functional/apps/endpoint/header_nav.ts b/x-pack/test/functional/apps/endpoint/header_nav.ts
index d1fa7311d61e8..c2c4068212484 100644
--- a/x-pack/test/functional/apps/endpoint/header_nav.ts
+++ b/x-pack/test/functional/apps/endpoint/header_nav.ts
@@ -41,7 +41,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('renders the policy page when Policy tab is selected', async () => {
await (await testSubjects.find('policiesEndpointTab')).click();
- await testSubjects.existOrFail('policyViewTitle');
+ await testSubjects.existOrFail('policyListPage');
});
it('renders the home page when Home tab is selected after selecting another tab', async () => {
From 933b6ee99651ba32827a74e88db7700e3c3a673a Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Thu, 26 Mar 2020 14:17:55 +0200
Subject: [PATCH 10/81] [SIEM][CASE] Track unsaved changes (#60925)
* Hide bottom bar when flyout is open
* Track unchanged saves
* Make function optional
* Show action bar when close flyout
Co-authored-by: Elastic Machine
---
.../case/configure/use_configure.tsx | 20 ++++
.../case/components/configure_cases/index.tsx | 96 +++++++++++++++----
.../components/configure_cases/mapping.tsx | 11 ++-
.../components/configure_cases/reducer.ts | 16 ++++
.../configure_cases/translations.ts | 7 ++
5 files changed, 126 insertions(+), 24 deletions(-)
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
index a24f8303824c5..b25667f070fdf 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
@@ -10,6 +10,7 @@ import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api';
import { useStateToaster, errorToToaster } from '../../../components/toasters';
import * as i18n from '../translations';
import { ClosureType } from './types';
+import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer';
interface PersistCaseConfigure {
connectorId: string;
@@ -27,14 +28,17 @@ export interface ReturnUseCaseConfigure {
interface UseCaseConfigure {
setConnector: (newConnectorId: string, newConnectorName?: string) => void;
setClosureType?: (newClosureType: ClosureType) => void;
+ setCurrentConfiguration?: (configuration: CurrentConfiguration) => void;
}
export const useCaseConfigure = ({
setConnector,
setClosureType,
+ setCurrentConfiguration,
}: UseCaseConfigure): ReturnUseCaseConfigure => {
const [, dispatchToaster] = useStateToaster();
const [loading, setLoading] = useState(true);
+ const [firstLoad, setFirstLoad] = useState(false);
const [persistLoading, setPersistLoading] = useState(false);
const [version, setVersion] = useState('');
@@ -54,6 +58,16 @@ export const useCaseConfigure = ({
setClosureType(res.closureType);
}
setVersion(res.version);
+
+ if (!firstLoad) {
+ setFirstLoad(true);
+ if (setCurrentConfiguration != null) {
+ setCurrentConfiguration({
+ connectorId: res.connectorId,
+ closureType: res.closureType,
+ });
+ }
+ }
}
}
} catch (error) {
@@ -104,6 +118,12 @@ export const useCaseConfigure = ({
setClosureType(res.closureType);
}
setVersion(res.version);
+ if (setCurrentConfiguration != null) {
+ setCurrentConfiguration({
+ connectorId: res.connectorId,
+ closureType: res.closureType,
+ });
+ }
}
} catch (error) {
if (!didCancel) {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
index 5f99ec362cd5e..a1f24275df6cd 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
@@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useReducer, useCallback, useEffect, useState } from 'react';
+import React, {
+ useReducer,
+ useCallback,
+ useEffect,
+ useState,
+ Dispatch,
+ SetStateAction,
+} from 'react';
import styled, { css } from 'styled-components';
import {
@@ -14,8 +21,9 @@ import {
EuiCallOut,
EuiBottomBar,
EuiButtonEmpty,
+ EuiText,
} from '@elastic/eui';
-import { isEmpty } from 'lodash/fp';
+import { isEmpty, difference } from 'lodash/fp';
import { useKibana } from '../../../../lib/kibana';
import { useConnectors } from '../../../../containers/case/configure/use_connectors';
import { useCaseConfigure } from '../../../../containers/case/configure/use_configure';
@@ -40,7 +48,7 @@ import { ClosureOptions } from '../configure_cases/closure_options';
import { Mapping } from '../configure_cases/mapping';
import { SectionWrapper } from '../wrappers';
import { navTabs } from '../../../../pages/home/home_navigations';
-import { configureCasesReducer, State } from './reducer';
+import { configureCasesReducer, State, CurrentConfiguration } from './reducer';
import * as i18n from './translations';
const FormWrapper = styled.div`
@@ -58,6 +66,7 @@ const initialState: State = {
connectorId: 'none',
closureType: 'close-by-user',
mapping: null,
+ currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' },
};
const actionTypes: ActionType[] = [
@@ -83,14 +92,20 @@ const ConfigureCasesComponent: React.FC = () => {
);
const [actionBarVisible, setActionBarVisible] = useState(false);
+ const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0);
- const handleShowAddFlyout = useCallback(() => setAddFlyoutVisibility(true), []);
-
- const [{ connectorId, closureType, mapping }, dispatch] = useReducer(
+ const [{ connectorId, closureType, mapping, currentConfiguration }, dispatch] = useReducer(
configureCasesReducer(),
initialState
);
+ const setCurrentConfiguration = useCallback((configuration: CurrentConfiguration) => {
+ dispatch({
+ type: 'setCurrentConfiguration',
+ currentConfiguration: { ...configuration },
+ });
+ }, []);
+
const setConnectorId = useCallback((newConnectorId: string) => {
dispatch({
type: 'setConnectorId',
@@ -115,6 +130,7 @@ const ConfigureCasesComponent: React.FC = () => {
const { loading: loadingCaseConfigure, persistLoading, persistCaseConfigure } = useCaseConfigure({
setConnector: setConnectorId,
setClosureType,
+ setCurrentConfiguration,
});
const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors();
@@ -137,16 +153,47 @@ const ConfigureCasesComponent: React.FC = () => {
[connectorId, connectors, closureType, mapping]
);
- const onChangeConnector = useCallback((newConnectorId: string) => {
- setActionBarVisible(true);
- setConnectorId(newConnectorId);
+ const onClickAddConnector = useCallback(() => {
+ setActionBarVisible(false);
+ setAddFlyoutVisibility(true);
}, []);
- const onChangeClosureType = useCallback((newClosureType: ClosureType) => {
- setActionBarVisible(true);
- setClosureType(newClosureType);
+ const onClickUpdateConnector = useCallback(() => {
+ setActionBarVisible(false);
+ setEditFlyoutVisibility(true);
}, []);
+ const handleActionBar = useCallback(() => {
+ const unsavedChanges = difference(Object.values(currentConfiguration), [
+ connectorId,
+ closureType,
+ ]).length;
+
+ if (unsavedChanges === 0) {
+ setActionBarVisible(false);
+ } else {
+ setActionBarVisible(true);
+ }
+
+ setTotalConfigurationChanges(unsavedChanges);
+ }, [currentConfiguration, connectorId, closureType]);
+
+ const handleSetAddFlyoutVisibility = useCallback(
+ (isVisible: boolean) => {
+ handleActionBar();
+ setAddFlyoutVisibility(isVisible);
+ },
+ [currentConfiguration, connectorId, closureType]
+ );
+
+ const handleSetEditFlyoutVisibility = useCallback(
+ (isVisible: boolean) => {
+ handleActionBar();
+ setEditFlyoutVisibility(isVisible);
+ },
+ [currentConfiguration, connectorId, closureType]
+ );
+
useEffect(() => {
if (
!isEmpty(connectors) &&
@@ -188,6 +235,10 @@ const ConfigureCasesComponent: React.FC = () => {
}
}, [connectors, connectorId]);
+ useEffect(() => {
+ handleActionBar();
+ }, [connectors, connectorId, closureType, currentConfiguration]);
+
return (
{!connectorIsValid && (
@@ -202,8 +253,8 @@ const ConfigureCasesComponent: React.FC = () => {
connectors={connectors ?? []}
disabled={persistLoading || isLoadingConnectors}
isLoading={isLoadingConnectors}
- onChangeConnector={onChangeConnector}
- handleShowAddFlyout={handleShowAddFlyout}
+ onChangeConnector={setConnectorId}
+ handleShowAddFlyout={onClickAddConnector}
selectedConnector={connectorId}
/>
@@ -211,7 +262,7 @@ const ConfigureCasesComponent: React.FC = () => {
@@ -220,12 +271,17 @@ const ConfigureCasesComponent: React.FC = () => {
updateConnectorDisabled={updateConnectorDisabled}
mapping={mapping}
onChangeMapping={setMapping}
- setEditFlyoutVisibility={setEditFlyoutVisibility}
+ setEditFlyoutVisibility={onClickUpdateConnector}
/>
{actionBarVisible && (
-
+
+
+
+ {i18n.UNSAVED_CHANGES(totalConfigurationChanges)}
+
+
@@ -269,7 +325,7 @@ const ConfigureCasesComponent: React.FC = () => {
>
>}
actionTypes={actionTypes}
/>
{editedConnectorItem && (
@@ -277,7 +333,9 @@ const ConfigureCasesComponent: React.FC = () => {
key={editedConnectorItem.id}
initialConnector={editedConnectorItem}
editFlyoutVisible={editFlyoutVisible}
- setEditFlyoutVisibility={setEditFlyoutVisibility}
+ setEditFlyoutVisibility={
+ handleSetEditFlyoutVisibility as Dispatch>
+ }
/>
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx
index 2600a9f4e13ac..8cba73d1249df 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useCallback } from 'react';
+import React from 'react';
import styled from 'styled-components';
import {
@@ -25,7 +25,7 @@ interface MappingProps {
updateConnectorDisabled: boolean;
mapping: CasesConfigurationMapping[] | null;
onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void;
- setEditFlyoutVisibility: React.Dispatch>;
+ setEditFlyoutVisibility: () => void;
}
const EuiButtonEmptyExtended = styled(EuiButtonEmpty)`
@@ -40,8 +40,6 @@ const MappingComponent: React.FC = ({
onChangeMapping,
setEditFlyoutVisibility,
}) => {
- const onClick = useCallback(() => setEditFlyoutVisibility(true), []);
-
return (
= ({
-
+
{i18n.UPDATE_CONNECTOR}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts
index f9e4a73b3c396..f6b9d38a76de3 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts
@@ -13,9 +13,19 @@ export interface State {
mapping: CasesConfigurationMapping[] | null;
connectorId: string;
closureType: ClosureType;
+ currentConfiguration: CurrentConfiguration;
+}
+
+export interface CurrentConfiguration {
+ connectorId: State['connectorId'];
+ closureType: State['closureType'];
}
export type Action =
+ | {
+ type: 'setCurrentConfiguration';
+ currentConfiguration: CurrentConfiguration;
+ }
| {
type: 'setConnectorId';
connectorId: string;
@@ -31,6 +41,12 @@ export type Action =
export const configureCasesReducer = () => (state: State, action: Action) => {
switch (action.type) {
+ case 'setCurrentConfiguration': {
+ return {
+ ...state,
+ currentConfiguration: { ...action.currentConfiguration },
+ };
+ }
case 'setConnectorId': {
return {
...state,
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts
index dd9bf82fb0b0d..33784cfe0058c 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts
@@ -190,3 +190,10 @@ export const FIELD_MAPPING_FIELD_COMMENTS = i18n.translate(
export const UPDATE_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.updateConnector', {
defaultMessage: 'Update connector',
});
+
+export const UNSAVED_CHANGES = (unsavedChanges: number): string => {
+ return i18n.translate('xpack.siem.case.configureCases.unsavedChanges', {
+ values: { unsavedChanges },
+ defaultMessage: '{unsavedChanges} unsaved changes',
+ });
+};
From 6222be59e4a8381add5cdd535ee2ca968761ca68 Mon Sep 17 00:00:00 2001
From: Andrew Cholakian
Date: Thu, 26 Mar 2020 07:32:18 -0500
Subject: [PATCH 11/81] Fix unhandled telemetry promise (#61376)
---
.../server/lib/adapters/telemetry/kibana_telemetry_adapter.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
index e10a476bcc668..3b2696ba23f7c 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
@@ -175,7 +175,7 @@ export class KibanaTelemetryAdapter {
this.collector[bucket].observer_location_name_stats = {
min_length: locationNameStats?.min_length ?? 0,
max_length: locationNameStats?.max_length ?? 0,
- avg_length: +locationNameStats?.avg_length.toFixed(2),
+ avg_length: +(locationNameStats?.avg_length?.toFixed(2) ?? 0),
};
this.collector[bucket].monitor_frequency = this.getMonitorsFrequency(uniqueMonitors);
From 9c123bc01841ade625f45bd5bd2d6503aad2540b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?=
Date: Thu, 26 Mar 2020 09:16:54 -0400
Subject: [PATCH 12/81] Use new grantAPIKeyAsInternalUser and
invalidateAPIKeyAsInternalUser APIs from the security plugin (#60967)
* Use new grantAPIKeyAsInternalUser and invalidateAPIKeyAsInternalUser APIs from the security plugin
* Update x-pack/plugins/alerting/server/alerts_client_factory.ts
Co-Authored-By: Patrick Mueller
* ESLint fixes
* Temp test using unverified snapshot
* Revert "Temp test using unverified snapshot"
This reverts commit a39499bed376c365d1f689e4379e5521673e5d49.
Co-authored-by: Patrick Mueller
Co-authored-by: Elastic Machine
---
x-pack/plugins/alerting/server/alerts_client.ts | 4 ++--
.../alerting/server/alerts_client_factory.test.ts | 13 +++++++++----
.../alerting/server/alerts_client_factory.ts | 14 +++++++-------
.../security/server/authentication/index.ts | 1 +
x-pack/plugins/security/server/index.ts | 1 +
5 files changed, 20 insertions(+), 13 deletions(-)
diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts
index 49c80af0072c9..5538b44b69fcb 100644
--- a/x-pack/plugins/alerting/server/alerts_client.ts
+++ b/x-pack/plugins/alerting/server/alerts_client.ts
@@ -27,7 +27,7 @@ import {
import { validateAlertTypeParams } from './lib';
import {
InvalidateAPIKeyParams,
- CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult,
+ GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult,
InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult,
} from '../../../plugins/security/server';
import { EncryptedSavedObjectsPluginStart } from '../../../plugins/encrypted_saved_objects/server';
@@ -37,7 +37,7 @@ import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instan
type NormalizedAlertAction = Omit;
export type CreateAPIKeyResult =
| { apiKeysEnabled: false }
- | { apiKeysEnabled: true; result: SecurityPluginCreateAPIKeyResult };
+ | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult };
export type InvalidateAPIKeyResult =
| { apiKeysEnabled: false }
| { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult };
diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts
index b0558ef1ea98c..4c74ca54a0d2f 100644
--- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts
@@ -17,7 +17,7 @@ jest.mock('./alerts_client');
const savedObjectsClient = savedObjectsClientMock.create();
const securityPluginSetup = {
authc: {
- createAPIKey: jest.fn(),
+ grantAPIKeyAsInternalUser: jest.fn(),
getCurrentUser: jest.fn(),
},
};
@@ -110,7 +110,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
- securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce(null);
+ securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null);
const createAPIKeyResult = await constructorCall.createAPIKey();
expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false });
});
@@ -124,7 +124,10 @@ test('createAPIKey() returns an API key when security is enabled', async () => {
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
- securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce({ api_key: '123', id: 'abc' });
+ securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({
+ api_key: '123',
+ id: 'abc',
+ });
const createAPIKeyResult = await constructorCall.createAPIKey();
expect(createAPIKeyResult).toEqual({
apiKeysEnabled: true,
@@ -141,7 +144,9 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error',
factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
- securityPluginSetup.authc.createAPIKey.mockRejectedValueOnce(new Error('TLS disabled'));
+ securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce(
+ new Error('TLS disabled')
+ );
await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot(
`"TLS disabled"`
);
diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts
index c502c0e5bf1cf..fd480658e236a 100644
--- a/x-pack/plugins/alerting/server/alerts_client_factory.ts
+++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import uuid from 'uuid';
import { AlertsClient } from './alerts_client';
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server';
@@ -71,10 +70,12 @@ export class AlertsClientFactory {
if (!securityPluginSetup) {
return { apiKeysEnabled: false };
}
- const createAPIKeyResult = await securityPluginSetup.authc.createAPIKey(request, {
- name: `source: alerting, generated uuid: "${uuid.v4()}"`,
- role_descriptors: {},
- });
+ // Create an API key using the new grant API - in this case the Kibana system user is creating the
+ // API key for the user, instead of having the user create it themselves, which requires api_key
+ // privileges
+ const createAPIKeyResult = await securityPluginSetup.authc.grantAPIKeyAsInternalUser(
+ request
+ );
if (!createAPIKeyResult) {
return { apiKeysEnabled: false };
}
@@ -87,8 +88,7 @@ export class AlertsClientFactory {
if (!securityPluginSetup) {
return { apiKeysEnabled: false };
}
- const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKey(
- request,
+ const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser(
params
);
// Null when Elasticsearch security is disabled
diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts
index 30ac84632cb7e..5d7b49de68d28 100644
--- a/x-pack/plugins/security/server/authentication/index.ts
+++ b/x-pack/plugins/security/server/authentication/index.ts
@@ -27,6 +27,7 @@ export {
InvalidateAPIKeyResult,
CreateAPIKeyParams,
InvalidateAPIKeyParams,
+ GrantAPIKeyResult,
} from './api_keys';
export {
BasicHTTPAuthorizationHeaderCredentials,
diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts
index caeb06e6f3153..0011737d85734 100644
--- a/x-pack/plugins/security/server/index.ts
+++ b/x-pack/plugins/security/server/index.ts
@@ -23,6 +23,7 @@ export {
CreateAPIKeyResult,
InvalidateAPIKeyParams,
InvalidateAPIKeyResult,
+ GrantAPIKeyResult,
SAMLLogin,
OIDCLogin,
} from './authentication';
From 90d70288ce86a7ee801c98087fa4064f18dfe9c0 Mon Sep 17 00:00:00 2001
From: Melissa Alvarez
Date: Thu, 26 Mar 2020 09:33:29 -0400
Subject: [PATCH 13/81] ensure reselected fields are sorted correctly (#61342)
---
.../results_table.tsx | 137 +++++++++---------
.../regression_exploration/results_table.tsx | 137 +++++++++---------
2 files changed, 140 insertions(+), 134 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx
index 84abbda71643c..20d126f4ac6ca 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx
@@ -44,6 +44,7 @@ import {
BASIC_NUMERICAL_TYPES,
EXTENDED_NUMERICAL_TYPES,
isKeywordAndTextType,
+ sortRegressionResultsFields,
} from '../../../../common/fields';
import {
@@ -134,79 +135,81 @@ export const ResultsTable: FC = React.memo(
tableItems,
} = useExploreData(jobConfig, selectedFields, setSelectedFields, setDocFields, setDepVarType);
- const columns: Array> = selectedFields.map(field => {
- const { type } = field;
- const isNumber =
- type !== undefined &&
- (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
-
- const column: ColumnType = {
- field: field.name,
- name: field.name,
- sortable: true,
- truncateText: true,
- };
-
- const render = (d: any, fullItem: EsDoc) => {
- if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
- // If the cells data is an array of strings, return as a comma separated list.
- // The list will get limited to 5 items with `…` at the end if there's more in the original array.
- return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
- } else if (Array.isArray(d)) {
- // If the cells data is an array of e.g. objects, display a 'array' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
+ const columns: Array> = selectedFields
+ .sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig))
+ .map(field => {
+ const { type } = field;
+ const isNumber =
+ type !== undefined &&
+ (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
+
+ const column: ColumnType = {
+ field: field.name,
+ name: field.name,
+ sortable: true,
+ truncateText: true,
+ };
+
+ const render = (d: any, fullItem: EsDoc) => {
+ if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
+ // If the cells data is an array of strings, return as a comma separated list.
+ // The list will get limited to 5 items with `…` at the end if there's more in the original array.
+ return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
+ } else if (Array.isArray(d)) {
+ // If the cells data is an array of e.g. objects, display a 'array' badge with a
+ // tooltip that explains that this type of field is not supported in this table.
+ return (
+
-
- );
- }
-
- return d;
- };
+ >
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
+ {
+ defaultMessage: 'array',
+ }
+ )}
+
+
+ );
+ }
- if (isNumber) {
- column.dataType = 'number';
- column.render = render;
- } else if (typeof type !== 'undefined') {
- switch (type) {
- case ES_FIELD_TYPES.BOOLEAN:
- column.dataType = ES_FIELD_TYPES.BOOLEAN;
- break;
- case ES_FIELD_TYPES.DATE:
- column.align = 'right';
- column.render = (d: any) => {
- if (d !== undefined) {
- return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
- }
- return d;
- };
- break;
- default:
- column.render = render;
- break;
+ return d;
+ };
+
+ if (isNumber) {
+ column.dataType = 'number';
+ column.render = render;
+ } else if (typeof type !== 'undefined') {
+ switch (type) {
+ case ES_FIELD_TYPES.BOOLEAN:
+ column.dataType = ES_FIELD_TYPES.BOOLEAN;
+ break;
+ case ES_FIELD_TYPES.DATE:
+ column.align = 'right';
+ column.render = (d: any) => {
+ if (d !== undefined) {
+ return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
+ }
+ return d;
+ };
+ break;
+ default:
+ column.render = render;
+ break;
+ }
+ } else {
+ column.render = render;
}
- } else {
- column.render = render;
- }
- return column;
- });
+ return column;
+ });
const docFieldsCount = docFields.length;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
index 4e9571ca4a34d..7a6b2b23ba7a3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
@@ -45,6 +45,7 @@ import {
EXTENDED_NUMERICAL_TYPES,
toggleSelectedField,
isKeywordAndTextType,
+ sortRegressionResultsFields,
} from '../../../../common/fields';
import {
@@ -134,79 +135,81 @@ export const ResultsTable: FC = React.memo(
tableItems,
} = useExploreData(jobConfig, selectedFields, setSelectedFields, setDocFields, setDepVarType);
- const columns: Array> = selectedFields.map(field => {
- const { type } = field;
- const isNumber =
- type !== undefined &&
- (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
-
- const column: ColumnType = {
- field: field.name,
- name: field.name,
- sortable: true,
- truncateText: true,
- };
-
- const render = (d: any, fullItem: EsDoc) => {
- if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
- // If the cells data is an array of strings, return as a comma separated list.
- // The list will get limited to 5 items with `…` at the end if there's more in the original array.
- return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
- } else if (Array.isArray(d)) {
- // If the cells data is an array of e.g. objects, display a 'array' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
+ const columns: Array> = selectedFields
+ .sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig))
+ .map(field => {
+ const { type } = field;
+ const isNumber =
+ type !== undefined &&
+ (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
+
+ const column: ColumnType = {
+ field: field.name,
+ name: field.name,
+ sortable: true,
+ truncateText: true,
+ };
+
+ const render = (d: any, fullItem: EsDoc) => {
+ if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
+ // If the cells data is an array of strings, return as a comma separated list.
+ // The list will get limited to 5 items with `…` at the end if there's more in the original array.
+ return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
+ } else if (Array.isArray(d)) {
+ // If the cells data is an array of e.g. objects, display a 'array' badge with a
+ // tooltip that explains that this type of field is not supported in this table.
+ return (
+
-
- );
- }
-
- return d;
- };
+ >
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
+ {
+ defaultMessage: 'array',
+ }
+ )}
+
+
+ );
+ }
- if (isNumber) {
- column.dataType = 'number';
- column.render = render;
- } else if (typeof type !== 'undefined') {
- switch (type) {
- case ES_FIELD_TYPES.BOOLEAN:
- column.dataType = ES_FIELD_TYPES.BOOLEAN;
- break;
- case ES_FIELD_TYPES.DATE:
- column.align = 'right';
- column.render = (d: any) => {
- if (d !== undefined) {
- return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
- }
- return d;
- };
- break;
- default:
- column.render = render;
- break;
+ return d;
+ };
+
+ if (isNumber) {
+ column.dataType = 'number';
+ column.render = render;
+ } else if (typeof type !== 'undefined') {
+ switch (type) {
+ case ES_FIELD_TYPES.BOOLEAN:
+ column.dataType = ES_FIELD_TYPES.BOOLEAN;
+ break;
+ case ES_FIELD_TYPES.DATE:
+ column.align = 'right';
+ column.render = (d: any) => {
+ if (d !== undefined) {
+ return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
+ }
+ return d;
+ };
+ break;
+ default:
+ column.render = render;
+ break;
+ }
+ } else {
+ column.render = render;
}
- } else {
- column.render = render;
- }
- return column;
- });
+ return column;
+ });
const docFieldsCount = docFields.length;
From f56d5c86e0c099a9bdbfe9abd0a657305f230055 Mon Sep 17 00:00:00 2001
From: Henry
Date: Thu, 26 Mar 2020 09:48:22 -0400
Subject: [PATCH 14/81] use fixed table layout (#61294)
---
.../sections/epm/screens/detail/markdown_renderers.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/markdown_renderers.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/markdown_renderers.tsx
index 2e321e8bfc36f..c164138e35740 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/markdown_renderers.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/markdown_renderers.tsx
@@ -28,9 +28,7 @@ export const markdownRenderers = {
{children}
),
table: ({ children }: { children: React.ReactNode[] }) => (
-
- {children}
-
+
{children}
),
tableRow: ({ children }: { children: React.ReactNode[] }) => (
{children}
From cec916566364d7b2b96ad232a74bad0443402491 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Thu, 26 Mar 2020 14:49:16 +0100
Subject: [PATCH 15/81] Migrate SO management libs to typescript (#60555)
* migrate most libs
* migrate last lib files
* fix get_relationships
* fix getSavedObject
* migrate tests to TS
* address review comments
* move test files outside of __jest__ folder
---
.../lib/__jest__/import_legacy_file.test.js | 63 ----------
.../extract_export_details.test.ts | 2 +-
....js => fetch_export_by_type_and_search.ts} | 6 +-
...ort_objects.js => fetch_export_objects.ts} | 5 +-
.../lib/{find_objects.js => find_objects.ts} | 5 +-
..._default_title.js => get_default_title.ts} | 2 +-
...hips.test.js => get_relationships.test.ts} | 34 +++--
..._relationships.js => get_relationships.ts} | 22 ++--
...t_counts.js => get_saved_object_counts.ts} | 12 +-
...ect_label.js => get_saved_object_label.ts} | 2 +-
.../lib/{import_file.js => import_file.ts} | 2 +-
.../objects/lib/import_legacy_file.test.ts | 37 ++++++
...t_legacy_file.js => import_legacy_file.ts} | 5 +-
.../in_app_url.test.js => in_app_url.test.ts} | 52 ++++----
.../management/sections/objects/lib/index.js | 35 ------
.../management/sections/objects/lib/index.ts | 45 +++++++
.../sections/objects/lib/is_same_query.js | 32 -----
..._legacy_import.js => log_legacy_import.ts} | 0
...arse_query.test.js => parse_query.test.ts} | 2 +-
.../lib/{parse_query.js => parse_query.ts} | 8 +-
...est.js => process_import_response.test.ts} | 117 ++++++++----------
.../objects/lib/process_import_response.ts | 18 +--
....test.js => resolve_import_errors.test.ts} | 33 ++---
...ort_errors.js => resolve_import_errors.ts} | 38 ++++--
....test.js => resolve_saved_objects.test.ts} | 83 ++++++++-----
...ed_objects.js => resolve_saved_objects.ts} | 89 ++++++++-----
.../management/sections/objects/types.ts | 20 ++-
.../saved_object/saved_object_loader.ts | 2 +-
28 files changed, 413 insertions(+), 358 deletions(-)
delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__ => }/extract_export_details.test.ts (98%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{fetch_export_by_type_and_search.js => fetch_export_by_type_and_search.ts} (86%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{fetch_export_objects.js => fetch_export_objects.ts} (89%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{find_objects.js => find_objects.ts} (85%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{get_default_title.js => get_default_title.ts} (92%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__/get_relationships.test.js => get_relationships.test.ts} (71%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{get_relationships.js => get_relationships.ts} (69%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{get_saved_object_counts.js => get_saved_object_counts.ts} (74%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{get_saved_object_label.js => get_saved_object_label.ts} (94%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{import_file.js => import_file.ts} (93%)
create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.test.ts
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{import_legacy_file.js => import_legacy_file.ts} (88%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__/in_app_url.test.js => in_app_url.test.ts} (79%)
delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts
delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/is_same_query.js
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{log_legacy_import.js => log_legacy_import.ts} (100%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__/parse_query.test.js => parse_query.test.ts} (96%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{parse_query.js => parse_query.ts} (89%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__/process_import_response.test.js => process_import_response.test.ts} (66%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__/resolve_import_errors.test.js => resolve_import_errors.test.ts} (90%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{resolve_import_errors.js => resolve_import_errors.ts} (84%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{__jest__/resolve_saved_objects.test.js => resolve_saved_objects.test.ts} (85%)
rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{resolve_saved_objects.js => resolve_saved_objects.ts} (78%)
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js
deleted file mode 100644
index 57b8557fb9afe..0000000000000
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/import_legacy_file.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { importLegacyFile } from '../import_legacy_file';
-
-describe('importFile', () => {
- it('should import a file', async () => {
- class FileReader {
- readAsText(text) {
- this.onload({
- target: {
- result: JSON.stringify({ text }),
- },
- });
- }
- }
-
- const file = 'foo';
-
- const imported = await importLegacyFile(file, FileReader);
- expect(imported).toEqual({ text: file });
- });
-
- it('should throw errors', async () => {
- class FileReader {
- readAsText() {
- this.onload({
- target: {
- result: 'not_parseable',
- },
- });
- }
- }
-
- const file = 'foo';
-
- try {
- await importLegacyFile(file, FileReader);
- } catch (e) {
- // There isn't a great way to handle throwing exceptions
- // with async/await but this seems to work :shrug:
- expect(() => {
- throw e;
- }).toThrow();
- }
- });
-});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.test.ts
similarity index 98%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.test.ts
index 4ecc3583e76ce..76f5c6e87ec90 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.test.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { extractExportDetails, SavedObjectsExportResultDetails } from '../extract_export_details';
+import { extractExportDetails, SavedObjectsExportResultDetails } from './extract_export_details';
describe('extractExportDetails', () => {
const objLine = (id: string, type: string) => {
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.ts
similarity index 86%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.ts
index 788a4635d8dac..d3e527b9f96b7 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_by_type_and_search.ts
@@ -19,7 +19,11 @@
import { kfetch } from 'ui/kfetch';
-export async function fetchExportByTypeAndSearch(types, search, includeReferencesDeep = false) {
+export async function fetchExportByTypeAndSearch(
+ types: string[],
+ search: string | undefined,
+ includeReferencesDeep: boolean = false
+): Promise {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_export',
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.ts
similarity index 89%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.ts
index 2521113f53752..744f8ef38af47 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/fetch_export_objects.ts
@@ -19,7 +19,10 @@
import { kfetch } from 'ui/kfetch';
-export async function fetchExportObjects(objects, includeReferencesDeep = false) {
+export async function fetchExportObjects(
+ objects: any[],
+ includeReferencesDeep: boolean = false
+): Promise {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_export',
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts
similarity index 85%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts
index caf2b5f503440..24e08f0524f62 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.ts
@@ -18,13 +18,14 @@
*/
import { kfetch } from 'ui/kfetch';
+import { SavedObjectsFindOptions } from 'src/core/public';
import { keysToCamelCaseShallow } from './case_conversion';
-export async function findObjects(findOptions) {
+export async function findObjects(findOptions: SavedObjectsFindOptions) {
const response = await kfetch({
method: 'GET',
pathname: '/api/kibana/management/saved_objects/_find',
- query: findOptions,
+ query: findOptions as Record,
});
return keysToCamelCaseShallow(response);
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.ts
similarity index 92%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.ts
index 5aa9601f04baa..0abfeee72915c 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_default_title.ts
@@ -17,6 +17,6 @@
* under the License.
*/
-export function getDefaultTitle(object) {
+export function getDefaultTitle(object: { id: string; type: string }) {
return `${object.type} [id=${object.id}]`;
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/get_relationships.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts
similarity index 71%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/get_relationships.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts
index 880d577a9373e..b45b51b4de293 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/get_relationships.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.test.ts
@@ -17,24 +17,24 @@
* under the License.
*/
-import { getRelationships } from '../get_relationships';
+import { getRelationships } from './get_relationships';
describe('getRelationships', () => {
it('should make an http request', async () => {
- const $http = jest.fn();
+ const $http = jest.fn() as any;
const basePath = 'test';
- await getRelationships('dashboard', 1, ['search', 'index-pattern'], $http, basePath);
+ await getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath);
expect($http.mock.calls.length).toBe(1);
});
it('should handle successful responses', async () => {
- const $http = jest.fn().mockImplementation(() => ({ data: [1, 2] }));
+ const $http = jest.fn().mockImplementation(() => ({ data: [1, 2] })) as any;
const basePath = 'test';
const response = await getRelationships(
'dashboard',
- 1,
+ '1',
['search', 'index-pattern'],
$http,
basePath
@@ -44,23 +44,17 @@ describe('getRelationships', () => {
it('should handle errors', async () => {
const $http = jest.fn().mockImplementation(() => {
- throw {
- data: {
- error: 'Test error',
- statusCode: 500,
- },
+ const err = new Error();
+ (err as any).data = {
+ error: 'Test error',
+ statusCode: 500,
};
- });
+ throw err;
+ }) as any;
const basePath = 'test';
- try {
- await getRelationships('dashboard', 1, ['search', 'index-pattern'], $http, basePath);
- } catch (e) {
- // There isn't a great way to handle throwing exceptions
- // with async/await but this seems to work :shrug:
- expect(() => {
- throw e;
- }).toThrow();
- }
+ await expect(
+ getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"Test error"`);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts
similarity index 69%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts
index d5060eb2dd735..07bdf2db68fa2 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_relationships.ts
@@ -17,9 +17,17 @@
* under the License.
*/
+import { IHttpService } from 'angular';
import { get } from 'lodash';
+import { SavedObjectRelation } from '../types';
-export async function getRelationships(type, id, savedObjectTypes, $http, basePath) {
+export async function getRelationships(
+ type: string,
+ id: string,
+ savedObjectTypes: string[],
+ $http: IHttpService,
+ basePath: string
+): Promise {
const url = `${basePath}/api/kibana/management/saved_objects/relationships/${encodeURIComponent(
type
)}/${encodeURIComponent(id)}`;
@@ -27,19 +35,19 @@ export async function getRelationships(type, id, savedObjectTypes, $http, basePa
method: 'GET',
url,
params: {
- savedObjectTypes: savedObjectTypes,
+ savedObjectTypes,
},
};
try {
- const response = await $http(options);
- return response ? response.data : undefined;
+ const response = await $http(options);
+ return response?.data;
} catch (resp) {
- const respBody = get(resp, 'data', {});
+ const respBody = get(resp, 'data', {}) as any;
const err = new Error(respBody.message || respBody.error || `${resp.status} Response`);
- err.statusCode = respBody.statusCode || resp.status;
- err.body = respBody;
+ (err as any).statusCode = respBody.statusCode || resp.status;
+ (err as any).body = respBody;
throw err;
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts
similarity index 74%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts
index 9fc531d53d378..d4dda1190bc43 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_counts.ts
@@ -17,10 +17,18 @@
* under the License.
*/
+import { IHttpService } from 'angular';
import chrome from 'ui/chrome';
const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll');
-export async function getSavedObjectCounts($http, typesToInclude, searchString) {
- const results = await $http.post(`${apiBase}/counts`, { typesToInclude, searchString });
+export async function getSavedObjectCounts(
+ $http: IHttpService,
+ typesToInclude: string[],
+ searchString: string
+): Promise> {
+ const results = await $http.post>(`${apiBase}/counts`, {
+ typesToInclude,
+ searchString,
+ });
return results.data;
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.ts
similarity index 94%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.ts
index b28d254f61d0d..9b34d8d0af321 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/get_saved_object_label.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-export function getSavedObjectLabel(type) {
+export function getSavedObjectLabel(type: string) {
switch (type) {
case 'index-pattern':
case 'index-patterns':
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.ts
similarity index 93%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.ts
index a76078d7e06e7..9bd5fbeed3a4c 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_file.ts
@@ -19,7 +19,7 @@
import { kfetch } from 'ui/kfetch';
-export async function importFile(file, overwriteAll = false) {
+export async function importFile(file: Blob, overwriteAll: boolean = false) {
const formData = new FormData();
formData.append('file', file);
return await kfetch({
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.test.ts
new file mode 100644
index 0000000000000..b2132979d3d19
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.test.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { importLegacyFile } from './import_legacy_file';
+
+describe('importFile', () => {
+ it('should import a file with valid json format', async () => {
+ const file = new File([`{"text": "foo"}`], 'file.json');
+
+ const imported = await importLegacyFile(file);
+ expect(imported).toEqual({ text: 'foo' });
+ });
+
+ it('should throw errors when file content is not parseable', async () => {
+ const file = new File([`not_parseable`], 'file.json');
+
+ await expect(importLegacyFile(file)).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Unexpected token o in JSON at position 1"`
+ );
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.ts
similarity index 88%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.ts
index 12aaf46b29445..0d86866fa3c1b 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/import_legacy_file.ts
@@ -17,10 +17,11 @@
* under the License.
*/
-export async function importLegacyFile(file, FileReader = window.FileReader) {
+export async function importLegacyFile(file: File) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
- fr.onload = ({ target: { result } }) => {
+ fr.onload = event => {
+ const result = event.target!.result as string;
try {
resolve(JSON.parse(result));
} catch (e) {
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/in_app_url.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.test.ts
similarity index 79%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/in_app_url.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.test.ts
index af2e7dcf704df..c0d6716391a1f 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/in_app_url.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/in_app_url.test.ts
@@ -17,100 +17,110 @@
* under the License.
*/
-import { canViewInApp } from '../in_app_url';
+import { Capabilities } from '../../../../../../../../core/public';
+import { canViewInApp } from './in_app_url';
+
+const createCapabilities = (sections: Record): Capabilities => {
+ return {
+ navLinks: {},
+ management: {},
+ catalogue: {},
+ ...sections,
+ };
+};
describe('canViewInApp', () => {
it('should handle saved searches', () => {
- let uiCapabilities = {
+ let uiCapabilities = createCapabilities({
discover: {
show: true,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'search')).toEqual(true);
expect(canViewInApp(uiCapabilities, 'searches')).toEqual(true);
- uiCapabilities = {
+ uiCapabilities = createCapabilities({
discover: {
show: false,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'search')).toEqual(false);
expect(canViewInApp(uiCapabilities, 'searches')).toEqual(false);
});
it('should handle visualizations', () => {
- let uiCapabilities = {
+ let uiCapabilities = createCapabilities({
visualize: {
show: true,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'visualization')).toEqual(true);
expect(canViewInApp(uiCapabilities, 'visualizations')).toEqual(true);
- uiCapabilities = {
+ uiCapabilities = createCapabilities({
visualize: {
show: false,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'visualization')).toEqual(false);
expect(canViewInApp(uiCapabilities, 'visualizations')).toEqual(false);
});
it('should handle index patterns', () => {
- let uiCapabilities = {
+ let uiCapabilities = createCapabilities({
management: {
kibana: {
index_patterns: true,
},
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'index-pattern')).toEqual(true);
expect(canViewInApp(uiCapabilities, 'index-patterns')).toEqual(true);
expect(canViewInApp(uiCapabilities, 'indexPatterns')).toEqual(true);
- uiCapabilities = {
+ uiCapabilities = createCapabilities({
management: {
kibana: {
index_patterns: false,
},
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'index-pattern')).toEqual(false);
expect(canViewInApp(uiCapabilities, 'index-patterns')).toEqual(false);
expect(canViewInApp(uiCapabilities, 'indexPatterns')).toEqual(false);
});
it('should handle dashboards', () => {
- let uiCapabilities = {
+ let uiCapabilities = createCapabilities({
dashboard: {
show: true,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'dashboard')).toEqual(true);
expect(canViewInApp(uiCapabilities, 'dashboards')).toEqual(true);
- uiCapabilities = {
+ uiCapabilities = createCapabilities({
dashboard: {
show: false,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'dashboard')).toEqual(false);
expect(canViewInApp(uiCapabilities, 'dashboards')).toEqual(false);
});
it('should have a default case', () => {
- let uiCapabilities = {
+ let uiCapabilities = createCapabilities({
foo: {
show: true,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'foo')).toEqual(true);
- uiCapabilities = {
+ uiCapabilities = createCapabilities({
foo: {
show: false,
},
- };
+ });
expect(canViewInApp(uiCapabilities, 'foo')).toEqual(false);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
deleted file mode 100644
index b6c8d25568446..0000000000000
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-export * from './fetch_export_by_type_and_search';
-export * from './fetch_export_objects';
-export * from './in_app_url';
-export * from './get_relationships';
-export * from './get_saved_object_counts';
-export * from './get_saved_object_label';
-export * from './import_file';
-export * from './import_legacy_file';
-export * from './parse_query';
-export * from './resolve_import_errors';
-export * from './resolve_saved_objects';
-export * from './log_legacy_import';
-export * from './process_import_response';
-export * from './get_default_title';
-export * from './find_objects';
-export * from './extract_export_details';
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts
new file mode 100644
index 0000000000000..ecdfa6549a54e
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { fetchExportByTypeAndSearch } from './fetch_export_by_type_and_search';
+export { fetchExportObjects } from './fetch_export_objects';
+export { canViewInApp } from './in_app_url';
+export { getRelationships } from './get_relationships';
+export { getSavedObjectCounts } from './get_saved_object_counts';
+export { getSavedObjectLabel } from './get_saved_object_label';
+export { importFile } from './import_file';
+export { importLegacyFile } from './import_legacy_file';
+export { parseQuery } from './parse_query';
+export { resolveImportErrors } from './resolve_import_errors';
+export {
+ resolveIndexPatternConflicts,
+ resolveSavedObjects,
+ resolveSavedSearches,
+ saveObject,
+ saveObjects,
+} from './resolve_saved_objects';
+export { logLegacyImport } from './log_legacy_import';
+export {
+ processImportResponse,
+ ProcessedImportResponse,
+ FailedImport,
+} from './process_import_response';
+export { getDefaultTitle } from './get_default_title';
+export { findObjects } from './find_objects';
+export { extractExportDetails, SavedObjectsExportResultDetails } from './extract_export_details';
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/is_same_query.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/is_same_query.js
deleted file mode 100644
index ce5de5ce45003..0000000000000
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/is_same_query.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { parseQuery } from '.';
-
-export const isSameQuery = (query1, query2) => {
- const parsedQuery1 = parseQuery(query1);
- const parsedQuery2 = parseQuery(query2);
-
- if (parsedQuery1.queryText === parsedQuery2.queryText) {
- if (parsedQuery1.visibleTypes === parsedQuery2.visibleTypes) {
- return true;
- }
- }
- return false;
-};
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.ts
similarity index 100%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/log_legacy_import.ts
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/parse_query.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.test.ts
similarity index 96%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/parse_query.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.test.ts
index 1b6c8e0d49dd2..77b34eccd9c6f 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/parse_query.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.test.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { parseQuery } from '../parse_query';
+import { parseQuery } from './parse_query';
describe('getQueryText', () => {
it('should know how to get the text out of the AST', () => {
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.ts
similarity index 89%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.ts
index 8e7419bb924de..9b33deedafd95 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/parse_query.ts
@@ -17,15 +17,15 @@
* under the License.
*/
-export function parseQuery(query) {
- let queryText = undefined;
- let visibleTypes = undefined;
+export function parseQuery(query: any) {
+ let queryText;
+ let visibleTypes;
if (query) {
if (query.ast.getTermClauses().length) {
queryText = query.ast
.getTermClauses()
- .map(clause => clause.value)
+ .map((clause: any) => clause.value)
.join(' ');
}
if (query.ast.getFieldClauses('type')) {
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/process_import_response.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.test.ts
similarity index 66%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/process_import_response.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.test.ts
index dc4d81dae8081..c1a153b800550 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/process_import_response.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.test.ts
@@ -17,7 +17,12 @@
* under the License.
*/
-import { processImportResponse } from '../process_import_response';
+import {
+ SavedObjectsImportConflictError,
+ SavedObjectsImportUnknownError,
+ SavedObjectsImportMissingReferencesError,
+} from 'src/core/public';
+import { processImportResponse } from './process_import_response';
describe('processImportResponse()', () => {
test('works when no errors exist in the response', () => {
@@ -36,32 +41,28 @@ describe('processImportResponse()', () => {
successCount: 0,
errors: [
{
- obj: {
- type: 'a',
- id: '1',
- },
+ type: 'a',
+ id: '1',
error: {
type: 'conflict',
- },
+ } as SavedObjectsImportConflictError,
},
],
};
const result = processImportResponse(response);
expect(result.failedImports).toMatchInlineSnapshot(`
-Array [
- Object {
- "error": Object {
- "type": "conflict",
- },
- "obj": Object {
- "obj": Object {
- "id": "1",
- "type": "a",
- },
- },
- },
-]
-`);
+ Array [
+ Object {
+ "error": Object {
+ "type": "conflict",
+ },
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ ]
+ `);
});
test('unknown errors get added to failedImports', () => {
@@ -70,32 +71,28 @@ Array [
successCount: 0,
errors: [
{
- obj: {
- type: 'a',
- id: '1',
- },
+ type: 'a',
+ id: '1',
error: {
type: 'unknown',
- },
+ } as SavedObjectsImportUnknownError,
},
],
};
const result = processImportResponse(response);
expect(result.failedImports).toMatchInlineSnapshot(`
-Array [
- Object {
- "error": Object {
- "type": "unknown",
- },
- "obj": Object {
- "obj": Object {
- "id": "1",
- "type": "a",
- },
- },
- },
-]
-`);
+ Array [
+ Object {
+ "error": Object {
+ "type": "unknown",
+ },
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
+ },
+ ]
+ `);
});
test('missing references get added to failedImports', () => {
@@ -104,10 +101,8 @@ Array [
successCount: 0,
errors: [
{
- obj: {
- type: 'a',
- id: '1',
- },
+ type: 'a',
+ id: '1',
error: {
type: 'missing_references',
references: [
@@ -116,31 +111,29 @@ Array [
id: '2',
},
],
- },
+ } as SavedObjectsImportMissingReferencesError,
},
],
};
const result = processImportResponse(response);
expect(result.failedImports).toMatchInlineSnapshot(`
-Array [
- Object {
- "error": Object {
- "references": Array [
+ Array [
Object {
- "id": "2",
- "type": "index-pattern",
+ "error": Object {
+ "references": Array [
+ Object {
+ "id": "2",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "missing_references",
+ },
+ "obj": Object {
+ "id": "1",
+ "type": "a",
+ },
},
- ],
- "type": "missing_references",
- },
- "obj": Object {
- "obj": Object {
- "id": "1",
- "type": "a",
- },
- },
- },
-]
-`);
+ ]
+ `);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts
index 2444d18133af4..cfb2eb29e7885 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts
@@ -26,15 +26,17 @@ import {
SavedObjectsImportError,
} from 'src/core/public';
+export interface FailedImport {
+ obj: Pick;
+ error:
+ | SavedObjectsImportConflictError
+ | SavedObjectsImportUnsupportedTypeError
+ | SavedObjectsImportMissingReferencesError
+ | SavedObjectsImportUnknownError;
+}
+
export interface ProcessedImportResponse {
- failedImports: Array<{
- obj: Pick;
- error:
- | SavedObjectsImportConflictError
- | SavedObjectsImportUnsupportedTypeError
- | SavedObjectsImportMissingReferencesError
- | SavedObjectsImportUnknownError;
- }>;
+ failedImports: FailedImport[];
unmatchedReferences: Array<{
existingIndexPatternId: string;
list: Array>;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_import_errors.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.test.ts
similarity index 90%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_import_errors.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.test.ts
index ecdf3d5abced6..b94b0a9d1291f 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_import_errors.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.test.ts
@@ -17,12 +17,16 @@
* under the License.
*/
-import { resolveImportErrors } from '../resolve_import_errors';
-
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
-function getFormData(form) {
- const formData = {};
+import { SavedObjectsImportUnknownError } from 'src/core/public';
+import { kfetch } from 'ui/kfetch';
+import { resolveImportErrors } from './resolve_import_errors';
+
+const kfetchMock = kfetch as jest.Mock;
+
+function getFormData(form: Map) {
+ const formData: Record = {};
for (const [key, val] of form.entries()) {
if (key === 'retries') {
formData[key] = JSON.parse(val);
@@ -69,7 +73,7 @@ Object {
},
error: {
type: 'unknown',
- },
+ } as SavedObjectsImportUnknownError,
},
],
},
@@ -94,8 +98,7 @@ Object {
});
test('resolves conflicts', async () => {
- const { kfetch } = require('ui/kfetch');
- kfetch.mockResolvedValueOnce({
+ kfetchMock.mockResolvedValueOnce({
success: true,
successCount: 1,
});
@@ -136,7 +139,7 @@ Object {
"status": "success",
}
`);
- const formData = getFormData(kfetch.mock.calls[0][0].body);
+ const formData = getFormData(kfetchMock.mock.calls[0][0].body);
expect(formData).toMatchInlineSnapshot(`
Object {
"file": "undefined",
@@ -153,8 +156,7 @@ Object {
});
test('resolves missing references', async () => {
- const { kfetch } = require('ui/kfetch');
- kfetch.mockResolvedValueOnce({
+ kfetchMock.mockResolvedValueOnce({
success: true,
successCount: 2,
});
@@ -201,7 +203,7 @@ Object {
"status": "success",
}
`);
- const formData = getFormData(kfetch.mock.calls[0][0].body);
+ const formData = getFormData(kfetchMock.mock.calls[0][0].body);
expect(formData).toMatchInlineSnapshot(`
Object {
"file": "undefined",
@@ -274,8 +276,7 @@ Object {
});
test('handles missing references then conflicts on the same errored objects', async () => {
- const { kfetch } = require('ui/kfetch');
- kfetch.mockResolvedValueOnce({
+ kfetchMock.mockResolvedValueOnce({
success: false,
successCount: 0,
errors: [
@@ -288,7 +289,7 @@ Object {
},
],
});
- kfetch.mockResolvedValueOnce({
+ kfetchMock.mockResolvedValueOnce({
success: true,
successCount: 1,
});
@@ -333,7 +334,7 @@ Object {
"status": "success",
}
`);
- const formData1 = getFormData(kfetch.mock.calls[0][0].body);
+ const formData1 = getFormData(kfetchMock.mock.calls[0][0].body);
expect(formData1).toMatchInlineSnapshot(`
Object {
"file": "undefined",
@@ -353,7 +354,7 @@ Object {
],
}
`);
- const formData2 = getFormData(kfetch.mock.calls[1][0].body);
+ const formData2 = getFormData(kfetchMock.mock.calls[1][0].body);
expect(formData2).toMatchInlineSnapshot(`
Object {
"file": "undefined",
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.ts
similarity index 84%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.ts
index f59e776b18046..dcc282402147d 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_import_errors.ts
@@ -18,8 +18,16 @@
*/
import { kfetch } from 'ui/kfetch';
+import { FailedImport } from './process_import_response';
-async function callResolveImportErrorsApi(file, retries) {
+interface RetryObject {
+ id: string;
+ type: string;
+ overwrite?: boolean;
+ replaceReferences?: any[];
+}
+
+async function callResolveImportErrorsApi(file: File, retries: any) {
const formData = new FormData();
formData.append('file', file);
formData.append('retries', JSON.stringify(retries));
@@ -39,7 +47,12 @@ function mapImportFailureToRetryObject({
overwriteDecisionCache,
replaceReferencesCache,
state,
-}) {
+}: {
+ failure: FailedImport;
+ overwriteDecisionCache: Map;
+ replaceReferencesCache: Map;
+ state: any;
+}): RetryObject | undefined {
const { isOverwriteAllChecked, unmatchedReferences } = state;
const isOverwriteGranted =
isOverwriteAllChecked ||
@@ -86,27 +99,32 @@ function mapImportFailureToRetryObject({
};
}
-export async function resolveImportErrors({ getConflictResolutions, state }) {
+export async function resolveImportErrors({
+ getConflictResolutions,
+ state,
+}: {
+ getConflictResolutions: (objects: any[]) => Promise>;
+ state: { importCount: number; failedImports?: FailedImport[] } & Record;
+}) {
const overwriteDecisionCache = new Map();
const replaceReferencesCache = new Map();
let { importCount: successImportCount, failedImports: importFailures = [] } = state;
const { file, isOverwriteAllChecked } = state;
- const doesntHaveOverwriteDecision = ({ obj }) => {
+ const doesntHaveOverwriteDecision = ({ obj }: FailedImport) => {
return !overwriteDecisionCache.has(`${obj.type}:${obj.id}`);
};
- const getOverwriteDecision = ({ obj }) => {
+ const getOverwriteDecision = ({ obj }: FailedImport) => {
return overwriteDecisionCache.get(`${obj.type}:${obj.id}`);
};
- const callMapImportFailure = failure => {
- return mapImportFailureToRetryObject({
+ const callMapImportFailure = (failure: FailedImport) =>
+ mapImportFailureToRetryObject({
failure,
overwriteDecisionCache,
replaceReferencesCache,
state,
});
- };
- const isNotSkipped = failure => {
+ const isNotSkipped = (failure: FailedImport) => {
return (
(failure.error.type !== 'conflict' && failure.error.type !== 'missing_references') ||
getOverwriteDecision(failure)
@@ -131,7 +149,7 @@ export async function resolveImportErrors({ getConflictResolutions, state }) {
}
// Build retries array
- const retries = importFailures.map(callMapImportFailure).filter(obj => !!obj);
+ const retries = importFailures.map(callMapImportFailure).filter(obj => !!obj) as RetryObject[];
for (const { error, obj } of importFailures) {
if (error.type !== 'missing_references') {
continue;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_saved_objects.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts
similarity index 85%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_saved_objects.test.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts
index 96c178b35a7b3..8243aa69ac082 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/resolve_saved_objects.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.test.ts
@@ -22,21 +22,30 @@ import {
resolveIndexPatternConflicts,
saveObjects,
saveObject,
-} from '../resolve_saved_objects';
-
-jest.mock('../../../../../../../../../plugins/kibana_utils/public', () => ({
- SavedObjectNotFound: class SavedObjectNotFound extends Error {
- constructor(options) {
- super();
- for (const option in options) {
- if (options.hasOwnProperty(option)) {
- this[option] = options[option];
- }
+} from './resolve_saved_objects';
+import {
+ SavedObject,
+ SavedObjectLoader,
+} from '../../../../../../../../plugins/saved_objects/public';
+import { IndexPatternsContract } from '../../../../../../../../plugins/data/public';
+
+class SavedObjectNotFound extends Error {
+ constructor(options: Record) {
+ super();
+ for (const option in options) {
+ if (options.hasOwnProperty(option)) {
+ (this as any)[option] = options[option];
}
}
- },
-}));
-import { SavedObjectNotFound } from '../../../../../../../../../plugins/kibana_utils/public';
+ }
+}
+
+const openModalMock = jest.fn();
+
+const createObj = (props: Partial): SavedObject =>
+ ({
+ ...props,
+ } as SavedObject);
describe('resolveSavedObjects', () => {
describe('resolveSavedObjects', () => {
@@ -61,7 +70,7 @@ describe('resolveSavedObjects', () => {
},
];
- const indexPatterns = {
+ const indexPatterns = ({
get: async () => {
return {
create: () => '2',
@@ -73,7 +82,7 @@ describe('resolveSavedObjects', () => {
cache: {
clear: () => {},
},
- };
+ } as unknown) as IndexPatternsContract;
const services = [
{
@@ -115,11 +124,17 @@ describe('resolveSavedObjects', () => {
};
},
},
- ];
+ ] as SavedObjectLoader[];
const overwriteAll = false;
- const result = await resolveSavedObjects(savedObjects, overwriteAll, services, indexPatterns);
+ const result = await resolveSavedObjects(
+ savedObjects,
+ overwriteAll,
+ services,
+ indexPatterns,
+ openModalMock
+ );
expect(result.conflictedIndexPatterns.length).toBe(3);
expect(result.conflictedSavedObjectsLinkedToSavedSearches.length).toBe(0);
@@ -147,7 +162,7 @@ describe('resolveSavedObjects', () => {
},
];
- const indexPatterns = {
+ const indexPatterns = ({
get: async () => {
return {
create: () => '2',
@@ -159,7 +174,7 @@ describe('resolveSavedObjects', () => {
cache: {
clear: () => {},
},
- };
+ } as unknown) as IndexPatternsContract;
const services = [
{
@@ -202,11 +217,17 @@ describe('resolveSavedObjects', () => {
};
},
},
- ];
+ ] as SavedObjectLoader[];
const overwriteAll = false;
- const result = await resolveSavedObjects(savedObjects, overwriteAll, services, indexPatterns);
+ const result = await resolveSavedObjects(
+ savedObjects,
+ overwriteAll,
+ services,
+ indexPatterns,
+ openModalMock
+ );
expect(result.conflictedIndexPatterns.length).toBe(1);
expect(result.conflictedSavedObjectsLinkedToSavedSearches.length).toBe(1);
@@ -223,7 +244,7 @@ describe('resolveSavedObjects', () => {
{
obj: {
searchSource: {
- getOwnField: field => {
+ getOwnField: (field: string) => {
return field === 'index' ? '1' : undefined;
},
},
@@ -234,7 +255,7 @@ describe('resolveSavedObjects', () => {
{
obj: {
searchSource: {
- getOwnField: field => {
+ getOwnField: (field: string) => {
return field === 'index' ? '3' : undefined;
},
},
@@ -277,7 +298,7 @@ describe('resolveSavedObjects', () => {
{
obj: {
searchSource: {
- getOwnField: field => {
+ getOwnField: (field: string) => {
return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }];
},
setField: jest.fn(),
@@ -289,7 +310,7 @@ describe('resolveSavedObjects', () => {
{
obj: {
searchSource: {
- getOwnField: field => {
+ getOwnField: (field: string) => {
return field === 'index' ? '3' : undefined;
},
},
@@ -330,12 +351,12 @@ describe('resolveSavedObjects', () => {
const save = jest.fn();
const objs = [
- {
+ createObj({
save,
- },
- {
+ }),
+ createObj({
save,
- },
+ }),
];
const overwriteAll = false;
@@ -349,9 +370,9 @@ describe('resolveSavedObjects', () => {
describe('saveObject', () => {
it('should save the object', async () => {
const save = jest.fn();
- const obj = {
+ const obj = createObj({
save,
- };
+ });
const overwriteAll = false;
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts
similarity index 78%
rename from src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts
index e13e8c1efe8f7..902de654f5f85 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/resolve_saved_objects.ts
@@ -18,9 +18,17 @@
*/
import { i18n } from '@kbn/i18n';
+import { OverlayStart } from 'src/core/public';
+import {
+ SavedObject,
+ SavedObjectLoader,
+} from '../../../../../../../../plugins/saved_objects/public';
+import { IndexPatternsContract, IIndexPattern } from '../../../../../../../../plugins/data/public';
-async function getSavedObject(doc, services) {
- const service = services.find(service => service.type === doc._type);
+type SavedObjectsRawDoc = Record;
+
+async function getSavedObject(doc: SavedObjectsRawDoc, services: SavedObjectLoader[]) {
+ const service = services.find(s => s.type === doc._type);
if (!service) {
return;
}
@@ -31,7 +39,12 @@ async function getSavedObject(doc, services) {
return obj;
}
-function addJsonFieldToIndexPattern(target, sourceString, fieldName, indexName) {
+function addJsonFieldToIndexPattern(
+ target: Record,
+ sourceString: string,
+ fieldName: string,
+ indexName: string
+) {
if (sourceString) {
try {
target[fieldName] = JSON.parse(sourceString);
@@ -50,7 +63,12 @@ function addJsonFieldToIndexPattern(target, sourceString, fieldName, indexName)
}
}
}
-async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModalPromise) {
+async function importIndexPattern(
+ doc: SavedObjectsRawDoc,
+ indexPatterns: IndexPatternsContract,
+ overwriteAll: boolean,
+ openConfirm: OverlayStart['openConfirm']
+) {
// TODO: consolidate this is the code in create_index_pattern_wizard.js
const emptyPattern = await indexPatterns.make();
const {
@@ -66,7 +84,7 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModal
id: doc._id,
title,
timeFieldName,
- };
+ } as IIndexPattern;
if (type) {
importedIndexPattern.type = type;
}
@@ -79,9 +97,9 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModal
let newId = await emptyPattern.create(overwriteAll);
if (!newId) {
// We can override and we want to prompt for confirmation
- const isConfirmed = await confirmModalPromise(
+ const isConfirmed = await openConfirm(
i18n.translate('kbn.management.indexPattern.confirmOverwriteLabel', {
- values: { title: this.title },
+ values: { title },
defaultMessage: "Are you sure you want to overwrite '{title}'?",
}),
{
@@ -96,7 +114,7 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModal
);
if (isConfirmed) {
- newId = await emptyPattern.create(true);
+ newId = (await emptyPattern.create(true)) as string;
} else {
return;
}
@@ -105,7 +123,7 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll, confirmModal
return newId;
}
-async function importDocument(obj, doc, overwriteAll) {
+async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) {
await obj.applyESResp({
references: doc._references || [],
...doc,
@@ -113,12 +131,12 @@ async function importDocument(obj, doc, overwriteAll) {
return await obj.save({ confirmOverwrite: !overwriteAll });
}
-function groupByType(docs) {
+function groupByType(docs: SavedObjectsRawDoc[]): Record {
const defaultDocTypes = {
searches: [],
indexPatterns: [],
other: [],
- };
+ } as Record;
return docs.reduce((types, doc) => {
switch (doc._type) {
@@ -135,14 +153,14 @@ function groupByType(docs) {
}, defaultDocTypes);
}
-async function awaitEachItemInParallel(list, op) {
+async function awaitEachItemInParallel(list: T[], op: (item: T) => R) {
return await Promise.all(list.map(item => op(item)));
}
export async function resolveIndexPatternConflicts(
- resolutions,
- conflictedIndexPatterns,
- overwriteAll
+ resolutions: Array<{ oldId: string; newId: string }>,
+ conflictedIndexPatterns: any[],
+ overwriteAll: boolean
) {
let importCount = 0;
@@ -160,15 +178,13 @@ export async function resolveIndexPatternConflicts(
}
// Resolve filter index reference:
- const filter = (obj.searchSource.getOwnField('filter') || []).map(filter => {
- if (!(filter.meta && filter.meta.index)) {
- return filter;
+ const filter = (obj.searchSource.getOwnField('filter') || []).map((f: any) => {
+ if (!(f.meta && f.meta.index)) {
+ return f;
}
- resolution = resolutions.find(({ oldId }) => oldId === filter.meta.index);
- return resolution
- ? { ...filter, ...{ meta: { ...filter.meta, index: resolution.newId } } }
- : filter;
+ resolution = resolutions.find(({ oldId }) => oldId === f.meta.index);
+ return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f;
});
if (filter.length > 0) {
@@ -186,7 +202,7 @@ export async function resolveIndexPatternConflicts(
return importCount;
}
-export async function saveObjects(objs, overwriteAll) {
+export async function saveObjects(objs: SavedObject[], overwriteAll: boolean) {
let importCount = 0;
await awaitEachItemInParallel(objs, async obj => {
if (await saveObject(obj, overwriteAll)) {
@@ -196,11 +212,16 @@ export async function saveObjects(objs, overwriteAll) {
return importCount;
}
-export async function saveObject(obj, overwriteAll) {
+export async function saveObject(obj: SavedObject, overwriteAll: boolean) {
return await obj.save({ confirmOverwrite: !overwriteAll });
}
-export async function resolveSavedSearches(savedSearches, services, indexPatterns, overwriteAll) {
+export async function resolveSavedSearches(
+ savedSearches: any[],
+ services: SavedObjectLoader[],
+ indexPatterns: IndexPatternsContract,
+ overwriteAll: boolean
+) {
let importCount = 0;
await awaitEachItemInParallel(savedSearches, async searchDoc => {
const obj = await getSavedObject(searchDoc, services);
@@ -216,18 +237,18 @@ export async function resolveSavedSearches(savedSearches, services, indexPattern
}
export async function resolveSavedObjects(
- savedObjects,
- overwriteAll,
- services,
- indexPatterns,
- confirmModalPromise
+ savedObjects: SavedObjectsRawDoc[],
+ overwriteAll: boolean,
+ services: SavedObjectLoader[],
+ indexPatterns: IndexPatternsContract,
+ confirmModalPromise: OverlayStart['openConfirm']
) {
const docTypes = groupByType(savedObjects);
// Keep track of how many we actually import because the user
// can cancel an override
let importedObjectCount = 0;
- const failedImports = [];
+ const failedImports: any[] = [];
// Start with the index patterns since everything is dependent on them
await awaitEachItemInParallel(docTypes.indexPatterns, async indexPatternDoc => {
try {
@@ -247,18 +268,18 @@ export async function resolveSavedObjects(
// We want to do the same for saved searches, but we want to keep them separate because they need
// to be applied _first_ because other saved objects can be dependent on those saved searches existing
- const conflictedSearchDocs = [];
+ const conflictedSearchDocs: any[] = [];
// Keep a record of the index patterns assigned to our imported saved objects that do not
// exist. We will provide a way for the user to manually select a new index pattern for those
// saved objects.
- const conflictedIndexPatterns = [];
+ const conflictedIndexPatterns: any[] = [];
// Keep a record of any objects which fail to import for unknown reasons.
// It's possible to have saved objects that link to saved searches which then link to index patterns
// and those could error out, but the error comes as an index pattern not found error. We can't resolve
// those the same as way as normal index pattern not found errors, but when those are fixed, it's very
// likely that these saved objects will work once resaved so keep them around to resave them.
- const conflictedSavedObjectsLinkedToSavedSearches = [];
+ const conflictedSavedObjectsLinkedToSavedSearches: any[] = [];
await awaitEachItemInParallel(docTypes.searches, async searchDoc => {
const obj = await getSavedObject(searchDoc, services);
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts
index 32436e96d4829..6a89142bc9798 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts
@@ -17,7 +17,25 @@
* under the License.
*/
-import { SavedObjectReference } from 'src/core/public';
+import { SavedObject, SavedObjectReference } from 'src/core/public';
+
+export interface SavedObjectMetadata {
+ icon?: string;
+ title?: string;
+ editUrl?: string;
+ inAppUrl?: { path: string; uiCapabilitiesPath: string };
+}
+
+export type SavedObjectWithMetadata = SavedObject & {
+ meta: SavedObjectMetadata;
+};
+
+export interface SavedObjectRelation {
+ id: string;
+ type: string;
+ relationship: 'child' | 'parent';
+ meta: SavedObjectMetadata;
+}
export interface ObjectField {
type: FieldType;
diff --git a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts
index 178edadaff6c4..e83f5c0b6bafb 100644
--- a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts
+++ b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts
@@ -56,7 +56,7 @@ export class SavedObjectLoader {
* @param id
* @returns {Promise}
*/
- async get(id: string) {
+ async get(id?: string) {
// @ts-ignore
const obj = new this.Class(id);
return obj.init();
From 6d237352231c1d8ef7af5fb3d0272de40aea7b8f Mon Sep 17 00:00:00 2001
From: Pete Harverson
Date: Thu, 26 Mar 2020 14:34:47 +0000
Subject: [PATCH 16/81] [ML] Fixes Anomaly Explorer swimlane label and chart
tooltips (#61327)
Co-authored-by: Elastic Machine
---
.../explorer/explorer_charts/_index.scss | 3 +--
.../explorer_chart_label/explorer_chart_label.js | 1 +
.../explorer_charts/explorer_chart_info_tooltip.js | 1 +
.../application/explorer/explorer_swimlane.js | 14 +++++++++++---
4 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/_index.scss b/x-pack/plugins/ml/public/application/explorer/explorer_charts/_index.scss
index dfa03861e32f3..4b3bf40b7b368 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/_index.scss
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/_index.scss
@@ -1,4 +1,3 @@
@import 'components/explorer_chart_label/index';
@import 'explorer_chart';
-@import 'explorer_chart_tooltip';
-@import 'explorer_charts_container';
\ No newline at end of file
+@import 'explorer_charts_container';
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js
index ea8bb3159202f..a9bff36461292 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import './_explorer_chart_label.scss';
import PropTypes from 'prop-types';
import React from 'react';
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js
index 5cf8245cd4739..0ee1eac19f64d 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import './_explorer_chart_tooltip.scss';
import PropTypes from 'prop-types';
import React from 'react';
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js
index a229537ba3ca1..e8cb8377a656d 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.js
@@ -387,9 +387,17 @@ export class ExplorerSwimlane extends React.Component {
.each(function() {
if (swimlaneData.fieldName !== undefined) {
d3.select(this)
- .on('mouseover', label => {
+ .on('mouseover', value => {
mlChartTooltipService.show(
- [{ skipHeader: true }, { name: swimlaneData.fieldName, value: label }],
+ [
+ { skipHeader: true },
+ {
+ label: swimlaneData.fieldName,
+ value,
+ seriesIdentifier: { key: value },
+ valueAccessor: 'fieldName',
+ },
+ ],
this,
{
x: laneLabelWidth,
@@ -400,7 +408,7 @@ export class ExplorerSwimlane extends React.Component {
.on('mouseout', () => {
mlChartTooltipService.hide();
})
- .attr('aria-label', label => `${mlEscape(swimlaneData.fieldName)}: ${mlEscape(label)}`);
+ .attr('aria-label', value => `${mlEscape(swimlaneData.fieldName)}: ${mlEscape(value)}`);
}
});
From cceb5382110d091dc7ead5999ff3b24a11217190 Mon Sep 17 00:00:00 2001
From: Peter Pisljar
Date: Thu, 26 Mar 2020 15:50:54 +0100
Subject: [PATCH 17/81] Fixing abort error on dashboard (#61279)
---
.../np_ready/public/embeddable/visualize_embeddable.ts | 6 ++++++
.../public/np_ready/public/legacy/build_pipeline.ts | 1 +
src/plugins/expressions/public/render_error_handler.ts | 5 +++++
3 files changed, 12 insertions(+)
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts
index 3739d1af6fc40..bcca4bdf67dcf 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts
@@ -86,6 +86,7 @@ export class VisualizeEmbeddable extends Embeddable {
const { indexPattern, searchSource } = vis.data;
diff --git a/src/plugins/expressions/public/render_error_handler.ts b/src/plugins/expressions/public/render_error_handler.ts
index 432ef3ed96536..27c3f9c5d8e65 100644
--- a/src/plugins/expressions/public/render_error_handler.ts
+++ b/src/plugins/expressions/public/render_error_handler.ts
@@ -27,6 +27,11 @@ export const renderErrorHandler: RenderErrorHandlerFnType = (
error: RenderError,
handlers: IInterpreterRenderHandlers
) => {
+ if (error.name === 'AbortError') {
+ handlers.done();
+ return;
+ }
+
getNotifications().toasts.addError(error, {
title: i18n.translate('expressions.defaultErrorRenderer.errorTitle', {
defaultMessage: 'Error in visualisation',
From 2371ee56c1409087742d2c1cc87da9851425e198 Mon Sep 17 00:00:00 2001
From: Robert Oskamp
Date: Thu, 26 Mar 2020 16:11:24 +0100
Subject: [PATCH 18/81] [ML] Functional API test - disable mml value check
(#61446)
This PR re-activates the model memory estimation endpoint test and disabled the response value check.
---
.../apis/ml/calculate_model_memory_limit.ts | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts
index 61e1763cc9f75..7fb0a10d94a4b 100644
--- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts
+++ b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts
@@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import expect from '@kbn/expect';
-
import { FtrProviderContext } from '../../ftr_provider_context';
import { USER } from '../../../functional/services/machine_learning/security_common';
@@ -142,13 +140,12 @@ export default ({ getService }: FtrProviderContext) => {
},
expected: {
responseCode: 200,
- responseBody: { estimatedModelMemoryLimit: '12MB', modelMemoryLimit: '12MB' },
+ responseBody: { estimatedModelMemoryLimit: '11MB', modelMemoryLimit: '11MB' },
},
},
];
- // failing test, see https://github.com/elastic/kibana/issues/61400
- describe.skip('calculate model memory limit', function() {
+ describe('calculate model memory limit', function() {
before(async () => {
await esArchiver.load('ml/ecommerce');
});
@@ -159,14 +156,16 @@ export default ({ getService }: FtrProviderContext) => {
for (const testData of testDataList) {
it(`calculates the model memory limit ${testData.testTitleSuffix}`, async () => {
- const { body } = await supertest
+ await supertest
.post('/api/ml/validate/calculate_model_memory_limit')
.auth(testData.user, mlSecurity.getPasswordForUser(testData.user))
.set(COMMON_HEADERS)
.send(testData.requestBody)
.expect(testData.expected.responseCode);
- expect(body).to.eql(testData.expected.responseBody);
+ // More backend changes to the model memory calculation are planned.
+ // This value check will be re-enabled when the final batch of updates is in.
+ // expect(body).to.eql(testData.expected.responseBody);
});
}
});
From 8673fb6c5940810f77521a3d3b0a23213f546f02 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 26 Mar 2020 08:21:17 -0700
Subject: [PATCH 19/81] Update ems landing page link (#61351)
---
src/legacy/server/config/schema.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js
index 769d9ba311281..4b7618712cdd8 100644
--- a/src/legacy/server/config/schema.js
+++ b/src/legacy/server/config/schema.js
@@ -263,7 +263,7 @@ export default () =>
.allow(''),
emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'),
emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'),
- emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.6'),
+ emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.7'),
emsFontLibraryUrl: Joi.string().default(
'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf'
),
From 45a5ad55bcfbb5914e05be566a3e0b0669edf307 Mon Sep 17 00:00:00 2001
From: Maryia Lapata
Date: Thu, 26 Mar 2020 18:24:03 +0300
Subject: [PATCH 20/81] [Default editor] Catch invalid calendar exception
(#60494)
* Catch invalid calendar exception
* Use isValidEsInterval directly
* Show field error message right away
* Fix for the case 2w
* Update time_interval.tsx
* Restructure validation
* Rename fn to isValidCalendarInterval
* Refactoring
* Update time_interval.tsx
* Add functional tests
* Add functional tests for interval
* Update _area_chart.js
* Don't show error when value is empty
* Use error message from InvalidEsCalendarIntervalError
* Update _area_chart.js
Co-authored-by: Elastic Machine
---
.../public/components/controls/field.tsx | 6 +-
.../components/controls/time_interval.tsx | 103 +++++++++++++-----
test/functional/apps/visualize/_area_chart.js | 89 +++++++++++++++
.../fixtures/es_archiver/visualize/data.json | 73 +++++++++++++
4 files changed, 240 insertions(+), 31 deletions(-)
diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx
index 59642ae4c25f7..fc1cbb51b70a7 100644
--- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx
+++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx
@@ -80,6 +80,8 @@ function FieldParamEditor({
}
const isValid = !!value && !errors.length && !isDirty;
+ // we show an error message right away if there is no compatible fields
+ const showErrorMessage = (showValidation || !indexedFields.length) && !isValid;
useValidation(setValidity, isValid);
@@ -103,7 +105,7 @@ function FieldParamEditor({
return (
);
- const errors = [];
-
- if (!isValid && value) {
- errors.push(
- i18n.translate('visDefaultEditor.controls.timeInterval.invalidFormatErrorMessage', {
- defaultMessage: 'Invalid interval format.',
- })
- );
- }
-
- const onCustomInterval = (customValue: string) => {
- const normalizedCustomValue = customValue.trim();
- setValue(normalizedCustomValue);
-
- if (normalizedCustomValue && search.aggs.isValidInterval(normalizedCustomValue, timeBase)) {
- agg.write();
- }
- };
+ const onCustomInterval = (customValue: string) => setValue(customValue.trim());
const onChange = (opts: EuiComboBoxOptionOption[]) => {
const selectedOpt: ComboBoxOption = get(opts, '0');
setValue(selectedOpt ? selectedOpt.key : '');
-
- if (selectedOpt) {
- agg.write();
- }
};
useEffect(() => {
@@ -121,10 +166,10 @@ function TimeIntervalParamEditor({
return (
{
+ before(async () => {
+ await PageObjects.visualize.loadSavedVisualization('AreaChart [no date field]');
+ await PageObjects.visChart.waitForVisualization();
+
+ log.debug('Click X-axis');
+ await PageObjects.visEditor.clickBucket('X-axis');
+ log.debug('Click Date Histogram');
+ await PageObjects.visEditor.selectAggregation('Date Histogram');
+ });
+
+ it('should show error message for field', async () => {
+ const fieldErrorMessage = await find.byCssSelector(
+ '[data-test-subj="visDefaultEditorField"] + .euiFormErrorText'
+ );
+ const errorMessage = await fieldErrorMessage.getVisibleText();
+ expect(errorMessage).to.be(
+ 'The index pattern test_index* does not contain any of the following compatible field types: date'
+ );
+ });
+ });
+
+ describe('date histogram when no time filter', () => {
+ before(async () => {
+ await PageObjects.visualize.loadSavedVisualization('AreaChart [no time filter]');
+ await PageObjects.visChart.waitForVisualization();
+
+ log.debug('Click X-axis');
+ await PageObjects.visEditor.clickBucket('X-axis');
+ log.debug('Click Date Histogram');
+ await PageObjects.visEditor.selectAggregation('Date Histogram');
+ });
+
+ it('should not show error message on init when the field is not selected', async () => {
+ const fieldValues = await PageObjects.visEditor.getField();
+ expect(fieldValues[0]).to.be(undefined);
+ const isFieldErrorMessageExists = await find.existsByCssSelector(
+ '[data-test-subj="visDefaultEditorField"] + .euiFormErrorText'
+ );
+ expect(isFieldErrorMessageExists).to.be(false);
+ });
+
+ describe('interval errors', () => {
+ before(async () => {
+ // to trigger displaying of error messages
+ await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
+ // this will avoid issues with the play tooltip covering the interval field
+ await testSubjects.scrollIntoView('advancedParams-2');
+ });
+
+ it('should not fail during changing interval when the field is not selected', async () => {
+ await PageObjects.visEditor.setInterval('m');
+ const intervalValues = await PageObjects.visEditor.getInterval();
+ expect(intervalValues[0]).to.be('Millisecond');
+ });
+
+ it('should not fail during changing custom interval when the field is not selected', async () => {
+ await PageObjects.visEditor.setInterval('4d', { type: 'custom' });
+ const isInvalidIntervalExists = await find.existsByCssSelector(
+ '.euiComboBox-isInvalid[data-test-subj="visEditorInterval"]'
+ );
+ expect(isInvalidIntervalExists).to.be(false);
+ });
+
+ it('should show error when interval invalid', async () => {
+ await PageObjects.visEditor.setInterval('xx', { type: 'custom' });
+ const isIntervalErrorMessageExists = await find.existsByCssSelector(
+ '[data-test-subj="visEditorInterval"] + .euiFormErrorText'
+ );
+ expect(isIntervalErrorMessageExists).to.be(true);
+ });
+
+ it('should show error when calendar interval invalid', async () => {
+ await PageObjects.visEditor.setInterval('14d', { type: 'custom' });
+ const intervalErrorMessage = await find.byCssSelector(
+ '[data-test-subj="visEditorInterval"] + .euiFormErrorText'
+ );
+ let errorMessage = await intervalErrorMessage.getVisibleText();
+ expect(errorMessage).to.be('Invalid calendar interval: 2w, value must be 1');
+
+ await PageObjects.visEditor.setInterval('3w', { type: 'custom' });
+ errorMessage = await intervalErrorMessage.getVisibleText();
+ expect(errorMessage).to.be('Invalid calendar interval: 3w, value must be 1');
+ });
+ });
+ });
});
}
diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json
index 9fb8747a7376a..845e9a5e08825 100644
--- a/test/functional/fixtures/es_archiver/visualize/data.json
+++ b/test/functional/fixtures/es_archiver/visualize/data.json
@@ -174,3 +174,76 @@
}
}
}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:test_index*",
+ "index": ".kibana",
+ "source": {
+ "index-pattern": {
+ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]",
+ "title": "test_index*"
+ },
+ "type": "test_index*"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "visualization:AreaChart-no-date-field",
+ "index": ".kibana",
+ "source": {
+ "type": "visualization",
+ "visualization": {
+ "description": "AreaChart",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"test_index*\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
+ },
+ "title": "AreaChart [no date field]",
+ "uiStateJSON": "{}",
+ "version": 1,
+ "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
+ }
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:log*",
+ "index": ".kibana",
+ "source": {
+ "index-pattern": {
+ "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}",
+ "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]",
+ "title": "log*"
+ },
+ "type": "index-pattern"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "visualization:AreaChart-no-time-filter",
+ "index": ".kibana",
+ "source": {
+ "type": "visualization",
+ "visualization": {
+ "description": "AreaChart",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"log*\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
+ },
+ "title": "AreaChart [no time filter]",
+ "uiStateJSON": "{}",
+ "version": 1,
+ "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}"
+ }
+ }
+ }
+}
From 9ec240a7529e0088786159c7d57d397a6b25dae7 Mon Sep 17 00:00:00 2001
From: Kaarina Tungseth
Date: Thu, 26 Mar 2020 10:28:55 -0500
Subject: [PATCH 21/81] [DOCS] Canvas assets (#57494)
* [DOCS] Canvas assets
* Structure reorganization
* Separated out workpad content
* Images, content reorg
* Review comments
* Review comments
Co-authored-by: Elastic Machine
---
docs/canvas/canvas-elements.asciidoc | 286 ++++++--------------
docs/canvas/canvas-present-workpad.asciidoc | 6 +-
docs/canvas/canvas-share-workpad.asciidoc | 20 +-
docs/canvas/canvas-tutorial.asciidoc | 28 +-
docs/canvas/canvas-workpad.asciidoc | 65 ++++-
docs/images/canvas-add-image.gif | Bin 0 -> 1287935 bytes
docs/images/canvas-map-embed.gif | Bin 915817 -> 1475332 bytes
docs/images/canvas-refresh-interval.png | Bin 775127 -> 583743 bytes
docs/images/canvas-zoom-controls.png | Bin 33945 -> 28503 bytes
docs/images/canvas_create_image.png | Bin 0 -> 43690 bytes
docs/images/canvas_element_options.png | Bin 0 -> 4750 bytes
docs/images/canvas_save_element.png | Bin 0 -> 4722 bytes
docs/user/canvas.asciidoc | 8 +-
13 files changed, 170 insertions(+), 243 deletions(-)
create mode 100644 docs/images/canvas-add-image.gif
create mode 100644 docs/images/canvas_create_image.png
create mode 100644 docs/images/canvas_element_options.png
create mode 100644 docs/images/canvas_save_element.png
diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc
index 1160b735154dc..85f0ee1150ba0 100644
--- a/docs/canvas/canvas-elements.asciidoc
+++ b/docs/canvas/canvas-elements.asciidoc
@@ -1,132 +1,132 @@
[role="xpack"]
-[[element-intro]]
-== Showcase your data with elements
+[[add-canvas-elements]]
+=== Add elements
-Canvas _elements_ are the building blocks of your workpad. With elements, you can combine images, text, and visualizations to tell a story about your data.
+Create a story about your data by adding elements to your workpad that include images, text, charts, and more. You can create your own elements and connect them to your data sources, add saved objects, and add your own images.
-When you add elements to your workpad, you can:
-
-* <>
-
-* <>
-
-* <>
+[float]
+[[create-canvas-element]]
+==== Create an element
-* <>
+Choose the type of element you want to use, then connect it to your own data.
-* <>
+. Click *Add element*, then select the element you want to use.
++
+[role="screenshot"]
+image::images/canvas-element-select.gif[Canvas elements]
-[float]
-[[add-canvas-element]]
-=== Add elements to your workpad
+. To familiarize yourself with the element, use the preconfigured data demo data.
++
+By default, most of the elements you create use demo data until you change the data source. The demo data includes a small data set that you can use to experiment with your element.
-Choose the elements to display on your workpad, then familiarize yourself with the element using the preconfigured demo data. By default, most elements use demo data until you change the data source. The demo data includes a small sample data set that you can use to experiment with your element.
+. To connect the element to your data, select *Data*, then select one of the following data sources:
-To add a Canvas element:
+* *{es} SQL* — Access your data in {es} using SQL syntax. For information about SQL syntax, refer to {ref}/sql-spec.html[SQL language].
-. Click *Add element*.
+* *{es} raw data* — Access your raw data in {es} without the use of aggregations. Use {es} raw data when you have low volume datasets, or to plot exact, non-aggregated values.
-. In the *Elements* window, select the element you want to use.
-+
-[role="screenshot"]
-image::images/canvas-element-select.gif[Canvas elements]
+* *Timelion* — Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>.
-. Play around with the default settings and see what the element can do.
+[float]
+[[canvas-add-object]]
+==== Add a saved object
-To add a map:
+Add a <>, such as a map or Lens visualization, then customize it to fit your display needs.
. Click *Embed object*.
-. Select the map you want to add to the workpad.
+. Select the object you want to add.
+
[role="screenshot"]
image::images/canvas-map-embed.gif[]
-NOTE: Demo data is only supported on Canvas elements. Maps do not support demo data.
+. To use the customization options, click the panel menu, then select one of the following options:
+
+* *Edit map* — Opens <> so that you can edit the original map.
+
+* *Customize panel* — Specifies the object title options.
-Want to use a different element? You can delete the element by selecting it, clicking the *Element options* icon in the top right, then selecting *Delete*.
+* *Inspect* — Allows you to drill down into the element data.
+
+* *Customize time range* — Exposes a time filter dedicated to the map.
[float]
-[[connect-element-data]]
-=== Connect the Canvas element to your data
+[[canvas-add-image]]
+==== Add your own image
-When you have finished using the demo data, connect the Canvas element to a data source.
+To personalize your workpad, add your own logos and graphics.
-NOTE: Maps do not support data sources. To change the map data, refer to <>.
+. Click *Manage assets*.
-. Make sure that the element is selected, then select *Data*.
+. On the *Manage workpad assets* window, drag and drop your images.
-. Click *Change your data source*.
+. To add the image to the workpad, click the *Create image element* icon.
++
+[role="screenshot"]
+image::images/canvas-add-image.gif[]
[float]
-[[elasticsearch-sql-data-source]]
-==== Connect to {es} SQL
+[[move-canvas-elements]]
+==== Organize elements
+
+Move and resize your elements to meet your design needs.
-Access your data in {es} using SQL syntax. For information about SQL syntax, refer to {ref}/sql-spec.html[SQL language].
+* To move, click and hold the element, then drag to the new location.
-. Click *{es} SQL*.
+* To move by 1 pixel, select the element, press and hold Shift, then use your arrow keys.
-. In the *{es} SQL query* box, enter your query, then *Preview* it.
+* To move by 10 pixels, select the element, then use your arrow keys.
-. If everything looks correct, *Save* it.
+* To resize, click and drag the resize handles to the new dimensions.
[float]
-[[elasticsearch-raw-doc-data-source]]
-==== Connect to {es} raw data
+[[format-canvas-elements]]
+==== Format elements
-Access your raw data in {es} without the use of aggregations. Use {es} raw data when you have low volume datasets, or to plot exact, non-aggregated values.
+Align, distribute, and reorder elements for consistency and readability across your workpad pages.
-To use targeted queries, you can enter a query using the <>.
+Access the align, distribute, and reorder options by clicking the *Element options* icon.
-. Click *{es} raw documents*.
+[role="screenshot"]
+image::images/canvas_element_options.png[]
-. In the *Index* field, enter the index pattern that you want to display.
+To align elements:
-. From the *Fields* dropdown, select the associated fields you want to display.
+. Press and hold Shift, then select the elements you want to align.
-. To sort the data, select an option from the *Sort Field* and *Sort Order* dropdowns.
+. Click the , then select *Group*.
-. For more targeted queries, enter a *Query* using the Lucene query string syntax.
+. Click the *Element options* icon, then select *Alignment*.
-. *Preview* the query.
+. Select the alignment option.
-. If your query looks correct, *Save* it.
+To distribute elements:
-[float]
-[[timelion-data-source]]
-==== Connect to Timelion
+. Press and hold Shift, then select the elements you want to distribute.
-Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>.
+. Click the *Element options* icon, then select *Group*.
-. Click *Timelion*.
+. Click the *Element options* icon, then select *Distribution*.
-. Enter a *Query* using the Lucene query string syntax.
+. Select the distribution option.
-. Enter the *Interval*, then *Preview* the query.
+To reorder elements:
-. If your query looks correct, *Save* it.
+. Select the element you want to reorder.
-[float]
-[[configure-display-options]]
-=== Choose the display options
+. Click the *Element options* icon, then select *Order*.
-Choose how you want the elements to appear on your workpad.
+. Select the order option.
[float]
[[data-display]]
-==== Specify how to display the data
-
-When you connect your element to a data source, the element often appears as a warning. To remove the error, specify the display options.
+==== Change the element display options
-. Click *Display*
+Each element has its own display options to fit your design needs.
-. Change the display options for the element.
+To choose the display options, click *Display*, then make your changes.
-[float]
-[[element-display-container]]
-==== Change the appearance of the element container
-
-Further define the appearance of the element container and border.
+To define the appearance of the container and border:
. Next to *Element style*, click *+*, then select *Container style*.
@@ -134,17 +134,13 @@ Further define the appearance of the element container and border.
. Change the *Appearance* and *Border* options.
-[float]
-[[apply-element-styles]]
-==== Apply a set of styles
-
-To make your element look exactly the way you want, apply CSS overrides.
+To apply CSS overrides:
. Next to *Element style*, click *+*, then select *CSS*.
. Enter the *CSS*. For example, to center the Markdown element, enter:
+
-[source,js]
+[source,text]
--------------------------------------------------
.canvasRenderEl h1 {
text.align: center;
@@ -154,137 +150,31 @@ text.align: center;
. Click *Apply stylesheet*.
[float]
-[[configure-auto-refresh-interval]]
-==== Change the data auto-refresh interval
-
-Increase or decrease how often your Canvas element data refreshes on your workpad.
-
-. In the top left corner, click the *Control settings* icon.
-
-. Under *Change auto-refresh interval*, select the interval you want to use.
-+
-[role="screenshot"]
-image::images/canvas-refresh-interval.png[Element data refresh interval]
-
-TIP: To manually refresh the data, click the *Refresh data* icon.
-
-[float]
-[[canvas-time-range]]
-==== Customize map time ranges
-
-Configure the maps on your workpad for a specific time range.
-
-From the panel menu, select *Customize time range* to expose a time filter dedicated to the map.
-
-[role="screenshot"]
-image::images/canvas_map-time-filter.gif[]
-
-[float]
-[[organize-element]]
-=== Organize the elements on your workpad
-
-Choose where you want the elements to appear on your workpad.
-
-[float]
-[[move-canvas-elements]]
-==== Move elements
-
-Move the element to a preferred location on your workpad. As you move the element, notice the alignment lines that appear to help you place the element exactly where you want it.
-
-* Click and drag the element to your preferred location.
-
-* To move the element by 1 pixel, select the element, press and hold Shift, then use your arrow keys.
-
-* To move the element by 10 pixels, select the element, then use your arrow keys.
-
-[float]
-[[resize-canvas-elements]]
-==== Resize elements
+[[save-elements]]
+==== Save elements
-Make your elements bigger or smaller than the default size.
+To use the elements across all workpads, save the elements.
-. Select the element.
-
-. Click and drag the resize handles to the size you want.
-
-[float]
-[[align-canvas-elements]]
-==== Align elements
+When you're ready to save your element, select the element, then click the *Save as new element* icon.
-Align two or more elements on your workpad.
-
-. Press and hold Shift, then select the elements you want to align.
-
-. Click the *Element options* icon in the top right corner, then select *Align elements*.
-
-. From the *Alignment* menu, select how you want to align the elements on the workpad.
-+
[role="screenshot"]
-image::images/canvas-align-elements.gif[Align elements]
-
-[float]
-[[distribute-canvas-elements]]
-==== Distribute elements
-
-Distribute three or more elements on your workpad.
-
-. Press and hold Shift, then select the elements you want to distribute.
-
-. Click the *Element options* icon in the top right corner, then select *Distribute elements*.
+image::images/canvas_save_element.png[]
-. From the *Distribution* menu, select how you want to distribute the elements on the workpad.
-+
-[role="screenshot"]
-image::images/canvas-distribute-elements.gif[Distribute elements]
-
-[float]
-[[change-element-order]]
-==== Change the element order
-
-Change the order of how the elements are displayed on your workpad.
-
-. Select an element.
-
-. In the top right corder, click the *Element options* icon.
-
-. Select *Order*, then select the order that you want the element to appear.
-
-[float]
-[[zoom-in-out]]
-=== Use the zoom options
-
-In the upper left corner, click the *Zoom controls* icon, then select one of the options.
-
-[role="screenshot"]
-image::images/canvas-zoom-controls.png[Zoom controls]
-
-[float]
-[[element-save]]
-=== Save elements
-
-After you have made changes to elements, save them so that you can reuse them across all of your workpads.
-
-. Select the element that you want to save.
-+
To save a group of elements, press and hold Shift, then select the elements you want to save.
-. Click the *Save as new element* icon.
-
-. In the *Create new element* window, enter a *Name*.
-
-. Enter an optional *Description*, then click *Save*.
-
-. To access the element, click *Add element*, then select *My elements*.
+To access your saved elements, click *Add element*, then select *My elements*.
[float]
-[[add-more-pages]]
-=== Add pages
+[[delete-elements]]
+==== Delete elements
-When you have run out of room on your workpad page, add more pages.
+When you no longer need an element, delete it from your workpad.
-. Click *Page 1*, then click *+*.
+. Select the element you want to delete.
-. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown.
+. Click the *Element options* icon.
+
[role="screenshot"]
-image::images/canvas-add-pages.gif[Add pages]
+image::images/canvas_element_options.png[]
+
+. Select *Delete*.
diff --git a/docs/canvas/canvas-present-workpad.asciidoc b/docs/canvas/canvas-present-workpad.asciidoc
index 21b112f68eb69..486686cd857b5 100644
--- a/docs/canvas/canvas-present-workpad.asciidoc
+++ b/docs/canvas/canvas-present-workpad.asciidoc
@@ -1,12 +1,12 @@
[role="xpack"]
[[canvas-present-workpad]]
-== Present your workpad
+=== Present your workpad
When you are ready to present your workpad, use and enable the presentation options.
[float]
[[view-fullscreen-mode]]
-=== View your workpad in fullscreen mode
+==== View your workpad in fullscreen mode
In the upper left corner, click the *Enter fullscreen mode* icon.
@@ -15,7 +15,7 @@ image::images/canvas-fullscreen.png[Fullscreen mode]
[float]
[[enable-autoplay]]
-=== Enable autoplay
+==== Enable autoplay
Automatically cycle through your workpads pages in fullscreen mode.
diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc
index c46ba8a980ce2..dbba12865b8ca 100644
--- a/docs/canvas/canvas-share-workpad.asciidoc
+++ b/docs/canvas/canvas-share-workpad.asciidoc
@@ -1,12 +1,12 @@
[role="xpack"]
[[workpad-share-options]]
-== Share your workpad
+=== Share your workpad
When you've finished your workpad, you can share it outside of {kib}.
[float]
[[export-single-workpad]]
-=== Export workpads
+==== Export workpads
Create a JSON file of your workpad that you can export outside of {kib}.
@@ -21,7 +21,7 @@ Want to export multiple workpads? Go to the *Canvas workpads* view, select the w
[float]
[[create-workpad-pdf]]
-=== Create a PDF
+==== Create a PDF
If you have a license that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}.
@@ -36,7 +36,7 @@ image::images/canvas-generate-pdf.gif[Generate PDF]
[float]
[[create-workpad-URL]]
-=== Create a POST URL
+==== Create a POST URL
If you have a license that supports the {report-features}, you can create a POST URL that you can use to automatically generate PDF reports using Watcher or a script.
@@ -51,7 +51,7 @@ image::images/canvas-create-URL.gif[Create POST URL]
[float]
[[add-workpad-website]]
-=== Share the workpad on a website
+==== Share the workpad on a website
beta[] Canvas allows you to create _shareables_, which are workpads that you download and securely share on any website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar.
@@ -70,13 +70,11 @@ NOTE: Shareable workpads encode the current state of the workpad in a JSON file.
[float]
[[change-the-workpad-settings]]
-=== Change the settings
+==== Change the settings
After you've added the workpad to your website, you can change the autoplay and toolbar settings.
-[float]
-[[shareable-workpad-enable-autoplay]]
-==== Change the autoplay settings
+To change the autoplay settings:
. In the lower right corner of the shareable workpad, click the settings icon.
@@ -85,9 +83,7 @@ After you've added the workpad to your website, you can change the autoplay and
[role="screenshot"]
image::images/canvas_share_autoplay_480.gif[Autoplay settings]
-[float]
-[[hide-workpad-toolbar]]
-==== Change the toolbar settings
+To change the toolbar settings:
. In the lower right corner, click the settings icon.
diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc
index efc03f1c6c494..b6d684bdf5dde 100644
--- a/docs/canvas/canvas-tutorial.asciidoc
+++ b/docs/canvas/canvas-tutorial.asciidoc
@@ -12,7 +12,7 @@ For this tutorial, you'll need to add the <>.
+* Build presentations of your own live data with <>.
-* Learn more about <>.
+* Learn more about <> — the building blocks of your workpad.
* Deep dive into the {kibana-ref}/canvas-function-reference.html[expression language and functions] that drive Canvas.
diff --git a/docs/canvas/canvas-workpad.asciidoc b/docs/canvas/canvas-workpad.asciidoc
index f833bd903b0bc..c5c163441439c 100644
--- a/docs/canvas/canvas-workpad.asciidoc
+++ b/docs/canvas/canvas-workpad.asciidoc
@@ -4,19 +4,19 @@
A Canvas _workpad_ provides you with a workspace where you can build presentations of your live data.
-To create a workpad, you can:
+To create a workpad, choose one of the following options:
* <>
* <>
-* <>
-
* <>
+* <>
+
[float]
[[blank-canvas-workpad]]
-=== Start with a blank page
+=== Start with a blank workpad
To use the background colors, images, and data of your choice, start with a blank workpad.
@@ -69,21 +69,21 @@ Each of the sample data sets comes with a Canvas workpad that you can use for yo
. Add a {kibana-ref}/add-sample-data.html[sample data set].
-. On the *Add Data to Kibana* page, click the *View data* dropdown list, then select *Canvas*.
-+
-Need some more workpad inspiration? Check out the link:https://www.elastic.co/blog/[Elastic Blog].
+. On the *Add Data* page, click *View data*, then select *Canvas*.
[float]
[[apply-workpad-styles]]
-== Apply a set of styles to the entire workpad
+=== Apply a set of styles to the entire workpad
-To make your workpad look exactly the way you want, use the editor on the right to apply CSS overrides.
+To make your workpad look exactly the way you want, use the editor to apply CSS overrides.
. Expand *Global CSS overrides*.
-. Enter the CSS. For example, to change the background on every page, enter:
+. Enter the CSS.
++
+For example, to change the background on every page, enter:
+
-[source,js]
+[source,text]
--------------------------------------------------
.canvasPage {
background-color: #3990e6;
@@ -91,3 +91,46 @@ background-color: #3990e6;
--------------------------------------------------
. Click *Apply stylesheet*.
+
+[float]
+[[configure-auto-refresh-interval]]
+=== Change the auto-refresh interval
+
+Increase or decrease how often the data refreshes on your workpad.
+
+. In the top left corner, click the *Control settings* icon.
+
+. Under *Change auto-refresh interval*, select the interval you want to use.
++
+[role="screenshot"]
+image::images/canvas-refresh-interval.png[Element data refresh interval]
+
+TIP: To manually refresh the data, click the *Refresh data* icon.
+
+[float]
+[[zoom-in-out]]
+=== Use the zoom options
+
+In the upper left corner, click the *Zoom controls* icon, then select one of the options.
+
+[role="screenshot"]
+image::images/canvas-zoom-controls.png[Zoom controls]
+
+[float]
+[[add-more-pages]]
+=== Add pages
+
+Organize your ideas onto separate pages by adding more pages.
+
+. Click *Page 1*, then click *+*.
+
+. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown.
++
+[role="screenshot"]
+image::images/canvas-add-pages.gif[Add pages]
+
+include::{kib-repo-dir}/canvas/canvas-elements.asciidoc[]
+
+include::{kib-repo-dir}/canvas/canvas-present-workpad.asciidoc[]
+
+include::{kib-repo-dir}/canvas/canvas-share-workpad.asciidoc[]
diff --git a/docs/images/canvas-add-image.gif b/docs/images/canvas-add-image.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a2263e22c4c49ffcb7fb60d4be0d3f75c3dd6b1c
GIT binary patch
literal 1287935
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