diff --git a/x-pack/plugins/spaces/mappings.json b/x-pack/plugins/spaces/mappings.json index 77e0c47c00ddd..91c6e324e197e 100644 --- a/x-pack/plugins/spaces/mappings.json +++ b/x-pack/plugins/spaces/mappings.json @@ -13,9 +13,6 @@ } } }, - "urlContext": { - "type": "keyword" - }, "description": { "type": "text" }, diff --git a/x-pack/plugins/spaces/public/views/components/space_card.js b/x-pack/plugins/spaces/public/views/components/space_card.js index 7441366d9206e..f83f009bdb728 100644 --- a/x-pack/plugins/spaces/public/views/components/space_card.js +++ b/x-pack/plugins/spaces/public/views/components/space_card.js @@ -36,11 +36,11 @@ function renderSpaceAvatar(space) { } function renderSpaceDescription(space) { - let description = space.description; - const needsTruncation = space.description.length > 120; + let description = space.description || ''; + const needsTruncation = description.length > 120; if (needsTruncation) { description = ( - {space.description.substr(0, 120) + '…'} + {description.substr(0, 120) + '…'} ); } return {description}; diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/url_context.test.js.snap b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap similarity index 63% rename from x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/url_context.test.js.snap rename to x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap index 363f3951202c6..db250113e044d 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/url_context.test.js.snap +++ b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap @@ -14,7 +14,7 @@ exports[`renders without crashing 1`] = ` isInvalid={false} label={

- URL Context + Space Identifier } > -

- -
+ `; diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js index c5e2624466412..8e4da3e399541 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js +++ b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js @@ -21,14 +21,15 @@ import { EuiButton, EuiPageContentBody, EuiHorizontalRule, + EuiLoadingSpinner, } from '@elastic/eui'; import { DeleteSpacesButton } from '../components'; import { SpaceAvatar } from '../../components'; import { Notifier, toastNotifications } from 'ui/notify'; -import { UrlContext } from './url_context'; -import { toUrlContext, isValidUrlContext } from '../lib/url_context_utils'; +import { SpaceIdentifier } from './space_identifier'; +import { toSpaceIdentifier } from '../lib'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; import { isReservedSpace } from '../../../../common'; import { ReservedSpaceBadge } from './reserved_space_badge'; @@ -37,6 +38,7 @@ import { SpaceValidator } from '../lib/validate_space'; export class ManageSpacePage extends Component { state = { space: {}, + isLoading: true, }; constructor(props) { @@ -57,7 +59,8 @@ export class ManageSpacePage extends Component { .then(result => { if (result.data) { this.setState({ - space: result.data + space: result.data, + isLoading: false }); } }) @@ -69,91 +72,21 @@ export class ManageSpacePage extends Component { toastNotifications.addDanger(`Error loading space: ${message}`); this.backToSpacesList(); }); + } else { + this.setState({ isLoading: false }); } } render() { - const { - name = '', - description = '', - } = this.state.space; + + const content = this.state.isLoading ? this.getLoadingIndicator() : this.getForm(); return ( - - {this.getFormHeading()} - - - - - - - - - - { - name && ( - - - - - - - - - - - ) - } - - - - - {isReservedSpace(this.state.space) - ? null - : ( - - - - ) - } - - - - - - - - - - {this.getFormButtons()} - - + {content} @@ -161,6 +94,90 @@ export class ManageSpacePage extends Component { ); } + getLoadingIndicator = () => { + return

Loading...

; + } + + getForm = () => { + const { + name = '', + description = '', + } = this.state.space; + + return ( + + {this.getFormHeading()} + + + + + + + + + + { + name && ( + + + + + + + + + + + ) + } + + + + + {isReservedSpace(this.state.space) + ? null + : ( + + + + ) + } + + + + + + + + + + {this.getFormButtons()} + + + ); + } + getFormHeading = () => { return (

{this.getTitle()}

@@ -211,21 +228,21 @@ export class ManageSpacePage extends Component { }; onNameChange = (e) => { - const canUpdateContext = !this.editingExistingSpace(); + const canUpdateId = !this.editingExistingSpace(); let { - urlContext + id } = this.state.space; - if (canUpdateContext) { - urlContext = toUrlContext(e.target.value); + if (canUpdateId) { + id = toSpaceIdentifier(e.target.value); } this.setState({ space: { ...this.state.space, name: e.target.value, - urlContext + id } }); }; @@ -239,11 +256,11 @@ export class ManageSpacePage extends Component { }); }; - onUrlContextChange = (e) => { + onSpaceIdentifierChange = (e) => { this.setState({ space: { ...this.state.space, - urlContext: toUrlContext(e.target.value) + id: toSpaceIdentifier(e.target.value) } }); }; @@ -272,11 +289,10 @@ export class ManageSpacePage extends Component { _performSave = () => { const { name = '', - id = toUrlContext(name), + id = toSpaceIdentifier(name), description, initials, color, - urlContext } = this.state.space; const params = { @@ -285,117 +301,34 @@ export class ManageSpacePage extends Component { description, initials, color, - urlContext }; - if (name && description) { - let action; - if (this.editingExistingSpace()) { - action = this.props.spacesManager.updateSpace(params); - } else { - action = this.props.spacesManager.createSpace(params); - } - - action - .then(result => { - this.props.spacesNavState.refreshSpacesList(); - toastNotifications.addSuccess(`Saved '${result.data.name}'`); - window.location.hash = `#/management/spaces/list`; - }) - .catch(error => { - const { - message = '' - } = error.data || {}; - - toastNotifications.addDanger(`Error saving space: ${message}`); - }); + let action; + if (this.editingExistingSpace()) { + action = this.props.spacesManager.updateSpace(params); + } else { + action = this.props.spacesManager.createSpace(params); } + + action + .then(result => { + this.props.spacesNavState.refreshSpacesList(); + toastNotifications.addSuccess(`Saved '${result.data.name}'`); + window.location.hash = `#/management/spaces/list`; + }) + .catch(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 {}; - }; - - validateUrlContext = () => { - if (!this.state.validate) { - return {}; - } - - const { - urlContext - } = this.state.space; - - if (!urlContext) { - return { - isInvalid: true, - error: 'URL Context is required' - }; - } - - if (!isValidUrlContext(urlContext)) { - return { - isInvalid: true, - error: 'URL Context only allows a-z, 0-9, and the "-" character' - }; - } - - return {}; - - }; - - validateForm = () => { - if (!this.state.validate) { - return {}; - } - - const validations = [this.validateName(), this.validateDescription(), this.validateUrlContext()]; - if (validations.some(validation => validation.isInvalid)) { - return { - isInvalid: true - }; - } - - return {}; - }; - editingExistingSpace = () => !!this.props.space; } diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/url_context.js b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js similarity index 54% rename from x-pack/plugins/spaces/public/views/management/edit_space/url_context.js rename to x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js index 853c0abde745e..9fcd7460114b0 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/url_context.js +++ b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js @@ -10,11 +10,9 @@ import { EuiFormRow, EuiLink, EuiFieldText, - EuiCallOut, - EuiSpacer } from '@elastic/eui'; -export class UrlContext extends Component { +export class SpaceIdentifier extends Component { textFieldRef = null; state = { @@ -23,7 +21,7 @@ export class UrlContext extends Component { render() { const { - urlContext = '' + id = '' } = this.props.space; return ( @@ -31,18 +29,15 @@ export class UrlContext extends Component { -
- this.textFieldRef = ref} - /> - {this.getCallOut()} -
+ this.textFieldRef = ref} + />
); @@ -50,34 +45,17 @@ export class UrlContext extends Component { getLabel = () => { if (!this.props.editable) { - return (

URL Context

); + return (

Space Identifier

); } const editLinkText = this.state.editing ? `[stop editing]` : `[edit]`; - return (

URL Context {editLinkText}

); + return (

Space Identifier {editLinkText}

); }; getHelpText = () => { return (

Links within Kibana will include this space identifier

); }; - getCallOut = () => { - if (this.props.editingExistingSpace && this.state.editing) { - return ( - - - - - ); - } - - return null; - }; - onEditClick = () => { this.setState({ editing: !this.state.editing @@ -94,10 +72,9 @@ export class UrlContext extends Component { }; } -UrlContext.propTypes = { +SpaceIdentifier.propTypes = { space: PropTypes.object.isRequired, editable: PropTypes.bool.isRequired, - editingExistingSpace: PropTypes.bool.isRequired, onChange: PropTypes.func, validator: PropTypes.object.isRequired, }; diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/url_context.test.js b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.js similarity index 80% rename from x-pack/plugins/spaces/public/views/management/edit_space/url_context.test.js rename to x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.js index 13c1caca89987..7970b2633ca85 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/url_context.test.js +++ b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.test.js @@ -6,17 +6,16 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { UrlContext } from './url_context'; +import { SpaceIdentifier } from './space_identifier'; import { SpaceValidator } from '../lib'; test('renders without crashing', () => { const props = { space: {}, editable: true, - editingExistingSpace: false, onChange: jest.fn(), validator: new SpaceValidator() }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); -}); \ No newline at end of file +}); diff --git a/x-pack/plugins/spaces/public/views/management/lib/index.js b/x-pack/plugins/spaces/public/views/management/lib/index.js index 0d213ec10b93c..ab5331a58a742 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/index.js +++ b/x-pack/plugins/spaces/public/views/management/lib/index.js @@ -5,10 +5,10 @@ */ export { - toUrlContext, - isValidUrlContext -} from './url_context_utils'; + toSpaceIdentifier, + isValidSpaceIdentifier, +} from './space_identifier_utils'; export { SpaceValidator -} from './validate_space'; \ No newline at end of file +} from './validate_space'; diff --git a/x-pack/plugins/spaces/public/views/management/lib/url_context_utils.js b/x-pack/plugins/spaces/public/views/management/lib/space_identifier_utils.js similarity index 54% rename from x-pack/plugins/spaces/public/views/management/lib/url_context_utils.js rename to x-pack/plugins/spaces/public/views/management/lib/space_identifier_utils.js index bf9fe48d4037a..d7defc266b715 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/url_context_utils.js +++ b/x-pack/plugins/spaces/public/views/management/lib/space_identifier_utils.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export function toUrlContext(value = '') { - return value.toLowerCase().replace(/[^a-z0-9]/g, '-'); +export function toSpaceIdentifier(value = '') { + return value.toLowerCase().replace(/[^a-z0-9_]/g, '-'); } -export function isValidUrlContext(value = '') { - return value === toUrlContext(value); +export function isValidSpaceIdentifier(value = '') { + return value === toSpaceIdentifier(value); } diff --git a/x-pack/plugins/spaces/public/views/management/lib/url_context_utils.test.js b/x-pack/plugins/spaces/public/views/management/lib/space_identifier_utils.test.js similarity index 56% rename from x-pack/plugins/spaces/public/views/management/lib/url_context_utils.test.js rename to x-pack/plugins/spaces/public/views/management/lib/space_identifier_utils.test.js index 522436163271f..1a3cd091bb861 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/url_context_utils.test.js +++ b/x-pack/plugins/spaces/public/views/management/lib/space_identifier_utils.test.js @@ -3,22 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { toUrlContext } from './url_context_utils'; +import { toSpaceIdentifier } from './space_identifier_utils'; test('it converts whitespace to dashes', () => { const input = `this is a test`; - expect(toUrlContext(input)).toEqual('this-is-a-test'); + expect(toSpaceIdentifier(input)).toEqual('this-is-a-test'); }); test('it converts everything to lowercase', () => { const input = `THIS IS A TEST`; - expect(toUrlContext(input)).toEqual('this-is-a-test'); + expect(toSpaceIdentifier(input)).toEqual('this-is-a-test'); }); -test('it converts non-alphanumeric characters to dashes', () => { - const input = `~!@#$%^&*()_+-=[]{}\|';:"/.,<>?` + "`"; +test('it converts non-alphanumeric characters except for "_" to dashes', () => { + const input = `~!@#$%^&*()+-=[]{}\|';:"/.,<>?` + "`"; const expectedResult = new Array(input.length + 1).join('-'); - expect(toUrlContext(input)).toEqual(expectedResult); + expect(toSpaceIdentifier(input)).toEqual(expectedResult); }); diff --git a/x-pack/plugins/spaces/public/views/management/lib/validate_space.js b/x-pack/plugins/spaces/public/views/management/lib/validate_space.js index a59a5ee0ef6f8..56d22ca39aad5 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/validate_space.js +++ b/x-pack/plugins/spaces/public/views/management/lib/validate_space.js @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { isValidUrlContext } from './url_context_utils'; +import { isValidSpaceIdentifier } from './space_identifier_utils'; import { isReservedSpace } from '../../../../common/is_reserved_space'; export class SpaceValidator { @@ -36,24 +36,24 @@ export class SpaceValidator { validateSpaceDescription(space) { if (!this._shouldValidate) return valid(); - if (!space.description) { - return invalid(`Please provide a space description`); + if (space.description && space.description.length > 2000) { + return invalid(`Description must not exceed 2000 characters`); } return valid(); } - validateUrlContext(space) { + validateSpaceIdentifier(space) { if (!this._shouldValidate) return valid(); if (isReservedSpace(space)) return valid(); - if (!space.urlContext) { - return invalid(`URL Context is required`); + if (!space.id) { + return invalid(`Space Identifier is required`); } - if (!isValidUrlContext(space.urlContext)) { - return invalid('URL Context only allows a-z, 0-9, and the "-" character'); + if (!isValidSpaceIdentifier(space.id)) { + return invalid('Space Identifier only allows a-z, 0-9, "_", and the "-" character'); } return valid(); @@ -62,9 +62,9 @@ export class SpaceValidator { validateForSave(space) { const { isInvalid: isNameInvalid } = this.validateSpaceName(space); const { isInvalid: isDescriptionInvalid } = this.validateSpaceDescription(space); - const { isInvalid: isContextInvalid } = this.validateUrlContext(space); + const { isInvalid: isIdentifierInvalid } = this.validateSpaceIdentifier(space); - if (isNameInvalid || isDescriptionInvalid || isContextInvalid) { + if (isNameInvalid || isDescriptionInvalid || isIdentifierInvalid) { return invalid(); } diff --git a/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js b/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js index 6c0f2c2bcc10e..5adbb2ec5090e 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js +++ b/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js @@ -39,46 +39,53 @@ describe('validateSpaceName', () => { }); describe('validateSpaceDescription', () => { - test('it requires a non-empty value', () => { + test('is optional', () => { const space = { - description: '' }; - expect(validator.validateSpaceDescription(space)).toEqual({ isInvalid: true, error: `Please provide a space description` }); + expect(validator.validateSpaceDescription(space)).toEqual({ isInvalid: false }); + }); + + test('it cannot exceed 2000 characters', () => { + const space = { + description: new Array(2002).join('A') + }; + + expect(validator.validateSpaceDescription(space)).toEqual({ isInvalid: true, error: `Description must not exceed 2000 characters` }); }); }); -describe('validateUrlContext', () => { +describe('validateSpaceIdentifier', () => { test('it does not validate reserved spaces', () => { const space = { _reserved: true }; - expect(validator.validateUrlContext(space)).toEqual({ isInvalid: false }); + expect(validator.validateSpaceIdentifier(space)).toEqual({ isInvalid: false }); }); test('it requires a non-empty value', () => { const space = { - urlContext: '' + id: '' }; - expect(validator.validateUrlContext(space)).toEqual({ isInvalid: true, error: `URL Context is required` }); + expect(validator.validateSpaceIdentifier(space)).toEqual({ isInvalid: true, error: `Space Identifier is required` }); }); - test('it requires a valid URL Context', () => { + test('it requires a valid Space Identifier', () => { const space = { - urlContext: 'invalid context' + id: 'invalid identifier' }; - expect(validator.validateUrlContext(space)) - .toEqual({ isInvalid: true, error: 'URL Context only allows a-z, 0-9, and the "-" character' }); + expect(validator.validateSpaceIdentifier(space)) + .toEqual({ isInvalid: true, error: 'Space Identifier only allows a-z, 0-9, "_", and the "-" character' }); }); - test('it allows a valid URL Context', () => { + test('it allows a valid Space Identifier', () => { const space = { - urlContext: '01-valid-context-01' + id: '01-valid-context-01' }; - expect(validator.validateUrlContext(space)).toEqual({ isInvalid: false }); + expect(validator.validateSpaceIdentifier(space)).toEqual({ isInvalid: false }); }); -}); \ No newline at end of file +}); diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js index f18db68dc5a74..8a5c3ecd9ae81 100644 --- a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js +++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js @@ -123,12 +123,12 @@ export class SpacesGridPage extends Component { ); } }, { - field: 'description', - name: 'Description', + field: 'id', + name: 'Identifier', sortable: true }, { - field: 'urlContext', - name: 'URL Context', + field: 'description', + name: 'Description', sortable: true }]; } diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.js b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.js index 2277ec4757a7e..eccdddb16c1e7 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.js +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.js @@ -13,7 +13,7 @@ import { SpacesManager } from '../../lib/spaces_manager'; function getHttpAgent(spaces = []) { - const httpAgent = () => {}; + const httpAgent = () => { }; httpAgent.get = jest.fn(() => Promise.resolve({ data: spaces })); return httpAgent; @@ -32,7 +32,7 @@ function getSpacesManager(spaces = []) { test('it renders without crashing', () => { const spacesManager = getSpacesManager(); const component = renderer.create( - + ); expect(component).toMatchSnapshot(); }); @@ -44,7 +44,6 @@ test('it uses the spaces on props, when provided', () => { id: 'space-1', name: 'Space 1', description: 'This is the first space', - urlContext: 'space-1-context' }]; const component = render( @@ -64,13 +63,12 @@ test('it queries for spaces when not provided on props', () => { id: 'space-1', name: 'Space 1', description: 'This is the first space', - urlContext: 'space-1-context' }]; const spacesManager = getSpacesManager(spaces); shallow( - + ); return Promise diff --git a/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.js.snap b/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.js.snap index a42d029097b67..d08be39f9282e 100644 --- a/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.js.snap +++ b/x-pack/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`addSpaceUrlContext it throws an error when the requested path does not start with a slash 1`] = `"path must start with a /"`; +exports[`addSpaceIdToPath it throws an error when the requested path does not start with a slash 1`] = `"path must start with a /"`; diff --git a/x-pack/plugins/spaces/server/lib/check_duplicate_context.js b/x-pack/plugins/spaces/server/lib/check_duplicate_context.js deleted file mode 100644 index a3f0125fb777c..0000000000000 --- a/x-pack/plugins/spaces/server/lib/check_duplicate_context.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function createDuplicateContextQuery(index, { id, urlContext }) { - let mustNotClause = {}; - - if (id) { - mustNotClause = { - must_not: { - term: { - '_id': `space:${id}` - } - } - }; - } - - return { - index, - ignore: [404], - body: { - query: { - bool: { - must: { - term: { - [`space.urlContext`]: urlContext - } - }, - ...mustNotClause - } - } - } - }; -} diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.js b/x-pack/plugins/spaces/server/lib/create_default_space.js index 0408105a6944c..fc19ee325d14f 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.js +++ b/x-pack/plugins/spaces/server/lib/create_default_space.js @@ -27,7 +27,6 @@ export async function createDefaultSpace(server) { await savedObjectsRepository.create('space', { name: 'Default Space', description: 'This is your Default Space!', - urlContext: '', _reserved: true }, options); } diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.test.js b/x-pack/plugins/spaces/server/lib/create_default_space.test.js index d2b4df363eb78..1d5a243315e92 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.test.js +++ b/x-pack/plugins/spaces/server/lib/create_default_space.test.js @@ -78,7 +78,7 @@ test(`it creates the default space when one does not exist`, async () => { expect(repository.create).toHaveBeenCalledTimes(1); expect(repository.create).toHaveBeenCalledWith( 'space', - { "_reserved": true, "description": "This is your Default Space!", "name": "Default Space", "urlContext": "" }, + { "_reserved": true, "description": "This is your Default Space!", "name": "Default Space" }, { "id": "default" } ); }); diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.js index d393a6937ef50..3ee55354da8dc 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSpaceUrlContext } from './spaces_url_parser'; +import { getSpaceIdFromPath } from './spaces_url_parser'; export function createSpacesService(server) { @@ -12,24 +12,24 @@ export function createSpacesService(server) { const contextCache = new WeakMap(); - function getUrlContext(request) { + function getSpaceId(request) { if (!contextCache.has(request)) { populateCache(request); } - const { urlContext } = contextCache.get(request); - return urlContext; + const { spaceId } = contextCache.get(request); + return spaceId; } function populateCache(request) { - const urlContext = getSpaceUrlContext(request.getBasePath(), serverBasePath); + const spaceId = getSpaceIdFromPath(request.getBasePath(), serverBasePath); contextCache.set(request, { - urlContext + spaceId }); } return { - getUrlContext, + getSpaceId, }; } diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js index 578a161229c41..c27a505616018 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.test.js @@ -5,9 +5,10 @@ */ import { createSpacesService } from "./create_spaces_service"; +import { DEFAULT_SPACE_ID } from '../../common/constants'; -const createRequest = (urlContext, serverBasePath = '') => ({ - getBasePath: () => urlContext ? `${serverBasePath}/s/${urlContext}` : serverBasePath +const createRequest = (spaceId, serverBasePath = '') => ({ + getBasePath: () => spaceId && spaceId !== DEFAULT_SPACE_ID ? `${serverBasePath}/s/${spaceId}` : serverBasePath }); const createMockServer = (config) => { @@ -22,31 +23,31 @@ const createMockServer = (config) => { }; }; -test('returns empty string for the default space', () => { +test('returns the default space ID', () => { const server = createMockServer({ 'server.basePath': '' }); const service = createSpacesService(server); - expect(service.getUrlContext(createRequest())).toEqual(''); + expect(service.getSpaceId(createRequest())).toEqual(DEFAULT_SPACE_ID); }); -test('returns the urlContext for the current space', () => { +test('returns the id for the current space', () => { const request = createRequest('my-space-context'); const server = createMockServer({ 'server.basePath': '' }); const service = createSpacesService(server); - expect(service.getUrlContext(request)).toEqual('my-space-context'); + expect(service.getSpaceId(request)).toEqual('my-space-context'); }); -test(`returns the urlContext for the current space when a server basepath is defined`, () => { +test(`returns the id for the current space when a server basepath is defined`, () => { const request = createRequest('my-space-context', '/foo'); const server = createMockServer({ 'server.basePath': '/foo' }); const service = createSpacesService(server); - expect(service.getUrlContext(request)).toEqual('my-space-context'); + expect(service.getSpaceId(request)).toEqual('my-space-context'); }); diff --git a/x-pack/plugins/spaces/server/lib/get_active_space.js b/x-pack/plugins/spaces/server/lib/get_active_space.js index 834682228eaa4..5ab9bb744b3c6 100644 --- a/x-pack/plugins/spaces/server/lib/get_active_space.js +++ b/x-pack/plugins/spaces/server/lib/get_active_space.js @@ -6,20 +6,15 @@ import Boom from 'boom'; import { wrapError } from './errors'; -import { getSpaceUrlContext } from './spaces_url_parser'; -import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { getSpaceIdFromPath } from './spaces_url_parser'; export async function getActiveSpace(savedObjectsClient, requestBasePath, serverBasePath) { - const spaceContext = getSpaceUrlContext(requestBasePath, serverBasePath); + const spaceId = getSpaceIdFromPath(requestBasePath, serverBasePath); let space; try { - if (spaceContext) { - space = await getSpaceByUrlContext(savedObjectsClient, spaceContext); - } else { - space = await getDefaultSpace(savedObjectsClient); - } + space = await getSpaceById(savedObjectsClient, spaceId); } catch (e) { throw wrapError(e); @@ -37,30 +32,6 @@ export async function getActiveSpace(savedObjectsClient, requestBasePath, server }; } -async function getDefaultSpace(savedObjectsClient) { - return savedObjectsClient.get('space', DEFAULT_SPACE_ID); -} - -async function getSpaceByUrlContext(savedObjectsClient, urlContext) { - const { - saved_objects: savedObjects - } = await savedObjectsClient.find({ - type: 'space', - search: `"${urlContext}"`, - search_fields: ['urlContext'], - }); - - if (savedObjects.length === 0) { - return null; - } - - if (savedObjects.length > 1) { - const spaceNames = savedObjects.map(s => s.attributes.name).join(', '); - - throw Boom.badRequest( - `Multiple Spaces share this URL Context: (${spaceNames}). Please correct this in the Management Section.` - ); - } - - return savedObjects[0]; +async function getSpaceById(savedObjectsClient, spaceId) { + return savedObjectsClient.get('space', spaceId); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js index 28471c3d928ba..9fc4064723184 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.js @@ -23,7 +23,7 @@ export class SpacesSavedObjectsClient { this._client = baseClient; this._types = types; - this._spaceUrlContext = spacesService.getUrlContext(request); + this._spaceId = spacesService.getSpaceId(request); } /** @@ -39,7 +39,7 @@ export class SpacesSavedObjectsClient { */ async create(type, attributes = {}, options = {}) { - const spaceId = await this._getSpaceId(); + const spaceId = this._spaceId; const createOptions = { ...options, @@ -66,7 +66,7 @@ export class SpacesSavedObjectsClient { * @returns {promise} - { saved_objects: [{ id, type, version, attributes, error: { message } }]} */ async bulkCreate(objects, options = {}) { - const spaceId = await this._getSpaceId(); + const spaceId = this._spaceId; const objectsToCreate = objects.map(object => { const objectToCreate = { @@ -127,7 +127,7 @@ export class SpacesSavedObjectsClient { const filters = options.filters || []; - const spaceId = await this._getSpaceId(); + const spaceId = this._spaceId; spaceOptions.filters = [...filters, ...getSpacesQueryFilters(spaceId, types)]; @@ -150,7 +150,7 @@ export class SpacesSavedObjectsClient { */ async bulkGet(objects = [], options = {}) { // ES 'mget' does not support queries, so we have to filter results after the fact. - const thisSpaceId = await this._getSpaceId(); + const thisSpaceId = this._spaceId; const extraDocumentProperties = this._collectExtraDocumentProperties(['spaceId', 'type'], options.extraDocumentProperties); @@ -200,7 +200,7 @@ export class SpacesSavedObjectsClient { const { spaceId: objectSpaceId = DEFAULT_SPACE_ID } = response; if (isTypeSpaceAware(type)) { - const thisSpaceId = await this._getSpaceId(); + const thisSpaceId = this._spaceId; if (objectSpaceId !== thisSpaceId) { throw this._client.errors.createGenericNotFoundError(); } @@ -232,7 +232,7 @@ export class SpacesSavedObjectsClient { if (isTypeSpaceAware(type)) { await this.get(type, id); - const spaceId = await this._getSpaceId(); + const spaceId = this._spaceId; if (this._shouldAssignSpaceId(type, spaceId)) { updateOptions.extraDocumentProperties.spaceId = spaceId; @@ -244,34 +244,6 @@ export class SpacesSavedObjectsClient { return await this._client.update(type, id, attributes, updateOptions); } - async _getSpaceId() { - if (!this._spaceUrlContext) { - return DEFAULT_SPACE_ID; - } - - if (!this._spaceId) { - this._spaceId = await this._findSpace(this._spaceUrlContext); - } - - return this._spaceId; - } - - async _findSpace(urlContext) { - const { - saved_objects: spaces = [] - } = await this._client.find({ - type: 'space', - search: `${urlContext}`, - search_fields: ['urlContext'] - }); - - if (spaces.length > 0) { - return spaces[0].id; - } - - return null; - } - _collectExtraDocumentProperties(thisClientProperties, optionalProperties = []) { return uniq([...thisClientProperties, ...optionalProperties]).value(); } diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js index 4c7454d43f5da..7e9fab3116d70 100644 --- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js +++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.test.js @@ -6,6 +6,7 @@ import { SpacesSavedObjectsClient } from './spaces_saved_objects_client'; import { createSpacesService } from '../create_spaces_service'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; const createObjectEntry = (type, id, spaceId) => ({ [id]: { @@ -35,7 +36,7 @@ const server = { }; const createMockRequest = (space) => ({ - getBasePath: () => space.urlContext ? `/s/${space.urlContext}` : '', + getBasePath: () => space.id !== DEFAULT_SPACE_ID ? `/s/${space.id}` : '', }); const createMockClient = (space) => { @@ -74,7 +75,6 @@ const createMockClient = (space) => { describe('default space', () => { const currentSpace = { id: 'default', - urlContext: '' }; describe('#get', () => { @@ -723,7 +723,6 @@ describe('default space', () => { describe('current space (space_1)', () => { const currentSpace = { id: 'space_1', - urlContext: 'space-1' }; describe('#get', () => { diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js index a5ec62b2f7e76..5f0612f821b65 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.js @@ -5,7 +5,8 @@ */ import { wrapError } from './errors'; -import { addSpaceUrlContext, getSpaceUrlContext } from './spaces_url_parser'; +import { addSpaceIdToPath, getSpaceIdFromPath } from './spaces_url_parser'; +import { DEFAULT_SPACE_ID } from '../../common/constants'; export function initSpacesRequestInterceptors(server) { @@ -16,10 +17,10 @@ export function initSpacesRequestInterceptors(server) { // If navigating within the context of a space, then we store the Space's URL Context on the request, // and rewrite the request to not include the space identifier in the URL. - const spaceUrlContext = getSpaceUrlContext(path, serverBasePath); + const spaceId = getSpaceIdFromPath(path, serverBasePath); - if (spaceUrlContext) { - const reqBasePath = `/s/${spaceUrlContext}`; + if (spaceId !== DEFAULT_SPACE_ID) { + const reqBasePath = `/s/${spaceId}`; request.setBasePath(reqBasePath); const newLocation = path.substr(reqBasePath.length) || '/'; @@ -41,13 +42,11 @@ export function initSpacesRequestInterceptors(server) { const path = request.path; const isRequestingKibanaRoot = path === '/'; - const { spaces } = server; - const urlContext = await spaces.getUrlContext(request); // if requesting the application root, then show the Space Selector UI to allow the user to choose which space // they wish to visit. This is done "onPostAuth" to allow the Saved Objects Client to use the request's auth scope, // which is not available at the time of "onRequest". - if (isRequestingKibanaRoot && !urlContext) { + if (isRequestingKibanaRoot) { try { const client = request.getSavedObjectsClient(); const { total, saved_objects: spaceObjects } = await client.find({ @@ -62,11 +61,8 @@ export function initSpacesRequestInterceptors(server) { // If only one space is available, then send user there directly. // No need for an interstitial screen where there is only one possible outcome. const space = spaceObjects[0]; - const { - urlContext - } = space.attributes; - const destination = addSpaceUrlContext(basePath, urlContext, defaultRoute); + const destination = addSpaceIdToPath(basePath, space.id, defaultRoute); return reply.redirect(destination); } diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js index 05fddbe43fbba..feb6bebeef5f1 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js +++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.js @@ -164,10 +164,9 @@ describe('interceptors', () => { }); const spaces = [{ - id: 'space:a-space', + id: 'a-space', attributes: { name: 'a space', - urlContext: 'a-space', } }]; @@ -197,10 +196,9 @@ describe('interceptors', () => { }); const spaces = [{ - id: 'space:default', + id: 'default', attributes: { name: 'Default Space', - urlContext: '', } }]; @@ -216,16 +214,14 @@ describe('interceptors', () => { test('it redirects to the Space Selector App when navigating to Kibana root', async () => { const spaces = [{ - id: 'space:a-space', + id: 'a-space', attributes: { name: 'a space', - urlContext: 'a-space', } }, { - id: 'space:b-space', + id: 'b-space', attributes: { name: 'b space', - urlContext: 'b-space', } }]; diff --git a/x-pack/plugins/spaces/server/lib/space_schema.js b/x-pack/plugins/spaces/server/lib/space_schema.js index 82dd81ee1705e..dc60c10a36bc2 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.js +++ b/x-pack/plugins/spaces/server/lib/space_schema.js @@ -8,10 +8,9 @@ import Joi from 'joi'; import { MAX_SPACE_INITIALS } from '../../common/constants'; export const spaceSchema = Joi.object({ - id: Joi.string(), + id: Joi.string().regex(/[a-z0-9_\-]*/, `lower case, a-z, 0-9, "_", and "-" are allowed`), name: Joi.string().required(), - description: Joi.string().required(), - urlContext: Joi.string().regex(/[a-z0-9\-]*/, `lower case, a-z, 0-9, and "-" are allowed`), + description: Joi.string(), initials: Joi.string().max(MAX_SPACE_INITIALS), color: Joi.string().regex(/^#[a-z0-9]{6}$/, `6 digit hex color, starting with a #`), _reserved: Joi.boolean() diff --git a/x-pack/plugins/spaces/server/lib/spaces_url_parser.js b/x-pack/plugins/spaces/server/lib/spaces_url_parser.js index 7c0cf5d32776b..397863785e86c 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_url_parser.js +++ b/x-pack/plugins/spaces/server/lib/spaces_url_parser.js @@ -3,33 +3,38 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { DEFAULT_SPACE_ID } from '../../common/constants'; -export function getSpaceUrlContext(requestBasePath = '/', serverBasePath = '/') { +export function getSpaceIdFromPath(requestBasePath = '/', serverBasePath = '/') { let pathToCheck = requestBasePath; if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { pathToCheck = requestBasePath.substr(serverBasePath.length); } // Look for `/s/space-url-context` in the base path - const matchResult = pathToCheck.match(/^\/s\/([a-z0-9\-]+)/); + const matchResult = pathToCheck.match(/^\/s\/([a-z0-9_\-]+)/); if (!matchResult || matchResult.length === 0) { - return ''; + return DEFAULT_SPACE_ID; } // Ignoring first result, we only want the capture group result at index 1 - const [, urlContext = ''] = matchResult; + const [, spaceId] = matchResult; - return urlContext; + if (!spaceId) { + throw new Error(`Unable to determine Space ID from request path: ${requestBasePath}`); + } + + return spaceId; } -export function addSpaceUrlContext(basePath = '/', urlContext = '', requestedPath = '') { +export function addSpaceIdToPath(basePath = '/', spaceId = '', requestedPath = '') { if (requestedPath && !requestedPath.startsWith('/')) { throw new Error(`path must start with a /`); } - if (urlContext) { - return `${basePath}/s/${urlContext}${requestedPath}`; + if (spaceId && spaceId !== DEFAULT_SPACE_ID) { + return `${basePath}/s/${spaceId}${requestedPath}`; } return `${basePath}${requestedPath}`; } diff --git a/x-pack/plugins/spaces/server/lib/spaces_url_parser.test.js b/x-pack/plugins/spaces/server/lib/spaces_url_parser.test.js index 3d2ddaee6137f..68234b3ba48fb 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_url_parser.test.js +++ b/x-pack/plugins/spaces/server/lib/spaces_url_parser.test.js @@ -3,65 +3,66 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getSpaceUrlContext, addSpaceUrlContext } from './spaces_url_parser'; +import { getSpaceIdFromPath, addSpaceIdToPath } from './spaces_url_parser'; +import { DEFAULT_SPACE_ID } from '../../common/constants'; -describe('getSpaceUrlContext', () => { +describe('getSpaceIdFromPath', () => { describe('without a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceUrlContext(basePath)).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath)).toEqual('my-awesome-space-lives-here'); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceUrlContext(basePath)).toEqual(''); + expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceUrlContext(basePath)).toEqual(''); + expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); }); }); describe('with a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceUrlContext(basePath, '/')).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath, '/')).toEqual('my-awesome-space-lives-here'); }); test('it identifies the space url context following the server base path', () => { const basePath = `/server-base-path-here/s/my-awesome-space-lives-here`; - expect(getSpaceUrlContext(basePath, '/server-base-path-here')).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual('my-awesome-space-lives-here'); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceUrlContext(basePath, '/this/is/a')).toEqual(''); + expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual(DEFAULT_SPACE_ID); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceUrlContext(basePath, basePath)).toEqual(''); + expect(getSpaceIdFromPath(basePath, basePath)).toEqual(DEFAULT_SPACE_ID); }); }); }); -describe('addSpaceUrlContext', () => { +describe('addSpaceIdToPath', () => { test('handles no parameters', () => { - expect(addSpaceUrlContext()).toEqual(`/`); + expect(addSpaceIdToPath()).toEqual(`/`); }); test('it adds to the basePath correctly', () => { - expect(addSpaceUrlContext('/my/base/path', 'url-context')).toEqual('/my/base/path/s/url-context'); + expect(addSpaceIdToPath('/my/base/path', 'url-context')).toEqual('/my/base/path/s/url-context'); }); test('it appends the requested path to the end of the url context', () => { - expect(addSpaceUrlContext('/base', 'context', '/final/destination')).toEqual('/base/s/context/final/destination'); + expect(addSpaceIdToPath('/base', 'context', '/final/destination')).toEqual('/base/s/context/final/destination'); }); test('it throws an error when the requested path does not start with a slash', () => { expect(() => { - addSpaceUrlContext('', '', 'foo'); + addSpaceIdToPath('', '', 'foo'); }).toThrowErrorMatchingSnapshot(); }); }); 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 60a60c01088ee..740b444e65e69 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js @@ -6,13 +6,11 @@ import Boom from 'boom'; import { omit } from 'lodash'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { spaceSchema } from '../../../lib/space_schema'; import { wrapError } from '../../../lib/errors'; import { isReservedSpace } from '../../../../common/is_reserved_space'; -import { createDuplicateContextQuery } from '../../../lib/check_duplicate_context'; -import { addSpaceUrlContext } from '../../../lib/spaces_url_parser'; +import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; export function initSpacesApi(server) { const routePreCheckLicenseFn = routePreCheckLicense(server); @@ -24,32 +22,6 @@ export function initSpacesApi(server) { }; } - const callWithInternalUser = getClient(server).callWithInternalUser; - - const config = server.config(); - - async function checkForDuplicateContext(space) { - const query = createDuplicateContextQuery(config.get('kibana.index'), space); - - // TODO(legrego): Once the SOC is split into a client & repository, this "callWithInternalUser" call should - // be replaced to use the repository instead. - const { hits } = await callWithInternalUser('search', query); - - const { total, hits: conflicts } = hits; - - let error; - - if (total > 0) { - const firstConflictName = conflicts[0]._source.space.name; - - error = Boom.badRequest( - `Another Space (${firstConflictName}) already uses this URL Context. Please choose a different URL Context.` - ); - } - - return { error }; - } - server.route({ method: 'GET', path: '/api/spaces/v1/spaces', @@ -102,36 +74,21 @@ export function initSpacesApi(server) { async handler(request, reply) { const client = request.getSavedObjectsClient(); - const { - overwrite = false - } = request.query; - const space = omit(request.payload, ['id', '_reserved']); - const { error: contextError } = await checkForDuplicateContext(space); + const id = request.payload.id; - if (contextError) { - return reply(wrapError(contextError)); + const existingSpace = await getSpaceById(client, id); + if (existingSpace) { + return reply(Boom.conflict(`A space with the identifier ${id} already exists. Please choose a different identifier`)); } - const id = request.params.id; - - let result; try { - const existingSpace = await getSpaceById(client, id); - - // Reserved Spaces cannot have their _reserved or urlContext properties altered. - if (isReservedSpace(existingSpace)) { - space._reserved = true; - space.urlContext = existingSpace.urlContext; - } - - result = await client.create('space', { ...space }, { id, overwrite }); + return reply(await client.create('space', { ...space }, { id, overwrite: false })); } catch (error) { return reply(wrapError(error)); } - return reply(convertSavedObjectToSpace(result)); }, config: { validate: { @@ -147,22 +104,20 @@ export function initSpacesApi(server) { async handler(request, reply) { const client = request.getSavedObjectsClient(); - const { - overwrite = false - } = request.query; - const space = omit(request.payload, ['id']); const id = request.params.id; - const { error } = await checkForDuplicateContext({ ...space, id }); + const existingSpace = await getSpaceById(client, id); - if (error) { - return reply(wrapError(error)); + if (existingSpace) { + space._reserved = existingSpace._reserved; + } else { + return reply(Boom.notFound(`Unable to find space with ID ${id}`)); } let result; try { - result = await client.create('space', { ...space }, { id, overwrite }); + result = await client.update('space', id, { ...space }); } catch (error) { return reply(wrapError(error)); } @@ -219,7 +174,7 @@ export function initSpacesApi(server) { const config = server.config(); return reply({ - location: addSpaceUrlContext(config.get('server.basePath'), existingSpace.urlContext, config.get('server.defaultRoute')) + location: addSpaceIdToPath(config.get('server.basePath'), existingSpace.id, config.get('server.defaultRoute')) }); } catch (error) { diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js index cd3910e539873..fb957e4586343 100644 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js @@ -24,22 +24,19 @@ jest.mock('../../../../../../server/lib/get_client_shield', () => { }); const spaces = [{ - id: 'space:a-space', + id: 'a-space', attributes: { name: 'a space', - urlContext: 'a-space', } }, { - id: 'space:b-space', + id: 'b-space', attributes: { name: 'b space', - urlContext: 'b-space', } }, { - id: 'space:default', + id: 'default', attributes: { name: 'Default Space', - urlContext: '', _reserved: true } }]; @@ -53,7 +50,13 @@ describe('Spaces API', () => { }; beforeEach(() => { - request = async (method, path, setupFn = () => { }, testConfig = {}) => { + request = async (method, path, options = {}) => { + + const { + setupFn = () => { }, + testConfig = {}, + payload, + } = options; const server = new Server(); @@ -78,27 +81,37 @@ describe('Spaces API', () => { server.decorate('request', 'setBasePath', jest.fn()); // Mock server.getSavedObjectsClient() - server.decorate('request', 'getSavedObjectsClient', () => { - return { - get: jest.fn((type, id) => { - return spaces.filter(s => s.id === `${type}:${id}`)[0]; - }), - find: jest.fn(() => { - return { - total: spaces.length, - saved_objects: spaces - }; - }), - delete: jest.fn() - }; - }); + const mockSavedObjectsClient = { + get: jest.fn((type, id) => { + return spaces.filter(s => s.id === id)[0]; + }), + find: jest.fn(() => { + return { + total: spaces.length, + saved_objects: spaces + }; + }), + create: jest.fn(() => ({})), + update: jest.fn(() => ({})), + delete: jest.fn(), + errors: { + isNotFoundError: jest.fn(() => true) + } + }; + + server.decorate('request', 'getSavedObjectsClient', () => mockSavedObjectsClient); teardowns.push(() => server.stop()); - return await server.inject({ - method, - url: path, - }); + return { + server, + mockSavedObjectsClient, + response: await server.inject({ + method, + url: path, + payload, + }) + }; }; }); @@ -107,7 +120,7 @@ describe('Spaces API', () => { }); test(`'GET spaces' returns all available spaces`, async () => { - const response = await request('GET', '/api/spaces/v1/spaces'); + const { response } = await request('GET', '/api/spaces/v1/spaces'); const { statusCode, @@ -120,7 +133,7 @@ describe('Spaces API', () => { }); test(`'GET spaces/{id}' returns the space with that id`, async () => { - const response = await request('GET', '/api/spaces/v1/space/default'); + const { response } = await request('GET', '/api/spaces/v1/space/default'); const { statusCode, @@ -129,11 +142,11 @@ describe('Spaces API', () => { expect(statusCode).toEqual(200); const resultSpace = JSON.parse(payload); - expect(resultSpace.id).toEqual('space:default'); + expect(resultSpace.id).toEqual('default'); }); test(`'DELETE spaces/{id}' deletes the space`, async () => { - const response = await request('DELETE', '/api/spaces/v1/space/a-space'); + const { response } = await request('DELETE', '/api/spaces/v1/space/a-space'); const { statusCode @@ -143,7 +156,7 @@ describe('Spaces API', () => { }); test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { - const response = await request('DELETE', '/api/spaces/v1/space/default'); + const { response } = await request('DELETE', '/api/spaces/v1/space/default'); const { statusCode, @@ -158,8 +171,84 @@ describe('Spaces API', () => { }); }); + test('POST /space should create a new space with the provided ID', async () => { + const payload = { + id: 'my-space-id', + name: 'my new space', + description: 'with a description', + }; + + const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces/v1/space', { payload }); + + const { + statusCode, + } = response; + + expect(statusCode).toEqual(200); + expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.create) + .toHaveBeenCalledWith('space', { name: 'my new space', description: 'with a description' }, { id: 'my-space-id', overwrite: false }); + }); + + test('POST /space should not allow a space to be updated', async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { response } = await request('POST', '/api/spaces/v1/space', { payload }); + + const { + statusCode, + payload: responsePayload, + } = response; + + expect(statusCode).toEqual(409); + expect(JSON.parse(responsePayload)).toEqual({ + error: 'Conflict', + message: "A space with the identifier a-space already exists. Please choose a different identifier", + statusCode: 409 + }); + }); + + test('PUT /space should update an existing space with the provided ID', async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/v1/space/a-space', { payload }); + + const { + statusCode, + } = response; + + expect(statusCode).toEqual(200); + expect(mockSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.update) + .toHaveBeenCalledWith('space', 'a-space', { name: 'my updated space', description: 'with a description' }); + }); + + test('PUT /space should not allow a new space to be created', async () => { + const payload = { + id: 'a-new-space', + name: 'my new space', + description: 'with a description', + }; + + const { response } = await request('PUT', '/api/spaces/v1/space/a-new-space', { payload }); + + const { + statusCode, + } = response; + + expect(statusCode).toEqual(404); + }); + test('POST space/{id}/select should respond with the new space location', async () => { - const response = await request('POST', '/api/spaces/v1/space/a-space/select'); + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); const { statusCode, @@ -172,10 +261,12 @@ describe('Spaces API', () => { expect(result.location).toEqual('/s/a-space'); }); - test('POST space/{id}/select should respond with the new space location when a baseUrl is provided', async () => { - const response = await request('POST', '/api/spaces/v1/space/a-space/select', () => { }, { + test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { + const testConfig = { 'server.basePath': '/my/base/path' - }); + }; + + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { testConfig }); const { statusCode, diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js index 3d747051d8c43..37bd80f56a5c8 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/bulk_get.js @@ -106,15 +106,15 @@ export default function ({ getService }) { }); }; - const bulkGetTest = (description, { spaceId, urlContext, tests }) => { + const bulkGetTest = (description, { spaceId, tests, otherSpaceId = spaceId }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); it(`should return ${tests.default.statusCode}`, async () => { await supertest - .post(`${getUrlPrefix(urlContext)}/api/saved_objects/_bulk_get`) - .send(createBulkRequests(spaceId)) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/_bulk_get`) + .send(createBulkRequests(otherSpaceId)) .expect(tests.default.statusCode) .then(tests.default.response); }); @@ -133,11 +133,11 @@ export default function ({ getService }) { bulkGetTest(`objects within another space`, { ...SPACES.SPACE_1, - urlContext: SPACES.SPACE_2.urlContext, + otherSpaceId: SPACES.SPACE_2.spaceId, tests: { default: { statusCode: 200, - response: expectNotFoundResults(SPACES.SPACE_1.spaceId) + response: expectNotFoundResults(SPACES.SPACE_2.spaceId) }, } }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js index 2eee219f739f6..59f18c131b1ec 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/create.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/create.js @@ -62,7 +62,6 @@ export default function ({ getService }) { version: 1, attributes: { name: 'My favorite space', - urlContext: 'my-favorite-space' } }); @@ -80,13 +79,13 @@ export default function ({ getService }) { expect(actualSpaceId).to.eql('**not defined**'); }; - const createTest = (description, { urlContext, tests }) => { + const createTest = (description, { spaceId, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); it(`should return ${tests.spaceAware.statusCode} for a space-aware type`, async () => { await supertest - .post(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization`) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization`) .send({ attributes: { title: 'My favorite vis' @@ -98,11 +97,10 @@ export default function ({ getService }) { it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware type`, async () => { await supertest - .post(`${getUrlPrefix(urlContext)}/api/saved_objects/space`) + .post(`${getUrlPrefix(spaceId)}/api/saved_objects/space`) .send({ attributes: { name: 'My favorite space', - urlContext: 'my-favorite-space' } }) .expect(tests.notSpaceAware.statusCode) diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js index dc1efb057d4e7..0e7b72a5553f2 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/delete.js @@ -26,28 +26,28 @@ export default function ({ getService }) { }); }; - const deleteTest = (description, { urlContext, spaceId, tests }) => { + const deleteTest = (description, { spaceId, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); it(`should return ${tests.spaceAware.statusCode} when deleting a space-aware doc`, async () => ( await supertest - .delete(`${getUrlPrefix(urlContext)}/api/saved_objects/dashboard/${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`) + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix(spaceId)}be3733a0-9efe-11e7-acb3-3dab96693fab`) .expect(tests.spaceAware.statusCode) .then(tests.spaceAware.response) )); it(`should return ${tests.notSpaceAware.statusCode} when deleting a non-space-aware doc`, async () => ( await supertest - .delete(`${getUrlPrefix(urlContext)}/api/saved_objects/space/space_2`) + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/space/space_2`) .expect(tests.notSpaceAware.statusCode) .then(tests.notSpaceAware.response) )); it(`should return ${tests.inOtherSpace.statusCode} when deleting a doc belonging to another space`, async () => { await supertest - .delete(`${getUrlPrefix(urlContext)}/api/saved_objects/dashboard/${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`) + .delete(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/${getIdPrefix('space_2')}be3733a0-9efe-11e7-acb3-3dab96693fab`) .expect(tests.inOtherSpace.statusCode) .then(tests.inOtherSpace.response); }); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js index 1daa2b2d04f4e..01383433774ef 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/find.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/find.js @@ -106,14 +106,14 @@ export default function ({ getService }) { }); }; - const findTest = (description, { urlContext, tests }) => { + const findTest = (description, { spaceId, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => ( await supertest - .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&fields=title`) + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&fields=title`) .expect(tests.normal.statusCode) .then(tests.normal.response) )); @@ -121,7 +121,7 @@ export default function ({ getService }) { describe('page beyond total', () => { it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => ( await supertest - .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=visualization&page=100&per_page=100`) + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=visualization&page=100&per_page=100`) .expect(tests.pageBeyondTotal.statusCode) .then(tests.pageBeyondTotal.response) )); @@ -130,7 +130,7 @@ export default function ({ getService }) { describe('unknown search field', () => { it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => ( await supertest - .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find?type=wigwags&search_fields=a`) + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find?type=wigwags&search_fields=a`) .expect(tests.unknownSearchField.statusCode) .then(tests.unknownSearchField.response) )); @@ -139,7 +139,7 @@ export default function ({ getService }) { describe('no type', () => { it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => ( await supertest - .get(`${getUrlPrefix(urlContext)}/api/saved_objects/_find`) + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/_find`) .expect(tests.noType.statusCode) .then(tests.noType.response) )); diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js index 2516752cbff17..092b9ae499b5c 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/get.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/get.js @@ -51,14 +51,15 @@ export default function ({ getService }) { }); }; - const getTest = (description, { spaceId, urlContext = '', tests }) => { + const getTest = (description, { spaceId, tests, otherSpaceId = spaceId }) => { describe(description, () => { before(async () => esArchiver.load(`saved_objects/spaces`)); after(async () => esArchiver.unload(`saved_objects/spaces`)); it(`should return ${tests.exists.statusCode}`, async () => { + const objectId = `${getIdPrefix(otherSpaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`; return supertest - .get(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${objectId}`) .expect(tests.exists.statusCode) .then(tests.exists.response); }); @@ -77,7 +78,7 @@ export default function ({ getService }) { getTest(`cannot access objects belonging to a different space (space_1)`, { ...SPACES.SPACE_1, - urlContext: 'space-2', + otherSpaceId: SPACES.SPACE_2.spaceId, tests: { exists: { statusCode: 404, @@ -98,7 +99,7 @@ export default function ({ getService }) { getTest(`cannot access objects belonging to a different space (default)`, { ...SPACES.DEFAULT, - urlContext: 'space-1', + otherSpaceId: SPACES.SPACE_1.spaceId, tests: { exists: { statusCode: 404, diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js index 186b75ccacf88..73df273c6e6e9 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/space_test_utils.js @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getUrlPrefix(urlContext) { - return urlContext ? `/s/${urlContext}` : ``; +import { DEFAULT_SPACE_ID } from '../../../../../plugins/spaces/common/constants'; + +export function getUrlPrefix(spaceId) { + return spaceId && spaceId !== DEFAULT_SPACE_ID ? `/s/${spaceId}` : ``; } // Spaces do not actually prefix the ID, but this simplifies testing positive and negative flows. export function getIdPrefix(spaceId) { - return spaceId === 'default' ? '' : `${spaceId}-`; + return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } export function getExpectedSpaceIdProperty(spaceId) { - if (spaceId === 'default') { + if (spaceId === DEFAULT_SPACE_ID) { return {}; } return { diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js index f5cd9109c9a9e..905b360c99949 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/lib/spaces.js @@ -7,14 +7,11 @@ export const SPACES = { SPACE_1: { spaceId: 'space_1', - urlContext: 'space-1' }, SPACE_2: { spaceId: 'space_2', - urlContext: 'space-2' }, DEFAULT: { spaceId: 'default', - urlContext: '' } }; diff --git a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js index d87d03d9b7250..409e4b9db549e 100644 --- a/x-pack/test/spaces_api_integration/apis/saved_objects/update.js +++ b/x-pack/test/spaces_api_integration/apis/saved_objects/update.js @@ -53,13 +53,13 @@ export default function ({ getService }) { }); }; - const updateTest = (description, { urlContext, spaceId, tests }) => { + const updateTest = (description, { spaceId, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); it(`should return ${tests.spaceAware.statusCode} for a space-aware doc`, async () => { await supertest - .put(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`) + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`) .send({ attributes: { title: 'My second favorite vis' @@ -71,7 +71,7 @@ export default function ({ getService }) { it(`should return ${tests.notSpaceAware.statusCode} for a non space-aware doc`, async () => { await supertest - .put(`${getUrlPrefix(urlContext)}/api/saved_objects/space/space_1`) + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/space/space_1`) .send({ attributes: { name: 'My second favorite space' @@ -84,7 +84,7 @@ export default function ({ getService }) { it(`should return ${tests.inOtherSpace.statusCode} for a doc in another space`, async () => { const id = `${getIdPrefix('space_2')}dd7caf20-9efd-11e7-acb3-3dab96693fab`; await supertest - .put(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/${id}`) + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/${id}`) .send({ attributes: { title: 'My second favorite vis' @@ -97,7 +97,7 @@ export default function ({ getService }) { describe('unknown id', () => { it(`should return ${tests.doesntExist.statusCode}`, async () => { await supertest - .put(`${getUrlPrefix(urlContext)}/api/saved_objects/visualization/not an id`) + .put(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/not an id`) .send({ attributes: { title: 'My second favorite vis' diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz index 33586e3642245..4c2d63913ebe0 100644 Binary files a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz and b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/data.json.gz differ diff --git a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json index 2e1a6d1f65fd9..9cca6b52a1caa 100644 --- a/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/spaces_api_integration/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -44,9 +44,6 @@ } } }, - "urlContext": { - "type": "keyword" - }, "description": { "type": "text" },