diff --git a/src/core_plugins/kibana/public/home/components/sample_data_set_card.js b/src/core_plugins/kibana/public/home/components/sample_data_set_card.js index 4e8554e7324f8..a19eaf1c5dd8a 100644 --- a/src/core_plugins/kibana/public/home/components/sample_data_set_card.js +++ b/src/core_plugins/kibana/public/home/components/sample_data_set_card.js @@ -28,70 +28,40 @@ import { EuiToolTip, } from '@elastic/eui'; -import { - installSampleDataSet, - uninstallSampleDataSet -} from '../sample_data_sets'; +export const INSTALLED_STATUS = 'installed'; +export const UNINSTALLED_STATUS = 'not_installed'; export class SampleDataSetCard extends React.Component { - constructor(props) { - super(props); - - this.state = { - isProcessingRequest: false, - }; - } - - startRequest = async () => { - const { - getConfig, - setConfig, - id, - name, - onRequestComplete, - defaultIndex, - clearIndexPatternsCache, - } = this.props; - - this.setState({ - isProcessingRequest: true, - }); - - if (this.isInstalled()) { - await uninstallSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache); - } else { - await installSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache); - } - - onRequestComplete(); - - this.setState({ - isProcessingRequest: false, - }); - } - isInstalled = () => { - if (this.props.status === 'installed') { + if (this.props.status === INSTALLED_STATUS) { return true; } return false; } + install = () => { + this.props.onInstall(this.props.id); + } + + uninstall = () => { + this.props.onUninstall(this.props.id); + } + renderBtn = () => { switch (this.props.status) { - case 'installed': + case INSTALLED_STATUS: return ( - {this.state.isProcessingRequest ? 'Removing' : 'Remove'} + {this.props.isProcessing ? 'Removing' : 'Remove'} @@ -105,16 +75,16 @@ export class SampleDataSetCard extends React.Component { ); - case 'not_installed': + case UNINSTALLED_STATUS: return ( - {this.state.isProcessingRequest ? 'Adding' : 'Add'} + {this.props.isProcessing ? 'Adding' : 'Add'} @@ -163,15 +133,13 @@ SampleDataSetCard.propTypes = { name: PropTypes.string.isRequired, launchUrl: PropTypes.string.isRequired, status: PropTypes.oneOf([ - 'installed', - 'not_installed', + INSTALLED_STATUS, + UNINSTALLED_STATUS, 'unknown', ]).isRequired, + isProcessing: PropTypes.bool.isRequired, statusMsg: PropTypes.string, - onRequestComplete: PropTypes.func.isRequired, - getConfig: PropTypes.func.isRequired, - setConfig: PropTypes.func.isRequired, - clearIndexPatternsCache: PropTypes.func.isRequired, - defaultIndex: PropTypes.string.isRequired, previewUrl: PropTypes.string.isRequired, + onInstall: PropTypes.func.isRequired, + onUninstall: PropTypes.func.isRequired, }; diff --git a/src/core_plugins/kibana/public/home/components/sample_data_set_cards.js b/src/core_plugins/kibana/public/home/components/sample_data_set_cards.js new file mode 100644 index 0000000000000..4f50af9677876 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/sample_data_set_cards.js @@ -0,0 +1,211 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { + EuiFlexGrid, + EuiFlexItem, +} from '@elastic/eui'; + +import { + SampleDataSetCard, + INSTALLED_STATUS, + UNINSTALLED_STATUS, +} from './sample_data_set_card'; + +import { toastNotifications } from 'ui/notify'; + +import { + listSampleDataSets, + installSampleDataSet, + uninstallSampleDataSet +} from '../sample_data_sets'; + +export class SampleDataSetCards extends React.Component { + + constructor(props) { + super(props); + + this.state = { + sampleDataSets: [], + processingStatus: {}, + }; + } + + componentWillUnmount() { + this._isMounted = false; + } + + async componentDidMount() { + this._isMounted = true; + + this.loadSampleDataSets(); + } + + loadSampleDataSets = async () => { + let sampleDataSets; + try { + sampleDataSets = await listSampleDataSets(); + } catch (fetchError) { + toastNotifications.addDanger({ + title: `Unable to load sample data sets list`, + text: `${fetchError.message}`, + }); + sampleDataSets = []; + } + + if (!this._isMounted) { + return; + } + + this.setState({ + sampleDataSets: sampleDataSets + .sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }), + processingStatus: {}, + }); + } + + install = async (id) => { + const { + getConfig, + setConfig, + clearIndexPatternsCache, + } = this.props; + + const targetSampleDataSet = this.state.sampleDataSets.find((sampleDataSet) => { + return sampleDataSet.id === id; + }); + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: true } + })); + + try { + await installSampleDataSet(id, targetSampleDataSet.defaultIndex, getConfig, setConfig, clearIndexPatternsCache); + } catch (fetchError) { + if (this._isMounted) { + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false } + })); + } + toastNotifications.addDanger({ + title: `Unable to install sample data set: ${targetSampleDataSet.name}`, + text: `${fetchError.message}`, + }); + return; + } + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false }, + sampleDataSets: prevState.sampleDataSets.map(sampleDataSet => { + if (sampleDataSet.id === id) { + sampleDataSet.status = INSTALLED_STATUS; + } + return sampleDataSet; + }), + })); + toastNotifications.addSuccess({ + title: `${targetSampleDataSet.name} installed`, + ['data-test-subj']: 'sampleDataSetInstallToast' + }); + } + + uninstall = async (id) => { + const { + getConfig, + setConfig, + clearIndexPatternsCache, + } = this.props; + + const targetSampleDataSet = this.state.sampleDataSets.find((sampleDataSet) => { + return sampleDataSet.id === id; + }); + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: true } + })); + + try { + await uninstallSampleDataSet(id, targetSampleDataSet.defaultIndex, getConfig, setConfig, clearIndexPatternsCache); + } catch (fetchError) { + if (this._isMounted) { + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false } + })); + } + toastNotifications.addDanger({ + title: `Unable to uninstall sample data set: ${targetSampleDataSet.name}`, + text: `${fetchError.message}`, + }); + return; + } + + this.setState((prevState) => ({ + processingStatus: { ...prevState.processingStatus, [id]: false }, + sampleDataSets: prevState.sampleDataSets.map(sampleDataSet => { + if (sampleDataSet.id === id) { + sampleDataSet.status = UNINSTALLED_STATUS; + } + return sampleDataSet; + }), + })); + toastNotifications.addSuccess({ + title: `${targetSampleDataSet.name} uninstalled`, + ['data-test-subj']: 'sampleDataSetUninstallToast' + }); + } + + render() { + return ( + + { + this.state.sampleDataSets.map(sampleDataSet => { + return ( + + + + ); + }) + } + + ); + } +} + +SampleDataSetCards.propTypes = { + getConfig: PropTypes.func.isRequired, + setConfig: PropTypes.func.isRequired, + clearIndexPatternsCache: PropTypes.func.isRequired, + addBasePath: PropTypes.func.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/components/tutorial_directory.js b/src/core_plugins/kibana/public/home/components/tutorial_directory.js index 77ca6e379d710..d1835e5546c43 100644 --- a/src/core_plugins/kibana/public/home/components/tutorial_directory.js +++ b/src/core_plugins/kibana/public/home/components/tutorial_directory.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import { Synopsis } from './synopsis'; -import { SampleDataSetCard } from './sample_data_set_card'; +import { SampleDataSetCards } from './sample_data_set_cards'; import { EuiPage, @@ -36,7 +36,6 @@ import { import { getTutorials } from '../load_tutorials'; -import { listSampleDataSets } from '../sample_data_sets'; const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; @@ -70,7 +69,6 @@ export class TutorialDirectory extends React.Component { this.state = { selectedTabId: openTab, tutorialCards: [], - sampleDataSets: [], }; } @@ -81,8 +79,6 @@ export class TutorialDirectory extends React.Component { async componentDidMount() { this._isMounted = true; - this.loadSampleDataSets(); - const tutorialConfigs = await getTutorials(); if (!this._isMounted) { @@ -126,20 +122,6 @@ export class TutorialDirectory extends React.Component { }); } - loadSampleDataSets = async () => { - const sampleDataSets = await listSampleDataSets(); - - if (!this._isMounted) { - return; - } - - this.setState({ - sampleDataSets: sampleDataSets.sort((a, b) => { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - }), - }); - } - onSelectedTabChanged = id => { this.setState({ selectedTabId: id, @@ -158,57 +140,43 @@ export class TutorialDirectory extends React.Component { )); } - renderTab = () => { + renderTabContent = () => { if (this.state.selectedTabId === SAMPLE_DATA_TAB_ID) { - return this.renderSampleDataSetsTab(); - } - - return this.renderTutorialsTab(); - } - - renderTutorialsTab = () => { - return this.state.tutorialCards - .filter((tutorial) => { - return this.state.selectedTabId === ALL_TAB_ID || this.state.selectedTabId === tutorial.category; - }) - .map((tutorial) => { - return ( - - - - ); - }); - }; - - renderSampleDataSetsTab = () => { - return this.state.sampleDataSets.map(sampleDataSet => { return ( - - - + ); - }); + } + + return ( + + { + this.state.tutorialCards + .filter((tutorial) => { + return this.state.selectedTabId === ALL_TAB_ID || this.state.selectedTabId === tutorial.category; + }) + .map((tutorial) => { + return ( + + + + ); + }) + } + + ); } render() { @@ -230,9 +198,7 @@ export class TutorialDirectory extends React.Component { {this.renderTabs()} - - { this.renderTab() } - + {this.renderTabContent()} diff --git a/src/core_plugins/kibana/public/home/sample_data_sets.js b/src/core_plugins/kibana/public/home/sample_data_sets.js index 363986623bbb9..84823e1c01fca 100644 --- a/src/core_plugins/kibana/public/home/sample_data_sets.js +++ b/src/core_plugins/kibana/public/home/sample_data_sets.js @@ -17,57 +17,16 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; +import { kfetch } from 'ui/kfetch'; -const sampleDataUrl = chrome.addBasePath('/api/sample_data'); -const headers = new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - 'kbn-xsrf': 'kibana', -}); +const sampleDataUrl = '/api/sample_data'; export async function listSampleDataSets() { - try { - const response = await fetch(sampleDataUrl, { - method: 'get', - credentials: 'include', - headers: headers, - }); - - if (response.status >= 300) { - throw new Error(`Request failed with status code: ${response.status}`); - } - - return await response.json(); - } catch (err) { - toastNotifications.addDanger({ - title: `Unable to load sample data sets list`, - text: `${err.message}`, - }); - return []; - } + return await kfetch({ method: 'GET', pathname: sampleDataUrl }); } -export async function installSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { - try { - const response = await fetch(`${sampleDataUrl}/${id}`, { - method: 'post', - credentials: 'include', - headers: headers, - }); - - if (response.status >= 300) { - const body = await response.text(); - throw new Error(`Request failed with status code: ${response.status}, message: ${body}`); - } - } catch (err) { - toastNotifications.addDanger({ - title: `Unable to install sample data set: ${name}`, - text: `${err.message}`, - }); - return; - } +export async function installSampleDataSet(id, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { + await kfetch({ method: 'POST', pathname: `${sampleDataUrl}/${id}` }); const existingDefaultIndex = await getConfig('defaultIndex'); if (existingDefaultIndex === null) { @@ -75,31 +34,10 @@ export async function installSampleDataSet(id, name, defaultIndex, getConfig, se } clearIndexPatternsCache(); - - toastNotifications.addSuccess({ - title: `${name} installed`, - ['data-test-subj']: 'sampleDataSetInstallToast' - }); } -export async function uninstallSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { - try { - const response = await fetch(`${sampleDataUrl}/${id}`, { - method: 'delete', - credentials: 'include', - headers: headers, - }); - if (response.status >= 300) { - const body = await response.text(); - throw new Error(`Request failed with status code: ${response.status}, message: ${body}`); - } - } catch (err) { - toastNotifications.addDanger({ - title: `Unable to uninstall sample data set`, - text: `${err.message}`, - }); - return; - } +export async function uninstallSampleDataSet(id, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) { + await kfetch({ method: 'DELETE', pathname: `${sampleDataUrl}/${id}` }); const existingDefaultIndex = await getConfig('defaultIndex'); if (existingDefaultIndex && existingDefaultIndex === defaultIndex) { @@ -107,9 +45,4 @@ export async function uninstallSampleDataSet(id, name, defaultIndex, getConfig, } clearIndexPatternsCache(); - - toastNotifications.addSuccess({ - title: `${name} uninstalled`, - ['data-test-subj']: 'sampleDataSetUninstallToast' - }); } diff --git a/src/server/sample_data/routes/uninstall.js b/src/server/sample_data/routes/uninstall.js index d28b04dad61c2..98b03dad06010 100644 --- a/src/server/sample_data/routes/uninstall.js +++ b/src/server/sample_data/routes/uninstall.js @@ -63,7 +63,7 @@ export const createUninstallRoute = () => ({ } } - reply(); + reply({}); } } }); diff --git a/test/functional/apps/home/_sample_data.js b/test/functional/apps/home/_sample_data.js index 48cc3cf509936..ea8174e279ab6 100644 --- a/test/functional/apps/home/_sample_data.js +++ b/test/functional/apps/home/_sample_data.js @@ -41,11 +41,6 @@ export default function ({ getService, getPageObjects }) { it('should install sample data set', async ()=> { await PageObjects.home.addSampleDataSet('flights'); - await retry.try(async () => { - const successToastExists = await PageObjects.home.doesSampleDataSetSuccessfulInstallToastExist(); - expect(successToastExists).to.be(true); - }); - const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); expect(isInstalled).to.be(true); }); @@ -98,11 +93,6 @@ export default function ({ getService, getPageObjects }) { describe('uninstall', () => { it('should uninstall sample data set', async ()=> { await PageObjects.home.removeSampleDataSet('flights'); - await retry.try(async () => { - const successToastExists = await PageObjects.home.doesSampleDataSetSuccessfulUninstallToastExist(); - expect(successToastExists).to.be(true); - }); - const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); expect(isInstalled).to.be(false); }); diff --git a/test/functional/page_objects/home_page.js b/test/functional/page_objects/home_page.js index 77205e7bc3323..78dae43bf0684 100644 --- a/test/functional/page_objects/home_page.js +++ b/test/functional/page_objects/home_page.js @@ -53,10 +53,22 @@ export function HomePageProvider({ getService }) { async addSampleDataSet(id) { await testSubjects.click(`addSampleDataSet${id}`); + await this._waitForSampleDataLoadingAction(id); } async removeSampleDataSet(id) { await testSubjects.click(`removeSampleDataSet${id}`); + await this._waitForSampleDataLoadingAction(id); + } + + // loading action is either uninstall and install + async _waitForSampleDataLoadingAction(id) { + const sampleDataCard = await testSubjects.find(`sampleDataSetCard${id}`); + await retry.try(async () => { + // waitForDeletedByClassName needs to be inside retry because it will timeout at least once + // before action is complete + await sampleDataCard.waitForDeletedByClassName('euiLoadingSpinner'); + }); } async launchSampleDataSet(id) {