[Spaces and Roles] Updates for finalized contents and UX (#193923)

## Summary

Follows https://github.com/elastic/kibana/pull/191795

* Minor content updates to Spaces Management
* [spaces grid] More space for "description" column in Spaces Grid
* [create space and edit space] Add "New" badge to Solution View picker
* [create space and edit space] Move avatar section down
* [create space] Remove the edit/update functionality from the Create
Space page
* [create space] Only show the Feature Visibility section if the
selected solution is `classic`
* [edit space] Rearrange the footer icons in the General tab
* [edit space] Show callout when classic is selected by default
* [edit space] Update the action icons shown on hover on the Assigned
Roles table

### Checklist

Delete any items that are not applicable to this PR.

- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [X] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [X] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Tim Sullivan 2024-10-01 12:22:29 -07:00 committed by GitHub
parent 5da67c71ae
commit 3f901562cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 372 additions and 759 deletions

View file

@ -160,9 +160,7 @@ export class FeatureTable extends Component<Props, State> {
<EuiSpacer size="s" />
{helpText && (
<>
<EuiCallOut iconType="iInCircle" size="s">
{helpText}
</EuiCallOut>
<EuiCallOut size="s" title={helpText} />
<EuiSpacer size="s" />
</>
)}
@ -404,7 +402,7 @@ export class FeatureTable extends Component<Props, State> {
'xpack.security.management.editRole.featureTable.managementCategoryHelpText',
{
defaultMessage:
'Access to Stack Management is determined by both Elasticsearch and Kibana privileges, and cannot be explicitly disabled.',
'Additional Stack Management permissions can be found outside of this menu, in index and cluster privileges.',
}
);
}

View file

@ -0,0 +1,106 @@
/*
* 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 { EuiDescribedFormGroup, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
import React, { Component, lazy, Suspense } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { CustomizeSpaceAvatar } from './customize_space_avatar';
import { getSpaceAvatarComponent } from '../../../space_avatar';
import type { SpaceValidator } from '../../lib';
import type { CustomizeSpaceFormValues } from '../../types';
import { SectionPanel } from '../section_panel';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
validator: SpaceValidator;
space: CustomizeSpaceFormValues;
onChange: (space: CustomizeSpaceFormValues) => void;
title?: string;
}
interface State {
customizingAvatar: boolean;
usingCustomIdentifier: boolean;
}
export class CustomizeAvatar extends Component<Props, State> {
public state = {
customizingAvatar: false,
usingCustomIdentifier: false,
};
public render() {
const { validator, space } = this.props;
return (
<SectionPanel dataTestSubj="customizeAvatarSection">
<EuiDescribedFormGroup
title={
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.avatarTitle"
defaultMessage="Define 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 onAvatarChange = (space: CustomizeSpaceFormValues) => {
this.props.onChange(space);
};
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { CustomizeAvatar } from './customize_avatar';

View file

@ -38,7 +38,7 @@ exports[`renders correctly 1`] = `
<EuiFormRow
data-test-subj="optionalDescription"
fullWidth={true}
helpText="The description appears on the space selection screen."
helpText="Appears on the space selection screen and spaces list."
isInvalid={false}
label="Description"
labelAppend={
@ -89,56 +89,5 @@ exports[`renders correctly 1`] = `
/>
</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>
<Memo(MemoizedFormattedMessage)
defaultMessage="Create an avatar"
id="xpack.spaces.management.manageSpacePage.avatarTitle"
/>
</h3>
</EuiTitle>
}
>
<CustomizeSpaceAvatar
onChange={[Function]}
space={
Object {
"id": "",
"name": "",
}
}
validator={
SpaceValidator {
"shouldValidate": true,
}
}
/>
</EuiDescribedFormGroup>
</SectionPanel>
`;

View file

@ -9,29 +9,22 @@ import {
EuiDescribedFormGroup,
EuiFieldText,
EuiFormRow,
EuiLoadingSpinner,
EuiText,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import type { ChangeEvent } from 'react';
import React, { Component, lazy, Suspense } from 'react';
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { CustomizeSpaceAvatar } from './customize_space_avatar';
import { getSpaceAvatarComponent, getSpaceColor, getSpaceInitials } from '../../../space_avatar';
import { getSpaceColor, getSpaceInitials } from '../../../space_avatar';
import type { SpaceValidator } from '../../lib';
import { toSpaceIdentifier } from '../../lib';
import type { CustomizeSpaceFormValues } from '../../types';
import { SectionPanel } from '../section_panel';
// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana.
const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);
interface Props {
validator: SpaceValidator;
space: CustomizeSpaceFormValues;
@ -112,7 +105,7 @@ export class CustomizeSpace extends Component<Props, State> {
helpText={i18n.translate(
'xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText',
{
defaultMessage: 'The description appears on the space selection screen.',
defaultMessage: 'Appears on the space selection screen and spaces list.',
}
)}
{...validator.validateSpaceDescription(this.props.space)}
@ -156,58 +149,6 @@ export class CustomizeSpace extends Component<Props, State> {
</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>
);
}

View file

@ -2,8 +2,7 @@
exports[`EnabledFeatures renders as expected 1`] = `
<SectionPanel
data-test-subj="enabled-features-panel"
title="Features"
dataTestSubj="enabled-features-panel"
>
<EuiFlexGroup>
<EuiFlexItem>
@ -26,14 +25,16 @@ exports[`EnabledFeatures renders as expected 1`] = `
>
<p>
<MemoizedFormattedMessage
defaultMessage="Hidden features are removed from the user interface, but not disabled. To secure access to features, {manageRolesLink}."
id="xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage"
defaultMessage="Choose the features to display in the navigation menu for users of this space. If you want to focus on a single solution, you can simplify the navigation even more by selecting a {solutionView}."
id="xpack.spaces.management.enabledSpaceFeatures.chooseFeaturesToDisplayMessage"
values={
Object {
"manageRolesLink": <Memo(MemoizedFormattedMessage)
defaultMessage="manage security roles"
id="xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText"
/>,
"solutionView": <strong>
<Memo(MemoizedFormattedMessage)
defaultMessage="Solution view"
id="xpack.spaces.management.enabledSpaceFeatures.chooseFeaturesToDisplaySolutionViewText"
/>
</strong>,
}
}
/>

View file

@ -5,14 +5,12 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import type { KibanaFeatureConfig } from '@kbn/features-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FeatureTable } from './feature_table';
import type { Space } from '../../../../common';
@ -25,16 +23,8 @@ interface Props {
}
export const EnabledFeatures: FunctionComponent<Props> = (props) => {
const { services } = useKibana();
const canManageRoles = services.application?.capabilities.management?.security?.roles === true;
return (
<SectionPanel
title={i18n.translate('xpack.spaces.management.manageSpacePage.featuresTitle', {
defaultMessage: 'Features',
})}
data-test-subj="enabled-features-panel"
>
<SectionPanel dataTestSubj="enabled-features-panel">
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
@ -49,25 +39,16 @@ export const EnabledFeatures: FunctionComponent<Props> = (props) => {
<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}."
id="xpack.spaces.management.enabledSpaceFeatures.chooseFeaturesToDisplayMessage"
defaultMessage="Choose the features to display in the navigation menu for users of this space. If you want to focus on a single solution, you can simplify the navigation even more by selecting a {solutionView}."
values={{
manageRolesLink: canManageRoles ? (
<EuiLink
href={services.application?.getUrlForApp('management', {
path: '/security/roles',
})}
>
solutionView: (
<strong>
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText"
defaultMessage="manage security roles"
id="xpack.spaces.management.enabledSpaceFeatures.chooseFeaturesToDisplaySolutionViewText"
defaultMessage="Solution view"
/>
</EuiLink>
) : (
<FormattedMessage
id="xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText"
defaultMessage="manage security roles"
/>
</strong>
),
}}
/>

View file

@ -7,6 +7,8 @@
import type { EuiSuperSelectOption, EuiThemeComputed } from '@elastic/eui';
import {
EuiBetaBadge,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@ -24,6 +26,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Space } from '../../../../common';
import { SOLUTION_VIEW_CLASSIC } from '../../../../common/constants';
import type { SpaceValidator } from '../../lib';
import { SectionPanel } from '../section_panel';
@ -40,9 +43,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array<EuiSuperSelectOption<Solu
<EuiIcon type="logoElasticsearch" css={iconCss} />
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.searchOptionLabel',
{
defaultMessage: 'Search',
}
{ defaultMessage: 'Search' }
)}
</>
),
@ -55,9 +56,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array<EuiSuperSelectOption<Solu
<EuiIcon type="logoObservability" css={iconCss} />
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.obltOptionLabel',
{
defaultMessage: 'Observability',
}
{ defaultMessage: 'Observability' }
)}
</>
),
@ -70,9 +69,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array<EuiSuperSelectOption<Solu
<EuiIcon type="logoSecurity" css={iconCss} />
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.securityOptionLabel',
{
defaultMessage: 'Security',
}
{ defaultMessage: 'Security' }
)}
</>
),
@ -85,9 +82,7 @@ const getOptions = ({ size }: EuiThemeComputed): Array<EuiSuperSelectOption<Solu
<EuiIcon type="logoKibana" css={iconCss} />
{i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.classicOptionLabel',
{
defaultMessage: 'Classic',
}
{ defaultMessage: 'Classic' }
)}
</>
),
@ -112,25 +107,40 @@ export const SolutionView: FunctionComponent<Props> = ({
sectionTitle,
}) => {
const { euiTheme } = useEuiTheme();
const showClassicDefaultViewCallout = isEditing && space.solution == null;
return (
<SectionPanel title={sectionTitle} dataTestSubj="navigationPanel">
<EuiFlexGroup>
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.setSolutionViewMessage"
defaultMessage="Set solution view"
/>
</h3>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<h3>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.setSolutionViewMessage"
defaultMessage="Select solution view"
/>
</h3>
</EuiFlexItem>
<EuiFlexItem>
<EuiBetaBadge
label={i18n.translate(
'xpack.spaces.management.manageSpacePage.setSolutionViewNewBadge',
{ defaultMessage: 'New' }
)}
color="accent"
size="s"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.setSolutionViewDescription"
defaultMessage="Determines the navigation all users will see for this space. Each solution view contains features from Analytics tools and Management."
defaultMessage="Focus the navigation and menus of this space on a specific solution. Features that are not relevant to the selected solution are no longer visible to users of this space."
/>
</p>
</EuiText>
@ -145,20 +155,43 @@ export const SolutionView: FunctionComponent<Props> = ({
>
<EuiSuperSelect
options={getOptions(euiTheme)}
valueOfSelected={space.solution}
valueOfSelected={
space.solution ??
(showClassicDefaultViewCallout ? SOLUTION_VIEW_CLASSIC : undefined)
}
data-test-subj="solutionViewSelect"
onChange={(solution) => {
onChange({ ...space, solution });
}}
placeholder={i18n.translate(
'xpack.spaces.management.navigation.solutionViewDefaultValue',
{
defaultMessage: 'Select view',
}
{ defaultMessage: 'Select solution view' }
)}
isInvalid={validator.validateSolutionView(space, isEditing).isInvalid}
/>
</EuiFormRow>
{showClassicDefaultViewCallout && (
<>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.solutionViewSelect.classicDefaultViewCallout"
defaultMessage="Affects all users of the space"
/>
</EuiText>
<EuiSpacer />
<EuiCallOut
color="primary"
size="s"
iconType="iInCircle"
title={i18n.translate(
'xpack.spaces.management.manageSpacePage.solutionViewSelect.classicDefaultViewCallout',
{ defaultMessage: 'By default your current view is Classic' }
)}
/>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</SectionPanel>

View file

@ -6,7 +6,6 @@
*/
import type { EuiCheckboxProps } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import type { ReactWrapper } from 'enzyme';
import React from 'react';
@ -23,7 +22,6 @@ import type { SolutionView, Space } from '../../../common/types/latest';
import { EventTracker } from '../../analytics';
import type { SpacesManager } from '../../spaces_manager';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { ConfirmAlterActiveSpaceModal } from '../components/confirm_alter_active_space_modal';
import { EnabledFeatures } from '../components/enabled_features';
// To be resolved by EUI team.
@ -153,8 +151,8 @@ describe('ManageSpacePage', () => {
expect(errors).toEqual([
'Enter a name.',
'Enter a URL identifier.',
'Select a solution.',
'Enter initials.',
'Select one solution.',
]);
expect(spacesManager.createSpace).not.toHaveBeenCalled();
@ -168,7 +166,7 @@ describe('ManageSpacePage', () => {
{
const errors = wrapper.find('div.euiFormErrorText').map((node) => node.text());
expect(errors).toEqual(['Select one solution.']); // requires solution view to be set
expect(errors).toEqual(['Select a solution.']); // requires solution view to be set
}
updateSpace(wrapper, false, 'oblt');
@ -274,7 +272,13 @@ describe('ManageSpacePage', () => {
expect(wrapper.find('input[name="name"]')).toHaveLength(1);
});
expect(wrapper.find(EnabledFeatures)).toHaveLength(1);
// expect visible features table to exist after setting the Solution View to Classic
await waitFor(() => {
// switch to classic
updateSpace(wrapper, false, 'classic');
// expect visible features table to exist again
expect(wrapper.find(EnabledFeatures)).toHaveLength(1);
});
});
it('hides feature visibility controls when not allowed', async () => {
@ -333,9 +337,6 @@ describe('ManageSpacePage', () => {
await Promise.resolve();
wrapper.update();
// default for create space: expect visible features table to exist
expect(wrapper.find(EnabledFeatures)).toHaveLength(1);
});
await waitFor(() => {
@ -353,147 +354,6 @@ describe('ManageSpacePage', () => {
});
});
it('allows a space to be updated', async () => {
const spaceToUpdate = {
id: 'existing-space',
name: 'Existing Space',
description: 'hey an existing space',
color: '#aabbcc',
initials: 'AB',
disabledFeatures: [],
solution: 'es',
};
const spacesManager = spacesManagerMock.create();
spacesManager.getSpace = jest.fn().mockResolvedValue({
...spaceToUpdate,
});
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
const onLoadSpace = jest.fn();
const wrapper = mountWithIntl(
<CreateSpacePage
spaceId={'existing-space'}
spacesManager={spacesManager as unknown as SpacesManager}
onLoadSpace={onLoadSpace}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
history={history}
capabilities={{
navLinks: {},
management: {},
catalogue: {},
spaces: { manage: true },
}}
eventTracker={eventTracker}
allowFeatureVisibility
allowSolutionVisibility
/>
);
await waitFor(() => {
wrapper.update();
expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space');
});
expect(onLoadSpace).toHaveBeenCalledWith({
...spaceToUpdate,
});
await Promise.resolve();
wrapper.update();
updateSpace(wrapper, true, 'oblt');
await clickSaveButton(wrapper);
expect(spacesManager.updateSpace).toHaveBeenCalledWith({
id: 'existing-space',
name: 'New Space Name',
description: 'some description',
color: '#AABBCC',
initials: 'AB',
imageUrl: '',
disabledFeatures: ['feature-1'],
solution: 'oblt', // solution has been changed
});
expect(reportEvent).toHaveBeenCalledWith('space_solution_changed', {
action: 'edit',
solution: 'oblt',
solution_prev: 'es',
space_id: 'existing-space',
});
});
it('sets calculated fields for existing spaces', async () => {
// The Spaces plugin provides functions to calculate the initials and color of a space if they have not been customized. The new space
// management page explicitly sets these fields when a new space is created, but it should also handle existing "legacy" spaces that do
// not already have these fields set.
const spaceToUpdate = {
id: 'existing-space',
name: 'Existing Space',
description: 'hey an existing space',
color: undefined,
initials: undefined,
imageUrl: undefined,
disabledFeatures: [],
};
const spacesManager = spacesManagerMock.create();
spacesManager.getSpace = jest.fn().mockResolvedValue({
...spaceToUpdate,
});
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
const onLoadSpace = jest.fn();
const wrapper = mountWithIntl(
<CreateSpacePage
spaceId={'existing-space'}
spacesManager={spacesManager as unknown as SpacesManager}
onLoadSpace={onLoadSpace}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
history={history}
capabilities={{
navLinks: {},
management: {},
catalogue: {},
spaces: { manage: true },
}}
eventTracker={eventTracker}
allowFeatureVisibility
allowSolutionVisibility
/>
);
await waitFor(() => {
wrapper.update();
expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space');
});
expect(onLoadSpace).toHaveBeenCalledWith({
...spaceToUpdate,
});
await Promise.resolve();
wrapper.update();
// not changing anything, just clicking the "Update space" button
await clickSaveButton(wrapper);
expect(spacesManager.updateSpace).toHaveBeenCalledWith({
...spaceToUpdate,
color: '#E7664C',
initials: 'ES',
imageUrl: '',
});
});
it('notifies when there is an error retrieving features', async () => {
const spacesManager = spacesManagerMock.create();
spacesManager.createSpace = jest.fn(spacesManager.createSpace);
@ -528,119 +388,6 @@ describe('ManageSpacePage', () => {
});
});
});
it('warns when updating features in the active space', async () => {
const spacesManager = spacesManagerMock.create();
spacesManager.getSpace = jest.fn().mockResolvedValue({
id: 'my-space',
name: 'Existing Space',
description: 'hey an existing space',
color: '#aabbcc',
initials: 'AB',
disabledFeatures: [],
});
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
const wrapper = mountWithIntl(
<CreateSpacePage
spaceId={'my-space'}
spacesManager={spacesManager as unknown as SpacesManager}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
history={history}
capabilities={{
navLinks: {},
management: {},
catalogue: {},
spaces: { manage: true },
}}
eventTracker={eventTracker}
allowFeatureVisibility
allowSolutionVisibility
/>
);
await waitFor(() => {
wrapper.update();
expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space');
});
await Promise.resolve();
wrapper.update();
updateSpace(wrapper);
await clickSaveButton(wrapper);
const warningDialog = wrapper.find(ConfirmAlterActiveSpaceModal);
expect(warningDialog).toHaveLength(1);
expect(spacesManager.updateSpace).toHaveBeenCalledTimes(0);
const confirmButton = warningDialog
.find(EuiButton)
.find('[data-test-subj="confirmModalConfirmButton"]')
.find('button');
confirmButton.simulate('click');
await Promise.resolve();
wrapper.update();
expect(spacesManager.updateSpace).toHaveBeenCalledTimes(1);
});
it('does not warn when features are left alone in the active space', async () => {
const spacesManager = spacesManagerMock.create();
spacesManager.getSpace = jest.fn().mockResolvedValue({
id: 'my-space',
name: 'Existing Space',
description: 'hey an existing space',
color: '#aabbcc',
initials: 'AB',
disabledFeatures: [],
});
spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space);
const wrapper = mountWithIntl(
<CreateSpacePage
spaceId={'my-space'}
spacesManager={spacesManager as unknown as SpacesManager}
getFeatures={featuresStart.getFeatures}
notifications={notificationServiceMock.createStartContract()}
history={history}
capabilities={{
navLinks: {},
management: {},
catalogue: {},
spaces: { manage: true },
}}
eventTracker={eventTracker}
allowFeatureVisibility
allowSolutionVisibility
/>
);
await waitFor(() => {
wrapper.update();
expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space');
});
await Promise.resolve();
wrapper.update();
updateSpace(wrapper, false);
await clickSaveButton(wrapper);
const warningDialog = wrapper.find(ConfirmAlterActiveSpaceModal);
expect(warningDialog).toHaveLength(0);
expect(spacesManager.updateSpace).toHaveBeenCalledTimes(1);
});
});
function updateSpace(
@ -680,15 +427,6 @@ function toggleFeature(wrapper: ReactWrapper<any, any>) {
wrapper.update();
}
async function clickSaveButton(wrapper: ReactWrapper<any, any>) {
const saveButton = wrapper.find('button[data-test-subj="save-space-button"]');
saveButton.simulate('click');
await Promise.resolve();
wrapper.update();
}
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View file

@ -27,15 +27,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Space } from '../../../common';
import { isReservedSpace } from '../../../common';
import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants';
import type { EventTracker } from '../../analytics';
import { getSpacesFeatureDescription } from '../../constants';
import { getSpaceColor, getSpaceInitials } from '../../space_avatar';
import type { SpacesManager } from '../../spaces_manager';
import { UnauthorizedPrompt } from '../components';
import { ConfirmAlterActiveSpaceModal } from '../components/confirm_alter_active_space_modal';
import { CustomizeAvatar } from '../components/customize_avatar';
import { CustomizeSpace } from '../components/customize_space';
import { DeleteSpacesButton } from '../components/delete_spaces_button';
import { EnabledFeatures } from '../components/enabled_features';
import { SolutionView } from '../components/solution_view';
import { toSpaceIdentifier } from '../lib';
@ -60,7 +60,6 @@ interface State {
features: KibanaFeature[];
originalSpace?: Partial<Space>;
showAlteringActiveSpaceDialog: boolean;
showVisibleFeaturesPicker: boolean;
haveDisabledFeaturesChanged: boolean;
hasSolutionViewChanged: boolean;
isLoading: boolean;
@ -80,7 +79,6 @@ export class CreateSpacePage extends Component<Props, State> {
this.state = {
isLoading: true,
showAlteringActiveSpaceDialog: false,
showVisibleFeaturesPicker: !!props.allowFeatureVisibility,
saveInProgress: false,
space: {
color: getSpaceColor({}),
@ -185,12 +183,9 @@ export class CreateSpacePage extends Component<Props, State> {
return (
<div data-test-subj="spaces-create-page">
<CustomizeSpace
title={i18n.translate('xpack.spaces.management.manageSpacePage.generalTitle', {
defaultMessage: 'General',
})}
space={this.state.space}
onChange={this.onSpaceChange}
editingExistingSpace={this.editingExistingSpace()}
editingExistingSpace={false}
validator={this.validator}
/>
@ -201,25 +196,30 @@ export class CreateSpacePage extends Component<Props, State> {
space={this.state.space}
onChange={this.onSolutionViewChange}
validator={this.validator}
isEditing={this.editingExistingSpace()}
sectionTitle={i18n.translate(
'xpack.spaces.management.manageSpacePage.navigationTitle',
{ defaultMessage: 'Navigation' }
)}
isEditing={false}
/>
</>
)}
{this.state.showVisibleFeaturesPicker && (
<>
<EuiSpacer />
<EnabledFeatures
space={this.state.space}
features={this.state.features}
onChange={this.onSpaceChange}
/>
</>
)}
{this.props.allowFeatureVisibility &&
(!this.state.space.solution || this.state.space.solution === SOLUTION_VIEW_CLASSIC) && (
<>
<EuiSpacer />
<EnabledFeatures
space={this.state.space}
features={this.state.features}
onChange={this.onSpaceChange}
/>
</>
)}
<EuiSpacer />
<CustomizeAvatar
space={this.state.space}
onChange={this.onSpaceChange}
validator={this.validator}
/>
<EuiSpacer />
@ -240,14 +240,6 @@ export class CreateSpacePage extends Component<Props, State> {
};
public getTitle = () => {
if (this.editingExistingSpace()) {
return (
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.editSpaceTitle"
defaultMessage="Edit space"
/>
);
}
return (
<FormattedMessage
id="xpack.spaces.management.manageSpacePage.createSpaceTitle"
@ -257,7 +249,6 @@ export class CreateSpacePage extends Component<Props, State> {
};
public getChangeImpactWarning = () => {
if (!this.editingExistingSpace()) return null;
const { haveDisabledFeaturesChanged, hasSolutionViewChanged } = this.state;
if (!haveDisabledFeaturesChanged && !hasSolutionViewChanged) return null;
@ -289,13 +280,6 @@ export class CreateSpacePage extends Component<Props, State> {
}
);
const updateSpaceText = i18n.translate(
'xpack.spaces.management.manageSpacePage.updateSpaceButton',
{
defaultMessage: 'Update space',
}
);
const cancelButtonText = i18n.translate(
'xpack.spaces.management.manageSpacePage.cancelSpaceButton',
{
@ -303,8 +287,6 @@ export class CreateSpacePage extends Component<Props, State> {
}
);
const saveText = this.editingExistingSpace() ? updateSpaceText : createSpaceText;
return (
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
@ -314,7 +296,7 @@ export class CreateSpacePage extends Component<Props, State> {
data-test-subj="save-space-button"
isLoading={this.state.saveInProgress}
>
{saveText}
{createSpaceText}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -323,37 +305,12 @@ export class CreateSpacePage extends Component<Props, State> {
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={true} />
{this.getActionButton()}
</EuiFlexGroup>
);
};
public getActionButton = () => {
if (this.state.space && this.editingExistingSpace() && !isReservedSpace(this.state.space)) {
return (
<EuiFlexItem grow={false}>
<DeleteSpacesButton
data-test-subj="delete-space-button"
space={this.state.space as Space}
spacesManager={this.props.spacesManager}
onDelete={this.backToSpacesList}
notifications={this.props.notifications}
/>
</EuiFlexItem>
);
}
return null;
};
private onSolutionViewChange = (space: Partial<Space>) => {
if (this.props.allowFeatureVisibility) {
let showVisibleFeaturesPicker = false;
if (space.solution === 'classic' || space.solution == null) {
showVisibleFeaturesPicker = true;
}
this.setState((state) => ({ ...state, showVisibleFeaturesPicker }));
}
this.setState((state) => ({ ...state, solution: space.solution }));
this.onSpaceChange(space);
};
@ -366,14 +323,8 @@ export class CreateSpacePage extends Component<Props, State> {
public saveSpace = () => {
this.validator.enableValidation();
const originalSpace: Space = this.state.originalSpace as Space;
const space: Space = this.state.space as Space;
const { haveDisabledFeaturesChanged, hasSolutionViewChanged } = this.state;
const result = this.validator.validateForSave(
space,
this.editingExistingSpace(),
this.props.allowSolutionVisibility
);
const result = this.validator.validateForSave(space, false, this.props.allowSolutionVisibility);
if (result.isInvalid) {
this.setState({
formError: result,
@ -382,24 +333,7 @@ export class CreateSpacePage extends Component<Props, State> {
return;
}
if (this.editingExistingSpace()) {
const { spacesManager } = this.props;
spacesManager.getActiveSpace().then((activeSpace) => {
const editingActiveSpace = activeSpace.id === originalSpace.id;
if (editingActiveSpace && (haveDisabledFeaturesChanged || hasSolutionViewChanged)) {
this.setState({
showAlteringActiveSpaceDialog: true,
});
return;
}
this.performSave();
});
} else {
this.performSave();
}
this.performSave();
};
private loadSpace = async (spaceId: string, featuresPromise: Promise<KibanaFeature[]>) => {
@ -472,15 +406,8 @@ export class CreateSpacePage extends Component<Props, State> {
solution,
};
let action;
const isEditing = this.editingExistingSpace();
const { spacesManager, eventTracker } = this.props;
if (isEditing) {
action = spacesManager.updateSpace(params);
} else {
action = spacesManager.createSpace(params);
}
const action = spacesManager.createSpace(params);
this.setState({ saveInProgress: true });
@ -493,7 +420,7 @@ export class CreateSpacePage extends Component<Props, State> {
spaceId: id,
solution,
solutionPrev: this.state.originalSpace?.solution,
action: isEditing ? 'edit' : 'create',
action: 'create',
});
};
@ -536,6 +463,4 @@ export class CreateSpacePage extends Component<Props, State> {
};
private backToSpacesList = () => this.props.history.push('/');
private editingExistingSpace = () => !!this.props.spaceId;
}

View file

@ -201,8 +201,8 @@ export const EditSpace: FC<PageProps> = ({
<HeaderAvatar />
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={true} al>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={true}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiTitle size="l">

View file

@ -1,77 +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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import type { FC } from 'react';
import React from 'react';
import type { KibanaFeature } from '@kbn/features-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { useEditSpaceServices } from './provider';
import type { Space } from '../../../common';
import { FeatureTable } from '../components/enabled_features/feature_table';
import { SectionPanel } from '../components/section_panel';
interface Props {
space: Partial<Space>;
features: KibanaFeature[];
onChange: (updatedSpace: Partial<Space>) => void;
}
export const EditSpaceEnabledFeatures: FC<Props> = ({ features, space, onChange }) => {
const { capabilities, getUrlForApp } = useEditSpaceServices();
const canManageRoles = capabilities.roles?.save === true;
if (!features) {
return null;
}
return (
<SectionPanel dataTestSubj="enabled-features-panel">
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.spaces.management.editSpaceFeatures.featuresVisibility"
defaultMessage="Set features visibility"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.spaces.management.editSpaceFeatures.notASecurityMechanismMessage"
defaultMessage="Hidden features are removed from the user interface, but not disabled. To secure access to features, {manageRolesLink}."
values={{
manageRolesLink: canManageRoles ? (
<EuiLink href={getUrlForApp('management', { path: '/security/roles' })}>
<FormattedMessage
id="xpack.spaces.management.editSpaceFeatures.manageRolesLinkText"
defaultMessage="manage security roles"
/>
</EuiLink>
) : (
<FormattedMessage
id="xpack.spaces.management.editSpaceFeatures.askAnAdministratorText"
defaultMessage="ask an administrator to manage roles"
/>
),
}}
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<FeatureTable features={features} space={space} onChange={onChange} />
</EuiFlexItem>
</EuiFlexGroup>
</SectionPanel>
);
};

View file

@ -13,7 +13,6 @@ import type { KibanaFeature } from '@kbn/features-plugin/common';
import { i18n } from '@kbn/i18n';
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
import { EditSpaceEnabledFeatures } from './edit_space_features_tab';
import { EditSpaceTabFooter } from './footer';
import { useEditSpaceServices } from './provider';
import type { Space } from '../../../common';
@ -21,7 +20,9 @@ import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants';
import { getSpaceInitials } from '../../space_avatar';
import { ConfirmDeleteModal } from '../components';
import { ConfirmAlterActiveSpaceModal } from '../components/confirm_alter_active_space_modal';
import { CustomizeAvatar } from '../components/customize_avatar';
import { CustomizeSpace } from '../components/customize_space';
import { EnabledFeatures } from '../components/enabled_features';
import { SolutionView } from '../components/solution_view';
import { SpaceValidator } from '../lib';
import type { CustomizeSpaceFormValues } from '../types';
@ -249,17 +250,15 @@ export const EditSpaceSettingsTab: React.FC<Props> = ({ space, features, history
<EuiSpacer />
<EuiCallOut
color="warning"
iconType="help"
title="Warning"
data-test-subj="space-edit-page-user-impact-warning"
>
{i18n.translate(
iconType="iInCircle"
title={i18n.translate(
'xpack.spaces.management.spaceDetails.spaceChangesWarning.impactAllUsersInSpace',
{
defaultMessage: 'The changes made will impact all users in the space.',
defaultMessage: 'The changes will apply to all users of the space.',
}
)}
</EuiCallOut>
data-test-subj="space-edit-page-user-impact-warning"
/>
</>
)
);
@ -289,10 +288,10 @@ export const EditSpaceSettingsTab: React.FC<Props> = ({ space, features, history
</>
)}
{props.allowFeatureVisibility && (solution == null || solution === SOLUTION_VIEW_CLASSIC) && (
{props.allowFeatureVisibility && (!solution || solution === SOLUTION_VIEW_CLASSIC) && (
<>
<EuiSpacer />
<EditSpaceEnabledFeatures
<EnabledFeatures
features={features}
space={getSpaceFromFormValues(formValues)}
onChange={onChangeFeatures}
@ -300,6 +299,14 @@ export const EditSpaceSettingsTab: React.FC<Props> = ({ space, features, history
</>
)}
<EuiSpacer />
<CustomizeAvatar
space={getSpaceFromFormValues(formValues)}
onChange={onChangeSpaceSettings}
validator={validator}
/>
{doShowUserImpactWarning()}
<EuiSpacer />

View file

@ -31,57 +31,57 @@ export const EditSpaceTabFooter: React.FC<Props> = ({
onClickSubmit,
onClickDeleteSpace,
}) => {
if (isLoading) {
return (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner />
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<>
{isLoading && (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
{isDirty && (
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
fill
onClick={onClickSubmit}
data-test-subj="save-space-button"
>
<FormattedMessage
id="xpack.spaces.management.spaceDetails.footerActions.updateSpace"
defaultMessage="Apply changes"
/>
</EuiButton>
</EuiFlexItem>
)}
{!isLoading && (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={onClickDeleteSpace}
color="danger"
data-test-subj="delete-space-button"
>
<FormattedMessage
id="xpack.spaces.management.spaceDetails.footerActions.deleteSpace"
defaultMessage="Delete space"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={true} />
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClickCancel} data-test-subj="cancel-space-button">
<FormattedMessage
id="xpack.spaces.management.spaceDetails.footerActions.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClickCancel} data-test-subj="cancel-space-button">
<FormattedMessage
id="xpack.spaces.management.spaceDetails.footerActions.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={true} />
{isDirty && (
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
fill
onClick={onClickSubmit}
data-test-subj="save-space-button"
>
<FormattedMessage
id="xpack.spaces.management.spaceDetails.footerActions.updateSpace"
defaultMessage="Update space"
/>
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={onClickDeleteSpace}
color="danger"
iconType="trash"
data-test-subj="delete-space-button"
>
<FormattedMessage
id="xpack.spaces.management.spaceDetails.footerActions.deleteSpace"
defaultMessage="Delete space"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -354,7 +354,7 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
<EuiFormRow
label={i18n.translate(
'xpack.spaces.management.spaceDetails.roles.selectRolesFormRowLabel',
{ defaultMessage: 'Select roles(s)' }
{ defaultMessage: 'Select roles' }
)}
labelAppend={
<EuiLink href={getUrlForApp('management', { deepLinkId: 'roles' })}>
@ -367,7 +367,8 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
helpText={i18n.translate(
'xpack.spaces.management.spaceDetails.roles.selectRolesHelp',
{
defaultMessage: 'Select Kibana spaces to which you wish to assign privileges.',
defaultMessage:
'Users assigned to selected roles will gain access to this space.',
}
)}
>
@ -380,6 +381,10 @@ export const PrivilegesRolesForm: FC<PrivilegesRolesFormProps> = (props) => {
values: { spaceName: space.name },
}
)}
placeholder={i18n.translate(
'xpack.spaces.management.spaceDetails.roles.selectRolesPlaceholder',
{ defaultMessage: 'Add a role...' }
)}
isLoading={fetchingDataDeps}
options={createRolesComboBoxOptions(spaceUnallocatedRoles)}
selectedOptions={selectedRoles}

View file

@ -78,6 +78,7 @@ const getTableColumns = ({
name: i18n.translate('xpack.spaces.management.spaceDetails.rolesTable.column.name.title', {
defaultMessage: 'Role',
}),
width: '45%',
},
{
field: 'privileges',
@ -118,25 +119,25 @@ const getTableColumns = ({
{ defaultMessage: 'Role type' }
),
render: (_value: Role['metadata']) => {
return React.createElement(EuiBadge, {
children: _value?._reserved
? i18n.translate(
return _value?._reserved
? React.createElement(EuiBadge, {
children: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.reserved',
{ defaultMessage: 'Reserved' }
)
: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.custom',
{ defaultMessage: 'Custom' }
),
color: _value?._reserved ? undefined : 'success',
});
color: 'primary',
})
: null;
},
},
];
if (!isReadOnly) {
columns.push({
name: 'Actions',
name: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.columnHeaderName',
{ defaultMessage: 'Actions' }
),
actions: [
{
type: 'icon',
@ -163,22 +164,22 @@ const getTableColumns = ({
: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.notEditableDescription.isAssignedToAll',
{
defaultMessage: `Can't perform actions on a role that is assigned to all spaces`,
defaultMessage: `You can't edit the access of a role that is assigned to all spaces.`,
}
),
isPrimary: true,
showOnHover: true,
enabled: () => false,
available: (rowRecord) => !isEditableRole(rowRecord),
},
{
type: 'icon',
icon: 'pencil',
isPrimary: true,
'data-test-subj': 'spaceRoleCellEditAction',
name: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.title',
{ defaultMessage: 'Remove from space' }
),
isPrimary: true,
description: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.description',
{
@ -186,15 +187,14 @@ const getTableColumns = ({
'Click this action to edit the role privileges of this user for this space.',
}
),
showOnHover: true,
available: (rowRecord) => isEditableRole(rowRecord),
onClick: onClickRowEditAction,
},
{
isPrimary: true,
type: 'icon',
icon: 'trash',
color: 'danger',
isPrimary: true,
'data-test-subj': 'spaceRoleCellDeleteAction',
name: i18n.translate(
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.remove.title',
@ -204,7 +204,6 @@ const getTableColumns = ({
'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.description',
{ defaultMessage: 'Click this action to remove the user from this space.' }
),
showOnHover: true,
available: (rowRecord) => isEditableRole(rowRecord),
onClick: onClickRowRemoveAction,
},

View file

@ -181,7 +181,7 @@ export class SpaceValidator {
if (!space.solution) {
return invalid(
i18n.translate('xpack.spaces.management.validateSpace.requiredSolutionViewErrorMessage', {
defaultMessage: 'Select one solution.',
defaultMessage: 'Select a solution.',
})
);
}

View file

@ -95,7 +95,7 @@ describe('SpacesGridPage', () => {
expect(wrapper.find('EuiInMemoryTable').prop('items')).toBe(spaces);
expect(wrapper.find('EuiInMemoryTable').prop('columns')).not.toContainEqual({
field: 'solution',
name: 'Solution View',
name: 'Solution view',
sortable: true,
render: expect.any(Function),
});
@ -155,7 +155,7 @@ describe('SpacesGridPage', () => {
expect(wrapper.find('EuiInMemoryTable').prop('items')).toBe(spacesWithSolution);
expect(wrapper.find('EuiInMemoryTable').prop('columns')).toContainEqual({
field: 'solution',
name: 'Solution View',
name: 'Solution view',
sortable: true,
render: expect.any(Function),
});

View file

@ -10,7 +10,7 @@ import {
type EuiBasicTableColumn,
EuiButton,
EuiCallOut,
EuiFlexGroup,
EuiFlexGrid,
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
@ -19,6 +19,7 @@ import {
EuiPageSection,
EuiSpacer,
EuiText,
useIsWithinBreakpoints,
} from '@elastic/eui';
import React, { Component, lazy, Suspense } from 'react';
@ -152,9 +153,7 @@ export class SpacesGridPage extends Component<Props, State> {
box: {
placeholder: i18n.translate(
'xpack.spaces.management.spacesGridPage.searchPlaceholder',
{
defaultMessage: 'Search',
}
{ defaultMessage: 'Search' }
),
},
}}
@ -281,28 +280,49 @@ export class SpacesGridPage extends Component<Props, State> {
defaultMessage: 'Space',
}),
sortable: true,
render: (value: string, rowRecord: Space) => (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiLink
{...reactRouterNavigate(this.props.history, this.getEditSpacePath(rowRecord))}
data-test-subj={`${rowRecord.id}-hyperlink`}
render: (value: string, rowRecord: Space) => {
const SpaceName = () => {
const isCurrent = this.state.activeSpace?.id === rowRecord.id;
const isWide = useIsWithinBreakpoints(['xl']);
const gridColumns = isCurrent && isWide ? 2 : 1;
return (
<EuiFlexGrid
responsive={false}
columns={gridColumns}
alignItems="center"
gutterSize="s"
>
{value}
</EuiLink>
</EuiFlexItem>
{this.state.activeSpace?.id === rowRecord.id && (
<EuiFlexItem grow={false}>
<EuiBadge color="primary" data-test-subj={`spacesListCurrentBadge-${rowRecord.id}`}>
{i18n.translate('xpack.spaces.management.spacesGridPage.currentSpaceMarkerText', {
defaultMessage: 'current',
})}
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
),
<EuiFlexItem>
<EuiLink
{...reactRouterNavigate(this.props.history, this.getEditSpacePath(rowRecord))}
data-test-subj={`${rowRecord.id}-hyperlink`}
>
{value}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem>
{isCurrent && (
<span>
<EuiBadge
color="primary"
data-test-subj={`spacesListCurrentBadge-${rowRecord.id}`}
>
{i18n.translate(
'xpack.spaces.management.spacesGridPage.currentSpaceMarkerText',
{ defaultMessage: 'current' }
)}
</EuiBadge>
</span>
)}
</EuiFlexItem>
</EuiFlexGrid>
);
};
return <SpaceName />;
},
'data-test-subj': 'spacesListTableRowNameCell',
width: '15%',
},
{
field: 'description',
@ -311,7 +331,7 @@ export class SpacesGridPage extends Component<Props, State> {
}),
sortable: true,
truncateText: true,
width: '30%',
width: '45%',
},
];
@ -331,7 +351,7 @@ export class SpacesGridPage extends Component<Props, State> {
return (
<FormattedMessage
id="xpack.spaces.management.spacesGridPage.allFeaturesEnabled"
defaultMessage="All features visible"
defaultMessage="All features"
/>
);
}
@ -377,7 +397,7 @@ export class SpacesGridPage extends Component<Props, State> {
config.push({
field: 'solution',
name: i18n.translate('xpack.spaces.management.spacesGridPage.solutionColumnName', {
defaultMessage: 'Solution View',
defaultMessage: 'Solution view',
}),
sortable: true,
render: (solution: Space['solution'], record: Space) => (

View file

@ -42710,8 +42710,6 @@
"xpack.spaces.management.deselectAllFeaturesLink": "Tout masquer",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "Touche bascule de catégorie",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "Définir la visibilité des fonctionnalités",
"xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText": "gérer les rôles de sécurité",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "Les fonctionnalités masquées sont supprimées de l'interface utilisateur, mais pas désactivées. Pour sécuriser l'accès aux fonctionnalités, {manageRolesLink}.",
"xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount} fonctionnalités visibles / {featureCount}",
"xpack.spaces.management.featureVisibilityTitle": "Visibilité des fonctionnalités",
"xpack.spaces.management.hideAllFeaturesText": "Tout masquer",
@ -42723,15 +42721,11 @@
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "Créer l'espace",
"xpack.spaces.management.manageSpacePage.describeSpaceDescription": "Attribuez à votre espace un nom facile à retenir.",
"xpack.spaces.management.manageSpacePage.describeSpaceTitle": "Décrire cet espace",
"xpack.spaces.management.manageSpacePage.editSpaceTitle": "Modifier l'espace",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "Erreur lors du chargement de l'espace : {message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "Erreur lors de l'enregistrement de l'espace : {message}",
"xpack.spaces.management.manageSpacePage.featuresTitle": "Fonctionnalités",
"xpack.spaces.management.manageSpacePage.generalTitle": "Général",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "Erreur lors du chargement des fonctionnalités disponibles",
"xpack.spaces.management.manageSpacePage.loadingMessage": "Chargement…",
"xpack.spaces.management.manageSpacePage.nameFormRowLabel": "Nom",
"xpack.spaces.management.manageSpacePage.navigationTitle": "Navigation",
"xpack.spaces.management.manageSpacePage.optionalLabel": "Facultatif",
"xpack.spaces.management.manageSpacePage.setSolutionViewDescription": "Détermine la navigation que tous les utilisateurs verront pour cet espace. Chaque vue de solution contient des fonctionnalités de Outils d'analyse et de Gestion.",
"xpack.spaces.management.manageSpacePage.setSolutionViewMessage": "Définir la vue de la solution",
@ -42742,7 +42736,6 @@
"xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel": "Description",
"xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText": "La description s'affiche sur l'écran de sélection de l'espace.",
"xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "L'espace {name} a été enregistré.",
"xpack.spaces.management.manageSpacePage.updateSpaceButton": "Mettre à jour l'espace",
"xpack.spaces.management.navigation.solutionViewLabel": "Afficher la solution",
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "Les espaces réservés sont intégrés et ne peuvent être que partiellement modifiés.",
"xpack.spaces.management.selectAllFeaturesLink": "Afficher tout",

View file

@ -42450,8 +42450,6 @@
"xpack.spaces.management.deselectAllFeaturesLink": "すべて非表示",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "カテゴリ切り替え",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "機能の表示を設定",
"xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText": "セキュリティロールを管理",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "非表示の機能はユーザーインターフェイスから削除されますが、無効にされません。機能へのアクセスを保護するには、{manageRolesLink}してください。",
"xpack.spaces.management.featureAccordionSwitchLabel": "{featureCount} 件中 {enabledCount} 件の機能を表示中",
"xpack.spaces.management.featureVisibilityTitle": "機能の表示",
"xpack.spaces.management.hideAllFeaturesText": "すべて非表示",
@ -42463,15 +42461,11 @@
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースを作成",
"xpack.spaces.management.manageSpacePage.describeSpaceDescription": "スペースに覚えやすい名前を付けます。",
"xpack.spaces.management.manageSpacePage.describeSpaceTitle": "このスペースを説明",
"xpack.spaces.management.manageSpacePage.editSpaceTitle": "スペースの編集",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生:{message}",
"xpack.spaces.management.manageSpacePage.featuresTitle": "機能",
"xpack.spaces.management.manageSpacePage.generalTitle": "一般",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "利用可能な機能の読み込みエラー",
"xpack.spaces.management.manageSpacePage.loadingMessage": "読み込み中…",
"xpack.spaces.management.manageSpacePage.nameFormRowLabel": "名前",
"xpack.spaces.management.manageSpacePage.navigationTitle": "ナビゲーション",
"xpack.spaces.management.manageSpacePage.optionalLabel": "オプション",
"xpack.spaces.management.manageSpacePage.setSolutionViewDescription": "すべてのユーザーにこのスペースで表示されるナビゲーションを決定します。各ソリューションビューには、分析ツールと管理の機能が含まれます。",
"xpack.spaces.management.manageSpacePage.setSolutionViewMessage": "ソリューションビューを設定",
@ -42482,7 +42476,6 @@
"xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel": "説明",
"xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText": "説明はスペース選択画面に表示されます。",
"xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "スペース {name} が保存されました。",
"xpack.spaces.management.manageSpacePage.updateSpaceButton": "スペースを更新",
"xpack.spaces.management.navigation.solutionViewLabel": "ソリューションビュー",
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "リザーブされたスペースはビルトインのため、部分的な変更しかできません。",
"xpack.spaces.management.selectAllFeaturesLink": "すべて表示",

View file

@ -42500,8 +42500,6 @@
"xpack.spaces.management.deselectAllFeaturesLink": "全部隐藏",
"xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "类别切换",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "设置功能可见性",
"xpack.spaces.management.enabledSpaceFeatures.manageRolesLinkText": "管理安全角色",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "将会从用户界面移除隐藏的功能,但不会禁用。要获取功能的访问权限,{manageRolesLink}。",
"xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount}/{featureCount} 个功能可见",
"xpack.spaces.management.featureVisibilityTitle": "功能可见性",
"xpack.spaces.management.hideAllFeaturesText": "全部隐藏",
@ -42513,15 +42511,11 @@
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建工作区",
"xpack.spaces.management.manageSpacePage.describeSpaceDescription": "为您的工作区提供好记的名称。",
"xpack.spaces.management.manageSpacePage.describeSpaceTitle": "描述此工作区",
"xpack.spaces.management.manageSpacePage.editSpaceTitle": "编辑工作区",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.featuresTitle": "功能",
"xpack.spaces.management.manageSpacePage.generalTitle": "常规",
"xpack.spaces.management.manageSpacePage.loadErrorTitle": "加载可用功能时出错",
"xpack.spaces.management.manageSpacePage.loadingMessage": "正在加载……",
"xpack.spaces.management.manageSpacePage.nameFormRowLabel": "名称",
"xpack.spaces.management.manageSpacePage.navigationTitle": "导航",
"xpack.spaces.management.manageSpacePage.optionalLabel": "可选",
"xpack.spaces.management.manageSpacePage.setSolutionViewDescription": "确定所有用户将在此工作区看到的导航。每个解决方案视图均包含来自分析工具的功能和管理功能。",
"xpack.spaces.management.manageSpacePage.setSolutionViewMessage": "设置解决方案视图",
@ -42532,7 +42526,6 @@
"xpack.spaces.management.manageSpacePage.spaceDescriptionFormRowLabel": "描述",
"xpack.spaces.management.manageSpacePage.spaceDescriptionHelpText": "描述显示在“工作区选择”屏幕上。",
"xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "空间 “{name}” 已保存。",
"xpack.spaces.management.manageSpacePage.updateSpaceButton": "更新工作区",
"xpack.spaces.management.navigation.solutionViewLabel": "解决方案视图",
"xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "保留的工作区是内置的,只能进行部分修改。",
"xpack.spaces.management.selectAllFeaturesLink": "全部显示",