From ec300f3b426ac930723ee29b593336439f10b771 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 27 Apr 2018 08:07:05 -0400 Subject: [PATCH] [Spaces WIP] Additional space management UI (#18607) Functional space management UI --- x-pack/plugins/spaces/index.js | 2 + x-pack/plugins/spaces/mappings.json | 12 + .../components/confirm_delete_modal.js | 50 ++++ .../components/delete_spaces_button.js | 107 ++++++++ .../components/manage_space_page.js | 228 +++++++++++++----- .../management/components/page_header.js | 43 ++++ .../management/components/spaces_grid_page.js | 186 +++++++++----- .../views/management/lib/spaces_data_store.js | 35 +++ .../management/lib/spaces_data_store.test.js | 44 ++++ .../views/management/manage_spaces.less | 4 + .../public/views/management/page_routes.js | 25 +- .../public/views/space_selector/index.js | 31 +-- .../views/space_selector/space_selector.html | 2 +- .../views/space_selector/space_selector.js | 118 +++++---- .../spaces/server/routes/api/v1/spaces.js | 84 +++++-- 15 files changed, 766 insertions(+), 205 deletions(-) create mode 100644 x-pack/plugins/spaces/mappings.json create mode 100644 x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.js create mode 100644 x-pack/plugins/spaces/public/views/management/components/delete_spaces_button.js create mode 100644 x-pack/plugins/spaces/public/views/management/components/page_header.js create mode 100644 x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.js create mode 100644 x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.test.js diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index e445768119d0b..71bc7da340c56 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -9,6 +9,7 @@ import { validateConfig } from './server/lib/validate_config'; import { checkLicense } from './server/lib/check_license'; import { initSpacesApi } from './server/routes/api/v1/spaces'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; +import mappings from './mappings.json'; export const spaces = (kibana) => new kibana.Plugin({ id: 'spaces', @@ -32,6 +33,7 @@ export const spaces = (kibana) => new kibana.Plugin({ hidden: true, }], hacks: [], + mappings, home: ['plugins/spaces/register_feature'], injectDefaultVars: function () { return { }; diff --git a/x-pack/plugins/spaces/mappings.json b/x-pack/plugins/spaces/mappings.json new file mode 100644 index 0000000000000..835d2c299e0ed --- /dev/null +++ b/x-pack/plugins/spaces/mappings.json @@ -0,0 +1,12 @@ +{ + "space": { + "properties": { + "name": { + "type": "text" + }, + "description": { + "type": "text" + } + } + } +} diff --git a/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.js b/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.js new file mode 100644 index 0000000000000..587f32911a383 --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/components/confirm_delete_modal.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiOverlayMask, + EuiConfirmModal, +} from '@elastic/eui'; + +export const ConfirmDeleteModal = (props) => { + const { + spaces + } = props; + + const buttonText = spaces.length > 1 + ? `Delete ${spaces.length} spaces` + : `Delete space`; + + const bodyQuestion = spaces.length > 1 + ? `Are you sure you want to delete these ${spaces.length} spaces?` + : `Are you sure you want to delete this space?`; + + return ( + + +

{bodyQuestion}

+

This operation cannot be undone!

+
+
+ ); +}; + +ConfirmDeleteModal.propTypes = { + spaces: PropTypes.array.isRequired, + onCancel: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/spaces/public/views/management/components/delete_spaces_button.js b/x-pack/plugins/spaces/public/views/management/components/delete_spaces_button.js new file mode 100644 index 0000000000000..f1b403671fa95 --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/components/delete_spaces_button.js @@ -0,0 +1,107 @@ +/* + * 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 { ConfirmDeleteModal } from './confirm_delete_modal'; +import { + EuiButton +} from '@elastic/eui'; +import { toastNotifications } from 'ui/notify'; + +export class DeleteSpacesButton extends Component { + state = { + showConfirmModal: false + }; + + render() { + const numSpaces = this.props.spaces.length; + + const buttonText = numSpaces > 1 + ? `Delete ${numSpaces} spaces` + : `Delete space`; + + return ( + + + {buttonText} + + {this.getConfirmDeleteModal()} + + ); + } + + onDeleteClick = () => { + this.setState({ + showConfirmModal: true + }); + }; + + getConfirmDeleteModal = () => { + if (!this.state.showConfirmModal) { + return null; + } + + return ( + { + this.setState({ + showConfirmModal: false + }); + }} + onConfirm={this.deleteSpaces} + /> + ); + }; + + deleteSpaces = () => { + const { + httpAgent, + chrome, + spaces + } = this.props; + + console.log(this.props, spaces); + + const deleteOperations = spaces.map(space => { + return httpAgent.delete( + chrome.addBasePath(`/api/spaces/v1/spaces/${encodeURIComponent(space.id)}`) + ); + }); + + Promise.all(deleteOperations) + .then(() => { + this.setState({ + showConfirmModal: false + }); + + const message = spaces.length > 1 + ? `Deleted ${spaces.length} spaces.` + : `Deleted "${spaces[0].name}" space.`; + + toastNotifications.addSuccess(message); + + if (this.props.onDelete) { + this.props.onDelete(); + } + }) + .catch(error => { + const { + message = '' + } = error.data || {}; + + toastNotifications.addDanger(`Error deleting space: ${message}`); + }); + }; +} + +DeleteSpacesButton.propTypes = { + spaces: PropTypes.array.isRequired, + httpAgent: PropTypes.func.isRequired, + chrome: PropTypes.object.isRequired, + onDelete: PropTypes.func +}; diff --git a/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js b/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js index 00d3d5a58ccf8..7e04213f6d466 100644 --- a/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js +++ b/x-pack/plugins/spaces/public/views/management/components/manage_space_page.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import { EuiText, EuiSpacer, + EuiPage, EuiPageContent, EuiForm, EuiFormRow, @@ -18,13 +19,16 @@ import { EuiButton, } from '@elastic/eui'; +import { PageHeader } from './page_header'; +import { DeleteSpacesButton } from './delete_spaces_button'; + import { Notifier, toastNotifications } from 'ui/notify'; export class ManageSpacePage extends React.Component { state = { space: {}, - error: null - } + validate: false + }; componentDidMount() { this.notifier = new Notifier({ location: 'Spaces' }); @@ -46,9 +50,12 @@ export class ManageSpacePage extends React.Component { } }) .catch(error => { - this.setState({ - error - }); + const { + message = '' + } = error.data || {}; + + toastNotifications.addDanger(`Error loading space: ${message}`); + this.backToSpacesList(); }); } } @@ -60,57 +67,83 @@ export class ManageSpacePage extends React.Component { } = this.state.space; return ( - - -

{this.getTitle()}

- - - - - - - - - - - Save - - - - Cancel - - - - - {this.getActionButtons} -
-
+ + + + + + +

{this.getTitle()}

+
+ {this.getActionButton()} +
+ + + + + + + + + + + + + Save + + + + Cancel + + + +
+
+
); } getTitle = () => { - const isEditing = !!this.props.space; - if (isEditing) { + if (this.editingExistingSpace()) { return `Edit space`; } return `Create a space`; - } + }; + + getActionButton = () => { + if (this.editingExistingSpace()) { + return ( + + + + ); + } + + return null; + }; onNameChange = (e) => { this.setState({ @@ -119,7 +152,7 @@ export class ManageSpacePage extends React.Component { name: e.target.value } }); - } + }; onDescriptionChange = (e) => { this.setState({ @@ -128,38 +161,115 @@ export class ManageSpacePage extends React.Component { description: e.target.value } }); - } + }; saveSpace = () => { + this.setState({ + validate: true + }, () => { + const { isInvalid } = this.validateForm(); + if (isInvalid) return; + this._performSave(); + }); + }; + + _performSave = () => { const { name = '', - id = name.toLowerCase(), + id = name.toLowerCase().replace(/\s/g, '-'), description } = this.state.space; const { httpAgent, chrome } = this.props; + const params = { + name, + id, + description + }; + + const overwrite = this.editingExistingSpace(); + if (name && description) { - console.log(this.state.space); httpAgent - .post(chrome.addBasePath(`/api/spaces/v1/spaces/${encodeURIComponent(id)}`), { id, name, description }) + .post(chrome.addBasePath(`/api/spaces/v1/spaces/${encodeURIComponent(id)}?overwrite=${overwrite}`), params) .then(result => { toastNotifications.addSuccess(`Saved '${result.data.id}'`); window.location.hash = `#/management/spaces/list`; }) .catch(error => { - toastNotifications.addError(error); + const { + message = '' + } = error.data || {}; + + toastNotifications.addDanger(`Error saving space: ${message}`); }); } - } + }; backToSpacesList = () => { window.location.hash = `#/management/spaces/list`; - } + }; + + validateName = () => { + if (!this.state.validate) { + return {}; + } + + const { + name + } = this.state.space; + + if (!name) { + return { + isInvalid: true, + error: 'Name is required' + }; + } + + return {}; + }; + + validateDescription = () => { + if (!this.state.validate) { + return {}; + } + + const { + description + } = this.state.space; + + if (!description) { + return { + isInvalid: true, + error: 'Description is required' + }; + } + + return {}; + }; + + validateForm = () => { + if (!this.state.validate) { + return {}; + } + + const validations = [this.validateName(), this.validateDescription()]; + if (validations.some(validation => validation.isInvalid)) { + return { + isInvalid: true + }; + } + + return {}; + }; + + editingExistingSpace = () => !!this.props.space; } ManageSpacePage.propTypes = { space: PropTypes.string, httpAgent: PropTypes.func.isRequired, - chrome: PropTypes.object -}; \ No newline at end of file + chrome: PropTypes.object, + breadcrumbs: PropTypes.array.isRequired, +}; diff --git a/x-pack/plugins/spaces/public/views/management/components/page_header.js b/x-pack/plugins/spaces/public/views/management/components/page_header.js new file mode 100644 index 0000000000000..956c2809b325f --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/components/page_header.js @@ -0,0 +1,43 @@ +/* + * 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 } from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiHeader, + EuiHeaderSection, + EuiHeaderBreadcrumb, + EuiHeaderBreadcrumbs +} from '@elastic/eui'; + +export class PageHeader extends Component { + render() { + return ( + + + + {this.props.breadcrumbs.map(this.buildBreadcrumb)} + + + + ); + } + + buildBreadcrumb = (breadcrumb) => { + return ( + + {breadcrumb.display} + + ); + } +} + + + +PageHeader.propTypes = { + breadcrumbs: PropTypes.array.isRequired +}; diff --git a/x-pack/plugins/spaces/public/views/management/components/spaces_grid_page.js b/x-pack/plugins/spaces/public/views/management/components/spaces_grid_page.js index 108f28c6e207d..4bb49bcbe23e7 100644 --- a/x-pack/plugins/spaces/public/views/management/components/spaces_grid_page.js +++ b/x-pack/plugins/spaces/public/views/management/components/spaces_grid_page.js @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { + EuiPage, EuiPageContent, EuiBasicTable, EuiSearchBar, @@ -19,80 +20,82 @@ import { EuiLink, } from '@elastic/eui'; +import { PageHeader } from './page_header'; +import { SpacesDataStore } from '../lib/spaces_data_store'; +import { DeleteSpacesButton } from './delete_spaces_button'; -const pagination = { - pageIndex: 0, - pageSize: 10, - totalItemCount: 10000, - pageSizeOptions: [10, 25, 50] -}; - -export class SpacesGridPage extends React.Component { +export class SpacesGridPage extends Component { state = { selectedSpaces: [], - spaces: [] + displayedSpaces: [], + loading: true, + searchCriteria: '', + pagination: { + pageIndex: 0, + pageSize: 10, + totalItemCount: 0, + pageSizeOptions: [10, 25, 50] + } + }; + + constructor(props) { + super(props); + this.dataStore = new SpacesDataStore(); } componentDidMount() { - const { - httpAgent, - chrome - } = this.props; - - httpAgent - .get(chrome.addBasePath(`/api/spaces/v1/spaces`)) - .then(response => { - this.setState({ - spaces: response.data - }); - }) - .catch(error => { - this.setState({ - error - }); - }); + this.loadGrid(); } render() { - const { - spaces - } = this.state; + const filteredSpaces = this.dataStore.search(this.state.searchCriteria); + + const pagination = { + ...this.state.pagination, + totalItemCount: filteredSpaces.length + }; return ( - - - -

Spaces

-
- {this.getPrimaryActionButton()} -
- - {}} - /> - - {}} - /> -
+ + + + + +

Spaces

+
+ {this.getPrimaryActionButton()} +
+ + + + +
+
); } getPrimaryActionButton() { if (this.state.selectedSpaces.length > 0) { - const count = this.state.selectedSpaces.length; return ( - - {`Delete ${count > 1 ? `${count} spaces` : 'space'}`} - + ); } @@ -101,8 +104,43 @@ export class SpacesGridPage extends React.Component { ); } + loadGrid = () => { + const { + httpAgent, + chrome + } = this.props; + + this.setState({ + loading: true, + displayedSpaces: [], + selectedSpaces: [] + }); + + this.dataStore.loadSpaces([]); + + const setSpaces = (spaces) => { + this.dataStore.loadSpaces(spaces); + this.setState({ + loading: false, + displayedSpaces: this.dataStore.getPage(this.state.pagination.pageIndex, this.state.pagination.pageSize) + }); + }; + + httpAgent + .get(chrome.addBasePath(`/api/spaces/v1/spaces`)) + .then(response => { + setSpaces(response.data); + }) + .catch(error => { + this.setState({ + loading: false, + error + }); + }); + }; + getColumnConfig() { - const columns = [{ + return [{ field: 'name', name: 'Space', sortable: true, @@ -118,8 +156,6 @@ export class SpacesGridPage extends React.Component { name: 'Description', sortable: true }]; - - return columns; } getSelectionConfig() { @@ -130,12 +166,36 @@ export class SpacesGridPage extends React.Component { }; } + onTableChange = ({ page = {} }) => { + const { + index: pageIndex, + size: pageSize + } = page; + + this.setState({ + pagination: { + ...this.state.pagination, + pageIndex, + pageSize + } + }); + }; + onSelectionChange = (selectedSpaces) => { this.setState({ selectedSpaces }); - } + }; + + onSearchChange = ({ text = '' }) => { + this.dataStore.search(text); + this.setState({ + searchCriteria: text, + displayedSpaces: this.dataStore.getPage(this.state.pagination.pageIndex, this.state.pagination.pageSize) + }); + }; } SpacesGridPage.propTypes = { chrome: PropTypes.object.isRequired, - httpAgent: PropTypes.func.isRequired -}; \ No newline at end of file + httpAgent: PropTypes.func.isRequired, + breadcrumbs: PropTypes.array.isRequired +}; diff --git a/x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.js b/x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.js new file mode 100644 index 0000000000000..64e7f6c5fa635 --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.js @@ -0,0 +1,35 @@ +/* + * 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 class SpacesDataStore { + constructor(spaces = []) { + this.loadSpaces(spaces); + } + + loadSpaces(spaces = []) { + this._spaces = [...spaces]; + this._matchedSpaces = [...spaces]; + } + + search(searchCriteria, caseSensitive = false) { + const criteria = caseSensitive ? searchCriteria : searchCriteria.toLowerCase(); + + this._matchedSpaces = this._spaces.filter(space => { + const spaceName = caseSensitive ? space.name : space.name.toLowerCase(); + + return spaceName.indexOf(criteria) >= 0; + }); + + return this._matchedSpaces; + } + + getPage(pageIndex, pageSize) { + const startIndex = Math.min(pageIndex * pageSize, this._matchedSpaces.length); + const endIndex = Math.min(startIndex + pageSize, this._matchedSpaces.length); + + return this._matchedSpaces.slice(startIndex, endIndex); + } +} diff --git a/x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.test.js b/x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.test.js new file mode 100644 index 0000000000000..eac71d490902d --- /dev/null +++ b/x-pack/plugins/spaces/public/views/management/lib/spaces_data_store.test.js @@ -0,0 +1,44 @@ +/* + * 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 { SpacesDataStore } from './spaces_data_store'; + +const spaces = [{ + id: 1, + name: 'foo', + description: 'foo' +}, { + id: 2, + name: 'bar', + description: 'bar' +}, { + id: 3, + name: 'sample text', + description: 'some sample text' +}]; + +test(`it doesn't filter results with no search applied`, () => { + const store = new SpacesDataStore(spaces); + expect(store.getPage(0, 3)).toEqual(spaces); +}); + +test(`it filters results when search is applied`, () => { + const store = new SpacesDataStore(spaces); + const filteredResults = store.search('bar'); + + expect(filteredResults).toEqual([spaces[1]]); + + expect(store.getPage(0, 3)).toEqual([spaces[1]]); +}); + +test(`it filters based on a partial match`, () => { + const store = new SpacesDataStore(spaces); + const filteredResults = store.search('mpl'); + + expect(filteredResults).toEqual([spaces[2]]); + + expect(store.getPage(0, 3)).toEqual([spaces[2]]); +}); diff --git a/x-pack/plugins/spaces/public/views/management/manage_spaces.less b/x-pack/plugins/spaces/public/views/management/manage_spaces.less index 67fc3da7f8171..a9696d0c2f824 100644 --- a/x-pack/plugins/spaces/public/views/management/manage_spaces.less +++ b/x-pack/plugins/spaces/public/views/management/manage_spaces.less @@ -1,3 +1,7 @@ .application, .euiPanel { background: #f5f5f5 } + +.euiPage { + padding: 0; +} diff --git a/x-pack/plugins/spaces/public/views/management/page_routes.js b/x-pack/plugins/spaces/public/views/management/page_routes.js index 4e8f5e53c19f4..c7fd24674f00d 100644 --- a/x-pack/plugins/spaces/public/views/management/page_routes.js +++ b/x-pack/plugins/spaces/public/views/management/page_routes.js @@ -21,7 +21,11 @@ routes.when('/management/spaces/list', { controller: function ($scope, $http, chrome) { const domNode = document.getElementById(reactRootNodeId); - render(, domNode); + render(, domNode); // unmount react on controller destroy $scope.$on('$destroy', () => { @@ -35,7 +39,11 @@ routes.when('/management/spaces/create', { controller: function ($scope, $http, chrome) { const domNode = document.getElementById(reactRootNodeId); - render(, domNode); + render(, domNode); // unmount react on controller destroy $scope.$on('$destroy', () => { @@ -44,6 +52,10 @@ routes.when('/management/spaces/create', { } }); +routes.when('/management/spaces/edit', { + redirectTo: '/management/spaces/list' +}); + routes.when('/management/spaces/edit/:space', { template, controller: function ($scope, $http, $route, chrome) { @@ -51,11 +63,16 @@ routes.when('/management/spaces/edit/:space', { const { space } = $route.current.params; - render(, domNode); + render(, domNode); // unmount react on controller destroy $scope.$on('$destroy', () => { unmountComponentAtNode(domNode); }); } -}); \ No newline at end of file +}); diff --git a/x-pack/plugins/spaces/public/views/space_selector/index.js b/x-pack/plugins/spaces/public/views/space_selector/index.js index a18a1ccdaed7f..ee258cc542bd1 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/index.js +++ b/x-pack/plugins/spaces/public/views/space_selector/index.js @@ -8,30 +8,23 @@ import 'ui/autoload/styles'; import chrome from 'ui/chrome'; import 'plugins/spaces/views/space_selector/space_selector.less'; import template from 'plugins/spaces/views/space_selector/space_selector.html'; +import { uiModules } from 'ui/modules'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; import { SpaceSelector } from './space_selector'; -import { mockSpaces } from '../../../common/mock_spaces'; +const module = uiModules.get('spaces_selector', []); +module.controller('spacesSelectorController', ($scope, $http) => { + const domNode = document.getElementById('spaceSelectorRoot'); + render(, domNode); + + // unmount react on controller destroy + $scope.$on('$destroy', () => { + unmountComponentAtNode(domNode); + }); +}); chrome .setVisible(false) .setRootTemplate(template); - -// hack to wait for angular template to be ready -const waitForAngularReady = new Promise(resolve => { - const checkInterval = setInterval(() => { - const hasElm = !!document.querySelector('#spaceSelectorRoot'); - if (hasElm) { - clearInterval(checkInterval); - resolve(); - } - }, 10); -}); - -waitForAngularReady.then(() => { - ReactDOM.render(, document.getElementById('spaceSelectorRoot')); -}); - - diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.html b/x-pack/plugins/spaces/public/views/space_selector/space_selector.html index 1c18634fd4419..73bf9f23236f7 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.html +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.html @@ -1,3 +1,3 @@ -
+
diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js index 48c42bf89b385..4a4a042c587f0 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import { EuiPage, EuiPageHeader, @@ -18,50 +18,82 @@ import { EuiFlexItem, } from '@elastic/eui'; -export const SpaceSelector = (props) => ( - - - - - - - - - -

Welcome to Kibana.

-

Select a space to begin.

-
+export class SpaceSelector extends Component { + state = { + loading: false, + spaces: [] + }; - - {props.spaces.map(renderSpace)} - + componentDidMount() { + this.loadSpaces(); + } - -

You can change your workspace at anytime by accessing your profile within Kibana.

-
-
-
-
-); + loadSpaces() { + this.setState({ loading: true }); + const { httpAgent, chrome } = this.props; -function renderSpace(space) { - return ( - - } - title={renderSpaceTitle(space)} - description={space.description} - onClick={() => window.alert('Card clicked')} - /> - - ); -} + httpAgent + .get(chrome.addBasePath(`/api/spaces/v1/spaces`)) + .then(response => { + this.setState({ + loading: false, + spaces: response.data + }); + }); + } + + render() { + const { + spaces + } = this.state; + + return ( + + + + + + + + + +

Welcome to Kibana.

+

Select a space to begin.

+
+ + + {spaces.map(this.renderSpace)} + + + +

You can change your workspace at anytime by accessing your profile within Kibana.

+
+
+
+
+ ); + } + + renderSpace = (space) => { + return ( + + } + title={this.renderSpaceTitle(space)} + description={space.description} + onClick={() => window.alert('Card clicked')} + /> + + ); + }; + + renderSpaceTitle = (space) => { + return ( +
+ {space.name} + {/* */} +
+ ); + }; -function renderSpaceTitle(space) { - return ( -
- {space.name} - {/* */} -
- ); } diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js index 223df491b1566..003e4e7f55cb2 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js @@ -4,22 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; +import { omit } from 'lodash'; import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { spaceSchema } from '../../../lib/space_schema'; - -import { mockSpaces } from '../../../../common/mock_spaces'; +import { wrapError } from '../../../lib/errors'; export function initSpacesApi(server) { - const callWithRequest = getClient(server).callWithRequest; // eslint-disable-line no-unused-vars const routePreCheckLicenseFn = routePreCheckLicense(server); server.route({ method: 'GET', path: '/api/spaces/v1/spaces', - handler(request, reply) { - reply(mockSpaces); + async handler(request, reply) { + const client = request.getSavedObjectsClient(); + + let spaces; + + try { + const result = await client.find({ + type: 'space' + }); + + spaces = result.saved_objects.map(space => ({ + ...space.attributes, + id: space.id + })); + } catch(e) { + return reply(wrapError(e)); + } + + return reply(spaces); }, config: { pre: [routePreCheckLicenseFn] @@ -29,10 +43,22 @@ export function initSpacesApi(server) { server.route({ method: 'GET', path: '/api/spaces/v1/spaces/{id}', - handler(request, reply) { + async handler(request, reply) { const id = request.params.id; - const space = mockSpaces.find(space => space.id === id); - reply(space || Boom.notFound()); + + const client = request.getSavedObjectsClient(); + + let space; + try { + space = await client.get('space', id); + } catch (e) { + return reply(wrapError(e)); + } + + return reply({ + ...space.attributes, + id: space.id + }); }, config: { pre: [routePreCheckLicenseFn] @@ -42,9 +68,24 @@ export function initSpacesApi(server) { server.route({ method: 'POST', path: '/api/spaces/v1/spaces/{id}', - handler(request, reply) { - mockSpaces.push(request.payload); - reply(request.payload); + async handler(request, reply) { + const client = request.getSavedObjectsClient(); + + const { + overwrite = false + } = request.query; + + const space = omit(request.payload, ['id']); + const id = request.params.id; + + let result; + try { + result = await client.create('space', { ...space }, { id, overwrite }); + } catch(e) { + return reply(wrapError(e)); + } + + return reply(result); }, config: { validate: { @@ -57,9 +98,20 @@ export function initSpacesApi(server) { server.route({ method: 'DELETE', path: '/api/spaces/v1/spaces/{id}', - handler(request, reply) { - mockSpaces = mockSpaces.filter(space => space !== request.params.id); - reply().code(204); + async handler(request, reply) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + let result; + + try { + result = await client.delete('space', id); + } catch(e) { + return reply(wrapError(e)); + } + + return reply(result).code(204); }, config: { pre: [routePreCheckLicenseFn]