Space management page UX improvements (#100448)

* Updated spaces management page

* Fixed test failures

* updated snapshot

* Added suggestions form code review

* Fixed unit test

* Review suggestion #2

* WIP

* Fix build errors

* fix type

* remove test for popup that doesnt exist anymore

* fix test

* fix a11y issues

* fix a11y issue

* Removed unused css

* Fix functional test

* Added suggestions from code review

* Fix typescript errors

* Added suggestions from code review

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Thom Heymann 2021-07-20 09:33:39 +01:00 committed by GitHub
parent 2fb1a47137
commit 1f5be1e1e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 916 additions and 961 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 571 KiB

Before After
Before After

View file

@ -0,0 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<SectionPanel
title="General"
>
<EuiDescribedFormGroup
description="Give your space a name that's memorable."
fullWidth={true}
title={
<EuiTitle
size="xs"
>
<h3>
<FormattedMessage
defaultMessage="Describe this space"
id="xpack.spaces.management.manageSpacePage.describeSpaceTitle"
values={Object {}}
/>
</h3>
</EuiTitle>
}
>
<EuiFormRow
describedByIds={Array []}
display="row"
error="Enter a name."
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
isInvalid={true}
label="Name"
labelType="label"
>
<EuiFieldText
data-test-subj="addSpaceName"
fullWidth={true}
isInvalid={true}
name="name"
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="optionalDescription"
describedByIds={Array []}
display="row"
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText="The description appears on the space selection screen."
isInvalid={false}
label="Description"
labelAppend={
<EuiText
color="subdued"
size="xs"
>
<FormattedMessage
defaultMessage="Optional"
id="xpack.spaces.management.manageSpacePage.optionalLabel"
values={Object {}}
/>
</EuiText>
}
labelType="label"
>
<EuiTextArea
data-test-subj="descriptionSpaceText"
fullWidth={true}
isInvalid={false}
name="description"
onChange={[Function]}
rows={2}
value=""
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
error="Enter a URL identifier."
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText={
<FormattedMessage
defaultMessage="You can't change the URL identifier once created."
id="xpack.spaces.management.spaceIdentifier.kibanaURLForSpaceIdentifierDescription"
values={Object {}}
/>
}
isInvalid={true}
label={
<FormattedMessage
defaultMessage="URL identifier"
id="xpack.spaces.management.spaceIdentifier.urlIdentifierTitle"
values={Object {}}
/>
}
labelType="label"
>
<EuiFieldText
data-test-subj="spaceURLDisplay"
fullWidth={true}
isInvalid={true}
onChange={[Function]}
value=""
/>
</EuiFormRow>
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
description={
<React.Fragment>
<p>
Choose how your space avatar appears across Kibana.
</p>
<React.Suspense
fallback={<EuiLoadingSpinner />}
>
<UNDEFINED
size="xl"
space={
Object {
"id": "",
"imageUrl": undefined,
"name": "",
}
}
/>
</React.Suspense>
</React.Fragment>
}
fullWidth={true}
title={
<EuiTitle
size="xs"
>
<h3>
<FormattedMessage
defaultMessage="Create an avatar"
id="xpack.spaces.management.manageSpacePage.avatarTitle"
values={Object {}}
/>
</h3>
</EuiTitle>
}
>
<CustomizeSpaceAvatar
onChange={[Function]}
space={
Object {
"id": "",
"name": "",
}
}
validator={
SpaceValidator {
"shouldValidate": true,
}
}
/>
</EuiDescribedFormGroup>
</SectionPanel>
`;

View file

@ -7,58 +7,67 @@ exports[`renders without crashing 1`] = `
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Initials (2 max)"
label="Avatar type"
labelType="label"
>
<EuiButtonGroup
buttonSize="m"
idSelected="initials"
legend=""
onChange={[Function]}
options={
Array [
Object {
"id": "initials",
"label": "Initials",
},
Object {
"id": "image",
"label": "Image",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
error="Enter initials."
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText="Enter up to two characters."
isInvalid={true}
label="Initials"
labelType="label"
>
<EuiFieldText
data-test-subj="spaceLetterInitial"
disabled={false}
inputRef={[Function]}
fullWidth={true}
isInvalid={true}
name="spaceInitials"
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
error="Select a background color."
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Color"
isInvalid={true}
label="Background color"
labelType="label"
>
<EuiColorPicker
color="#DA8B45"
isInvalid={false}
onChange={[Function]}
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Custom image"
labelType="label"
>
<EuiFilePicker
accept="image/svg+xml,image/jpeg,image/png,image/gif"
compressed={false}
data-test-subj="uploadCustomImageFile"
display="default"
initialPromptText="Select image file"
color=""
fullWidth={true}
isInvalid={true}
onChange={[Function]}
/>
</EuiFormRow>

View file

@ -1,63 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders without crashing 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={true}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText={
<p
className="eui-textBreakAll"
>
<FormattedMessage
defaultMessage="Example: https://my-kibana.example{spaceIdentifier}/app/kibana."
id="xpack.spaces.management.spaceIdentifier.kibanaURLForSpaceIdentifierDescription"
values={
Object {
"spaceIdentifier": <strong>
/s/
</strong>,
}
}
/>
</p>
}
isInvalid={false}
label={
<p>
<FormattedMessage
defaultMessage="URL identifier "
id="xpack.spaces.management.spaceIdentifier.urlIdentifierLabel"
values={Object {}}
/>
<EuiLink
aria-label="Customize the URL identifier"
data-test-subj="CustomizeOrReset"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="[customize]"
id="xpack.spaces.management.spaceIdentifier.customizeSpaceLinkText"
values={Object {}}
/>
</EuiLink>
</p>
}
labelType="label"
>
<EuiFieldText
data-test-subj="spaceURLDisplay"
fullWidth={true}
inputRef={[Function]}
onChange={[Function]}
placeholder="awesome-space"
readOnly={true}
value=""
/>
</EuiFormRow>
</Fragment>
`;

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { SpaceValidator } from '../../lib';
import { CustomizeSpace } from './customize_space';
const validator = new SpaceValidator({ shouldValidate: true });
test('renders correctly', () => {
const wrapper = shallowWithIntl(
<CustomizeSpace
space={{
id: '',
name: '',
}}
editingExistingSpace={false}
validator={validator}
onChange={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});
test('updates identifier, initials and color when name is changed', () => {
const space = {
id: 'space-1',
name: 'Space 1',
initials: 'S1',
color: '#ABCDEF',
};
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<CustomizeSpace
space={space}
editingExistingSpace={false}
validator={validator}
onChange={changeHandler}
/>
);
wrapper.find('input[name="name"]').simulate('change', { target: { value: 'Space 2' } });
expect(changeHandler).toHaveBeenCalledWith({
...space,
id: 'space-2',
name: 'Space 2',
initials: 'S2',
color: '#9170B8',
});
});
test('does not update custom identifier, initials or color name is changed', () => {
const space = {
id: 'space-1',
name: 'Space 1',
initials: 'S1',
color: '#ABCDEF',
customAvatarInitials: true,
customAvatarColor: true,
};
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<CustomizeSpace
space={space}
editingExistingSpace={true}
validator={validator}
onChange={changeHandler}
/>
);
wrapper.find('input[name="name"]').simulate('change', { target: { value: 'Space 2' } });
expect(changeHandler).toHaveBeenCalledWith({
...space,
name: 'Space 2',
});
});

View file

@ -5,31 +5,27 @@
* 2.0.
*/
import type { EuiPopoverProps } from '@elastic/eui';
import {
EuiDescribedFormGroup,
EuiFieldText,
EuiFormRow,
EuiLoadingSpinner,
EuiPopover,
EuiSpacer,
EuiText,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import type { ChangeEvent } from 'react';
import React, { Component, Fragment, lazy, Suspense } from 'react';
import React, { Component, lazy, Suspense } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { Space } from 'src/plugins/spaces_oss/common';
import { isReservedSpace } from '../../../../common';
import { getSpaceAvatarComponent } from '../../../space_avatar';
import { getSpaceAvatarComponent, getSpaceColor, getSpaceInitials } from '../../../space_avatar';
import type { SpaceValidator } from '../../lib';
import { toSpaceIdentifier } from '../../lib';
import type { FormValues } from '../manage_space_page';
import { SectionPanel } from '../section_panel';
import { CustomizeSpaceAvatar } from './customize_space_avatar';
import { SpaceIdentifier } from './space_identifier';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
@ -38,9 +34,9 @@ const LazySpaceAvatar = lazy(() =>
interface Props {
validator: SpaceValidator;
space: Partial<Space>;
space: FormValues;
editingExistingSpace: boolean;
onChange: (space: Partial<Space>) => void;
onChange: (space: FormValues) => void;
}
interface State {
@ -55,33 +51,31 @@ export class CustomizeSpace extends Component<Props, State> {
};
public render() {
const { validator, editingExistingSpace } = this.props;
const { name = '', description = '' } = this.props.space;
const panelTitle = i18n.translate(
'xpack.spaces.management.manageSpacePage.customizeSpaceTitle',
{
defaultMessage: 'Customize your space',
}
);
const extraPopoverProps: Partial<EuiPopoverProps> = {
initialFocus: 'input[name="spaceInitials"]',
};
const { validator, editingExistingSpace, space } = this.props;
const { name = '', description = '' } = space;
const panelTitle = i18n.translate('xpack.spaces.management.manageSpacePage.generalTitle', {
defaultMessage: 'General',
});
return (
<SectionPanel title={panelTitle} description={panelTitle}>
<SectionPanel title={panelTitle}>
<EuiDescribedFormGroup
title={
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription"
defaultMessage="Name your space and customize its avatar."
id="xpack.spaces.management.manageSpacePage.describeSpaceTitle"
defaultMessage="Describe this space"
/>
</h3>
</EuiTitle>
}
description={this.getPanelDescription()}
description={i18n.translate(
'xpack.spaces.management.manageSpacePage.describeSpaceDescription',
{
defaultMessage: "Give your space a name that's memorable.",
}
)}
fullWidth
>
<EuiFormRow
@ -94,43 +88,33 @@ export class CustomizeSpace extends Component<Props, State> {
<EuiFieldText
name="name"
data-test-subj="addSpaceName"
placeholder={i18n.translate(
'xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder',
{
defaultMessage: 'Awesome space',
}
)}
value={name}
value={name ?? ''}
onChange={this.onNameChange}
isInvalid={validator.validateSpaceName(this.props.space).isInvalid}
fullWidth
/>
</EuiFormRow>
<EuiSpacer />
{this.props.space && isReservedSpace(this.props.space) ? null : (
<Fragment>
<SpaceIdentifier
space={this.props.space}
editable={!editingExistingSpace}
onChange={this.onSpaceIdentifierChange}
validator={validator}
/>
</Fragment>
)}
<EuiFormRow
data-test-subj="optionalDescription"
label={i18n.translate(
'xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel',
{
defaultMessage: 'Description (optional)',
defaultMessage: 'Description',
}
)}
labelAppend={
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.optionalLabel"
defaultMessage="Optional"
/>
</EuiText>
}
helpText={i18n.translate(
'xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText',
{
defaultMessage: 'The description appears on the Space selection screen.',
defaultMessage: 'The description appears on the space selection screen.',
}
)}
{...validator.validateSpaceDescription(this.props.space)}
@ -139,80 +123,97 @@ export class CustomizeSpace extends Component<Props, State> {
<EuiTextArea
data-test-subj="descriptionSpaceText"
name="description"
value={description}
value={description ?? ''}
onChange={this.onDescriptionChange}
isInvalid={validator.validateSpaceDescription(this.props.space).isInvalid}
fullWidth
rows={2}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.spaces.management.manageSpacePage.avatarFormRowLabel', {
defaultMessage: 'Avatar',
})}
>
<EuiPopover
id="customizeAvatarPopover"
button={
<button
title={i18n.translate(
'xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip',
{
defaultMessage: 'Click to customize this space avatar',
}
)}
onClick={this.togglePopover}
>
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar space={this.props.space} size="l" />
</Suspense>
</button>
{editingExistingSpace ? null : (
<EuiFormRow
label={
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.urlIdentifierTitle"
defaultMessage="URL identifier"
/>
}
closePopover={this.closePopover}
{...extraPopoverProps}
ownFocus={true}
isOpen={this.state.customizingAvatar}
helpText={
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.kibanaURLForSpaceIdentifierDescription"
defaultMessage="You can't change the URL identifier once created."
/>
}
{...this.props.validator.validateURLIdentifier(this.props.space)}
fullWidth
>
<div style={{ maxWidth: 240 }}>
<CustomizeSpaceAvatar space={this.props.space} onChange={this.onAvatarChange} />
</div>
</EuiPopover>
</EuiFormRow>
<EuiFieldText
data-test-subj="spaceURLDisplay"
value={this.props.space.id ?? ''}
onChange={this.onSpaceIdentifierChange}
isInvalid={this.props.validator.validateURLIdentifier(this.props.space).isInvalid}
fullWidth
/>
</EuiFormRow>
)}
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
title={
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.avatarTitle"
defaultMessage="Create an avatar"
/>
</h3>
</EuiTitle>
}
description={
<>
<p>
{i18n.translate('xpack.spaces.management.manageSpacePage.avatarDescription', {
defaultMessage: 'Choose how your space avatar appears across Kibana.',
})}
</p>
{space.avatarType === 'image' ? (
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar
space={{
...space,
initials: '?',
name: undefined,
}}
size="xl"
/>
</Suspense>
) : (
<Suspense fallback={<EuiLoadingSpinner />}>
<LazySpaceAvatar
space={{
name: '?',
...space,
imageUrl: undefined,
}}
size="xl"
/>
</Suspense>
)}
</>
}
fullWidth
>
<CustomizeSpaceAvatar
space={this.props.space}
onChange={this.onAvatarChange}
validator={validator}
/>
</EuiDescribedFormGroup>
</SectionPanel>
);
}
public togglePopover = () => {
this.setState({
customizingAvatar: !this.state.customizingAvatar,
});
};
public closePopover = () => {
this.setState({
customizingAvatar: false,
});
};
public getPanelDescription = () => {
return this.props.editingExistingSpace ? (
<p>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable"
defaultMessage="The url identifier cannot be changed."
/>
</p>
) : (
<p>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable"
defaultMessage="Note the URL identifier. You cannot change it after you create the space."
/>
</p>
);
};
public onNameChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!this.props.space) {
return;
@ -230,6 +231,12 @@ export class CustomizeSpace extends Component<Props, State> {
...this.props.space,
name: e.target.value,
id,
initials: this.props.space.customAvatarInitials
? this.props.space.initials
: getSpaceInitials({ name: e.target.value }),
color: this.props.space.customAvatarColor
? this.props.space.color
: getSpaceColor({ name: e.target.value }),
});
};
@ -240,7 +247,8 @@ export class CustomizeSpace extends Component<Props, State> {
});
};
public onSpaceIdentifierChange = (updatedIdentifier: string) => {
public onSpaceIdentifierChange = (e: ChangeEvent<HTMLInputElement>) => {
const updatedIdentifier = e.target.value;
const usingCustomIdentifier = updatedIdentifier !== toSpaceIdentifier(this.props.space.name);
this.setState({
@ -252,7 +260,7 @@ export class CustomizeSpace extends Component<Props, State> {
});
};
public onAvatarChange = (space: Partial<Space>) => {
public onAvatarChange = (space: FormValues) => {
this.props.onChange(space);
};
}

View file

@ -10,6 +10,7 @@ import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { SpaceValidator } from '../../lib';
import { CustomizeSpaceAvatar } from './customize_space_avatar';
const space = {
@ -17,13 +18,19 @@ const space = {
name: '',
};
const validator = new SpaceValidator({ shouldValidate: true });
test('renders without crashing', () => {
const wrapper = shallowWithIntl(<CustomizeSpaceAvatar space={space} onChange={jest.fn()} />);
const wrapper = shallowWithIntl(
<CustomizeSpaceAvatar space={space} validator={validator} onChange={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
});
test('shows customization fields', () => {
const wrapper = mountWithIntl(<CustomizeSpaceAvatar space={space} onChange={jest.fn()} />);
const wrapper = mountWithIntl(
<CustomizeSpaceAvatar space={space} validator={validator} onChange={jest.fn()} />
);
expect(wrapper.find(EuiLink)).toHaveLength(0);
expect(wrapper.find(EuiFieldText)).toHaveLength(2); // EuiColorPicker contains an EuiFieldText element
@ -41,17 +48,14 @@ test('invokes onChange callback when avatar is customized', () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
<CustomizeSpaceAvatar space={customizedSpace} onChange={changeHandler} />
<CustomizeSpaceAvatar space={customizedSpace} validator={validator} onChange={changeHandler} />
);
wrapper
.find(EuiFieldText)
.first()
.find('input')
.simulate('change', { target: { value: 'NV' } });
wrapper.find('input[name="spaceInitials"]').simulate('change', { target: { value: 'NV' } });
expect(changeHandler).toHaveBeenCalledWith({
...customizedSpace,
initials: 'NV',
customAvatarInitials: true,
});
});

View file

@ -6,46 +6,30 @@
*/
import {
EuiButton,
EuiButtonGroup,
EuiColorPicker,
EuiFieldText,
EuiFilePicker,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
isValidHex,
} from '@elastic/eui';
import type { ChangeEvent } from 'react';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import type { Space } from 'src/plugins/spaces_oss/common';
import { MAX_SPACE_INITIALS } from '../../../../common';
import { encode, imageTypes } from '../../../../common/lib/dataurl';
import { getSpaceColor, getSpaceInitials } from '../../../space_avatar';
import type { SpaceValidator } from '../../lib';
import type { FormValues } from '../manage_space_page';
interface Props {
space: Partial<Space>;
onChange: (space: Partial<Space>) => void;
space: FormValues;
onChange: (space: FormValues) => void;
validator: SpaceValidator;
}
interface State {
initialsHasFocus: boolean;
pendingInitials?: string | null;
}
export class CustomizeSpaceAvatar extends Component<Props, State> {
private initialsRef: HTMLInputElement | null = null;
constructor(props: Props) {
super(props);
this.state = {
initialsHasFocus: false,
};
}
private storeImageChanges(imageUrl: string) {
export class CustomizeSpaceAvatar extends Component<Props> {
private storeImageChanges(imageUrl: string | undefined) {
this.props.onChange({
...this.props.space,
imageUrl,
@ -96,7 +80,10 @@ export class CustomizeSpaceAvatar extends Component<Props, State> {
};
private onFileUpload = (files: FileList | null) => {
if (files == null) return;
if (files == null || files.length === 0) {
this.storeImageChanges(undefined);
return;
}
const file = files[0];
if (imageTypes.indexOf(file.type) > -1) {
encode(file).then((dataurl: string) => this.handleImageUpload(dataurl));
@ -106,130 +93,117 @@ export class CustomizeSpaceAvatar extends Component<Props, State> {
public render() {
const { space } = this.props;
const { initialsHasFocus, pendingInitials } = this.state;
const spaceColor = getSpaceColor(space);
const isInvalidSpaceColor = !isValidHex(spaceColor) && spaceColor !== '';
return (
<form onSubmit={() => false}>
<EuiFormRow
label={i18n.translate(
'xpack.spaces.management.customizeSpaceAvatar.initialItemsFormRowLabel',
'xpack.spaces.management.customizeSpaceAvatar.avatarTypeFormRowLabel',
{
defaultMessage: 'Initials (2 max)',
defaultMessage: 'Avatar type',
}
)}
fullWidth
>
<EuiFieldText
inputRef={this.initialsInputRef}
data-test-subj="spaceLetterInitial"
name="spaceInitials"
// allows input to be cleared or otherwise invalidated while user is editing the initials,
// without defaulting to the derived initials provided by `getSpaceInitials`
value={initialsHasFocus ? pendingInitials || '' : getSpaceInitials(space)}
onChange={this.onInitialsChange}
disabled={this.props.space.imageUrl && this.props.space.imageUrl !== '' ? true : false}
<EuiButtonGroup
legend=""
options={[
{
id: `initials`,
label: i18n.translate(
'xpack.spaces.management.customizeSpaceAvatar.initialsLabel',
{
defaultMessage: 'Initials',
}
),
},
{
id: `image`,
label: i18n.translate('xpack.spaces.management.customizeSpaceAvatar.imageLabel', {
defaultMessage: 'Image',
}),
},
]}
idSelected={space.avatarType ?? 'initials'}
onChange={(avatarType: string) =>
this.props.onChange({
...space,
avatarType: avatarType as FormValues['avatarType'],
})
}
buttonSize="m"
/>
</EuiFormRow>
<EuiSpacer size="m" />
{space.avatarType !== 'image' ? (
<EuiFormRow
label={i18n.translate('xpack.spaces.management.customizeSpaceAvatar.initialsLabel', {
defaultMessage: 'Initials',
})}
helpText={i18n.translate(
'xpack.spaces.management.customizeSpaceAvatar.initialsHelpText',
{
defaultMessage: 'Enter up to two characters.',
}
)}
{...this.props.validator.validateAvatarInitials(space)}
fullWidth
>
<EuiFieldText
data-test-subj="spaceLetterInitial"
name="spaceInitials"
value={space.initials ?? ''}
onChange={this.onInitialsChange}
isInvalid={this.props.validator.validateAvatarInitials(space).isInvalid}
fullWidth
/>
</EuiFormRow>
) : (
<EuiFormRow
label={i18n.translate('xpack.spaces.management.customizeSpaceAvatar.imageUrlLabel', {
defaultMessage: 'Image',
})}
{...this.props.validator.validateAvatarImage(space)}
fullWidth
>
<EuiFilePicker
display="default"
data-test-subj="uploadCustomImageFile"
initialPromptText={i18n.translate(
'xpack.spaces.management.customizeSpaceAvatar.imageUrlPromptText',
{
defaultMessage: 'Select image file',
}
)}
onChange={this.onFileUpload}
accept={imageTypes.join(',')}
isInvalid={this.props.validator.validateAvatarImage(space).isInvalid}
fullWidth
/>
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel', {
defaultMessage: 'Color',
label={i18n.translate('xpack.spaces.management.customizeSpaceAvatar.colorLabel', {
defaultMessage: 'Background color',
})}
isInvalid={isInvalidSpaceColor}
{...this.props.validator.validateAvatarColor(space)}
fullWidth
>
<EuiColorPicker
color={spaceColor}
color={space.color ?? ''}
onChange={this.onColorChange}
isInvalid={isInvalidSpaceColor}
isInvalid={this.props.validator.validateAvatarColor(space).isInvalid}
fullWidth
/>
</EuiFormRow>
<EuiSpacer size="m" />
{this.filePickerOrImage()}
</form>
);
}
private removeImageUrl() {
this.props.onChange({
...this.props.space,
imageUrl: '',
});
}
public filePickerOrImage() {
if (!this.props.space.imageUrl) {
return (
<EuiFormRow
label={i18n.translate('xpack.spaces.management.customizeSpaceAvatar.imageUrl', {
defaultMessage: 'Custom image',
})}
>
<EuiFilePicker
display="default"
data-test-subj="uploadCustomImageFile"
initialPromptText={i18n.translate(
'xpack.spaces.management.customizeSpaceAvatar.selectImageUrl',
{
defaultMessage: 'Select image file',
}
)}
onChange={this.onFileUpload}
accept={imageTypes.join(',')}
/>
</EuiFormRow>
);
} else {
return (
<EuiFlexItem grow={true}>
<EuiButton onClick={() => this.removeImageUrl()} color="danger" iconType="trash">
{i18n.translate('xpack.spaces.management.customizeSpaceAvatar.removeImage', {
defaultMessage: 'Remove custom image',
})}
</EuiButton>
</EuiFlexItem>
);
}
}
public initialsInputRef = (ref: HTMLInputElement) => {
if (ref) {
this.initialsRef = ref;
this.initialsRef.addEventListener('focus', this.onInitialsFocus);
this.initialsRef.addEventListener('blur', this.onInitialsBlur);
} else {
if (this.initialsRef) {
this.initialsRef.removeEventListener('focus', this.onInitialsFocus);
this.initialsRef.removeEventListener('blur', this.onInitialsBlur);
this.initialsRef = null;
}
}
};
public onInitialsFocus = () => {
this.setState({
initialsHasFocus: true,
pendingInitials: getSpaceInitials(this.props.space),
});
};
public onInitialsBlur = () => {
this.setState({
initialsHasFocus: false,
pendingInitials: null,
});
};
public onInitialsChange = (e: ChangeEvent<HTMLInputElement>) => {
const initials = (e.target.value || '').substring(0, MAX_SPACE_INITIALS);
this.setState({
pendingInitials: initials,
});
this.props.onChange({
...this.props.space,
customAvatarInitials: true,
initials,
});
};
@ -237,6 +211,7 @@ export class CustomizeSpaceAvatar extends Component<Props, State> {
public onColorChange = (color: string) => {
this.props.onChange({
...this.props.space,
customAvatarColor: true,
color,
});
};

View file

@ -1,29 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { SpaceValidator } from '../../lib';
import { SpaceIdentifier } from './space_identifier';
test('renders without crashing', () => {
const props = {
space: {
id: '',
name: '',
},
editable: true,
onChange: jest.fn(),
validator: new SpaceValidator(),
};
const wrapper = shallowWithIntl(
<SpaceIdentifier.WrappedComponent {...props} intl={null as any} />
);
expect(wrapper).toMatchSnapshot();
});

View file

@ -1,177 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
import type { ChangeEvent } from 'react';
import React, { Component, Fragment } from 'react';
import type { InjectedIntl } from '@kbn/i18n/react';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import type { Space } from 'src/plugins/spaces_oss/common';
import type { SpaceValidator } from '../../lib';
import { toSpaceIdentifier } from '../../lib';
interface Props {
space: Partial<Space>;
editable: boolean;
validator: SpaceValidator;
intl: InjectedIntl;
onChange: (updatedIdentifier: string) => void;
}
interface State {
editing: boolean;
}
class SpaceIdentifierUI extends Component<Props, State> {
private textFieldRef: HTMLInputElement | null = null;
constructor(props: Props) {
super(props);
this.state = {
editing: false,
};
}
public render() {
const { intl } = this.props;
const { id = '' } = this.props.space;
return (
<Fragment>
<EuiFormRow
label={this.getLabel()}
helpText={this.getHelpText(id)}
{...this.props.validator.validateURLIdentifier(this.props.space)}
fullWidth
>
<EuiFieldText
readOnly={!this.state.editing}
data-test-subj="spaceURLDisplay"
placeholder={
this.state.editing || !this.props.editable
? undefined
: intl.formatMessage({
id:
'xpack.spaces.management.spaceIdentifier.urlIdentifierGeneratedFromSpaceNameTooltip',
defaultMessage: 'awesome-space',
})
}
value={id}
onChange={this.onChange}
inputRef={(ref) => (this.textFieldRef = ref)}
fullWidth
/>
</EuiFormRow>
</Fragment>
);
}
public getLabel = () => {
if (!this.props.editable) {
return (
<p>
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.urlIdentifierTitle"
defaultMessage="URL identifier"
/>
</p>
);
}
const editLinkText = this.state.editing ? (
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.resetSpaceNameLinkText"
defaultMessage="[reset]"
/>
) : (
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.customizeSpaceLinkText"
defaultMessage="[customize]"
/>
);
const editLinkLabel = this.state.editing
? this.props.intl.formatMessage({
id: 'xpack.spaces.management.spaceIdentifier.resetSpaceNameLinkLabel',
defaultMessage: 'Reset the URL identifier',
})
: this.props.intl.formatMessage({
id: 'xpack.spaces.management.spaceIdentifier.customizeSpaceNameLinkLabel',
defaultMessage: 'Customize the URL identifier',
});
return (
<p>
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.urlIdentifierLabel"
defaultMessage="URL identifier "
/>
<EuiLink
data-test-subj="CustomizeOrReset"
onClick={this.onEditClick}
aria-label={editLinkLabel}
>
{editLinkText}
</EuiLink>
</p>
);
};
public getHelpText = (
identifier: string = this.props.intl.formatMessage({
id: 'xpack.spaces.management.spaceIdentifier.emptySpaceIdentifierText',
defaultMessage: 'awesome-space',
})
) => {
return (
<p className="eui-textBreakAll">
<FormattedMessage
id="xpack.spaces.management.spaceIdentifier.kibanaURLForSpaceIdentifierDescription"
defaultMessage="Example: https://my-kibana.example{spaceIdentifier}/app/kibana."
values={{
spaceIdentifier: <strong>/s/{identifier}</strong>,
}}
/>
</p>
);
};
public onEditClick = () => {
const currentlyEditing = this.state.editing;
if (currentlyEditing) {
// "Reset" clicked. Create space identifier based on the space name.
const resetIdentifier = toSpaceIdentifier(this.props.space.name);
this.setState({
editing: false,
});
this.props.onChange(resetIdentifier);
} else {
this.setState(
{
editing: true,
},
() => {
if (this.textFieldRef) {
this.textFieldRef.focus();
}
}
);
}
};
public onChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!this.state.editing) {
return;
}
this.props.onChange(e.target.value);
};
}
export const SpaceIdentifier = injectI18n(SpaceIdentifierUI);

View file

@ -3,34 +3,7 @@
exports[`EnabledFeatures renders as expected 1`] = `
<SectionPanel
data-test-subj="enabled-features-panel"
description="Customize visible features"
title={
<span>
<FormattedMessage
defaultMessage="Features"
id="xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage"
values={Object {}}
/>
<EuiText
color="danger"
size="s"
style={
Object {
"display": "inline-block",
}
}
>
<em>
<FormattedMessage
defaultMessage="(no features visible)"
id="xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage"
values={Object {}}
/>
</em>
</EuiText>
</span>
}
title="Features"
>
<EuiFlexGroup>
<EuiFlexItem>
@ -39,7 +12,7 @@ exports[`EnabledFeatures renders as expected 1`] = `
>
<h3>
<FormattedMessage
defaultMessage="Set feature visibility for this space"
defaultMessage="Set feature visibility"
id="xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage"
values={Object {}}
/>
@ -54,9 +27,17 @@ exports[`EnabledFeatures renders as expected 1`] = `
>
<p>
<FormattedMessage
defaultMessage="The feature is hidden in the UI, but is not disabled."
defaultMessage="Hidden features are removed from the user interface, but not disabled. To secure access to features, {manageRolesLink}."
id="xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage"
values={Object {}}
values={
Object {
"manageRolesLink": <FormattedMessage
defaultMessage="manage security roles"
id="xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText"
values={Object {}}
/>,
}
}
/>
</p>
</EuiText>

View file

@ -32,11 +32,9 @@ const features: KibanaFeatureConfig[] = [
];
describe('EnabledFeatures', () => {
const getUrlForApp = (appId: string) => appId;
it(`renders as expected`, () => {
expect(
shallowWithIntl<EnabledFeatures>(
shallowWithIntl(
<EnabledFeatures
features={features}
space={{
@ -45,7 +43,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: ['feature-1', 'feature-2'],
}}
onChange={jest.fn()}
getUrlForApp={getUrlForApp}
/>
)
).toMatchSnapshot();
@ -63,7 +60,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: ['feature-1', 'feature-2'],
}}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
@ -97,7 +93,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: [],
}}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
@ -134,7 +129,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: [],
}}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
@ -164,7 +158,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: ['feature-1', 'feature-2'],
}}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
@ -192,7 +185,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: ['feature-1'],
}}
onChange={jest.fn()}
getUrlForApp={getUrlForApp}
/>
);
expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(1);
@ -211,7 +203,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: [],
}}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);
@ -239,7 +230,6 @@ describe('EnabledFeatures', () => {
disabledFeatures: [],
}}
onChange={changeHandler}
getUrlForApp={getUrlForApp}
/>
);

View file

@ -5,17 +5,16 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { ReactNode } from 'react';
import React, { Component, Fragment } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { ApplicationStart } from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import type { KibanaFeatureConfig } from '../../../../../features/public';
import { getEnabledFeatures } from '../../lib/feature_utils';
import { SectionPanel } from '../section_panel';
import { FeatureTable } from './feature_table';
@ -23,117 +22,62 @@ interface Props {
space: Partial<Space>;
features: KibanaFeatureConfig[];
onChange: (space: Partial<Space>) => void;
getUrlForApp: ApplicationStart['getUrlForApp'];
}
export class EnabledFeatures extends Component<Props, {}> {
public render() {
const description = i18n.translate(
'xpack.spaces.management.manageSpacePage.customizeVisibleFeatures',
{
defaultMessage: 'Customize visible features',
}
);
export const EnabledFeatures: FunctionComponent<Props> = (props) => {
const { services } = useKibana();
const canManageRoles = services.application?.capabilities.management?.security?.roles === true;
return (
<SectionPanel
title={this.getPanelTitle()}
description={description}
data-test-subj="enabled-features-panel"
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage"
defaultMessage="Set feature visibility for this space"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
{this.getDescription()}
</EuiFlexItem>
<EuiFlexItem>
<FeatureTable
features={this.props.features}
space={this.props.space}
onChange={this.props.onChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</SectionPanel>
);
}
private getPanelTitle = () => {
const featureCount = this.props.features.length;
const enabledCount = getEnabledFeatures(this.props.features, this.props.space).length;
let details: null | ReactNode = null;
if (enabledCount === featureCount) {
details = (
<EuiText size={'s'} style={{ display: 'inline-block' }}>
<em>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage"
defaultMessage="(all features visible)"
/>
</em>
</EuiText>
);
} else if (enabledCount === 0) {
details = (
<EuiText color="danger" size={'s'} style={{ display: 'inline-block' }}>
<em>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage"
defaultMessage="(no features visible)"
/>
</em>
</EuiText>
);
} else {
details = (
<EuiText size={'s'} style={{ display: 'inline-block' }}>
<em>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage"
defaultMessage="({enabledCount} / {featureCount} features visible)"
values={{
enabledCount,
featureCount,
}}
/>
</em>
</EuiText>
);
}
return (
<span>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage"
defaultMessage="Features"
/>{' '}
{details}
</span>
);
};
private getDescription = () => {
return (
<Fragment>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage"
defaultMessage="The feature is hidden in the UI, but is not disabled."
/>
</p>
</EuiText>
</Fragment>
);
};
}
return (
<SectionPanel
title={i18n.translate('xpack.spaces.management.manageSpacePage.featuresTitle', {
defaultMessage: 'Features',
})}
data-test-subj="enabled-features-panel"
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage"
defaultMessage="Set feature visibility"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage"
defaultMessage="Hidden features are removed from the user interface, but not disabled. To secure access to features, {manageRolesLink}."
values={{
manageRolesLink: canManageRoles ? (
<EuiLink
href={services.application?.getUrlForApp('management', {
path: '/security/roles',
})}
>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText"
defaultMessage="manage security roles"
/>
</EuiLink>
) : (
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText"
defaultMessage="manage security roles"
/>
),
}}
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<FeatureTable features={props.features} space={props.space} onChange={props.onChange} />
</EuiFlexItem>
</EuiFlexGroup>
</SectionPanel>
);
};

View file

@ -1,4 +0,0 @@
.spcFeatureTableAccordionContent {
// Align accordion content with the feature category logo in the accordion's buttonContent
padding-left: $euiSizeXL;
}

View file

@ -5,18 +5,16 @@
* 2.0.
*/
import './feature_table.scss';
import type { EuiCheckboxProps } from '@elastic/eui';
import {
EuiAccordion,
EuiButtonEmpty,
EuiCallOut,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
@ -88,9 +86,9 @@ export class FeatureTable extends Component<Props, {}> {
const buttonContent = (
<EuiFlexGroup
data-test-subj={`featureCategoryButton_${category.id}`}
alignItems={'center'}
alignItems="center"
responsive={false}
gutterSize="m"
gutterSize="s"
onClick={() => {
if (!canExpandCategory) {
const isChecked = enabledCount > 0;
@ -101,31 +99,28 @@ export class FeatureTable extends Component<Props, {}> {
}
}}
>
<EuiFlexItem grow={false}>
<EuiCheckbox {...checkboxProps} />
</EuiFlexItem>
{category.euiIconType ? (
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={category.euiIconType} />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={1}>
<EuiTitle size="xs">
<h4 className="eui-displayInlineBlock">{category.label}</h4>
<EuiTitle size="xxs">
<h4>{category.label}</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
const label: string = i18n.translate('xpack.spaces.management.featureAccordionSwitchLabel', {
defaultMessage: '{enabledCount} / {featureCount} features visible',
defaultMessage: '{enabledCount}/{featureCount} features visible',
values: {
enabledCount,
featureCount,
},
});
const extraAction = (
<EuiText size="s" aria-hidden="true" color={'subdued'}>
<EuiText size="xs" aria-hidden="true" color="subdued">
{label}
</EuiText>
);
@ -133,46 +128,50 @@ export class FeatureTable extends Component<Props, {}> {
const helpText = this.getCategoryHelpText(category);
const accordion = (
<EuiAccordion
id={`featureCategory_${category.id}`}
data-test-subj={`featureCategory_${category.id}`}
key={category.id}
arrowDisplay={canExpandCategory ? 'right' : 'none'}
forceState={canExpandCategory ? undefined : 'closed'}
buttonContent={buttonContent}
extraAction={canExpandCategory ? extraAction : undefined}
>
<div className="spcFeatureTableAccordionContent">
<EuiSpacer size="s" />
{helpText && (
<>
<EuiCallOut iconType="iInCircle" size="s">
{helpText}
</EuiCallOut>
<EuiSpacer size="s" />
</>
)}
{featuresInCategory.map((feature) => {
const featureChecked = !(
space.disabledFeatures && space.disabledFeatures.includes(feature.id)
);
<EuiFlexGroup key={category.id} alignItems="baseline" responsive={false} gutterSize="s">
<EuiFlexItem grow={false}>
<EuiCheckbox {...checkboxProps} />
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiAccordion
id={`featureCategory_${category.id}`}
data-test-subj={`featureCategory_${category.id}`}
arrowDisplay={canExpandCategory ? 'right' : 'none'}
forceState={canExpandCategory ? undefined : 'closed'}
buttonContent={buttonContent}
extraAction={canExpandCategory ? extraAction : undefined}
>
<EuiSpacer size="m" />
{helpText && (
<>
<EuiCallOut iconType="iInCircle" size="s">
{helpText}
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
{featuresInCategory.map((feature) => {
const featureChecked = !(
space.disabledFeatures && space.disabledFeatures.includes(feature.id)
);
return (
<EuiFlexGroup key={`${feature.id}-toggle`}>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`featureCheckbox_${feature.id}`}
data-test-subj={`featureCheckbox_${feature.id}`}
checked={featureChecked}
onChange={this.onChange(feature.id) as any}
label={feature.name}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</div>
</EuiAccordion>
return (
<EuiFlexGroup key={`${feature.id}-toggle`}>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`featureCheckbox_${feature.id}`}
data-test-subj={`featureCheckbox_${feature.id}`}
checked={featureChecked}
onChange={this.onChange(feature.id) as any}
label={feature.name}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</EuiAccordion>
</EuiFlexItem>
</EuiFlexGroup>
);
accordions.push({
@ -188,30 +187,34 @@ export class FeatureTable extends Component<Props, {}> {
const controls = [];
if (enabledCount < featureCount) {
controls.push(
<EuiLink onClick={() => this.showAll()} data-test-subj="showAllFeaturesLink">
<EuiText size="xs">
{i18n.translate('xpack.spaces.management.selectAllFeaturesLink', {
defaultMessage: 'Select all',
})}
</EuiText>
</EuiLink>
<EuiButtonEmpty
onClick={() => this.showAll()}
size="xs"
data-test-subj="showAllFeaturesLink"
>
{i18n.translate('xpack.spaces.management.selectAllFeaturesLink', {
defaultMessage: 'Show all',
})}
</EuiButtonEmpty>
);
}
if (enabledCount > 0) {
controls.push(
<EuiLink onClick={() => this.hideAll()} data-test-subj="hideAllFeaturesLink">
<EuiText size="xs">
{i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', {
defaultMessage: 'Deselect all',
})}
</EuiText>
</EuiLink>
<EuiButtonEmpty
onClick={() => this.hideAll()}
size="xs"
data-test-subj="hideAllFeaturesLink"
>
{i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', {
defaultMessage: 'Hide all',
})}
</EuiButtonEmpty>
);
}
return (
<div>
<EuiFlexGroup alignItems={'flexEnd'}>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiText size="xs">
<b>
@ -227,10 +230,10 @@ export class FeatureTable extends Component<Props, {}> {
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiHorizontalRule margin={'m'} />
<EuiHorizontalRule margin="m" />
{accordions.flatMap((a, idx) => [
a.element,
<EuiHorizontalRule key={`accordion-hr-${idx}`} margin={'m'} />,
<EuiHorizontalRule key={`accordion-hr-${idx}`} margin="m" />,
])}
</div>
);

View file

@ -55,7 +55,6 @@ describe('ManageSpacePage', () => {
});
});
const getUrlForApp = (appId: string) => appId;
const history = scopedHistoryMock.create();
it('allows a space to be created', async () => {
@ -68,7 +67,6 @@ describe('ManageSpacePage', () => {
spacesManager={(spacesManager as unknown) as SpacesManager}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
getUrlForApp={getUrlForApp}
history={history}
capabilities={{
navLinks: {},
@ -98,8 +96,9 @@ describe('ManageSpacePage', () => {
id: 'new-space-name',
name: 'New Space Name',
description: 'some description',
color: undefined,
initials: undefined,
initials: 'NS',
color: '#AA6556',
imageUrl: '',
disabledFeatures: [],
});
});
@ -129,7 +128,6 @@ describe('ManageSpacePage', () => {
onLoadSpace={onLoadSpace}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
getUrlForApp={getUrlForApp}
history={history}
capabilities={{
navLinks: {},
@ -161,8 +159,9 @@ describe('ManageSpacePage', () => {
id: 'existing-space',
name: 'New Space Name',
description: 'some description',
color: '#aabbcc',
color: '#AABBCC',
initials: 'AB',
imageUrl: '',
disabledFeatures: ['feature-1'],
});
});
@ -181,7 +180,6 @@ describe('ManageSpacePage', () => {
spacesManager={(spacesManager as unknown) as SpacesManager}
getFeatures={() => Promise.reject(error)}
notifications={notifications}
getUrlForApp={getUrlForApp}
history={history}
capabilities={{
navLinks: {},
@ -218,7 +216,6 @@ describe('ManageSpacePage', () => {
spacesManager={(spacesManager as unknown) as SpacesManager}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
getUrlForApp={getUrlForApp}
history={history}
capabilities={{
navLinks: {},
@ -279,7 +276,6 @@ describe('ManageSpacePage', () => {
spacesManager={(spacesManager as unknown) as SpacesManager}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
getUrlForApp={getUrlForApp}
history={history}
capabilities={{
navLinks: {},

View file

@ -11,27 +11,25 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiSpacer,
EuiTitle,
hexToHsv,
hsvToHex,
} from '@elastic/eui';
import _ from 'lodash';
import React, { Component, Fragment } from 'react';
import { difference } from 'lodash';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type {
ApplicationStart,
Capabilities,
NotificationsStart,
ScopedHistory,
} from 'src/core/public';
import type { Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public';
import type { Space } from 'src/plugins/spaces_oss/common';
import { SectionLoading } from '../../../../../../src/plugins/es_ui_shared/public';
import type { FeaturesPluginStart, KibanaFeature } from '../../../../features/public';
import { isReservedSpace } from '../../../common';
import { getSpacesFeatureDescription } from '../../constants';
import { getSpaceColor, getSpaceInitials } from '../../space_avatar';
import type { SpacesManager } from '../../spaces_manager';
import { UnauthorizedPrompt } from '../components';
import { toSpaceIdentifier } from '../lib';
@ -40,7 +38,13 @@ import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal
import { CustomizeSpace } from './customize_space';
import { DeleteSpacesButton } from './delete_spaces_button';
import { EnabledFeatures } from './enabled_features';
import { ReservedSpaceBadge } from './reserved_space_badge';
export interface FormValues extends Partial<Space> {
customIdentifier?: boolean;
avatarType?: 'initials' | 'image';
customAvatarInitials?: boolean;
customAvatarColor?: boolean;
}
interface Props {
getFeatures: FeaturesPluginStart['getFeatures'];
@ -50,11 +54,10 @@ interface Props {
onLoadSpace?: (space: Space) => void;
capabilities: Capabilities;
history: ScopedHistory;
getUrlForApp: ApplicationStart['getUrlForApp'];
}
interface State {
space: Partial<Space>;
space: FormValues;
features: KibanaFeature[];
originalSpace?: Partial<Space>;
showAlteringActiveSpaceDialog: boolean;
@ -76,7 +79,9 @@ export class ManageSpacePage extends Component<Props, State> {
isLoading: true,
showAlteringActiveSpaceDialog: false,
saveInProgress: false,
space: {},
space: {
color: getSpaceColor({}),
},
features: [],
};
}
@ -124,16 +129,12 @@ export class ManageSpacePage extends Component<Props, State> {
}
return (
<Fragment>
<EuiPageHeader
bottomBorder
pageTitle={this.getTitle()}
description={getSpacesFeatureDescription()}
/>
<EuiPageContentBody restrictWidth>
<EuiPageHeader pageTitle={this.getTitle()} description={getSpacesFeatureDescription()} />
<EuiSpacer size="l" />
{this.getForm()}
</Fragment>
</EuiPageContentBody>
);
}
@ -166,7 +167,6 @@ export class ManageSpacePage extends Component<Props, State> {
space={this.state.space}
features={this.state.features}
onChange={this.onSpaceChange}
getUrlForApp={this.props.getUrlForApp}
/>
<EuiSpacer />
@ -185,27 +185,19 @@ export class ManageSpacePage extends Component<Props, State> {
);
};
public getFormHeading = () => (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h1 className="eui-displayInlineBlock">{this.getTitle()}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ReservedSpaceBadge space={this.state.space as Space} />
</EuiFlexItem>
</EuiFlexGroup>
);
public getTitle = () => {
if (this.editingExistingSpace()) {
return `Edit space`;
return (
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.editSpaceTitle"
defaultMessage="Edit space"
/>
);
}
return (
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.createSpaceTitle"
defaultMessage="Create a space"
defaultMessage="Create space"
/>
);
};
@ -274,7 +266,7 @@ export class ManageSpacePage extends Component<Props, State> {
return null;
};
public onSpaceChange = (updatedSpace: Partial<Space>) => {
public onSpaceChange = (updatedSpace: FormValues) => {
this.setState({
space: updatedSpace,
});
@ -283,7 +275,9 @@ export class ManageSpacePage extends Component<Props, State> {
public saveSpace = () => {
this.validator.enableValidation();
const result = this.validator.validateForSave(this.state.space as Space);
const originalSpace: Space = this.state.originalSpace as Space;
const space: Space = this.state.space as Space;
const result = this.validator.validateForSave(space);
if (result.isInvalid) {
this.setState({
formError: result,
@ -295,15 +289,12 @@ export class ManageSpacePage extends Component<Props, State> {
if (this.editingExistingSpace()) {
const { spacesManager } = this.props;
const originalSpace: Space = this.state.originalSpace as Space;
const space: Space = this.state.space as Space;
spacesManager.getActiveSpace().then((activeSpace) => {
const editingActiveSpace = activeSpace.id === originalSpace.id;
const haveDisabledFeaturesChanged =
space.disabledFeatures.length !== originalSpace.disabledFeatures.length ||
_.difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0;
difference(space.disabledFeatures, originalSpace.disabledFeatures).length > 0;
if (editingActiveSpace && haveDisabledFeaturesChanged) {
this.setState({
@ -333,7 +324,14 @@ export class ManageSpacePage extends Component<Props, State> {
}
this.setState({
space,
space: {
...space,
avatarType: space.imageUrl ? 'image' : 'initials',
initials: space.initials || getSpaceInitials(space),
customIdentifier: false,
customAvatarInitials: getSpaceInitials({ name: space.name }) !== space.initials,
customAvatarColor: getSpaceColor({ name: space.name }) !== space.color,
},
features,
originalSpace: space,
isLoading: false,
@ -365,16 +363,17 @@ export class ManageSpacePage extends Component<Props, State> {
color,
disabledFeatures = [],
imageUrl,
avatarType,
} = this.state.space;
const params = {
name,
id,
description,
initials,
color,
initials: avatarType !== 'image' ? initials : '',
color: color ? hsvToHex(hexToHsv(color)).toUpperCase() : color, // Convert 3 digit hex codes to 6 digits since Spaces API requires 6 digits
disabledFeatures,
imageUrl,
imageUrl: avatarType === 'image' ? imageUrl : '',
};
let action;

View file

@ -14,7 +14,7 @@ exports[`it renders without blowing up 1`] = `
grow={false}
>
<EuiTitle
size="m"
size="s"
>
<h2>
<EuiIcon

View file

@ -13,7 +13,7 @@ import { SectionPanel } from './section_panel';
test('it renders without blowing up', () => {
const wrapper = shallowWithIntl(
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch">
<p>child</p>
</SectionPanel>
);
@ -23,7 +23,7 @@ test('it renders without blowing up', () => {
test('it renders children', () => {
const wrapper = mountWithIntl(
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch" description="desc">
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch">
<p className="child">child 1</p>
<p className="child">child 2</p>
</SectionPanel>

View file

@ -13,7 +13,6 @@ import React, { Component, Fragment } from 'react';
interface Props {
iconType?: IconType;
title: string | ReactNode;
description: string;
}
export class SectionPanel extends Component<Props, {}> {
@ -30,7 +29,7 @@ export class SectionPanel extends Component<Props, {}> {
return (
<EuiFlexGroup alignItems={'baseline'} gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<EuiTitle size="s">
<h2>
{this.props.iconType && (
<Fragment>

View file

@ -9,20 +9,20 @@ import { SpaceValidator } from './validate_space';
let validator: SpaceValidator;
describe('validateSpaceName', () => {
beforeEach(() => {
validator = new SpaceValidator({
shouldValidate: true,
});
beforeEach(() => {
validator = new SpaceValidator({
shouldValidate: true,
});
});
describe('validateSpaceName', () => {
test('it allows a name with special characters', () => {
const space = {
id: '',
name: 'This is the name of my Space! @#$%^&*()_+-=',
};
expect(validator.validateSpaceName(space)).toEqual({ isInvalid: false });
expect(validator.validateSpaceName(space)).toHaveProperty('isInvalid', false);
});
test('it requires a non-empty value', () => {
@ -31,10 +31,7 @@ describe('validateSpaceName', () => {
name: '',
};
expect(validator.validateSpaceName(space)).toEqual({
isInvalid: true,
error: `Name is required.`,
});
expect(validator.validateSpaceName(space)).toHaveProperty('isInvalid', true);
});
test('it cannot be composed entirely of whitespace', () => {
@ -43,10 +40,7 @@ describe('validateSpaceName', () => {
name: ' ',
};
expect(validator.validateSpaceName(space)).toEqual({
isInvalid: true,
error: `Name is required.`,
});
expect(validator.validateSpaceName(space)).toHaveProperty('isInvalid', true);
});
test('it cannot exceed 1024 characters', () => {
@ -55,10 +49,7 @@ describe('validateSpaceName', () => {
name: new Array(1026).join('A'),
};
expect(validator.validateSpaceName(space)).toEqual({
isInvalid: true,
error: `Name must not exceed 1024 characters.`,
});
expect(validator.validateSpaceName(space)).toHaveProperty('isInvalid', true);
});
});
@ -69,7 +60,7 @@ describe('validateSpaceDescription', () => {
name: '',
};
expect(validator.validateSpaceDescription(space)).toEqual({ isInvalid: false });
expect(validator.validateSpaceDescription(space)).toHaveProperty('isInvalid', false);
});
test('it cannot exceed 2000 characters', () => {
@ -79,10 +70,7 @@ describe('validateSpaceDescription', () => {
description: new Array(2002).join('A'),
};
expect(validator.validateSpaceDescription(space)).toEqual({
isInvalid: true,
error: `Description must not exceed 2000 characters.`,
});
expect(validator.validateSpaceDescription(space)).toHaveProperty('isInvalid', true);
});
});
@ -94,7 +82,7 @@ describe('validateURLIdentifier', () => {
_reserved: true,
};
expect(validator.validateURLIdentifier(space)).toEqual({ isInvalid: false });
expect(validator.validateURLIdentifier(space)).toHaveProperty('isInvalid', false);
});
test('it requires a non-empty value', () => {
@ -103,10 +91,7 @@ describe('validateURLIdentifier', () => {
name: '',
};
expect(validator.validateURLIdentifier(space)).toEqual({
isInvalid: true,
error: `URL identifier is required.`,
});
expect(validator.validateURLIdentifier(space)).toHaveProperty('isInvalid', true);
});
test('it requires a valid Space Identifier', () => {
@ -115,10 +100,7 @@ describe('validateURLIdentifier', () => {
name: '',
};
expect(validator.validateURLIdentifier(space)).toEqual({
isInvalid: true,
error: 'URL identifier can only contain a-z, 0-9, and the characters "_" and "-".',
});
expect(validator.validateURLIdentifier(space)).toHaveProperty('isInvalid', true);
});
test('it allows a valid Space Identifier', () => {
@ -131,6 +113,95 @@ describe('validateURLIdentifier', () => {
});
});
describe('validateAvatarInitials', () => {
it('it allows valid initials', () => {
const space = {
initials: 'FF',
};
expect(validator.validateAvatarInitials(space)).toHaveProperty('isInvalid', false);
});
it('it requires a non-empty value', () => {
const space = {
initials: '',
};
expect(validator.validateAvatarInitials(space)).toHaveProperty('isInvalid', true);
});
it('must not exceed 2 characters', () => {
const space = {
initials: 'FFF',
};
expect(validator.validateAvatarInitials(space)).toHaveProperty('isInvalid', true);
});
it('it does not validate image avatars', () => {
const space = {
avatarType: 'image' as 'image',
initials: '',
};
expect(validator.validateAvatarInitials(space)).toHaveProperty('isInvalid', false);
});
});
describe('validateAvatarColor', () => {
it('it allows valid colors', () => {
const space = {
color: '#000000',
};
expect(validator.validateAvatarColor(space)).toHaveProperty('isInvalid', false);
});
it('it requires a non-empty value', () => {
const space = {
color: '',
};
expect(validator.validateAvatarColor(space)).toHaveProperty('isInvalid', true);
});
it('it requires a valid hex code', () => {
const space = {
color: 'red',
};
expect(validator.validateAvatarColor(space)).toHaveProperty('isInvalid', true);
});
});
describe('validateAvatarImage', () => {
it('it allows valid image url', () => {
const space = {
avatarType: 'image' as 'image',
imageUrl: 'foo',
};
expect(validator.validateAvatarImage(space)).toHaveProperty('isInvalid', false);
});
it('it requires a non-empty value', () => {
const space = {
avatarType: 'image' as 'image',
imageUrl: '',
};
expect(validator.validateAvatarImage(space)).toHaveProperty('isInvalid', true);
});
it('it does not validate non-image avatars', () => {
const space = {
imageUrl: '',
};
expect(validator.validateAvatarImage(space)).toHaveProperty('isInvalid', false);
});
});
describe('validateSpaceFeatures', () => {
it('allows features to be disabled', () => {
const space = {
@ -139,7 +210,7 @@ describe('validateSpaceFeatures', () => {
disabledFeatures: ['foo'],
};
expect(validator.validateEnabledFeatures(space)).toEqual({ isInvalid: false });
expect(validator.validateEnabledFeatures(space)).toHaveProperty('isInvalid', false);
});
it('allows all features to be disabled', () => {
@ -149,8 +220,6 @@ describe('validateSpaceFeatures', () => {
disabledFeatures: ['foo', 'bar'],
};
expect(validator.validateEnabledFeatures(space)).toEqual({
isInvalid: false,
});
expect(validator.validateEnabledFeatures(space)).toHaveProperty('isInvalid', false);
});
});

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import { isValidHex } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { Space } from 'src/plugins/spaces_oss/common';
import { isReservedSpace } from '../../../common/is_reserved_space';
import type { FormValues } from '../edit_space/manage_space_page';
import { isValidSpaceIdentifier } from './space_identifier_utils';
interface SpaceValidatorOptions {
@ -30,7 +32,7 @@ export class SpaceValidator {
this.shouldValidate = false;
}
public validateSpaceName(space: Partial<Space>) {
public validateSpaceName(space: FormValues) {
if (!this.shouldValidate) {
return valid();
}
@ -38,7 +40,7 @@ export class SpaceValidator {
if (!space.name || !space.name.trim()) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.requiredNameErrorMessage', {
defaultMessage: 'Name is required.',
defaultMessage: 'Enter a name.',
})
);
}
@ -54,7 +56,7 @@ export class SpaceValidator {
return valid();
}
public validateSpaceDescription(space: Partial<Space>) {
public validateSpaceDescription(space: FormValues) {
if (!this.shouldValidate) {
return valid();
}
@ -70,7 +72,7 @@ export class SpaceValidator {
return valid();
}
public validateURLIdentifier(space: Partial<Space>) {
public validateURLIdentifier(space: FormValues) {
if (!this.shouldValidate) {
return valid();
}
@ -82,7 +84,7 @@ export class SpaceValidator {
if (!space.id) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.urlIdentifierRequiredErrorMessage', {
defaultMessage: 'URL identifier is required.',
defaultMessage: 'Enter a URL identifier.',
})
);
}
@ -102,17 +104,93 @@ export class SpaceValidator {
return valid();
}
public validateEnabledFeatures(space: Partial<Space>) {
public validateAvatarInitials(space: FormValues) {
if (!this.shouldValidate) {
return valid();
}
if (space.avatarType !== 'image') {
if (!space.initials) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.requiredInitialsErrorMessage', {
defaultMessage: 'Enter initials.',
})
);
}
if (space.initials.length > 2) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.maxLengthInitialsErrorMessage', {
defaultMessage: 'Enter no more than 2 characters.',
})
);
}
}
return valid();
}
public validateForSave(space: Space) {
public validateAvatarColor(space: FormValues) {
if (!this.shouldValidate) {
return valid();
}
if (!space.color) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.requiredColorErrorMessage', {
defaultMessage: 'Select a background color.',
})
);
}
if (!isValidHex(space.color)) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.invalidColorErrorMessage', {
defaultMessage: 'Enter a valid HEX color code.',
})
);
}
return valid();
}
public validateAvatarImage(space: FormValues) {
if (!this.shouldValidate) {
return valid();
}
if (space.avatarType === 'image' && !space.imageUrl) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.requiredImageErrorMessage', {
defaultMessage: 'Upload an image.',
})
);
}
return valid();
}
public validateEnabledFeatures(space: FormValues) {
return valid();
}
public validateForSave(space: FormValues) {
const { isInvalid: isNameInvalid } = this.validateSpaceName(space);
const { isInvalid: isDescriptionInvalid } = this.validateSpaceDescription(space);
const { isInvalid: isIdentifierInvalid } = this.validateURLIdentifier(space);
const { isInvalid: isAvatarInitialsInvalid } = this.validateAvatarInitials(space);
const { isInvalid: isAvatarColorInvalid } = this.validateAvatarColor(space);
const { isInvalid: isAvatarImageInvalid } = this.validateAvatarImage(space);
const { isInvalid: areFeaturesInvalid } = this.validateEnabledFeatures(space);
if (isNameInvalid || isDescriptionInvalid || isIdentifierInvalid || areFeaturesInvalid) {
if (
isNameInvalid ||
isDescriptionInvalid ||
isIdentifierInvalid ||
isAvatarInitialsInvalid ||
isAvatarColorInvalid ||
isAvatarImageInvalid ||
areFeaturesInvalid
) {
return invalid();
}

View file

@ -24,7 +24,7 @@ exports[`SpacesGridPage renders as expected 1`] = `
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Create a space"
defaultMessage="Create space"
id="xpack.spaces.management.spacesGridPage.createSpaceButtonLabel"
values={Object {}}
/>

View file

@ -153,7 +153,7 @@ export class SpacesGridPage extends Component<Props, State> {
>
<FormattedMessage
id="xpack.spaces.management.spacesGridPage.createSpaceButtonLabel"
defaultMessage="Create a space"
defaultMessage="Create space"
/>
</EuiButton>
);

View file

@ -102,7 +102,7 @@ describe('spacesManagementApp', () => {
expect(setBreadcrumbs).toHaveBeenCalledTimes(1);
expect(setBreadcrumbs).toHaveBeenCalledWith([
{ href: `/`, text: 'Spaces' },
{ text: 'Create' },
{ href: '/create', text: 'Create' },
]);
expect(docTitle.change).toHaveBeenCalledWith('Spaces');
expect(docTitle.reset).not.toHaveBeenCalled();

View file

@ -81,6 +81,7 @@ export const spacesManagementApp = Object.freeze({
text: i18n.translate('xpack.spaces.management.createSpaceBreadcrumb', {
defaultMessage: 'Create',
}),
href: '/create',
},
]);
@ -91,7 +92,6 @@ export const spacesManagementApp = Object.freeze({
notifications={notifications}
spacesManager={spacesManager}
history={history}
getUrlForApp={application.getUrlForApp}
/>
);
};
@ -118,7 +118,6 @@ export const spacesManagementApp = Object.freeze({
spaceId={spaceId}
onLoadSpace={onLoadSpace}
history={history}
getUrlForApp={application.getUrlForApp}
/>
);
};

View file

@ -21918,35 +21918,17 @@
"xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "コピー完了",
"xpack.spaces.management.copyToSpaceFlyoutHeader": "スペースにコピー",
"xpack.spaces.management.createSpaceBreadcrumb": "作成",
"xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "色",
"xpack.spaces.management.customizeSpaceAvatar.imageUrl": "カスタム画像",
"xpack.spaces.management.customizeSpaceAvatar.initialItemsFormRowLabel": "イニシャル (最大 2 文字) ",
"xpack.spaces.management.customizeSpaceAvatar.removeImage": "カスタム画像を削除",
"xpack.spaces.management.customizeSpaceAvatar.selectImageUrl": "画像ファイルを選択",
"xpack.spaces.management.deleteSpacesButton.deleteSpaceAriaLabel": "スペースを削除",
"xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "スペースを削除",
"xpack.spaces.management.deselectAllFeaturesLink": "すべて選択解除",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "カテゴリ切り替え",
"xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": " (表示されているすべての機能) ",
"xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースの機能の表示を設定",
"xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": " (表示されている機能がありません) ",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "この機能は UI で非表示になっていますが、無効ではありません。",
"xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": " ({featureCount} 件中 {enabledCount} 件の機能を表示中) ",
"xpack.spaces.management.featureAccordionSwitchLabel": "{featureCount}件中{enabledCount}件の機能を表示中",
"xpack.spaces.management.featureVisibilityTitle": "機能の表示",
"xpack.spaces.management.hideAllFeaturesText": "すべて非表示",
"xpack.spaces.management.managementCategoryHelpText": "スタック管理へのアクセスは割り当てられた権限によって決まり、スペースで非表示にすることはできません。",
"xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "アバター",
"xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "素晴らしいスペース",
"xpack.spaces.management.manageSpacePage.cancelSpaceButton": "キャンセル",
"xpack.spaces.management.manageSpacePage.createSpaceButton": "スペースを作成",
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースの作成",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "スペースに名前を付けてアバターをカスタマイズします。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "URL 識別子に注意してください。スペースの作成後に変更することはできません。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 識別子は変更できません。",
"xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "スペースのカスタマイズ",
"xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "表示される機能のカスタマイズ",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生:{message}",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "利用可能な機能の読み込みエラー",
@ -21958,15 +21940,6 @@
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "リザーブされたスペースはビルトインのため、部分的な変更しかできません。",
"xpack.spaces.management.selectAllFeaturesLink": "すべて選択",
"xpack.spaces.management.showAllFeaturesText": "すべて表示",
"xpack.spaces.management.spaceIdentifier.customizeSpaceLinkText": "[カスタマイズ]",
"xpack.spaces.management.spaceIdentifier.customizeSpaceNameLinkLabel": "URL 識別子をカスタマイズ",
"xpack.spaces.management.spaceIdentifier.emptySpaceIdentifierText": "awesome-space",
"xpack.spaces.management.spaceIdentifier.kibanaURLForSpaceIdentifierDescription": "例https://my-kibana.example{spaceIdentifier}/app/kibana.",
"xpack.spaces.management.spaceIdentifier.resetSpaceNameLinkLabel": "URL 識別子をリセット",
"xpack.spaces.management.spaceIdentifier.resetSpaceNameLinkText": "[リセット]",
"xpack.spaces.management.spaceIdentifier.urlIdentifierGeneratedFromSpaceNameTooltip": "awesome-space",
"xpack.spaces.management.spaceIdentifier.urlIdentifierLabel": "URL 識別子 ",
"xpack.spaces.management.spaceIdentifier.urlIdentifierTitle": "URL 識別子",
"xpack.spaces.management.spacesGridPage.actionsColumnName": "アクション",
"xpack.spaces.management.spacesGridPage.allFeaturesEnabled": "表示されているすべての機能",
"xpack.spaces.management.spacesGridPage.createSpaceButtonLabel": "スペースの作成",

View file

@ -22272,35 +22272,17 @@
"xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "已复制",
"xpack.spaces.management.copyToSpaceFlyoutHeader": "复制到工作区",
"xpack.spaces.management.createSpaceBreadcrumb": "创建",
"xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "颜色",
"xpack.spaces.management.customizeSpaceAvatar.imageUrl": "定制图像",
"xpack.spaces.management.customizeSpaceAvatar.initialItemsFormRowLabel": "名字缩写 (最多两个字符) ",
"xpack.spaces.management.customizeSpaceAvatar.removeImage": "删除定制图像",
"xpack.spaces.management.customizeSpaceAvatar.selectImageUrl": "选择图像文件",
"xpack.spaces.management.deleteSpacesButton.deleteSpaceAriaLabel": "删除此空间",
"xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "删除空间",
"xpack.spaces.management.deselectAllFeaturesLink": "取消全选",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "类别切换",
"xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": " (所有可见功能) ",
"xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "功能",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "为此工作区设置功能可见性",
"xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": " (没有可见功能) ",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "该功能在 UI 中已隐藏,但未禁用。",
"xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": " ({enabledCount} / {featureCount} 个功能可见) ",
"xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount} / {featureCount} 个功能可见",
"xpack.spaces.management.featureVisibilityTitle": "功能可见性",
"xpack.spaces.management.hideAllFeaturesText": "全部隐藏",
"xpack.spaces.management.managementCategoryHelpText": "对堆栈管理的访问由您的权限决定,并且不能被工作区隐藏。",
"xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "头像",
"xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间",
"xpack.spaces.management.manageSpacePage.cancelSpaceButton": "取消",
"xpack.spaces.management.manageSpacePage.createSpaceButton": "创建工作区",
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "命名您的工作区并定制其头像。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "记下 URL 标识符。创建工作区后,将不能更改它。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 标识符无法更改。",
"xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "定制您的工作区",
"xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "定制可见功能",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "加载可用功能时出错",
@ -22312,15 +22294,6 @@
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "保留的工作区是内置的,只能进行部分修改。",
"xpack.spaces.management.selectAllFeaturesLink": "全选",
"xpack.spaces.management.showAllFeaturesText": "全部显示",
"xpack.spaces.management.spaceIdentifier.customizeSpaceLinkText": "[定制]",
"xpack.spaces.management.spaceIdentifier.customizeSpaceNameLinkLabel": "定制 URL 标识符",
"xpack.spaces.management.spaceIdentifier.emptySpaceIdentifierText": "awesome-space",
"xpack.spaces.management.spaceIdentifier.kibanaURLForSpaceIdentifierDescription": "示例https://my-kibana.example{spaceIdentifier}/app/kibana。",
"xpack.spaces.management.spaceIdentifier.resetSpaceNameLinkLabel": "重置 URL 标识符",
"xpack.spaces.management.spaceIdentifier.resetSpaceNameLinkText": "[重置]",
"xpack.spaces.management.spaceIdentifier.urlIdentifierGeneratedFromSpaceNameTooltip": "awesome-space",
"xpack.spaces.management.spaceIdentifier.urlIdentifierLabel": "URL 标识符 ",
"xpack.spaces.management.spaceIdentifier.urlIdentifierTitle": "URL 标识符",
"xpack.spaces.management.spacesGridPage.actionsColumnName": "操作",
"xpack.spaces.management.spacesGridPage.allFeaturesEnabled": "所有可见功能",
"xpack.spaces.management.spacesGridPage.createSpaceButtonLabel": "创建一个空间",
@ -22340,11 +22313,6 @@
"xpack.spaces.management.toggleAllFeaturesLink": " (全部更改) ",
"xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "您无权管理工作区。",
"xpack.spaces.management.unauthorizedPrompt.permissionDeniedTitle": "权限被拒绝",
"xpack.spaces.management.validateSpace.describeMaxLengthErrorMessage": "描述不能超过 2000 个字符。",
"xpack.spaces.management.validateSpace.nameMaxLengthErrorMessage": "名称不能超过 1024 个字符。",
"xpack.spaces.management.validateSpace.requiredNameErrorMessage": "“名称”必填。",
"xpack.spaces.management.validateSpace.urlIdentifierAllowedCharactersErrorMessage": "URL 标识符只能包含 a-z、0-9 和字符“_”及“-”。",
"xpack.spaces.management.validateSpace.urlIdentifierRequiredErrorMessage": "“URL 标识符”必填。",
"xpack.spaces.manageSpacesButton.manageSpacesButtonLabel": "管理空间",
"xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle": "更改当前空间",
"xpack.spaces.navControl.spacesMenu.findSpacePlaceholder": "查找工作区",

View file

@ -46,15 +46,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('a11y test for click on create space page', async () => {
await PageObjects.spaceSelector.clickCreateSpace();
await a11y.testAppSnapshot();
});
it('a11y test for for customize space card', async () => {
await PageObjects.spaceSelector.clickEnterSpaceName();
await PageObjects.spaceSelector.addSpaceName('space_a');
await PageObjects.spaceSelector.clickCustomizeSpaceAvatar('space_a');
await a11y.testAppSnapshot();
await browser.pressKeys(browser.keys.ESCAPE);
});
// EUI issue - https://github.com/elastic/eui/issues/3999
@ -64,18 +58,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await browser.pressKeys(browser.keys.ESCAPE);
});
it('a11y test for customize and reset space URL identifier', async () => {
await PageObjects.spaceSelector.clickOnCustomizeURL();
await a11y.testAppSnapshot();
await PageObjects.spaceSelector.clickOnCustomizeURL();
await a11y.testAppSnapshot();
});
it('a11y test for describe space text space', async () => {
await PageObjects.spaceSelector.clickOnDescriptionOfSpace();
await a11y.testAppSnapshot();
});
it('a11y test for toggling an entire feature category', async () => {
await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana');
await a11y.testAppSnapshot();