diff --git a/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap
index f219c199b1d87..85abe0571d116 100644
--- a/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap
+++ b/src/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap
@@ -302,6 +302,27 @@ exports[`AdvancedSettings should render normally 1`] = `
],
}
}
+ showNoResultsMessage={true}
+ />
+
`;
@@ -420,6 +441,45 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
],
}
}
+ showNoResultsMessage={true}
+ />
+
`;
diff --git a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js
index 2c67da99b6bf7..7ce4341f59ed8 100644
--- a/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js
+++ b/src/core_plugins/kibana/public/management/sections/settings/advanced_settings.js
@@ -35,7 +35,7 @@ import { Form } from './components/form';
import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib';
import './advanced_settings.less';
-import { registerDefaultComponents, PAGE_TITLE_COMPONENT } from './components/default_component_registry';
+import { registerDefaultComponents, PAGE_TITLE_COMPONENT, PAGE_FOOTER_COMPONENT } from './components/default_component_registry';
import { getSettingsComponent } from './components/component_registry';
export class AdvancedSettings extends Component {
@@ -51,6 +51,7 @@ export class AdvancedSettings extends Component {
this.init(config);
this.state = {
query: parsedQuery,
+ footerQueryMatched: false,
filteredSettings: this.mapSettings(Query.execute(parsedQuery, this.settings)),
};
@@ -129,14 +130,22 @@ export class AdvancedSettings extends Component {
clearQuery = () => {
this.setState({
query: Query.parse(''),
+ footerQueryMatched: false,
filteredSettings: this.groupedSettings,
});
}
+ onFooterQueryMatchChange = (matched) => {
+ this.setState({
+ footerQueryMatched: matched
+ });
+ }
+
render() {
- const { filteredSettings, query } = this.state;
+ const { filteredSettings, query, footerQueryMatched } = this.state;
const PageTitle = getSettingsComponent(PAGE_TITLE_COMPONENT);
+ const PageFooter = getSettingsComponent(PAGE_FOOTER_COMPONENT);
return (
@@ -162,7 +171,9 @@ export class AdvancedSettings extends Component {
clearQuery={this.clearQuery}
save={this.saveConfig}
clear={this.clearConfig}
+ showNoResultsMessage={!footerQueryMatched}
/>
+
);
}
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js
index b88fb11d63bbc..221f8c2f82bf8 100644
--- a/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/default_component_registry.js
@@ -19,9 +19,12 @@
import { tryRegisterSettingsComponent } from './component_registry';
import { PageTitle } from './page_title';
+import { PageFooter } from './page_footer';
export const PAGE_TITLE_COMPONENT = 'advanced_settings_page_title';
+export const PAGE_FOOTER_COMPONENT = 'advanced_settings_page_footer';
export function registerDefaultComponents() {
tryRegisterSettingsComponent(PAGE_TITLE_COMPONENT, PageTitle);
+ tryRegisterSettingsComponent(PAGE_FOOTER_COMPONENT, PageFooter);
}
\ No newline at end of file
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js
index 16ea544028c83..a4ed24873f4d9 100644
--- a/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/field/field.js
@@ -87,7 +87,7 @@ export class Field extends PureComponent {
getEditableValue(type, value, defVal) {
const val = (value === null || value === undefined) ? defVal : value;
- switch(type) {
+ switch (type) {
case 'array':
return val.join(', ');
case 'boolean':
@@ -102,10 +102,10 @@ export class Field extends PureComponent {
}
getDisplayedDefaultValue(type, defVal) {
- if(defVal === undefined || defVal === null || defVal === '') {
+ if (defVal === undefined || defVal === null || defVal === '') {
return 'null';
}
- switch(type) {
+ switch (type) {
case 'array':
return defVal.join(', ');
default:
@@ -193,7 +193,7 @@ export class Field extends PureComponent {
}
onImageChange = async (files) => {
- if(!files.length) {
+ if (!files.length) {
this.clearError();
this.setState({
unsavedValue: null,
@@ -212,18 +212,18 @@ export class Field extends PureComponent {
changeImage: true,
unsavedValue: base64Image,
});
- } catch(err) {
+ } catch (err) {
toastNotifications.addDanger('Image could not be saved');
this.cancelChangeImage();
}
}
getImageAsBase64(file) {
- if(!file instanceof File) {
+ if (!file instanceof File) {
return null;
}
- const reader = new FileReader();
+ const reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve, reject) => {
@@ -245,7 +245,7 @@ export class Field extends PureComponent {
cancelChangeImage = () => {
const { savedValue } = this.state;
- if(this.changeImageForm) {
+ if (this.changeImageForm) {
this.changeImageForm.fileInput.value = null;
this.changeImageForm.handleChange();
}
@@ -268,14 +268,14 @@ export class Field extends PureComponent {
const { name, defVal, type } = this.props.setting;
const { changeImage, savedValue, unsavedValue, isJsonArray } = this.state;
- if(savedValue === unsavedValue) {
+ if (savedValue === unsavedValue) {
return;
}
let valueToSave = unsavedValue;
let isSameValue = false;
- switch(type) {
+ switch (type) {
case 'array':
valueToSave = valueToSave.split(',').map(val => val.trim());
isSameValue = valueToSave.join(',') === defVal.join(',');
@@ -295,10 +295,10 @@ export class Field extends PureComponent {
await this.props.save(name, valueToSave);
}
- if(changeImage) {
+ if (changeImage) {
this.cancelChangeImage();
}
- } catch(e) {
+ } catch (e) {
toastNotifications.addDanger(`Unable to save ${name}`);
}
this.setLoading(false);
@@ -311,7 +311,7 @@ export class Field extends PureComponent {
await this.props.clear(name);
this.cancelChangeImage();
this.clearError();
- } catch(e) {
+ } catch (e) {
toastNotifications.addDanger(`Unable to reset ${name}`);
}
this.setLoading(false);
@@ -321,7 +321,7 @@ export class Field extends PureComponent {
const { loading, changeImage, unsavedValue } = this.state;
const { name, value, type, options, isOverridden } = setting;
- switch(type) {
+ switch (type) {
case 'boolean':
return (
);
case 'image':
- if(!isDefaultValue(setting) && !changeImage) {
+ if (!isDefaultValue(setting) && !changeImage) {
return (
{setting.name}
@@ -438,7 +438,7 @@ export class Field extends PureComponent {
const defaultLink = this.renderResetToDefaultLink(setting);
const imageLink = this.renderChangeImageLink(setting);
- if(defaultLink || imageLink) {
+ if (defaultLink || imageLink) {
return (
{defaultLink}
@@ -462,8 +462,12 @@ export class Field extends PureComponent {
}
renderDescription(setting) {
- return (
-
+ let description;
+
+ if (React.isValidElement(setting.description)) {
+ description = setting.description;
+ } else {
+ description = (
+ );
+ }
+
+ return (
+
+ {description}
{this.renderDefaultValue(setting)}
);
@@ -478,14 +488,14 @@ export class Field extends PureComponent {
renderDefaultValue(setting) {
const { type, defVal } = setting;
- if(isDefaultValue(setting)) {
+ if (isDefaultValue(setting)) {
return;
}
return (
- { type === 'json' ? (
+ {type === 'json' ? (
Default:
) : (
- Default: {this.getDisplayedDefaultValue(type, defVal)}
+ Default: {this.getDisplayedDefaultValue(type, defVal)}
- ) }
+ )}
);
@@ -508,7 +518,7 @@ export class Field extends PureComponent {
renderResetToDefaultLink(setting) {
const { ariaName, name } = setting;
- if(isDefaultValue(setting)) {
+ if (isDefaultValue(setting)) {
return;
}
return (
@@ -528,7 +538,7 @@ export class Field extends PureComponent {
renderChangeImageLink(setting) {
const { changeImage } = this.state;
const { type, value, ariaName, name } = setting;
- if(type !== 'image' || !value || changeImage) {
+ if (type !== 'image' || !value || changeImage) {
return;
}
return (
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap
index 627f5d864d1d2..7d51699e975e7 100644
--- a/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/__snapshots__/form.test.js.snap
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Form should not render no settings message when instructed not to 1`] = ``;
+
exports[`Form should render no settings message when there are no settings 1`] = `
@@ -95,12 +96,23 @@ export class Form extends PureComponent {
);
}
+ maybeRenderNoSettings(clearQuery) {
+ if (this.props.showNoResultsMessage) {
+ return (
+
+ No settings found (Clear search)
+
+ );
+ }
+ return null;
+ }
+
render() {
const { settings, categories, categoryCounts, clearQuery } = this.props;
const currentCategories = [];
categories.forEach(category => {
- if(settings[category] && settings[category].length) {
+ if (settings[category] && settings[category].length) {
currentCategories.push(category);
}
});
@@ -112,11 +124,7 @@ export class Form extends PureComponent {
return (
this.renderCategory(category, settings[category], categoryCounts[category]) // fix this
);
- }) : (
-
- No settings found (Clear search)
-
- )
+ }) : this.maybeRenderNoSettings(clearQuery)
}
);
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js
index cd3b3f2db5fb3..fddaae79ec44e 100644
--- a/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/form/form.test.js
@@ -69,9 +69,9 @@ const categoryCounts = {
dashboard: 1,
'x-pack': 10,
};
-const save = () => {};
-const clear = () => {};
-const clearQuery = () => {};
+const save = () => { };
+const clear = () => { };
+const clearQuery = () => { };
describe('Form', () => {
it('should render normally', async () => {
@@ -83,6 +83,7 @@ describe('Form', () => {
save={save}
clear={clear}
clearQuery={clearQuery}
+ showNoResultsMessage={true}
/>
);
@@ -98,6 +99,23 @@ describe('Form', () => {
save={save}
clear={clear}
clearQuery={clearQuery}
+ showNoResultsMessage={true}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it('should not render no settings message when instructed not to', async () => {
+ const component = shallow(
+
);
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/__snapshots__/page_footer.test.js.snap b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/__snapshots__/page_footer.test.js.snap
new file mode 100644
index 0000000000000..eea1003c8eb95
--- /dev/null
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/__snapshots__/page_footer.test.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PageFooter should render normally 1`] = `""`;
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/index.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/index.js
new file mode 100644
index 0000000000000..2fae89ceb0380
--- /dev/null
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/index.js
@@ -0,0 +1,20 @@
+/*
+ * 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 { PageFooter } from './page_footer';
\ No newline at end of file
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.js
new file mode 100644
index 0000000000000..e55fbbae3b5f8
--- /dev/null
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.js
@@ -0,0 +1,20 @@
+/*
+ * 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 const PageFooter = () => null;
\ No newline at end of file
diff --git a/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.test.js b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.test.js
new file mode 100644
index 0000000000000..e4ac6af0a88fe
--- /dev/null
+++ b/src/core_plugins/kibana/public/management/sections/settings/components/page_footer/page_footer.test.js
@@ -0,0 +1,28 @@
+/*
+ * 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 React from 'react';
+import { shallow } from 'enzyme';
+
+import { PageFooter } from './page_footer';
+
+describe('PageFooter', () => {
+ it('should render normally', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+});
\ No newline at end of file
diff --git a/src/ui/public/management/index.js b/src/ui/public/management/index.js
index 616600f1ac1de..62f9850c839f5 100644
--- a/src/ui/public/management/index.js
+++ b/src/ui/public/management/index.js
@@ -19,8 +19,15 @@
import { ManagementSection } from './section';
+export {
+ PAGE_TITLE_COMPONENT,
+ PAGE_FOOTER_COMPONENT,
+} from '../../../core_plugins/kibana/public/management/sections/settings/components/default_component_registry';
+
export { registerSettingsComponent } from '../../../core_plugins/kibana/public/management/sections/settings/components/component_registry';
+export { Field } from '../../../core_plugins/kibana/public/management/sections/settings/components/field/field';
+
export const management = new ManagementSection('management', {
display: 'Management'
});
diff --git a/x-pack/plugins/xpack_main/common/constants.js b/x-pack/plugins/xpack_main/common/constants.js
index d939038a3986b..d59a4ab9e5a4a 100644
--- a/x-pack/plugins/xpack_main/common/constants.js
+++ b/x-pack/plugins/xpack_main/common/constants.js
@@ -14,7 +14,7 @@ export const CONFIG_TELEMETRY = 'telemetry:optIn';
* @type {string}
*/
export const CONFIG_TELEMETRY_DESC = (
- 'Help us improve the Elastic Stack by providing basic feature usage statistics? We will never share this data outside of Elastic.'
+ 'Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.'
);
/**
@@ -53,3 +53,8 @@ export const REPORT_INTERVAL_MS = 86400000;
* Key for the localStorage service
*/
export const LOCALSTORAGE_KEY = 'xpack.data';
+
+/**
+ * Link to the Elastic Telemetry privacy statement.
+ */
+export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/telemetry-privacy-statement`;
diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js
index 1fe2393f61d36..6e6ed25893ced 100644
--- a/x-pack/plugins/xpack_main/index.js
+++ b/x-pack/plugins/xpack_main/index.js
@@ -22,6 +22,7 @@ import {
CONFIG_TELEMETRY_DESC,
} from './common/constants';
import { settingsRoute } from './server/routes/api/v1/settings';
+import mappings from './mappings.json';
export { callClusterFactory } from './server/lib/call_cluster_factory';
@@ -65,11 +66,13 @@ export const xpackMain = (kibana) => {
},
uiExports: {
+ managementSections: ['plugins/xpack_main/views/management'],
uiSettingDefaults: {
[CONFIG_TELEMETRY]: {
name: 'Telemetry opt-in',
description: CONFIG_TELEMETRY_DESC,
- value: false
+ value: false,
+ readonly: true,
},
[XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING]: {
name: 'Admin email',
@@ -84,6 +87,7 @@ export const xpackMain = (kibana) => {
return {
telemetryUrl: config.get('xpack.xpack_main.telemetry.url'),
telemetryEnabled: isTelemetryEnabled(config),
+ telemetryOptedIn: null,
};
},
hacks: [
@@ -101,6 +105,7 @@ export const xpackMain = (kibana) => {
raw: true,
});
},
+ mappings,
},
init(server) {
diff --git a/x-pack/plugins/xpack_main/mappings.json b/x-pack/plugins/xpack_main/mappings.json
new file mode 100644
index 0000000000000..d83f7f5967630
--- /dev/null
+++ b/x-pack/plugins/xpack_main/mappings.json
@@ -0,0 +1,9 @@
+{
+ "telemetry": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/xpack_main/public/components/index.js b/x-pack/plugins/xpack_main/public/components/index.js
index c18cf4665f4ac..c8dc260717da0 100644
--- a/x-pack/plugins/xpack_main/public/components/index.js
+++ b/x-pack/plugins/xpack_main/public/components/index.js
@@ -11,3 +11,6 @@ export { AddLicense } from '../../../license_management/public/sections/license_
* For to link to management
*/
export { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../license_management/common/constants';
+
+export { TelemetryForm } from './telemetry/telemetry_form';
+export { OptInExampleFlyout } from './telemetry/opt_in_details_component';
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/components/telemetry/__snapshots__/opt_in_details_component.test.js.snap b/x-pack/plugins/xpack_main/public/components/telemetry/__snapshots__/opt_in_details_component.test.js.snap
new file mode 100644
index 0000000000000..19a9bbb169b35
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/__snapshots__/opt_in_details_component.test.js.snap
@@ -0,0 +1,56 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`OptInDetailsComponent renders as expected 1`] = `
+
+
+
+
+
+ Cluster statistics
+
+
+
+
+ This is an example of the basic cluster statistics that we’ll collect. It includes the number of indices, shards, and nodes. It also includes high-level usage statistics, such as whether monitoring is turned on.
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/xpack_main/public/components/telemetry/__snapshots__/telemetry_form.test.js.snap b/x-pack/plugins/xpack_main/public/components/telemetry/__snapshots__/telemetry_form.test.js.snap
new file mode 100644
index 0000000000000..a24872ec6c4fd
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/__snapshots__/telemetry_form.test.js.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TelemetryForm renders as expected 1`] = `
+
+
+
+
+
+
+
+ Usage Data
+
+
+
+
+
+
+
+ Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.
+
+
+
+ See an example of what we collect
+
+
+
+
+ Read our usage data privacy statement
+
+
+ ,
+ "type": "boolean",
+ "value": false,
+ }
+ }
+ />
+
+
+
+`;
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_details_component.js b/x-pack/plugins/xpack_main/public/components/telemetry/opt_in_details_component.js
similarity index 87%
rename from x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_details_component.js
rename to x-pack/plugins/xpack_main/public/components/telemetry/opt_in_details_component.js
index f980a10604fbd..678b30d261466 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_details_component.js
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/opt_in_details_component.js
@@ -58,7 +58,7 @@ export class OptInExampleFlyout extends Component {
return (
-
+
);
@@ -79,7 +79,7 @@ export class OptInExampleFlyout extends Component {
return (
- { JSON.stringify(data, null, 2) }
+ {JSON.stringify(data, null, 2)}
);
}
@@ -90,21 +90,22 @@ export class OptInExampleFlyout extends Component {
- Cluster Statistics
+ Cluster statistics
- This is an example of the basic cluster statistics we’ll gather, which includes number of indexes,
- number of shards, number of nodes, and high-level usage statistics, such as whether monitoring is enabled.
+ This is an example of the basic cluster statistics that we’ll collect.
+ It includes the number of indices, shards, and nodes.
+ It also includes high-level usage statistics, such as whether monitoring is turned on.
- { this.renderBody(this.state) }
+ {this.renderBody(this.state)}
diff --git a/x-pack/plugins/xpack_main/public/components/telemetry/opt_in_details_component.test.js b/x-pack/plugins/xpack_main/public/components/telemetry/opt_in_details_component.test.js
new file mode 100644
index 0000000000000..a649f932e025f
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/opt_in_details_component.test.js
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import { OptInExampleFlyout } from './opt_in_details_component';
+
+describe('OptInDetailsComponent', () => {
+ it('renders as expected', () => {
+ expect(shallow( ({ data: [] }))} onClose={jest.fn()} />)).toMatchSnapshot();
+ });
+});
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.js b/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.js
new file mode 100644
index 0000000000000..1a7826f56d0d2
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.js
@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import {
+ EuiPanel,
+ EuiForm,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { CONFIG_TELEMETRY_DESC, PRIVACY_STATEMENT_URL } from '../../../common/constants';
+import { OptInExampleFlyout } from './opt_in_details_component';
+import './telemetry_form.less';
+import { Field } from 'ui/management';
+
+const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data'];
+
+export class TelemetryForm extends Component {
+ static propTypes = {
+ telemetryOptInProvider: PropTypes.object.isRequired,
+ query: PropTypes.object,
+ onQueryMatchChange: PropTypes.func.isRequired,
+ };
+
+ state = {
+ processing: false,
+ showExample: false,
+ queryMatches: null,
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const {
+ query
+ } = nextProps;
+
+ const searchTerm = (query.text || '').toLowerCase();
+ const searchTermMatches = SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0);
+
+ if (searchTermMatches !== this.state.queryMatches) {
+ this.setState({
+ queryMatches: searchTermMatches
+ }, () => {
+ this.props.onQueryMatchChange(searchTermMatches);
+ });
+ }
+ }
+
+ render() {
+ const {
+ telemetryOptInProvider,
+ } = this.props;
+
+ const {
+ showExample,
+ queryMatches,
+ } = this.state;
+
+ if (queryMatches !== null && !queryMatches) {
+ return null;
+ }
+
+ return (
+
+ {showExample &&
+ telemetryOptInProvider.fetchExample()} onClose={this.toggleExample} />
+ }
+
+
+
+
+
+ Usage Data
+
+
+
+
+
+
+
+
+ );
+ }
+
+ renderDescription = () => (
+
+ {CONFIG_TELEMETRY_DESC}
+ See an example of what we collect
+
+
+ Read our usage data privacy statement
+
+
+
+ )
+
+ toggleOptIn = async () => {
+ const newOptInValue = !this.props.telemetryOptInProvider.getOptIn();
+
+ return new Promise((resolve, reject) => {
+ this.setState({
+ enabled: newOptInValue,
+ processing: true
+ }, () => {
+ this.props.telemetryOptInProvider.setOptIn(newOptInValue).then(() => {
+ this.setState({ processing: false });
+ resolve();
+ }, (e) => {
+ // something went wrong
+ this.setState({ processing: false });
+ reject(e);
+ });
+ });
+ });
+
+ }
+
+ toggleExample = () => {
+ this.setState({
+ showExample: !this.state.showExample
+ });
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.less b/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.less
new file mode 100644
index 0000000000000..041b0c9461e23
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.less
@@ -0,0 +1,3 @@
+.telemetryForm {
+ margin: 10px 6px 6px 6px;
+}
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.test.js b/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.test.js
new file mode 100644
index 0000000000000..53717aa0b15a2
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/components/telemetry/telemetry_form.test.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { TelemetryForm } from './telemetry_form';
+import { TelemetryOptInProvider } from '../../services/telemetry_opt_in';
+
+const buildTelemetryOptInProvider = () => {
+ const mockHttp = {
+ post: jest.fn()
+ };
+
+ function mockNotifier() {
+ this.notify = jest.fn();
+ }
+
+ const mockInjector = {
+ get: (key) => {
+ switch (key) {
+ case '$http':
+ return mockHttp;
+ case 'Notifier':
+ return mockNotifier;
+ default:
+ return null;
+ }
+ }
+ };
+
+ const chrome = {
+ addBasePath: (url) => url
+ };
+
+ return new TelemetryOptInProvider(mockInjector, chrome);
+};
+
+describe('TelemetryForm', () => {
+ it('renders as expected', () => {
+ expect(shallow(
+ )
+ ).toMatchSnapshot();
+ });
+});
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/hacks/__tests__/telemetry.js b/x-pack/plugins/xpack_main/public/hacks/__tests__/telemetry.js
index e027530cc08fe..20227b40359ae 100644
--- a/x-pack/plugins/xpack_main/public/hacks/__tests__/telemetry.js
+++ b/x-pack/plugins/xpack_main/public/hacks/__tests__/telemetry.js
@@ -13,6 +13,7 @@ uiModules.get('kibana')
// disable stat reporting while running tests,
// MockInjector used in these tests is not impacted
.constant('telemetryEnabled', false)
+ .constant('telemetryOptedIn', null)
.constant('telemetryUrl', 'not.a.valid.url.0');
const getMockInjector = ({ allowReport, lastReport }) => {
@@ -21,9 +22,7 @@ const getMockInjector = ({ allowReport, lastReport }) => {
get: sinon.stub().returns({ lastReport: lastReport }),
set: sinon.stub()
});
- get.withArgs('config').returns({
- get: () => allowReport
- });
+ get.withArgs('telemetryOptedIn').returns(allowReport);
const mockHttp = (req) => {
return req;
};
diff --git a/x-pack/plugins/xpack_main/public/hacks/telemetry.js b/x-pack/plugins/xpack_main/public/hacks/telemetry.js
index 66abbdbaee9a0..4b03f1c951118 100644
--- a/x-pack/plugins/xpack_main/public/hacks/telemetry.js
+++ b/x-pack/plugins/xpack_main/public/hacks/telemetry.js
@@ -6,7 +6,6 @@
import Promise from 'bluebird';
import {
- CONFIG_TELEMETRY,
REPORT_INTERVAL_MS,
LOCALSTORAGE_KEY,
} from '../../common/constants';
@@ -19,9 +18,9 @@ export class Telemetry {
*/
constructor($injector, fetchTelemetry) {
this._storage = $injector.get('localStorage');
- this._config = $injector.get('config');
this._$http = $injector.get('$http');
this._telemetryUrl = $injector.get('telemetryUrl');
+ this._telemetryOptedIn = $injector.get('telemetryOptedIn');
this._attributes = this._storage.get(LOCALSTORAGE_KEY) || {};
this._fetchTelemetry = fetchTelemetry;
}
@@ -42,8 +41,8 @@ export class Telemetry {
* Check time interval passage
*/
_checkReportStatus() {
- // check if opt-in for telemetry is enabled in config
- if (this._config.get(CONFIG_TELEMETRY, false)) {
+ // check if opt-in for telemetry is enabled
+ if (this._telemetryOptedIn) {
// If the last report is empty it means we've never sent telemetry and
// now is the time to send it.
if (!this._get('lastReport')) {
@@ -78,13 +77,13 @@ export class Telemetry {
});
})
.then(response => {
- // we sent a report, so we need to record and store the current time stamp
+ // we sent a report, so we need to record and store the current time stamp
this._set('lastReport', Date.now());
this._saveToBrowser();
return response;
})
.catch(() => {
- // no ajaxErrorHandlers for telemetry
+ // no ajaxErrorHandlers for telemetry
return Promise.resolve(null);
});
}
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/click_banner.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/click_banner.js
index 21b3ad9ccafe3..3ef092bde18ec 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/click_banner.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/click_banner.js
@@ -6,11 +6,54 @@
import expect from 'expect.js';
import sinon from 'sinon';
+import { uiModules } from 'ui/modules';
+
+uiModules.get('kibana')
+ // disable stat reporting while running tests,
+ // MockInjector used in these tests is not impacted
+ .constant('Notifier', function mockNotifier() { this.notify = sinon.stub(); })
+ .constant('telemetryOptedIn', null);
-import { CONFIG_TELEMETRY } from '../../../../common/constants';
import {
clickBanner,
} from '../click_banner';
+import { TelemetryOptInProvider } from '../../../services/telemetry_opt_in';
+
+const getMockInjector = ({ simulateFailure }) => {
+ const get = sinon.stub();
+
+ get.withArgs('telemetryOptedIn').returns(null);
+ get.withArgs('Notifier').returns(function mockNotifier() { this.notify = sinon.stub(); });
+
+ const mockHttp = {
+ post: sinon.stub()
+ };
+
+ if (simulateFailure) {
+ mockHttp.post.returns(Promise.reject(new Error('something happened')));
+ } else {
+ mockHttp.post.returns(Promise.resolve({}));
+ }
+
+ get.withArgs('$http').returns(mockHttp);
+
+ return { get };
+};
+
+const getTelemetryOptInProvider = ({ simulateFailure = false, simulateError = false } = {}) => {
+ const injector = getMockInjector({ simulateFailure });
+ const chrome = {
+ addBasePath: (url) => url
+ };
+
+ const provider = new TelemetryOptInProvider(injector, chrome);
+
+ if (simulateError) {
+ provider.setOptIn = () => Promise.reject('unhandled error');
+ }
+
+ return provider;
+};
describe('click_banner', () => {
@@ -18,17 +61,15 @@ describe('click_banner', () => {
const banners = {
remove: sinon.spy()
};
- const config = {
- set: sinon.stub()
- };
+
+ const telemetryOptInProvider = getTelemetryOptInProvider();
+
const bannerId = 'bruce-banner';
const optIn = true;
- config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(true));
-
- await clickBanner(bannerId, config, optIn, { _banners: banners });
+ await clickBanner(bannerId, telemetryOptInProvider, optIn, { _banners: banners });
- expect(config.set.calledOnce).to.be(true);
+ expect(telemetryOptInProvider.getOptIn()).to.be(optIn);
expect(banners.remove.calledOnce).to.be(true);
expect(banners.remove.calledWith(bannerId)).to.be(true);
});
@@ -40,17 +81,13 @@ describe('click_banner', () => {
const banners = {
remove: sinon.spy()
};
- const config = {
- set: sinon.stub()
- };
+ const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true });
const bannerId = 'bruce-banner';
const optIn = true;
- config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(false));
-
- await clickBanner(bannerId, config, optIn, { _banners: banners, _toastNotifications: toastNotifications });
+ await clickBanner(bannerId, telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications });
- expect(config.set.calledOnce).to.be(true);
+ expect(telemetryOptInProvider.getOptIn()).to.be(null);
expect(toastNotifications.addDanger.calledOnce).to.be(true);
expect(banners.remove.notCalled).to.be(true);
});
@@ -62,17 +99,13 @@ describe('click_banner', () => {
const banners = {
remove: sinon.spy()
};
- const config = {
- set: sinon.stub()
- };
+ const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true });
const bannerId = 'bruce-banner';
const optIn = false;
- config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.reject());
-
- await clickBanner(bannerId, config, optIn, { _banners: banners, _toastNotifications: toastNotifications });
+ await clickBanner(bannerId, telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications });
- expect(config.set.calledOnce).to.be(true);
+ expect(telemetryOptInProvider.getOptIn()).to.be(null);
expect(toastNotifications.addDanger.calledOnce).to.be(true);
expect(banners.remove.notCalled).to.be(true);
});
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/handle_old_settings.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/handle_old_settings.js
index 178bfb9cdfa2a..3ceb31cb2eb30 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/handle_old_settings.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/handle_old_settings.js
@@ -9,67 +9,143 @@ import sinon from 'sinon';
import { CONFIG_TELEMETRY } from '../../../../common/constants';
import { handleOldSettings } from '../handle_old_settings';
+import { TelemetryOptInProvider } from '../../../services/telemetry_opt_in';
+
+const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => {
+ const $http = {
+ post: async () => {
+ if (simulateFailure) {
+ return Promise.reject(new Error('something happened'));
+ }
+ return {};
+ }
+ };
+
+ const chrome = {
+ addBasePath: url => url
+ };
+
+ const $injector = {
+ get: (key) => {
+ if (key === '$http') {
+ return $http;
+ }
+ if (key === 'telemetryOptedIn') {
+ return enabled;
+ }
+ if (key === 'Notifier') {
+ return function mockNotifier() {
+ this.notify = sinon.stub();
+ };
+ }
+ throw new Error(`unexpected mock injector usage for ${key}`);
+ }
+ };
+
+ return new TelemetryOptInProvider($injector, chrome);
+};
describe('handle_old_settings', () => {
- it('re-uses old setting and stays opted in', async () => {
+ it('re-uses old "allowReport" setting and stays opted in', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
+ const telemetryOptInProvider = getTelemetryOptInProvider(null);
+ expect(telemetryOptInProvider.getOptIn()).to.be(null);
+
config.get.withArgs('xPackMonitoring:allowReport', null).returns(true);
config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(true));
- expect(await handleOldSettings(config)).to.be(false);
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(false);
+
+ expect(config.get.calledTwice).to.be(true);
+ expect(config.set.called).to.be(false);
- expect(config.get.calledOnce).to.be(true);
- expect(config.set.calledOnce).to.be(true);
- expect(config.set.getCall(0).args).to.eql([ CONFIG_TELEMETRY, true ]);
- expect(config.remove.calledTwice).to.be(true);
+ expect(config.remove.calledThrice).to.be(true);
expect(config.remove.getCall(0).args[0]).to.be('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).to.be('xPackMonitoring:showBanner');
+ expect(config.remove.getCall(2).args[0]).to.be(CONFIG_TELEMETRY);
+
+ expect(telemetryOptInProvider.getOptIn()).to.be(true);
});
- it('re-uses old setting and stays opted out', async () => {
+ it('re-uses old "telemetry:optIn" setting and stays opted in', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
+ const telemetryOptInProvider = getTelemetryOptInProvider(null);
+ expect(telemetryOptInProvider.getOptIn()).to.be(null);
+
config.get.withArgs('xPackMonitoring:allowReport', null).returns(false);
- config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(true));
+ config.get.withArgs(CONFIG_TELEMETRY, null).returns(true);
+
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(false);
- expect(await handleOldSettings(config)).to.be(false);
+ expect(config.get.calledTwice).to.be(true);
+ expect(config.set.called).to.be(false);
- expect(config.get.calledOnce).to.be(true);
- expect(config.set.calledOnce).to.be(true);
- expect(config.set.getCall(0).args).to.eql([ CONFIG_TELEMETRY, false ]);
- expect(config.remove.calledTwice).to.be(true);
+ expect(config.remove.calledThrice).to.be(true);
expect(config.remove.getCall(0).args[0]).to.be('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).to.be('xPackMonitoring:showBanner');
+ expect(config.remove.getCall(2).args[0]).to.be(CONFIG_TELEMETRY);
+
+ expect(telemetryOptInProvider.getOptIn()).to.be(true);
});
- it('re-uses old setting and stays opted out', async () => {
+ it('re-uses old "allowReport" setting and stays opted out', async () => {
const config = {
get: sinon.stub(),
remove: sinon.spy(),
set: sinon.stub(),
};
+ const telemetryOptInProvider = getTelemetryOptInProvider(null);
+ expect(telemetryOptInProvider.getOptIn()).to.be(null);
+
config.get.withArgs('xPackMonitoring:allowReport', null).returns(false);
config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(true));
- expect(await handleOldSettings(config)).to.be(false);
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(false);
+
+ expect(config.get.calledTwice).to.be(true);
+ expect(config.set.called).to.be(false);
+ expect(config.remove.calledThrice).to.be(true);
+ expect(config.remove.getCall(0).args[0]).to.be('xPackMonitoring:allowReport');
+ expect(config.remove.getCall(1).args[0]).to.be('xPackMonitoring:showBanner');
+ expect(config.remove.getCall(2).args[0]).to.be(CONFIG_TELEMETRY);
+
+ expect(telemetryOptInProvider.getOptIn()).to.be(false);
+ });
+
+ it('re-uses old "telemetry:optIn" setting and stays opted out', async () => {
+ const config = {
+ get: sinon.stub(),
+ remove: sinon.spy(),
+ set: sinon.stub(),
+ };
+
+ const telemetryOptInProvider = getTelemetryOptInProvider(null);
+
+ config.get.withArgs(CONFIG_TELEMETRY, null).returns(false);
+ config.get.withArgs('xPackMonitoring:allowReport', null).returns(true);
+
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(false);
- expect(config.get.calledOnce).to.be(true);
- expect(config.set.calledOnce).to.be(true);
- expect(config.set.getCall(0).args).to.eql([ CONFIG_TELEMETRY, false ]);
- expect(config.remove.calledTwice).to.be(true);
+ expect(config.get.calledTwice).to.be(true);
+ expect(config.set.called).to.be(false);
+ expect(config.remove.calledThrice).to.be(true);
expect(config.remove.getCall(0).args[0]).to.be('xPackMonitoring:allowReport');
expect(config.remove.getCall(1).args[0]).to.be('xPackMonitoring:showBanner');
+ expect(config.remove.getCall(2).args[0]).to.be(CONFIG_TELEMETRY);
+
+ expect(telemetryOptInProvider.getOptIn()).to.be(false);
});
it('acknowledges users old setting even if re-setting fails', async () => {
@@ -78,15 +154,17 @@ describe('handle_old_settings', () => {
set: sinon.stub(),
};
+ const telemetryOptInProvider = getTelemetryOptInProvider(null, { simulateFailure: true });
+
config.get.withArgs('xPackMonitoring:allowReport', null).returns(false);
+ //todo: make the new version of this fail!
config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(false));
// note: because it doesn't remove the old settings _and_ returns false, there's no risk of suddenly being opted in
- expect(await handleOldSettings(config)).to.be(false);
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(false);
- expect(config.get.calledOnce).to.be(true);
- expect(config.set.calledOnce).to.be(true);
- expect(config.set.getCall(0).args).to.eql([ CONFIG_TELEMETRY, false ]);
+ expect(config.get.calledTwice).to.be(true);
+ expect(config.set.called).to.be(false);
});
it('removes show banner setting and presents user with choice', async () => {
@@ -95,12 +173,14 @@ describe('handle_old_settings', () => {
remove: sinon.spy(),
};
+ const telemetryOptInProvider = getTelemetryOptInProvider(null);
+
config.get.withArgs('xPackMonitoring:allowReport', null).returns(null);
config.get.withArgs('xPackMonitoring:showBanner', null).returns(false);
- expect(await handleOldSettings(config)).to.be(true);
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(true);
- expect(config.get.calledTwice).to.be(true);
+ expect(config.get.calledThrice).to.be(true);
expect(config.remove.calledOnce).to.be(true);
expect(config.remove.getCall(0).args[0]).to.be('xPackMonitoring:showBanner');
});
@@ -110,12 +190,14 @@ describe('handle_old_settings', () => {
get: sinon.stub(),
};
+ const telemetryOptInProvider = getTelemetryOptInProvider(null);
+
config.get.withArgs('xPackMonitoring:allowReport', null).returns(null);
config.get.withArgs('xPackMonitoring:showBanner', null).returns(null);
- expect(await handleOldSettings(config)).to.be(true);
+ expect(await handleOldSettings(config, telemetryOptInProvider)).to.be(true);
- expect(config.get.calledTwice).to.be(true);
+ expect(config.get.calledThrice).to.be(true);
});
-});
\ No newline at end of file
+});
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/should_show_banner.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/should_show_banner.js
index a215d21e7cdbf..33fde83241f8a 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/should_show_banner.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/__tests__/should_show_banner.js
@@ -9,11 +9,37 @@ import sinon from 'sinon';
import { CONFIG_TELEMETRY } from '../../../../common/constants';
import { shouldShowBanner } from '../should_show_banner';
+import { TelemetryOptInProvider } from '../../../services/telemetry_opt_in';
+
+const getMockInjector = ({ telemetryEnabled }) => {
+ const get = sinon.stub();
+
+ get.withArgs('telemetryOptedIn').returns(telemetryEnabled);
+ get.withArgs('Notifier').returns(function mockNotifier() { this.notify = sinon.stub(); });
+
+ const mockHttp = {
+ post: sinon.stub()
+ };
+
+ get.withArgs('$http').returns(mockHttp);
+
+ return { get };
+};
+
+const getTelemetryOptInProvider = ({ telemetryEnabled = null } = {}) => {
+ const injector = getMockInjector({ telemetryEnabled });
+ const chrome = {
+ addBasePath: (url) => url
+ };
+
+ return new TelemetryOptInProvider(injector, chrome);
+};
describe('should_show_banner', () => {
it('returns whatever handleOldSettings does when telemetry opt-in setting is unset', async () => {
const config = { get: sinon.stub() };
+ const telemetryOptInProvider = getTelemetryOptInProvider();
const handleOldSettingsTrue = sinon.stub();
const handleOldSettingsFalse = sinon.stub();
@@ -21,13 +47,13 @@ describe('should_show_banner', () => {
handleOldSettingsTrue.returns(Promise.resolve(true));
handleOldSettingsFalse.returns(Promise.resolve(false));
- const showBannerTrue = await shouldShowBanner(config, { _handleOldSettings: handleOldSettingsTrue });
- const showBannerFalse = await shouldShowBanner(config, { _handleOldSettings: handleOldSettingsFalse });
+ const showBannerTrue = await shouldShowBanner(telemetryOptInProvider, config, { _handleOldSettings: handleOldSettingsTrue });
+ const showBannerFalse = await shouldShowBanner(telemetryOptInProvider, config, { _handleOldSettings: handleOldSettingsFalse });
expect(showBannerTrue).to.be(true);
expect(showBannerFalse).to.be(false);
- expect(config.get.calledTwice).to.be(true);
+ expect(config.get.callCount).to.be(0);
expect(handleOldSettingsTrue.calledOnce).to.be(true);
expect(handleOldSettingsFalse.calledOnce).to.be(true);
});
@@ -35,17 +61,17 @@ describe('should_show_banner', () => {
it('returns false if telemetry opt-in setting is set to true', async () => {
const config = { get: sinon.stub() };
- config.get.withArgs(CONFIG_TELEMETRY, null).returns(true);
+ const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryEnabled: true });
- expect(await shouldShowBanner(config)).to.be(false);
+ expect(await shouldShowBanner(telemetryOptInProvider, config)).to.be(false);
});
it('returns false if telemetry opt-in setting is set to false', async () => {
const config = { get: sinon.stub() };
- config.get.withArgs(CONFIG_TELEMETRY, null).returns(false);
+ const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryEnabled: false });
- expect(await shouldShowBanner(config)).to.be(false);
+ expect(await shouldShowBanner(telemetryOptInProvider, config)).to.be(false);
});
});
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/click_banner.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/click_banner.js
index ce0f2a98b416c..efc07f49c5864 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/click_banner.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/click_banner.js
@@ -12,22 +12,25 @@ import {
} from 'ui/notify';
import { EuiText } from '@elastic/eui';
-import { CONFIG_TELEMETRY } from '../../../common/constants';
-
/**
* Handle clicks from the user on the opt-in banner.
*
* @param {String} bannerId Banner ID to close upon success.
- * @param {Object} config Advanced settings configuration to set opt-in.
+ * @param {Object} telemetryOptInProvider the telemetry opt-in provider
* @param {Boolean} optIn {@code true} to opt into telemetry.
* @param {Object} _banners Singleton banners. Can be overridden for tests.
* @param {Object} _toastNotifications Singleton toast notifications. Can be overridden for tests.
*/
-export async function clickBanner(bannerId, config, optIn, { _banners = banners, _toastNotifications = toastNotifications } = { }) {
+export async function clickBanner(
+ bannerId,
+ telemetryOptInProvider,
+ optIn,
+ { _banners = banners, _toastNotifications = toastNotifications } = {}) {
+
let set = false;
try {
- set = await config.set(CONFIG_TELEMETRY, Boolean(optIn));
+ set = await telemetryOptInProvider.setOptIn(optIn);
} catch (err) {
// set is already false
console.log('Unexpected error while trying to save setting.', err);
@@ -37,10 +40,10 @@ export async function clickBanner(bannerId, config, optIn, { _banners = banners,
_banners.remove(bannerId);
} else {
_toastNotifications.addDanger({
- title: 'Advanced Setting Error',
+ title: 'Telemetry Error',
text: (
- Unable to save advanced setting.
+ Unable to save telemetry preference.
Check that Kibana and Elasticsearch are still running, then try again.
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/handle_old_settings.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/handle_old_settings.js
index fba5a22d5b16f..4188676bad1e0 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/handle_old_settings.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/handle_old_settings.js
@@ -14,19 +14,31 @@ import { CONFIG_TELEMETRY } from '../../../common/constants';
* @param {Object} config The advanced settings config object.
* @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed.
*/
-export async function handleOldSettings(config) {
+export async function handleOldSettings(config, telemetryOptInProvider) {
const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport';
const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner';
- const oldSetting = config.get(CONFIG_ALLOW_REPORT, null);
+ const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null);
+ const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null);
+
+ let legacyOptInValue = null;
+
+ if (typeof oldTelemetrySetting === 'boolean') {
+ legacyOptInValue = oldTelemetrySetting;
+ } else if (typeof oldAllowReportSetting === 'boolean') {
+ legacyOptInValue = oldAllowReportSetting;
+ }
+
+ if (legacyOptInValue !== null) {
+ try {
+ await telemetryOptInProvider.setOptIn(legacyOptInValue);
- if (oldSetting !== null) {
- if (await config.set(CONFIG_TELEMETRY, Boolean(oldSetting))) {
// delete old keys once we've successfully changed the setting (if it fails, we just wait until next time)
config.remove(CONFIG_ALLOW_REPORT);
config.remove(CONFIG_SHOW_BANNER);
+ config.remove(CONFIG_TELEMETRY);
+ } finally {
+ return false;
}
-
- return false;
}
const oldShowSetting = config.get(CONFIG_SHOW_BANNER, null);
@@ -36,4 +48,4 @@ export async function handleOldSettings(config) {
}
return true;
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/inject_banner.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/inject_banner.js
index 7fd8a5027d407..d3142fd3e2dac 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/inject_banner.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/inject_banner.js
@@ -9,6 +9,7 @@ import { PathProvider } from 'plugins/xpack_main/services/path';
import { fetchTelemetry } from '../fetch_telemetry';
import { renderBanner } from './render_banner';
import { shouldShowBanner } from './should_show_banner';
+import { TelemetryOptInProvider } from '../../services/telemetry_opt_in';
/**
* Add the Telemetry opt-in banner if the user has not already made a decision.
@@ -21,6 +22,7 @@ import { shouldShowBanner } from './should_show_banner';
async function asyncInjectBanner($injector) {
const telemetryEnabled = $injector.get('telemetryEnabled');
const Private = $injector.get('Private');
+ const telemetryOptInProvider = Private(TelemetryOptInProvider);
const config = $injector.get('config');
// no banner if the server config has telemetry disabled
@@ -39,10 +41,10 @@ async function asyncInjectBanner($injector) {
}
// determine if the banner should be displayed
- if (await shouldShowBanner(config)) {
- const $http = $injector.get('$http');
+ if (await shouldShowBanner(telemetryOptInProvider, config)) {
+ const $http = $injector.get("$http");
- renderBanner(config, () => fetchTelemetry($http));
+ renderBanner(telemetryOptInProvider, () => fetchTelemetry($http));
}
}
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_banner_component.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_banner_component.js
index 805a9541191f5..30d19f4c32478 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_banner_component.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/opt_in_banner_component.js
@@ -17,8 +17,8 @@ import {
EuiText,
} from '@elastic/eui';
-import { CONFIG_TELEMETRY_DESC } from '../../../common/constants';
-import { OptInExampleFlyout } from './opt_in_details_component';
+import { CONFIG_TELEMETRY_DESC, PRIVACY_STATEMENT_URL } from '../../../common/constants';
+import { OptInExampleFlyout } from '../../components';
/**
* React component for displaying the Telemetry opt-in banner.
@@ -63,7 +63,7 @@ export class OptInBanner extends Component {
)} or read our {(
telemetry privacy statement
@@ -84,7 +84,7 @@ export class OptInBanner extends Component {
} else {
title = (
- { CONFIG_TELEMETRY_DESC } {(
+ {CONFIG_TELEMETRY_DESC} {(
this.setState({ showDetails: true })}>
Read more
@@ -95,8 +95,8 @@ export class OptInBanner extends Component {
return (
- { details }
- { flyoutDetails }
+ {details}
+ {flyoutDetails}
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/render_banner.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/render_banner.js
index 59ccced7ac714..1fa1287cc2d98 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/render_banner.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/render_banner.js
@@ -14,15 +14,15 @@ import { OptInBanner } from './opt_in_banner_component';
/**
* Render the Telemetry Opt-in banner.
*
- * @param {Object} config The advanced settings config.
+ * @param {Object} telemetryOptInProvider The telemetry opt-in provider.
* @param {Function} fetchTelemetry Function to pull telemetry on demand.
* @param {Object} _banners Banners singleton, which can be overridden for tests.
*/
-export function renderBanner(config, fetchTelemetry, { _banners = banners } = { }) {
+export function renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners = banners } = {}) {
const bannerId = _banners.add({
component: (
clickBanner(bannerId, config, optIn)}
+ optInClick={optIn => clickBanner(bannerId, telemetryOptInProvider, optIn)}
fetchTelemetry={fetchTelemetry}
/>
),
diff --git a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/should_show_banner.js b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/should_show_banner.js
index 4e224173997b6..5685132a95061 100644
--- a/x-pack/plugins/xpack_main/public/hacks/welcome_banner/should_show_banner.js
+++ b/x-pack/plugins/xpack_main/public/hacks/welcome_banner/should_show_banner.js
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { CONFIG_TELEMETRY } from '../../../common/constants';
import { handleOldSettings } from './handle_old_settings';
/**
@@ -16,6 +15,6 @@ import { handleOldSettings } from './handle_old_settings';
* @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests.
* @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise.
*/
-export async function shouldShowBanner(config, { _handleOldSettings = handleOldSettings } = { }) {
- return config.get(CONFIG_TELEMETRY, null) === null && await _handleOldSettings(config);
-}
\ No newline at end of file
+export async function shouldShowBanner(telemetryOptInProvider, config, { _handleOldSettings = handleOldSettings } = {}) {
+ return telemetryOptInProvider.getOptIn() === null && await _handleOldSettings(config, telemetryOptInProvider);
+}
diff --git a/x-pack/plugins/xpack_main/public/services/telemetry_opt_in.js b/x-pack/plugins/xpack_main/public/services/telemetry_opt_in.js
new file mode 100644
index 0000000000000..b4070e4587f87
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/services/telemetry_opt_in.js
@@ -0,0 +1,40 @@
+/*
+ * 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 moment from 'moment';
+
+export function TelemetryOptInProvider($injector, chrome) {
+
+ const Notifier = $injector.get('Notifier');
+ const notify = new Notifier();
+ let currentOptInStatus = $injector.get('telemetryOptedIn');
+
+ return {
+ getOptIn: () => currentOptInStatus,
+ setOptIn: async (enabled) => {
+ const $http = $injector.get('$http');
+
+ try {
+ await $http.post(chrome.addBasePath('/api/telemetry/v1/optIn'), { enabled });
+ currentOptInStatus = enabled;
+ } catch (error) {
+ notify.error(error);
+ return false;
+ }
+
+ return true;
+ },
+ fetchExample: async () => {
+ const $http = $injector.get('$http');
+ return $http.post(chrome.addBasePath(`/api/telemetry/v1/clusters/_stats`), {
+ timeRange: {
+ min: moment().subtract(20, 'minutes').toISOString(),
+ max: moment().toISOString()
+ }
+ });
+ }
+ };
+}
diff --git a/x-pack/plugins/xpack_main/public/services/telemetry_opt_in.test.js b/x-pack/plugins/xpack_main/public/services/telemetry_opt_in.test.js
new file mode 100644
index 0000000000000..60c08e5a338e5
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/services/telemetry_opt_in.test.js
@@ -0,0 +1,89 @@
+/*
+ * 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 { TelemetryOptInProvider } from "./telemetry_opt_in";
+
+describe('TelemetryOptInProvider', () => {
+ const setup = ({ optedIn, simulatePostError }) => {
+ const mockHttp = {
+ post: jest.fn(async () => {
+ if (simulatePostError) {
+ return Promise.reject("Something happened");
+ }
+ })
+ };
+
+ const mockChrome = {
+ addBasePath: (url) => url
+ };
+
+ class MockNotifier {
+ constructor() {
+ this.error = jest.fn();
+ }
+ }
+
+ const mockInjector = {
+ get: (key) => {
+ switch (key) {
+ case 'telemetryOptedIn': {
+ return optedIn;
+ }
+ case 'Notifier': {
+ return MockNotifier;
+ }
+ case '$http': {
+ return mockHttp;
+ }
+ default:
+ throw new Error('unexpected injector request: ' + key);
+ }
+ }
+ };
+
+ const provider = new TelemetryOptInProvider(mockInjector, mockChrome);
+ return {
+ provider,
+ mockHttp,
+ };
+ };
+
+
+ it('should return the current opt-in status', () => {
+ const { provider: optedInProvider } = setup({ optedIn: true });
+ expect(optedInProvider.getOptIn()).toEqual(true);
+
+ const { provider: optedOutProvider } = setup({ optedIn: false });
+ expect(optedOutProvider.getOptIn()).toEqual(false);
+ });
+
+ it('should allow an opt-out to take place', async () => {
+ const { provider, mockHttp } = setup({ optedIn: true });
+ await provider.setOptIn(false);
+
+ expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v1/optIn`, { enabled: false });
+
+ expect(provider.getOptIn()).toEqual(false);
+ });
+
+ it('should allow an opt-in to take place', async () => {
+ const { provider, mockHttp } = setup({ optedIn: false });
+ await provider.setOptIn(true);
+
+ expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v1/optIn`, { enabled: true });
+
+ expect(provider.getOptIn()).toEqual(true);
+ });
+
+ it('should gracefully handle errors', async () => {
+ const { provider, mockHttp } = setup({ optedIn: false, simulatePostError: true });
+ await provider.setOptIn(true);
+
+ expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v1/optIn`, { enabled: true });
+
+ // opt-in change should not be reflected
+ expect(provider.getOptIn()).toEqual(false);
+ });
+});
\ No newline at end of file
diff --git a/x-pack/plugins/xpack_main/public/views/management/index.js b/x-pack/plugins/xpack_main/public/views/management/index.js
new file mode 100644
index 0000000000000..0ed6fe09ef80a
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/views/management/index.js
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import './management';
diff --git a/x-pack/plugins/xpack_main/public/views/management/management.js b/x-pack/plugins/xpack_main/public/views/management/management.js
new file mode 100644
index 0000000000000..8c244f8ae933f
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/views/management/management.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import routes from 'ui/routes';
+
+import { registerSettingsComponent, PAGE_FOOTER_COMPONENT } from 'ui/management';
+import { TelemetryOptInProvider } from '../../services/telemetry_opt_in';
+import { TelemetryForm } from '../../components';
+
+routes.defaults(/\/management/, {
+ resolve: {
+ telemetryManagementSection: function (Private) {
+ const telemetryOptInProvider = Private(TelemetryOptInProvider);
+ const Component = (props) => ;
+
+ registerSettingsComponent(PAGE_FOOTER_COMPONENT, Component, true);
+ }
+ }
+});
diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js
index 283f3e09b9d60..f75ec5678f1c6 100644
--- a/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js
+++ b/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js
@@ -9,15 +9,40 @@ import expect from 'expect.js';
import { replaceInjectedVars } from '../replace_injected_vars';
+const buildRequest = (telemetryOptedIn = null) => {
+ const get = sinon.stub();
+ if (telemetryOptedIn === null) {
+ get.withArgs('telemetry', 'telemetry').returns(Promise.reject(new Error('not found exception')));
+ } else {
+ get.withArgs('telemetry', 'telemetry').returns(Promise.resolve({ attributes: { enabled: telemetryOptedIn } }));
+ }
+
+ return {
+ getSavedObjectsClient: () => {
+ return {
+ get,
+ create: sinon.stub(),
+
+ errors: {
+ isNotFoundError: (error) => {
+ return error.message === 'not found exception';
+ }
+ }
+ };
+ }
+ };
+};
+
describe('replaceInjectedVars uiExport', () => {
it('sends xpack info if request is authenticated and license is not basic', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
const server = mockServer();
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
a: 1,
+ telemetryOptedIn: null,
xpackInitialInfo: {
b: 1
}
@@ -29,13 +54,14 @@ describe('replaceInjectedVars uiExport', () => {
it('sends the xpack info if security plugin is disabled', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
const server = mockServer();
delete server.plugins.security;
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
a: 1,
+ telemetryOptedIn: null,
xpackInitialInfo: {
b: 1
}
@@ -44,13 +70,46 @@ describe('replaceInjectedVars uiExport', () => {
it('sends the xpack info if xpack license is basic', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
+ const server = mockServer();
+ server.plugins.xpack_main.info.license.isOneOf.returns(true);
+
+ const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
+ expect(newVars).to.eql({
+ a: 1,
+ telemetryOptedIn: null,
+ xpackInitialInfo: {
+ b: 1
+ }
+ });
+ });
+
+ it('respects the telemetry opt-in document when opted-out', async () => {
+ const originalInjectedVars = { a: 1 };
+ const request = buildRequest(false);
+ const server = mockServer();
+ server.plugins.xpack_main.info.license.isOneOf.returns(true);
+
+ const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
+ expect(newVars).to.eql({
+ a: 1,
+ telemetryOptedIn: false,
+ xpackInitialInfo: {
+ b: 1
+ }
+ });
+ });
+
+ it('respects the telemetry opt-in document when opted-in', async () => {
+ const originalInjectedVars = { a: 1 };
+ const request = buildRequest(true);
const server = mockServer();
server.plugins.xpack_main.info.license.isOneOf.returns(true);
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
a: 1,
+ telemetryOptedIn: true,
xpackInitialInfo: {
b: 1
}
@@ -59,7 +118,7 @@ describe('replaceInjectedVars uiExport', () => {
it('sends the originalInjectedVars if not authenticated', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
const server = mockServer();
server.plugins.security.isAuthenticated.returns(false);
@@ -69,7 +128,7 @@ describe('replaceInjectedVars uiExport', () => {
it('sends the originalInjectedVars if xpack info is unavailable', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
const server = mockServer();
server.plugins.xpack_main.info.isAvailable.returns(false);
@@ -79,7 +138,7 @@ describe('replaceInjectedVars uiExport', () => {
it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
const server = mockServer();
delete server.plugins.security;
server.plugins.xpack_main.info.isAvailable.returns(false);
@@ -87,13 +146,14 @@ describe('replaceInjectedVars uiExport', () => {
const newVars = await replaceInjectedVars(originalInjectedVars, request, server);
expect(newVars).to.eql({
a: 1,
+ telemetryOptedIn: null,
xpackInitialInfo: undefined
});
});
it('sends the originalInjectedVars if the license check result is not available', async () => {
const originalInjectedVars = { a: 1 };
- const request = {};
+ const request = buildRequest();
const server = mockServer();
server.plugins.xpack_main.info.feature().getLicenseCheckResults.returns(undefined);
diff --git a/x-pack/plugins/xpack_main/server/lib/get_telemetry_opt_in.js b/x-pack/plugins/xpack_main/server/lib/get_telemetry_opt_in.js
new file mode 100644
index 0000000000000..1f9d6c5849e2f
--- /dev/null
+++ b/x-pack/plugins/xpack_main/server/lib/get_telemetry_opt_in.js
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export async function getTelemetryOptIn(request) {
+ const savedObjectsClient = request.getSavedObjectsClient();
+
+ try {
+ const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry');
+ return attributes.enabled;
+ } catch (error) {
+ if (savedObjectsClient.errors.isNotFoundError(error)) {
+ return null;
+ }
+ throw error;
+ }
+}
diff --git a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js
index 178795fd88ab6..990e4e1a7d53a 100644
--- a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js
+++ b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js
@@ -4,16 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { getTelemetryOptIn } from "./get_telemetry_opt_in";
+
export async function replaceInjectedVars(originalInjectedVars, request, server) {
const xpackInfo = server.plugins.xpack_main.info;
- const withXpackInfo = () => ({
+ const withXpackInfo = async () => ({
...originalInjectedVars,
+ telemetryOptedIn: await getTelemetryOptIn(request),
xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined
});
// security feature is disabled
if (!server.plugins.security) {
- return withXpackInfo();
+ return await withXpackInfo();
}
// not enough license info to make decision one way or another
@@ -23,7 +26,7 @@ export async function replaceInjectedVars(originalInjectedVars, request, server)
// authentication is not a thing you can do
if (xpackInfo.license.isOneOf('basic')) {
- return withXpackInfo();
+ return await withXpackInfo();
}
// request is not authenticated
@@ -32,5 +35,5 @@ export async function replaceInjectedVars(originalInjectedVars, request, server)
}
// plugin enabled, license is appropriate, request is authenticated
- return withXpackInfo();
+ return await withXpackInfo();
}
diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/telemetry/telemetry.js b/x-pack/plugins/xpack_main/server/routes/api/v1/telemetry/telemetry.js
index 00f1695fa789b..9700b527698f3 100644
--- a/x-pack/plugins/xpack_main/server/routes/api/v1/telemetry/telemetry.js
+++ b/x-pack/plugins/xpack_main/server/routes/api/v1/telemetry/telemetry.js
@@ -17,7 +17,7 @@ import { getAllStats, getLocalStats } from '../../../../lib/telemetry';
* @param {String} end The end time of the request.
* @return {Promise} An array of telemetry objects.
*/
-export async function getTelemetry(req, config, start, end, { _getAllStats = getAllStats, _getLocalStats = getLocalStats } = { }) {
+export async function getTelemetry(req, config, start, end, { _getAllStats = getAllStats, _getLocalStats = getLocalStats } = {}) {
let response = [];
if (config.get('xpack.monitoring.enabled')) {
@@ -26,13 +26,43 @@ export async function getTelemetry(req, config, start, end, { _getAllStats = get
if (!Array.isArray(response) || response.length === 0) {
// return it as an array for a consistent API response
- response = [ await _getLocalStats(req) ];
+ response = [await _getLocalStats(req)];
}
return response;
}
export function telemetryRoute(server) {
+ /**
+ * Change Telemetry Opt-In preference.
+ */
+ server.route({
+ method: 'POST',
+ path: '/api/telemetry/v1/optIn',
+ config: {
+ validate: {
+ payload: Joi.object({
+ enabled: Joi.bool().required()
+ })
+ }
+ },
+ handler: async (req, reply) => {
+ const savedObjectsClient = req.getSavedObjectsClient();
+ try {
+ await savedObjectsClient.create('telemetry', {
+ enabled: req.payload.enabled
+ }, {
+ id: 'telemetry',
+ overwrite: true,
+ });
+ } catch (err) {
+ return reply(wrap(err));
+ }
+ reply({}).code(200);
+ }
+ });
+
+
/**
* Telemetry Data
*
@@ -61,10 +91,10 @@ export function telemetryRoute(server) {
reply(await getTelemetry(req, config, start, end));
} catch (err) {
if (config.get('env.dev')) {
- // don't ignore errors when running in dev mode
+ // don't ignore errors when running in dev mode
reply(wrap(err));
} else {
- // ignore errors, return empty set and a 200
+ // ignore errors, return empty set and a 200
reply([]).code(200);
}
}
diff --git a/x-pack/test/rbac_api_integration/apis/privileges/index.js b/x-pack/test/rbac_api_integration/apis/privileges/index.js
index 9563a1b65540f..12ca92f3fe33c 100644
--- a/x-pack/test/rbac_api_integration/apis/privileges/index.js
+++ b/x-pack/test/rbac_api_integration/apis/privileges/index.js
@@ -35,6 +35,9 @@ export default function ({ getService }) {
'action:saved_objects/timelion-sheet/get',
'action:saved_objects/timelion-sheet/bulk_get',
'action:saved_objects/timelion-sheet/find',
+ 'action:saved_objects/telemetry/get',
+ 'action:saved_objects/telemetry/bulk_get',
+ 'action:saved_objects/telemetry/find',
'action:saved_objects/graph-workspace/get',
'action:saved_objects/graph-workspace/bulk_get',
'action:saved_objects/graph-workspace/find',