Fix "Assign object to spaces" flyout UX problems (#128946)

This commit is contained in:
Joe Portner 2022-03-31 12:11:31 -04:00 committed by GitHub
parent a5620502d0
commit 19e43a305d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 90 additions and 58 deletions

View file

@ -447,13 +447,17 @@ const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = {
onClose: () => setShowFlyout(false),
};
const canAssignSpaces = !capabilities || !!capabilities.savedObjectsManagement.shareIntoSpace;
const clickProperties = canAssignSpaces
? { cursorStyle: 'pointer', listOnClick: () => setShowFlyout(true) }
: { cursorStyle: 'not-allowed' };
return (
<>
<LazySpaceList
namespaces={spaceIds}
displayLimit={8}
behaviorContext="outside-space" <4>
listOnClick={() => setShowFlyout(true)}
{...clickProperties}
/>
{showFlyout && <LazyShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
</>

View file

@ -70,6 +70,7 @@ export const IndexPatternTable = ({
setBreadcrumbs,
uiSettings,
indexPatternManagementStart,
application,
chrome,
dataViews,
IndexPatternEditor,
@ -231,6 +232,7 @@ export const IndexPatternTable = ({
return spaces ? (
<SpacesList
spacesApi={spaces}
capabilities={application?.capabilities}
spaceIds={dataView.namespaces || []}
id={dataView.id}
title={dataView.title}

View file

@ -9,6 +9,7 @@
import React, { FC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import type { Capabilities } from 'src/core/public';
import type {
SpacesPluginStart,
ShareToSpaceFlyoutProps,
@ -17,6 +18,7 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../data_views/public';
interface Props {
spacesApi: SpacesPluginStart;
capabilities: Capabilities | undefined;
spaceIds: string[];
id: string;
title: string;
@ -27,7 +29,14 @@ const noun = i18n.translate('indexPatternManagement.indexPatternTable.savedObjec
defaultMessage: 'data view',
});
export const SpacesList: FC<Props> = ({ spacesApi, spaceIds, id, title, refresh }) => {
export const SpacesList: FC<Props> = ({
spacesApi,
capabilities,
spaceIds,
id,
title,
refresh,
}) => {
const [showFlyout, setShowFlyout] = useState(false);
function onClose() {
@ -49,13 +58,17 @@ export const SpacesList: FC<Props> = ({ spacesApi, spaceIds, id, title, refresh
onClose,
};
const canAssignSpaces = !capabilities || !!capabilities.savedObjectsManagement.shareIntoSpace;
const clickProperties = canAssignSpaces
? { cursorStyle: 'pointer', listOnClick: () => setShowFlyout(true) }
: { cursorStyle: 'not-allowed' };
return (
<>
<LazySpaceList
namespaces={spaceIds}
displayLimit={8}
behaviorContext="outside-space"
listOnClick={() => setShowFlyout(true)}
{...clickProperties}
/>
{showFlyout && <LazyShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
</>

View file

@ -39,7 +39,7 @@ export async function mountManagementSection(
params: ManagementAppMountParams
) {
const [
{ chrome, uiSettings, notifications, overlays, http, docLinks, theme },
{ application, chrome, uiSettings, notifications, overlays, http, docLinks, theme },
{ data, dataViewFieldEditor, dataViewEditor, dataViews, fieldFormats, spaces },
indexPatternManagementStart,
] = await getStartServices();
@ -50,6 +50,7 @@ export async function mountManagementSection(
}
const deps: IndexPatternManagmentContext = {
application,
chrome,
uiSettings,
notifications,

View file

@ -55,13 +55,14 @@ const docLinks = {
const createIndexPatternManagmentContext = (): {
[key in keyof IndexPatternManagmentContext]: any;
} => {
const { chrome, uiSettings, notifications, overlays } = coreMock.createStart();
const { application, chrome, uiSettings, notifications, overlays } = coreMock.createStart();
const { http } = coreMock.createSetup();
const data = dataPluginMock.createStartContract();
const dataViewFieldEditor = indexPatternFieldEditorPluginMock.createStartContract();
const dataViews = dataViewPluginMocks.createStartContract();
return {
application,
chrome,
uiSettings,
notifications,

View file

@ -13,6 +13,7 @@ import {
NotificationsStart,
DocLinksStart,
HttpSetup,
ApplicationStart,
} from 'src/core/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { ManagementAppMountParams } from '../../management/public';
@ -25,6 +26,7 @@ import { FieldFormatsStart } from '../../field_formats/public';
import { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public';
export interface IndexPatternManagmentContext {
application: ApplicationStart;
chrome: ChromeStart;
uiSettings: IUiSettingsClient;
notifications: NotificationsStart;

View file

@ -10,7 +10,7 @@ import React, { useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import type { SavedObjectsNamespaceType } from 'src/core/public';
import type { Capabilities, SavedObjectsNamespaceType } from 'src/core/public';
import { EuiIconTip, EuiToolTip } from '@elastic/eui';
import type {
@ -26,6 +26,7 @@ interface WrapperProps {
objectType: string;
objectNamespaceType: SavedObjectsNamespaceType;
spacesApiUi: SpacesApiUi;
capabilities: Capabilities | undefined;
spaceListProps: SpaceListProps;
flyoutProps: ShareToSpaceFlyoutProps;
}
@ -71,6 +72,7 @@ const Wrapper = ({
objectType,
objectNamespaceType,
spacesApiUi,
capabilities,
spaceListProps,
flyoutProps,
}: WrapperProps) => {
@ -114,9 +116,13 @@ const Wrapper = ({
);
}
const canAssignSpaces = !capabilities || !!capabilities.savedObjectsManagement.shareIntoSpace;
const clickProperties = canAssignSpaces
? { cursorStyle: 'pointer', listOnClick }
: { cursorStyle: 'not-allowed' };
return (
<>
<LazySpaceList {...spaceListProps} listOnClick={listOnClick} />
<LazySpaceList {...spaceListProps} {...clickProperties} />
{showFlyout && <LazyShareToSpaceFlyout {...flyoutProps} onClose={onClose} />}
</>
);
@ -155,6 +161,7 @@ export class ShareToSpaceSavedObjectsManagementColumn extends SavedObjectsManage
objectType={record.type}
objectNamespaceType={record.meta.namespaceType}
spacesApiUi={this.spacesApiUi}
capabilities={this.columnContext?.capabilities}
spaceListProps={spaceListProps}
flyoutProps={flyoutProps}
/>

View file

@ -50,7 +50,9 @@ type SpaceOption = EuiSelectableOption & { ['data-space-id']: string };
const ROW_HEIGHT = 40;
const APPEND_ACTIVE_SPACE = (
<EuiBadge color="hollow">
{i18n.translate('xpack.spaces.shareToSpace.currentSpaceBadge', { defaultMessage: 'Current' })}
{i18n.translate('xpack.spaces.shareToSpace.currentSpaceBadge', {
defaultMessage: 'This space',
})}
</EuiBadge>
);
const APPEND_CANNOT_SELECT = (
@ -92,23 +94,26 @@ export const SelectableSpacesControl = (props: Props) => {
const activeSpaceId =
!enableSpaceAgnosticBehavior && spaces.find((space) => space.isActiveSpace)!.id;
const isGlobalControlChecked = selectedSpaceIds.includes(ALL_SPACES_ID);
const options = spaces
.filter(
// filter out spaces that are not already selected and have the feature disabled in that space
({ id, isFeatureDisabled }) => !isFeatureDisabled || initiallySelectedSpaceIds.includes(id)
)
const filteredSpaces = spaces.filter(
// filter out spaces that are not already selected and have the feature disabled in that space
({ id, isFeatureDisabled }) =>
!isFeatureDisabled || initiallySelectedSpaceIds.includes(id) || isGlobalControlChecked
);
const options = filteredSpaces
.sort(createSpacesComparator(activeSpaceId))
.map<SpaceOption>((space) => {
const checked = selectedSpaceIds.includes(space.id);
const { isAvatarDisabled, ...additionalProps } = getAdditionalProps(
space,
activeSpaceId,
checked
checked,
isGlobalControlChecked
);
return {
label: space.name,
prepend: <LazySpaceAvatar space={space} isDisabled={isAvatarDisabled} size={'s'} />, // wrapped in a Suspense below
checked: checked ? 'on' : undefined,
checked: checked || isGlobalControlChecked ? 'on' : undefined,
['data-space-id']: space.id,
['data-test-subj']: `sts-space-selector-row-${space.id}`,
...(isGlobalControlChecked && { disabled: true }),
@ -134,14 +139,16 @@ export const SelectableSpacesControl = (props: Props) => {
return null;
}
const hiddenCount = selectedSpaceIds.filter((id) => id === UNKNOWN_SPACE).length;
const docLink = docLinks?.links.security.kibanaPrivileges;
return (
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.spaces.shareToSpace.unknownSpacesLabel.text"
defaultMessage="To view hidden spaces, you need {additionalPrivilegesLink}."
defaultMessage="To view {hiddenCount} hidden spaces, you need {additionalPrivilegesLink}."
values={{
hiddenCount,
additionalPrivilegesLink: (
<EuiLink href={docLink} target="_blank">
<FormattedMessage
@ -169,41 +176,37 @@ export const SelectableSpacesControl = (props: Props) => {
// if space-agnostic behavior is not enabled, the active space is not selected or deselected by the user, so we have to artificially pad the count for this label
const selectedCountPad = enableSpaceAgnosticBehavior ? 0 : 1;
const selectedCount =
selectedSpaceIds.filter((id) => id !== ALL_SPACES_ID && id !== UNKNOWN_SPACE).length +
selectedCountPad;
const hiddenCount = selectedSpaceIds.filter((id) => id === UNKNOWN_SPACE).length;
const selectedCount = isGlobalControlChecked
? filteredSpaces.length
: selectedSpaceIds.filter((id) => id !== ALL_SPACES_ID && id !== UNKNOWN_SPACE).length +
selectedCountPad;
const selectSpacesLabel = i18n.translate(
'xpack.spaces.shareToSpace.shareModeControl.selectSpacesLabel',
{ defaultMessage: 'Select spaces' }
);
const selectedSpacesLabel = i18n.translate(
'xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel',
{ defaultMessage: '{selectedCount} selected', values: { selectedCount } }
{
defaultMessage: '{selectedCount}/{totalCount} selected',
values: { selectedCount, totalCount: filteredSpaces.length },
}
);
const hiddenSpacesLabel = i18n.translate(
'xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel',
{ defaultMessage: '+{hiddenCount} hidden', values: { hiddenCount } }
);
const hiddenSpaces = hiddenCount ? <EuiText size="xs">{hiddenSpacesLabel}</EuiText> : null;
return (
<>
<EuiFormRow
label={selectSpacesLabel}
labelAppend={
<EuiFlexGroup direction="column" gutterSize="none" alignItems="flexEnd">
<EuiFlexItem grow={false}>
<EuiText size="xs">{selectedSpacesLabel}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>{hiddenSpaces}</EuiFlexItem>
</EuiFlexGroup>
}
labelAppend={<EuiText size="xs">{selectedSpacesLabel}</EuiText>}
fullWidth
>
<></>
</EuiFormRow>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup
direction="column"
gutterSize="none"
responsive={false}
style={{ minHeight: 200 }}
>
<EuiFlexItem>
<Suspense fallback={<EuiLoadingSpinner />}>
<EuiSelectable
@ -242,7 +245,8 @@ export const SelectableSpacesControl = (props: Props) => {
function getAdditionalProps(
space: SpacesDataEntry,
activeSpaceId: string | false,
checked: boolean
checked: boolean,
isGlobalControlChecked: boolean
) {
if (space.id === activeSpaceId) {
return {
@ -251,7 +255,7 @@ function getAdditionalProps(
checked: 'on' as 'on',
};
}
if (!space.isAuthorizedForPurpose('shareSavedObjectsIntoSpace')) {
if (!isGlobalControlChecked && !space.isAuthorizedForPurpose('shareSavedObjectsIntoSpace')) {
return {
append: (
<>
@ -259,6 +263,7 @@ function getAdditionalProps(
{space.isFeatureDisabled ? APPEND_FEATURE_IS_DISABLED : null}
</>
),
...(space.isFeatureDisabled && { isAvatarDisabled: true }),
disabled: true,
};
}

View file

@ -147,7 +147,7 @@ export const ShareModeControl = (props: Props) => {
<EuiSpacer size="s" />
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexGroup responsive={false}>
<EuiFlexItem>
<EuiText
color="subdued"
@ -183,7 +183,7 @@ export const ShareModeControl = (props: Props) => {
<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<SelectableSpacesControl
spaces={spaces}
shareOptions={shareOptions}

View file

@ -495,7 +495,7 @@ describe('ShareToSpaceFlyout', () => {
<EuiBadge
color="hollow"
>
Current
This space
</EuiBadge>
`);
// by definition, the active space will always be checked

View file

@ -346,7 +346,7 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => {
referenceGraph={referenceGraph}
isDisabled={isStartShareButtonDisabled}
/>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={() => onClose()}
@ -407,10 +407,10 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => {
return (
<EuiFlyout onClose={onClose} maxWidth={500} data-test-subj="share-to-space-flyout">
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
{flyoutIcon && (
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={flyoutIcon} />
<EuiIcon size="l" type={flyoutIcon} />
</EuiFlexItem>
)}
<EuiFlexItem>
@ -424,10 +424,11 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => {
<EuiFlexGroup
direction="column"
gutterSize="none"
className="spcShareToSpace__flyoutBodyWrapper"
className="spcShareToSpace__flyoutBodyWrapper eui-yScroll"
responsive={false}
>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
{savedObjectTarget.icon && (
<EuiFlexItem grow={false}>
<EuiIcon type={savedObjectTarget.icon} />

View file

@ -44,6 +44,7 @@ export const SpaceListInternal = ({
displayLimit = DEFAULT_DISPLAY_LIMIT,
behaviorContext,
listOnClick = () => {},
cursorStyle,
}: SpaceListProps) => {
const { spacesDataPromise } = useSpaces();
@ -148,6 +149,9 @@ export const SpaceListInternal = ({
</EuiToolTip>
</EuiFlexItem>
) : null;
const styleProps = {
style: cursorStyle ? { cursor: cursorStyle } : undefined,
};
return (
<Suspense fallback={<EuiLoadingSpinner />}>
@ -162,7 +166,7 @@ export const SpaceListInternal = ({
size={'s'}
onClick={listOnClick}
onKeyPress={listOnClick}
style={{ cursor: 'pointer' }}
{...styleProps}
/>
</EuiFlexItem>
);

View file

@ -32,4 +32,8 @@ export interface SpaceListProps {
* Click handler for spaces list, specifically excluding expand and contract buttons.
*/
listOnClick?: () => void;
/**
* Style for the cursor when mousing over space avatars.
*/
cursorStyle?: string;
}

View file

@ -23269,7 +23269,6 @@
"xpack.spaces.shareToSpace.allSpacesTarget": "tous les espaces",
"xpack.spaces.shareToSpace.cancelButton": "Annuler",
"xpack.spaces.shareToSpace.continueButton": "Continuer",
"xpack.spaces.shareToSpace.currentSpaceBadge": "Actuel",
"xpack.spaces.shareToSpace.featureIsDisabledTooltip": "Cette fonctionnalité est désactivée dans cet espace.",
"xpack.spaces.shareToSpace.flyoutTitle": "Attribuer {objectNoun} aux espaces",
"xpack.spaces.shareToSpace.noAvailableSpaces.canCreateNewSpace.linkText": "créer un nouvel espace",
@ -23284,8 +23283,6 @@
"xpack.spaces.shareToSpace.saveButton": "Enregistrer et fermer",
"xpack.spaces.shareToSpace.shareErrorTitle": "Erreur lors de la mise à jour de {objectNoun}",
"xpack.spaces.shareToSpace.shareModeControl.buttonGroupLegend": "Choisir le mode de partage",
"xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount} masqué(s)",
"xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount} sélectionné(s)",
"xpack.spaces.shareToSpace.shareModeControl.selectSpacesLabel": "Sélectionner les espaces",
"xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.buttonLabel": "Tous les espaces",
"xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.cannotChangeTooltip": "Vous avez besoin de privilèges supplémentaires pour modifier cette option.",
@ -23302,7 +23299,6 @@
"xpack.spaces.shareToSpace.spacesLoadErrorTitle": "Erreur lors du chargement des espaces disponibles",
"xpack.spaces.shareToSpace.spacesTarget": "{spacesCount, plural, one {# espace} other {# espaces}}",
"xpack.spaces.shareToSpace.unknownSpacesLabel.additionalPrivilegesLink": "privilèges supplémentaires",
"xpack.spaces.shareToSpace.unknownSpacesLabel.text": "Pour afficher les espaces masqués, vous avez besoin de {additionalPrivilegesLink}.",
"xpack.spaces.spaceList.allSpacesLabel": "* Tous les espaces",
"xpack.spaces.spaceList.showLessSpacesLink": "afficher moins",
"xpack.spaces.spaceList.showMoreSpacesLink": "+{count} de plus",

View file

@ -26593,7 +26593,6 @@
"xpack.spaces.shareToSpace.allSpacesTarget": "すべてのスペース",
"xpack.spaces.shareToSpace.cancelButton": "キャンセル",
"xpack.spaces.shareToSpace.continueButton": "続行",
"xpack.spaces.shareToSpace.currentSpaceBadge": "現在",
"xpack.spaces.shareToSpace.featureIsDisabledTooltip": "この機能はこのスペースでは無効です。",
"xpack.spaces.shareToSpace.flyoutTitle": "{objectNoun}をスペースに割り当てる",
"xpack.spaces.shareToSpace.noAvailableSpaces.canCreateNewSpace.linkText": "新しいスペースを作成",
@ -26608,8 +26607,6 @@
"xpack.spaces.shareToSpace.saveButton": "保存して閉じる",
"xpack.spaces.shareToSpace.shareErrorTitle": "{objectNoun}の更新エラー",
"xpack.spaces.shareToSpace.shareModeControl.buttonGroupLegend": "この共有方法を選択",
"xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount}個が非表示",
"xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount}個が選択済み",
"xpack.spaces.shareToSpace.shareModeControl.selectSpacesLabel": "スペースを選択",
"xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.buttonLabel": "すべてのスペース",
"xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.cannotChangeTooltip": "このオプションを変更するには、追加権限が必要です。",
@ -26626,7 +26623,6 @@
"xpack.spaces.shareToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生",
"xpack.spaces.shareToSpace.spacesTarget": "{spacesCount, plural, other {# 個のスペース}}",
"xpack.spaces.shareToSpace.unknownSpacesLabel.additionalPrivilegesLink": "追加権限",
"xpack.spaces.shareToSpace.unknownSpacesLabel.text": "非表示のスペースを表示するには、{additionalPrivilegesLink}が必要です。",
"xpack.spaces.spaceList.allSpacesLabel": "*すべてのスペース",
"xpack.spaces.spaceList.showLessSpacesLink": "縮小表示",
"xpack.spaces.spaceList.showMoreSpacesLink": "他 {count} 件",

View file

@ -26622,7 +26622,6 @@
"xpack.spaces.shareToSpace.allSpacesTarget": "所有工作区",
"xpack.spaces.shareToSpace.cancelButton": "取消",
"xpack.spaces.shareToSpace.continueButton": "继续",
"xpack.spaces.shareToSpace.currentSpaceBadge": "当前",
"xpack.spaces.shareToSpace.featureIsDisabledTooltip": "此功能在此工作区中已禁用。",
"xpack.spaces.shareToSpace.flyoutTitle": "将 {objectNoun} 分配给工作区",
"xpack.spaces.shareToSpace.noAvailableSpaces.canCreateNewSpace.linkText": "创建新工作区",
@ -26637,8 +26636,6 @@
"xpack.spaces.shareToSpace.saveButton": "保存并关闭",
"xpack.spaces.shareToSpace.shareErrorTitle": "更新 {objectNoun} 时出错",
"xpack.spaces.shareToSpace.shareModeControl.buttonGroupLegend": "选择共享此对象的方式",
"xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount} 个已隐藏",
"xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount} 个已选择",
"xpack.spaces.shareToSpace.shareModeControl.selectSpacesLabel": "选择工作区",
"xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.buttonLabel": "所有工作区",
"xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.cannotChangeTooltip": "您还需要其他权限,才能更改此选项。",
@ -26655,7 +26652,6 @@
"xpack.spaces.shareToSpace.spacesLoadErrorTitle": "加载可用工作区时出错",
"xpack.spaces.shareToSpace.spacesTarget": "{spacesCount, plural, other {# 个工作区}}",
"xpack.spaces.shareToSpace.unknownSpacesLabel.additionalPrivilegesLink": "其他权限",
"xpack.spaces.shareToSpace.unknownSpacesLabel.text": "要查看隐藏的工作区,您需要{additionalPrivilegesLink}。",
"xpack.spaces.spaceList.allSpacesLabel": "* 所有工作区",
"xpack.spaces.spaceList.showLessSpacesLink": "显示更少",
"xpack.spaces.spaceList.showMoreSpacesLink": "另外 {count} 个",