mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
2fb1a47137
commit
1f5be1e1e1
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 |
Binary file not shown.
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 571 KiB |
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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',
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
.spcFeatureTableAccordionContent {
|
||||
// Align accordion content with the feature category logo in the accordion's buttonContent
|
||||
padding-left: $euiSizeXL;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -14,7 +14,7 @@ exports[`it renders without blowing up 1`] = `
|
|||
grow={false}
|
||||
>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
size="s"
|
||||
>
|
||||
<h2>
|
||||
<EuiIcon
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {}}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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": "スペースの作成",
|
||||
|
|
|
@ -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": "查找工作区",
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue