From 7730412fb4eb0ef579d6bab8dabbc24566c03c5a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 23 May 2024 19:24:58 -0700 Subject: [PATCH 001/129] New Security Management page: View Space Warnings suppression Add tabs header grid and tabs Settings button fix Switch button getContentForSpace Enabled features Assigned roles View space tabs useTabs hook enabledFeatureCount / totalFeatureCount Count of assigned roles Assign role button working routing for selected tab Content tab: map app names for display and icon New Security Management page: View Space Warnings suppression Add tabs header grid and tabs getContentForSpace Enabled features View space tabs useTabs hook Assign role button --- .../enabled_features/enabled_features.tsx | 2 +- .../enabled_features/feature_table.tsx | 17 +- .../spaces_grid/spaces_grid_page.tsx | 10 +- .../management/spaces_management_app.tsx | 67 ++++- .../public/management/view_space/constants.ts | 10 + .../management/view_space/hooks/use_tabs.ts | 32 +++ .../hooks/view_space_context_provider.tsx | 35 +++ .../public/management/view_space/index.ts | 8 + .../management/view_space/view_space.tsx | 229 ++++++++++++++++++ .../view_space/view_space_content_tab.tsx | 146 +++++++++++ .../view_space_enabled_features_tab.tsx | 27 +++ .../view_space/view_space_roles.tsx | 186 ++++++++++++++ .../management/view_space/view_space_tabs.tsx | 60 +++++ .../public/spaces_manager/spaces_manager.ts | 12 + x-pack/plugins/spaces/public/types.ts | 7 + 15 files changed, 828 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/constants.ts create mode 100644 x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts create mode 100644 x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx create mode 100644 x-pack/plugins/spaces/public/management/view_space/index.ts create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space.tsx create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 36d0694953242..bd8492015f88b 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -21,7 +21,7 @@ import { SectionPanel } from '../section_panel'; interface Props { space: Partial; features: KibanaFeatureConfig[]; - onChange: (space: Partial) => void; + onChange?: (space: Partial) => void; } export const EnabledFeatures: FunctionComponent = (props) => { diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index bc4b586c6cd1d..7167271ece4a1 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -33,14 +33,17 @@ import { getEnabledFeatures } from '../../lib/feature_utils'; interface Props { space: Partial; features: KibanaFeatureConfig[]; - onChange: (space: Partial) => void; + onChange?: (space: Partial) => void; } export class FeatureTable extends Component { private featureCategories: Map = new Map(); + private isReadOnly: boolean; constructor(props: Props) { super(props); + this.isReadOnly = props.onChange == null; + // features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner props.features.forEach((feature) => { if (!this.featureCategories.has(feature.category.id)) { @@ -66,6 +69,8 @@ export class FeatureTable extends Component { id: `featureCategoryCheckbox_${category.id}`, indeterminate: enabledCount > 0 && enabledCount < featureCount, checked: featureCount === enabledCount, + readOnly: this.isReadOnly, + disabled: this.isReadOnly, ['aria-label']: i18n.translate( 'xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel', { defaultMessage: 'Category toggle' } @@ -162,6 +167,8 @@ export class FeatureTable extends Component { id={`featureCheckbox_${feature.id}`} data-test-subj={`featureCheckbox_${feature.id}`} checked={featureChecked} + readOnly={this.isReadOnly} + disabled={this.isReadOnly} onChange={this.onChange(feature.id) as any} label={feature.name} /> @@ -254,7 +261,9 @@ export class FeatureTable extends Component { } updatedSpace.disabledFeatures = disabledFeatures; - this.props.onChange(updatedSpace); + if (this.props.onChange) { + this.props.onChange(updatedSpace); + } }; private getAllFeatureIds = () => @@ -283,7 +292,9 @@ export class FeatureTable extends Component { ); } - this.props.onChange(updatedSpace); + if (this.props.onChange) { + this.props.onChange(updatedSpace); + } }; private getCategoryHelpText = (category: AppCategory) => { diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 6d2cd1a9bd05d..0c5b360398223 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -238,10 +238,10 @@ export class SpacesGridPage extends Component { field: 'initials', name: '', width: '50px', - render: (value: string, record: Space) => { + render: (_value: string, record: Space) => { return ( }> - + @@ -255,7 +255,7 @@ export class SpacesGridPage extends Component { }), sortable: true, render: (value: string, record: Space) => ( - + {value} ), @@ -275,7 +275,7 @@ export class SpacesGridPage extends Component { sortable: (space: Space) => { return getEnabledFeatures(this.state.features, space).length; }, - render: (disabledFeatures: string[], record: Space) => { + render: (_disabledFeatures: string[], record: Space) => { const enabledFeatureCount = getEnabledFeatures(this.state.features, record).length; if (enabledFeatureCount === this.state.features.length) { return ( @@ -365,6 +365,8 @@ export class SpacesGridPage extends Component { ]; } + private getViewSpacePath = (space: Space) => `view/${encodeURIComponent(space.id)}`; + private getEditSpacePath = (space: Space) => `edit/${encodeURIComponent(space.id)}`; private onDeleteSpaceClick = (space: Space) => { diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 1819b22cb7f3b..ad10194e48f32 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -41,14 +41,23 @@ export const spacesManagementApp = Object.freeze({ title, async mount({ element, setBreadcrumbs, history }) { - const [[coreStart, { features }], { SpacesGridPage }, { ManageSpacePage }] = - await Promise.all([getStartServices(), import('./spaces_grid'), import('./edit_space')]); + const [ + [coreStart, { features }], + { SpacesGridPage }, + { ManageSpacePage }, + { ViewSpacePage }, + ] = await Promise.all([ + getStartServices(), + import('./spaces_grid'), + import('./edit_space'), + import('./view_space'), + ]); const spacesFirstBreadcrumb = { text: title, href: `/`, }; - const { notifications, application, chrome } = coreStart; + const { notifications, application, chrome, http } = coreStart; chrome.docTitle.change(title); @@ -89,28 +98,59 @@ export const spacesManagementApp = Object.freeze({ ); }; - const EditSpacePageWithBreadcrumbs = () => { - const { spaceId } = useParams<{ spaceId: string }>(); + const SpacePageWithBreadcrumbs = ({ context }: { context: 'edit' | 'view' }) => { + const { spaceId, selectedTabId } = useParams<{ + spaceId: string; + selectedTabId?: string; + }>(); + + const breadcrumbText = (space: Space) => + context === 'edit' + ? i18n.translate('xpack.spaces.management.editSpaceBreadcrumb', { + defaultMessage: 'Edit {space}', + values: { space: space.name }, + }) + : i18n.translate('xpack.spaces.management.viewSpaceBreadcrumb', { + defaultMessage: 'View {space}', + values: { space: space.name }, + }); const onLoadSpace = (space: Space) => { setBreadcrumbs([ spacesFirstBreadcrumb, { - text: space.name, + text: breadcrumbText(space), }, ]); }; + if (context === 'edit') { + return ( + + ); + } + return ( - ); }; @@ -128,7 +168,10 @@ export const spacesManagementApp = Object.freeze({ - + + + + diff --git a/x-pack/plugins/spaces/public/management/view_space/constants.ts b/x-pack/plugins/spaces/public/management/view_space/constants.ts new file mode 100644 index 0000000000000..460bb8c5c4b3d --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 const TAB_ID_CONTENT = 'content'; +export const TAB_ID_FEATURES = 'features'; +export const TAB_ID_ROLES = 'roles'; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts new file mode 100644 index 0000000000000..3c518c7250dc6 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -0,0 +1,32 @@ +/* + * 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 { useMemo } from 'react'; + +import type { KibanaFeature } from '@kbn/features-plugin/public'; +import type { Role } from '@kbn/security-plugin-types-common'; + +import type { Space } from '../../../../common'; +import type { ViewSpaceTab } from '../view_space_tabs'; +import { getTabs } from '../view_space_tabs'; + +export const useTabs = ( + space: Space | null, + features: KibanaFeature[] | null, + roles: Role[], + currentSelectedTabId: string +): [ViewSpaceTab[], JSX.Element | undefined] => { + const [tabs, selectedTabContent] = useMemo(() => { + if (space == null || features == null) { + return [[]]; + } + const _tabs = space != null ? getTabs(space, features, roles) : []; + return [_tabs, _tabs.find((obj) => obj.id === currentSelectedTabId)?.content]; + }, [space, currentSelectedTabId, features, roles]); + + return [tabs, selectedTabContent]; +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx new file mode 100644 index 0000000000000..fc5bcef897790 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -0,0 +1,35 @@ +/* + * 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 type { FC, PropsWithChildren } from 'react'; +import React, { createContext, useContext } from 'react'; + +import type { SpacesManager } from '../../../spaces_manager'; + +interface ViewSpaceServices { + spacesManager: SpacesManager; +} + +const ViewSpaceContext = createContext(null); + +export const ViewSpaceContextProvider: FC> = ({ + children, + ...services +}) => { + return {children}; +}; + +export const useViewSpaceServices = (): ViewSpaceServices => { + const context = useContext(ViewSpaceContext); + if (!context) { + throw new Error( + 'ViewSpace Context is mising. Ensure the component or React root is wrapped with ViewSpaceContext' + ); + } + + return context; +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/index.ts b/x-pack/plugins/spaces/public/management/view_space/index.ts new file mode 100644 index 0000000000000..ff9ddac4a28e5 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/index.ts @@ -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 { ViewSpacePage } from './view_space'; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx new file mode 100644 index 0000000000000..180f59c01ac3e --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -0,0 +1,229 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSpacer, + EuiTab, + EuiTabs, + EuiText, +} from '@elastic/eui'; +import React, { lazy, Suspense, useEffect, useState } from 'react'; +import type { FC } from 'react'; + +import type { ApplicationStart, Capabilities, ScopedHistory } from '@kbn/core/public'; +import type { FeaturesPluginStart, KibanaFeature } from '@kbn/features-plugin/public'; +import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; +import type { Role } from '@kbn/security-plugin-types-common'; + +import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; +import { useTabs } from './hooks/use_tabs'; +import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; +import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; +import { getSpaceAvatarComponent } from '../../space_avatar'; +import type { SpacesManager } from '../../spaces_manager'; + +// 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 })) +); + +const getSelectedTabId = (selectedTabId?: string) => { + // Validation of the selectedTabId routing parameter, default to the Content tab + return selectedTabId && [TAB_ID_FEATURES, TAB_ID_ROLES].includes(selectedTabId) + ? selectedTabId + : TAB_ID_CONTENT; +}; + +interface PageProps { + capabilities: Capabilities; + getFeatures: FeaturesPluginStart['getFeatures']; + getUrlForApp: ApplicationStart['getUrlForApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; + serverBasePath: string; + spacesManager: SpacesManager; + history: ScopedHistory; + onLoadSpace: (space: Space) => void; + spaceId?: string; + selectedTabId?: string; +} + +const handleApiError = (error: Error) => { + // eslint-disable-next-line no-console + console.error(error); + throw error; +}; + +export const ViewSpacePage: FC = (props) => { + const { + spaceId, + getFeatures, + spacesManager, + history, + onLoadSpace, + selectedTabId: _selectedTabId, + } = props; + + const selectedTabId = getSelectedTabId(_selectedTabId); + const [space, setSpace] = useState(null); + const [features, setFeatures] = useState(null); + const [roles, setRoles] = useState([]); + const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); + + useEffect(() => { + if (!spaceId) { + return; + } + + const getSpace = async () => { + const result = await spacesManager.getSpace(spaceId); + if (!result) { + throw new Error(`Could not get resulting space by id ${spaceId}`); + } + setSpace(result); + }; + + getSpace().catch(handleApiError); + }, [spaceId, spacesManager]); + + useEffect(() => { + const _getFeatures = async () => { + const result = await getFeatures(); + setFeatures(result); + }; + _getFeatures().catch(handleApiError); + }, [getFeatures]); + + useEffect(() => { + if (spaceId) { + const getRoles = async () => { + const result = await spacesManager.getRolesForSpace(spaceId); + setRoles(result); + }; + + getRoles().catch(handleApiError); + } + }, [spaceId, spacesManager]); + + if (!space) { + return null; + } + + if (onLoadSpace) { + onLoadSpace(space); + } + + const HeaderAvatar = () => { + return space.imageUrl != null ? ( + }> + + + ) : ( + }> + + + ); + }; + + const SettingsButton = () => { + const { capabilities, getUrlForApp, navigateToUrl } = props; + + const href = getUrlForApp('management', { + path: `kibana/spaces/edit/${space.id}`, + }); + + return capabilities.spaces.manage ? ( + { + navigateToUrl(href); + }} + > + Settings + + ) : null; + }; + + const SwitchButton = () => { + const { serverBasePath } = props; + const urlToSelectedSpace = addSpaceIdToPath( + serverBasePath, + space.id, + `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${space.id}` + ); + + // use href to force full page reload (needed in order to change spaces) + return ( + + Switch to this space + + ); + }; + + return ( + + + + + + + +

{space.name}

+

+ + Organize your saved objects and show related features for creating new content. + +

+
+ + + + + + +
+ + + + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + + + {selectedTabContent ?? null} +
+
+ ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx new file mode 100644 index 0000000000000..69f6daedc12e1 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -0,0 +1,146 @@ +/* + * 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 type { EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import type { FC } from 'react'; +import React, { useEffect, useState } from 'react'; + +import { useViewSpaceServices } from './hooks/view_space_context_provider'; +import type { Space } from '../../../common'; +import type { SpaceContentTypeSummaryItem } from '../../types'; + +const mapForDisplay = (savedObjectType: string) => { + switch (savedObjectType) { + case 'canvas-workpad': + return { + icon: 'canvasApp', + displayName: 'Canvas', + }; + case 'config': + return { + icon: 'advancedSettingsApp', + displayName: 'Config', + }; + case 'dashboard': + return { + icon: 'dashboardApp', + displayName: 'Dashboard', + }; + case 'index-pattern': + return { + icon: 'indexSettings', + displayName: 'Index Pattern', + }; + case 'graph-workspace': + return { + icon: 'graphApp', + displayName: 'Graph Workspace', + }; + case 'lens': + return { + icon: 'lensApp', + displayName: 'Lens', + }; + case 'map': + return { + icon: 'emsApp', + displayName: 'Map', + }; + case 'search': + return { + icon: 'discoverApp', + displayName: 'Saved Search', + }; + case 'visualization': + return { + icon: 'visualizeApp', + displayName: 'Visualization', + }; + default: + // eslint-disable-next-line no-console + console.error(`Can not map for display type: ${savedObjectType}`); + return { + icon: 'gear', + displayName: savedObjectType, + }; + } +}; + +export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { + const { id: spaceId } = space; + const { spacesManager } = useViewSpaceServices(); + const [items, setItems] = useState(null); + + const columns: Array> = [ + { + field: 'type', + name: 'Type', + render: (value: string) => { + const { icon, displayName } = mapForDisplay(value); + return ( + + + + + {displayName} + + ); + }, + }, + { + field: 'count', + name: 'Count', + }, + ]; + + const getRowProps = (item: SpaceContentTypeSummaryItem) => { + const { type } = item; + return { + 'data-test-subj': `space-content-row-${type}`, + onClick: () => {}, + }; + }; + + const getCellProps = ( + item: SpaceContentTypeSummaryItem, + column: EuiTableFieldDataColumnType + ) => { + const { type } = item; + const { field } = column; + return { + 'data-test-subj': `space-content-cell-${type}-${String(field)}`, + textOnly: true, + }; + }; + + useEffect(() => { + const getItems = async () => { + const result = await spacesManager.getContentForSpace(spaceId); + const { summary } = result; + setItems(summary); + }; + + // eslint-disable-next-line no-console + getItems().catch(console.error); + }, [spaceId, spacesManager]); + + if (!items) { + return null; + } + + return ( + + ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx new file mode 100644 index 0000000000000..2f83b06d6c44a --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx @@ -0,0 +1,27 @@ +/* + * 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 type { FC } from 'react'; +import React from 'react'; + +import type { KibanaFeature } from '@kbn/features-plugin/common'; + +import type { Space } from '../../../common'; +import { EnabledFeatures } from '../edit_space/enabled_features'; + +interface Props { + space: Space; + features: KibanaFeature[]; +} + +export const ViewSpaceEnabledFeatures: FC = ({ features, space }) => { + if (!features) { + return null; + } + + return ; +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx new file mode 100644 index 0000000000000..a3119f775c4fc --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -0,0 +1,186 @@ +/* + * 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 type { EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; +import { + EuiBasicTable, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; +import type { FC } from 'react'; +import React, { useState } from 'react'; + +import type { Role } from '@kbn/security-plugin-types-common'; + +import type { Space } from '../../../common'; + +interface Props { + space: Space; + roles: Role[]; +} + +export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { + const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); + const getRowProps = (item: Role) => { + const { name } = item; + return { + 'data-test-subj': `space-role-row-${name}`, + onClick: () => {}, + }; + }; + + const getCellProps = (item: Role, column: EuiTableFieldDataColumnType) => { + const { name } = item; + const { field } = column; + return { + 'data-test-subj': `space-role-cell-${name}-${String(field)}`, + textOnly: true, + }; + }; + + const columns: Array> = [ + { + field: 'name', + name: 'Role', + }, + { + field: 'privileges', + name: 'Privileges', + render: (_value, record) => { + return record.kibana.map((kibanaPrivilege) => { + return kibanaPrivilege.base.join(', '); + }); + }, + }, + { + name: 'Actions', + actions: [ + { + name: 'Remove from space', + description: 'Click this action to remove the role privileges from this space.', + onClick: () => { + window.alert('Not yet implemented.'); + }, + }, + ], + }, + ]; + + const rolesInUse = roles.filter((role) => { + const privilegesSum = role.kibana.reduce((sum, privilege) => { + return sum + privilege.base.length; + }, 0); + return privilegesSum > 0; + }); + + if (!rolesInUse) { + return null; + } + + return ( + <> + {showRolesPrivilegeEditor && ( + { + setShowRolesPrivilegeEditor(false); + }} + onSaveClick={() => { + window.alert('your wish is granted'); + setShowRolesPrivilegeEditor(false); + }} + /> + )} + + +

Roles that can access this space. Privileges are managed at the role level.

+
+ + { + setShowRolesPrivilegeEditor(true); + }} + > + Assign role + + +
+ + + + ); +}; + +interface PrivilegesRolesFormProps { + space: Space; + roles: Role[]; + closeFlyout: () => void; + onSaveClick: () => void; +} + +export const PrivilegesRolesForm: FC = (props) => { + const { space, roles, onSaveClick, closeFlyout } = props; + + const getForm = () => { + return ; + }; + + const getSaveButton = () => { + return ( + + Assign roles + + ); + }; + + return ( + + + +

Assign role to {space.name}

+
+
+ +

+ Roles will be granted access to the current space according to their default privileges. + Use the ‘Customize’ option to override default privileges. +

+ {getForm()} +
+ + + + + Cancel + + + {getSaveButton()} + + +
+ ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx new file mode 100644 index 0000000000000..e64b21b0782c5 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiNotificationBadge } from '@elastic/eui'; +import React from 'react'; + +import type { KibanaFeature } from '@kbn/features-plugin/common'; +import type { Role } from '@kbn/security-plugin-types-common'; + +import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; +import { ViewSpaceContent } from './view_space_content_tab'; +import { ViewSpaceEnabledFeatures } from './view_space_enabled_features_tab'; +import { ViewSpaceAssignedRoles } from './view_space_roles'; +import type { Space } from '../../../common'; +import { getEnabledFeatures } from '../lib/feature_utils'; + +export interface ViewSpaceTab { + id: string; + name: string; + content: JSX.Element; + append?: JSX.Element; + href?: string; +} + +export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): ViewSpaceTab[] => { + const enabledFeatureCount = getEnabledFeatures(features, space).length; + const totalFeatureCount = features.length; + + return [ + { + id: TAB_ID_CONTENT, + name: 'Content', + content: , + }, + { + id: TAB_ID_FEATURES, + name: 'Feature visibility', + append: ( + + {enabledFeatureCount} / {totalFeatureCount} + + ), + content: , + }, + { + id: TAB_ID_ROLES, + name: 'Assigned roles', + append: ( + + {roles.length} + + ), + content: , + }, + ]; +}; diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index 7762158a4379b..962f02ca2bd79 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -11,9 +11,11 @@ import { BehaviorSubject, skipWhile } from 'rxjs'; import type { HttpSetup } from '@kbn/core/public'; import type { SavedObjectsCollectMultiNamespaceReferencesResponse } from '@kbn/core-saved-objects-api-server'; import type { LegacyUrlAliasTarget } from '@kbn/core-saved-objects-common'; +import type { Role } from '@kbn/security-plugin-types-common'; import type { GetAllSpacesOptions, GetSpaceResult, Space } from '../../common'; import type { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; +import type { SpaceContentTypeSummaryItem } from '../types'; interface SavedObjectTarget { type: string; @@ -192,4 +194,14 @@ export class SpacesManager { private isAnonymousPath() { return this.http.anonymousPaths.isAnonymous(window.location.pathname); } + + public getContentForSpace( + id: string + ): Promise<{ summary: SpaceContentTypeSummaryItem[]; total: number }> { + return this.http.get(`/internal/spaces/${id}/content_summary`); + } + + public getRolesForSpace(id: string): Promise { + return this.http.get(`/internal/security/roles/${id}`); + } } diff --git a/x-pack/plugins/spaces/public/types.ts b/x-pack/plugins/spaces/public/types.ts index 1ab253262c3c1..76efeead6abf3 100644 --- a/x-pack/plugins/spaces/public/types.ts +++ b/x-pack/plugins/spaces/public/types.ts @@ -61,3 +61,10 @@ export interface SpacesApi { */ ui: SpacesApiUi; } + +export interface SpaceContentTypeSummaryItem { + displayName: string; + icon?: string; + count: number; + type: string; +} From 827c2b0708144cd39b7550b5d80295cea85add25 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 3 Jun 2024 15:49:26 -0700 Subject: [PATCH 002/129] Content counts link to saved objects page --- .../hooks/view_space_context_provider.tsx | 4 + .../management/view_space/view_space.tsx | 11 ++- .../view_space/view_space_content_tab.tsx | 84 +++++-------------- 3 files changed, 32 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index fc5bcef897790..a36a4b7cf199b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -8,9 +8,13 @@ import type { FC, PropsWithChildren } from 'react'; import React, { createContext, useContext } from 'react'; +import type { ApplicationStart } from '@kbn/core-application-browser'; + import type { SpacesManager } from '../../../spaces_manager'; interface ViewSpaceServices { + getUrlForApp: ApplicationStart['getUrlForApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; spacesManager: SpacesManager; } diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 180f59c01ac3e..f9bf1b14e1e3d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -77,6 +77,7 @@ export const ViewSpacePage: FC = (props) => { const [features, setFeatures] = useState(null); const [roles, setRoles] = useState([]); const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); + const { capabilities, getUrlForApp, navigateToUrl } = props; useEffect(() => { if (!spaceId) { @@ -148,10 +149,8 @@ export const ViewSpacePage: FC = (props) => { }; const SettingsButton = () => { - const { capabilities, getUrlForApp, navigateToUrl } = props; - const href = getUrlForApp('management', { - path: `kibana/spaces/edit/${space.id}`, + path: `/kibana/spaces/edit/${space.id}`, }); return capabilities.spaces.manage ? ( @@ -183,7 +182,11 @@ export const ViewSpacePage: FC = (props) => { }; return ( - + diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx index 69f6daedc12e1..c0ee10652c6a0 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -6,7 +6,8 @@ */ import type { EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; -import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { capitalize } from 'lodash'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; @@ -14,80 +15,23 @@ import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; import type { SpaceContentTypeSummaryItem } from '../../types'; -const mapForDisplay = (savedObjectType: string) => { - switch (savedObjectType) { - case 'canvas-workpad': - return { - icon: 'canvasApp', - displayName: 'Canvas', - }; - case 'config': - return { - icon: 'advancedSettingsApp', - displayName: 'Config', - }; - case 'dashboard': - return { - icon: 'dashboardApp', - displayName: 'Dashboard', - }; - case 'index-pattern': - return { - icon: 'indexSettings', - displayName: 'Index Pattern', - }; - case 'graph-workspace': - return { - icon: 'graphApp', - displayName: 'Graph Workspace', - }; - case 'lens': - return { - icon: 'lensApp', - displayName: 'Lens', - }; - case 'map': - return { - icon: 'emsApp', - displayName: 'Map', - }; - case 'search': - return { - icon: 'discoverApp', - displayName: 'Saved Search', - }; - case 'visualization': - return { - icon: 'visualizeApp', - displayName: 'Visualization', - }; - default: - // eslint-disable-next-line no-console - console.error(`Can not map for display type: ${savedObjectType}`); - return { - icon: 'gear', - displayName: savedObjectType, - }; - } -}; - export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { const { id: spaceId } = space; - const { spacesManager } = useViewSpaceServices(); + const { spacesManager, getUrlForApp, navigateToUrl } = useViewSpaceServices(); const [items, setItems] = useState(null); const columns: Array> = [ { field: 'type', name: 'Type', - render: (value: string) => { - const { icon, displayName } = mapForDisplay(value); + render: (_value: string, item: SpaceContentTypeSummaryItem) => { + const { icon, displayName } = item; return ( - + - {displayName} + {capitalize(displayName)} ); }, @@ -95,6 +39,20 @@ export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { { field: 'count', name: 'Count', + render: (value: string, item: SpaceContentTypeSummaryItem) => { + const href = getUrlForApp('management', { + path: `/kibana/objects?type=${item.type}`, + }); + return ( + { + navigateToUrl(href); + }} + > + {value} + + ); + }, }, ]; From a3af1d857164c9bff7e33fded2f82c8f4973d2a1 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 4 Jun 2024 12:44:51 -0700 Subject: [PATCH 003/129] link to content working --- .../hooks/view_space_context_provider.tsx | 1 + .../management/view_space/view_space.tsx | 12 +++++++----- .../view_space/view_space_content_tab.tsx | 19 +++++++------------ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index a36a4b7cf199b..c37b9329f942e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -13,6 +13,7 @@ import type { ApplicationStart } from '@kbn/core-application-browser'; import type { SpacesManager } from '../../../spaces_manager'; interface ViewSpaceServices { + serverBasePath: string; getUrlForApp: ApplicationStart['getUrlForApp']; navigateToUrl: ApplicationStart['navigateToUrl']; spacesManager: SpacesManager; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index f9bf1b14e1e3d..580488321f671 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -154,14 +154,15 @@ export const ViewSpacePage: FC = (props) => { }); return capabilities.spaces.manage ? ( - { + { + event.preventDefault(); navigateToUrl(href); }} > - Settings - + Settings + ) : null; }; @@ -184,6 +185,7 @@ export const ViewSpacePage: FC = (props) => { return ( diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx index c0ee10652c6a0..66ade3f10087d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -17,7 +17,7 @@ import type { SpaceContentTypeSummaryItem } from '../../types'; export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { const { id: spaceId } = space; - const { spacesManager, getUrlForApp, navigateToUrl } = useViewSpaceServices(); + const { spacesManager, serverBasePath } = useViewSpaceServices(); const [items, setItems] = useState(null); const columns: Array> = [ @@ -40,18 +40,13 @@ export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { field: 'count', name: 'Count', render: (value: string, item: SpaceContentTypeSummaryItem) => { - const href = getUrlForApp('management', { - path: `/kibana/objects?type=${item.type}`, - }); - return ( - { - navigateToUrl(href); - }} - > - {value} - + const hrefToSelectedSavedObjects = addSpaceIdToPath( + serverBasePath, + space.id, + `${ENTER_SPACE_PATH}?next=/app/management/kibana/objects?initialQuery=type:(${item.type})` ); + + return {value}; }, }, ]; From 826185403fdfb93824eec9c787c158441b2ad78c Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 4 Jun 2024 14:49:56 -0700 Subject: [PATCH 004/129] Polish features tab --- .../management/view_space/view_space.tsx | 3 +- .../view_space_enabled_features_tab.tsx | 55 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 580488321f671..489c36f03f9bb 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -198,7 +198,8 @@ export const ViewSpacePage: FC = (props) => {

{space.name}

- Organize your saved objects and show related features for creating new content. + {space.description ?? + 'Organize your saved objects and show related features for creating new content.'}

diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx index 2f83b06d6c44a..b4b89d0a20145 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx @@ -5,13 +5,16 @@ * 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 { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Space } from '../../../common'; -import { EnabledFeatures } from '../edit_space/enabled_features'; +import { FeatureTable } from '../edit_space/enabled_features/feature_table'; interface Props { space: Space; @@ -19,9 +22,57 @@ interface Props { } export const ViewSpaceEnabledFeatures: FC = ({ features, space }) => { + const { services } = useKibana(); + if (!features) { return null; } - return ; + const canManageRoles = services.application?.capabilities.management?.security?.roles === true; + + return ( + + + +

+ +

+
+ + +

+ + + + ) : ( + + ), + }} + /> +

+
+
+ + + +
+ ); }; From 250b9833d0b0cc68d5f7c599a64145bf0439ffc8 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 4 Jun 2024 15:27:54 -0700 Subject: [PATCH 005/129] Loading states --- .../management/view_space/view_space.tsx | 16 +++++++++++++ .../view_space/view_space_content_tab.tsx | 23 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 489c36f03f9bb..901d5ac85f02f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -76,6 +76,9 @@ export const ViewSpacePage: FC = (props) => { const [space, setSpace] = useState(null); const [features, setFeatures] = useState(null); const [roles, setRoles] = useState([]); + const [isLoadingSpace, setIsLoadingSpace] = useState(true); + const [isLoadingFeatures, setIsLoadingFeatures] = useState(true); + const [isLoadingRoles, setIsLoadingRoles] = useState(true); const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); const { capabilities, getUrlForApp, navigateToUrl } = props; @@ -90,6 +93,7 @@ export const ViewSpacePage: FC = (props) => { throw new Error(`Could not get resulting space by id ${spaceId}`); } setSpace(result); + setIsLoadingSpace(false); }; getSpace().catch(handleApiError); @@ -99,6 +103,7 @@ export const ViewSpacePage: FC = (props) => { const _getFeatures = async () => { const result = await getFeatures(); setFeatures(result); + setIsLoadingFeatures(false); }; _getFeatures().catch(handleApiError); }, [getFeatures]); @@ -108,6 +113,7 @@ export const ViewSpacePage: FC = (props) => { const getRoles = async () => { const result = await spacesManager.getRolesForSpace(spaceId); setRoles(result); + setIsLoadingRoles(false); }; getRoles().catch(handleApiError); @@ -122,6 +128,16 @@ export const ViewSpacePage: FC = (props) => { onLoadSpace(space); } + if (isLoadingSpace || isLoadingFeatures || isLoadingRoles) { + return ( + + + + + + ); + } + const HeaderAvatar = () => { return space.imageUrl != null ? ( }> diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx index 66ade3f10087d..c88f5c8730df9 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -6,18 +6,26 @@ */ import type { EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; -import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiLoadingSpinner, +} from '@elastic/eui'; import { capitalize } from 'lodash'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; -import type { Space } from '../../../common'; +import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import type { SpaceContentTypeSummaryItem } from '../../types'; export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { const { id: spaceId } = space; const { spacesManager, serverBasePath } = useViewSpaceServices(); + const [isLoading, setIsLoading] = useState(true); const [items, setItems] = useState(null); const columns: Array> = [ @@ -76,12 +84,23 @@ export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { const result = await spacesManager.getContentForSpace(spaceId); const { summary } = result; setItems(summary); + setIsLoading(false); }; // eslint-disable-next-line no-console getItems().catch(console.error); }, [spaceId, spacesManager]); + if (isLoading) { + return ( + + + + + + ); + } + if (!items) { return null; } From 90862b1c915ca7bca19d0e1a1c1fb7846bf9ca94 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 4 Jun 2024 16:22:28 -0700 Subject: [PATCH 006/129] minor copy update to features tab --- .../management/view_space/view_space_enabled_features_tab.tsx | 2 +- .../spaces/public/management/view_space/view_space_tabs.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx index b4b89d0a20145..863dca3f95f9d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx @@ -37,7 +37,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space }) => {

diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index e64b21b0782c5..51e44f6a4b68a 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -38,7 +38,7 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): }, { id: TAB_ID_FEATURES, - name: 'Feature visibility', + name: 'Features', append: ( {enabledFeatureCount} / {totalFeatureCount} From 08b197ef9143558a90bc1f470194a319a302ae65 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 4 Jun 2024 18:12:11 -0700 Subject: [PATCH 007/129] use encodeURIComponent in params of href when navigating to content --- .../management/view_space/view_space_content_tab.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx index c88f5c8730df9..eae31edb8e2e2 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -48,13 +48,15 @@ export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { field: 'count', name: 'Count', render: (value: string, item: SpaceContentTypeSummaryItem) => { - const hrefToSelectedSavedObjects = addSpaceIdToPath( + const uriComponent = encodeURIComponent( + `/app/management/kibana/objects?initialQuery=type:(${item.type})` + ); + const href = addSpaceIdToPath( serverBasePath, space.id, - `${ENTER_SPACE_PATH}?next=/app/management/kibana/objects?initialQuery=type:(${item.type})` + `${ENTER_SPACE_PATH}?next=${uriComponent}` ); - - return {value}; + return {value}; }, }, ]; From e43310157f179fe3f0609f247a46f988a889cd07 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 7 Jun 2024 07:22:20 -0700 Subject: [PATCH 008/129] [wip] callout for current space --- .../management/view_space/view_space.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 901d5ac85f02f..f15bae1c36dfe 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, @@ -72,6 +73,7 @@ export const ViewSpacePage: FC = (props) => { selectedTabId: _selectedTabId, } = props; + const [activeSpaceId, setActiveSpaceId] = useState(null); const selectedTabId = getSelectedTabId(_selectedTabId); const [space, setSpace] = useState(null); const [features, setFeatures] = useState(null); @@ -82,11 +84,16 @@ export const ViewSpacePage: FC = (props) => { const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); const { capabilities, getUrlForApp, navigateToUrl } = props; + useEffect(() => { + spacesManager.getActiveSpace().then(({ id: nextSpaceId }) => { + setActiveSpaceId(nextSpaceId); + }); + }, [spacesManager]); + useEffect(() => { if (!spaceId) { return; } - const getSpace = async () => { const result = await spacesManager.getSpace(spaceId); if (!result) { @@ -183,6 +190,10 @@ export const ViewSpacePage: FC = (props) => { }; const SwitchButton = () => { + if (activeSpaceId === space.id) { + return This is the current space.; + } + const { serverBasePath } = props; const urlToSelectedSpace = addSpaceIdToPath( serverBasePath, @@ -220,10 +231,14 @@ export const ViewSpacePage: FC = (props) => {

- - - - + + + + + + + +
From 509479f37cb243a9a196b99e9964ab4fb08d773a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 7 Jun 2024 11:39:04 -0700 Subject: [PATCH 009/129] Feature table: optional header text --- .../enabled_features/enabled_features.tsx | 15 ++++++++++++++- .../edit_space/enabled_features/feature_table.tsx | 11 ++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index bd8492015f88b..93e37b4d68a77 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -75,7 +75,20 @@ export const EnabledFeatures: FunctionComponent = (props) => {
- + + + {i18n.translate('xpack.spaces.management.featureVisibilityTitle', { + defaultMessage: 'Feature visibility', + })} + + + } + /> diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index 7167271ece4a1..e85ce725eb4e9 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -31,6 +31,7 @@ import type { Space } from '../../../../common'; import { getEnabledFeatures } from '../../lib/feature_utils'; interface Props { + headerText?: JSX.Element; space: Partial; features: KibanaFeatureConfig[]; onChange?: (space: Partial) => void; @@ -222,15 +223,7 @@ export class FeatureTable extends Component { return (
- - - - {i18n.translate('xpack.spaces.management.featureVisibilityTitle', { - defaultMessage: 'Feature visibility', - })} - - - + {this.props.headerText} {controls.map((control, idx) => ( {control} From 2cacaa36f2bfac18308db4a2a665f1a04e86f62a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 7 Jun 2024 11:39:26 -0700 Subject: [PATCH 010/129] =?UTF-8?q?Feature=20table:=20dynamic=20=E2=80=9Cs?= =?UTF-8?q?how=20all=E2=80=9D=20/=20=E2=80=9Chide=20all=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enabled_features/feature_table.tsx | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index e85ce725eb4e9..19e41b7ca9d03 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -193,31 +193,33 @@ export class FeatureTable extends Component { const featureCount = this.props.features.length; const enabledCount = getEnabledFeatures(this.props.features, this.props.space).length; const controls = []; - if (enabledCount < featureCount) { - controls.push( - this.showAll()} - size="xs" - data-test-subj="showAllFeaturesLink" - > - {i18n.translate('xpack.spaces.management.selectAllFeaturesLink', { - defaultMessage: 'Show all', - })} - - ); - } - if (enabledCount > 0) { - controls.push( - this.hideAll()} - size="xs" - data-test-subj="hideAllFeaturesLink" - > - {i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', { - defaultMessage: 'Hide all', - })} - - ); + if (this.props.onChange) { + if (enabledCount < featureCount) { + controls.push( + this.showAll()} + size="xs" + data-test-subj="showAllFeaturesLink" + > + {i18n.translate('xpack.spaces.management.selectAllFeaturesLink', { + defaultMessage: 'Show all', + })} + + ); + } + if (enabledCount > 0) { + controls.push( + this.hideAll()} + size="xs" + data-test-subj="hideAllFeaturesLink" + > + {i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', { + defaultMessage: 'Hide all', + })} + + ); + } } return ( From 59ac3843bbed353eb2e779079c5aca7ec9d3fbf1 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 7 Jun 2024 11:39:46 -0700 Subject: [PATCH 011/129] Enabled Features tab header text update --- .../management/view_space/view_space_enabled_features_tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx index 863dca3f95f9d..b4b89d0a20145 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx @@ -37,7 +37,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space }) => {

From e968986d08db8209ca2c1f6f6ee11443fa9098bc Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:15:19 +0000 Subject: [PATCH 012/129] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/spaces/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 048004ff11334..89f3f98a6f9f4 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -36,6 +36,8 @@ "@kbn/react-kibana-context-render", "@kbn/utility-types-jest", "@kbn/security-plugin-types-public", + "@kbn/security-plugin-types-common", + "@kbn/core-application-browser", ], "exclude": [ "target/**/*", From 612165905343697e575aed664969815423b0d009 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 3 Jun 2024 13:32:04 +0200 Subject: [PATCH 013/129] display switch to space only when space is not active space --- .../enabled_features/feature_table.tsx | 8 +- .../management/view_space/view_space.tsx | 139 ++++++++++-------- 2 files changed, 76 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index 19e41b7ca9d03..1fec0ad8d3f2f 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -256,9 +256,7 @@ export class FeatureTable extends Component { } updatedSpace.disabledFeatures = disabledFeatures; - if (this.props.onChange) { - this.props.onChange(updatedSpace); - } + this.props.onChange?.(updatedSpace); }; private getAllFeatureIds = () => @@ -287,9 +285,7 @@ export class FeatureTable extends Component { ); } - if (this.props.onChange) { - this.props.onChange(updatedSpace); - } + this.props.onChange?.(updatedSpace); }; private getCategoryHelpText = (category: AppCategory) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index f15bae1c36dfe..f6bc90f244675 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -16,12 +16,14 @@ import { EuiTab, EuiTabs, EuiText, + EuiTitle, } from '@elastic/eui'; import React, { lazy, Suspense, useEffect, useState } from 'react'; import type { FC } from 'react'; import type { ApplicationStart, Capabilities, ScopedHistory } from '@kbn/core/public'; import type { FeaturesPluginStart, KibanaFeature } from '@kbn/features-plugin/public'; +import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import type { Role } from '@kbn/security-plugin-types-common'; @@ -76,6 +78,7 @@ export const ViewSpacePage: FC = (props) => { const [activeSpaceId, setActiveSpaceId] = useState(null); const selectedTabId = getSelectedTabId(_selectedTabId); const [space, setSpace] = useState(null); + const [userActiveSpace, setUserActiveSpace] = useState(null); const [features, setFeatures] = useState(null); const [roles, setRoles] = useState([]); const [isLoadingSpace, setIsLoadingSpace] = useState(true); @@ -94,27 +97,21 @@ export const ViewSpacePage: FC = (props) => { if (!spaceId) { return; } - const getSpace = async () => { - const result = await spacesManager.getSpace(spaceId); - if (!result) { - throw new Error(`Could not get resulting space by id ${spaceId}`); - } - setSpace(result); + + const getSpaceInfo = async () => { + const [activeSpace, currentSpace] = await Promise.all([ + spacesManager.getActiveSpace(), + spacesManager.getSpace(spaceId), + ]); + + setSpace(currentSpace); + setUserActiveSpace(activeSpace); setIsLoadingSpace(false); }; - getSpace().catch(handleApiError); + getSpaceInfo().catch(handleApiError); }, [spaceId, spacesManager]); - useEffect(() => { - const _getFeatures = async () => { - const result = await getFeatures(); - setFeatures(result); - setIsLoadingFeatures(false); - }; - _getFeatures().catch(handleApiError); - }, [getFeatures]); - useEffect(() => { if (spaceId) { const getRoles = async () => { @@ -127,14 +124,25 @@ export const ViewSpacePage: FC = (props) => { } }, [spaceId, spacesManager]); + useEffect(() => { + const _getFeatures = async () => { + const result = await getFeatures(); + setFeatures(result); + setIsLoadingFeatures(false); + }; + _getFeatures().catch(handleApiError); + }, [getFeatures]); + + useEffect(() => { + if (space) { + onLoadSpace?.(space); + } + }, [onLoadSpace, space]); + if (!space) { return null; } - if (onLoadSpace) { - onLoadSpace(space); - } - if (isLoadingSpace || isLoadingFeatures || isLoadingRoles) { return ( @@ -146,27 +154,9 @@ export const ViewSpacePage: FC = (props) => { } const HeaderAvatar = () => { - return space.imageUrl != null ? ( - }> - - - ) : ( + return ( }> - + ); }; @@ -184,7 +174,9 @@ export const ViewSpacePage: FC = (props) => { navigateToUrl(href); }} > - Settings + + + ) : null; }; @@ -201,10 +193,17 @@ export const ViewSpacePage: FC = (props) => { `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${space.id}` ); + if (userActiveSpace?.id === space.id) { + return null; + } + // use href to force full page reload (needed in order to change spaces) return ( - Switch to this space + ); }; @@ -222,13 +221,19 @@ export const ViewSpacePage: FC = (props) => {
-

{space.name}

-

- - {space.description ?? - 'Organize your saved objects and show related features for creating new content.'} - -

+ +

{space.name}

+
+ +

+ {space.description ?? ( + + )} +

+
@@ -244,22 +249,26 @@ export const ViewSpacePage: FC = (props) => { - - {tabs.map((tab, index) => ( - - {tab.name} - - ))} - - - - - {selectedTabContent ?? null} + + + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + {selectedTabContent ?? null} + ); From 7f90e7f002840cb1297260cef182ee48dcc336d4 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 4 Jun 2024 12:38:31 +0200 Subject: [PATCH 014/129] swap hardcoded strings for translated ones --- .../management/view_space/view_space.tsx | 10 +-- .../view_space/view_space_roles.tsx | 90 +++++++++++++------ .../management/view_space/view_space_tabs.tsx | 13 ++- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index f6bc90f244675..d9bca36de4784 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -199,9 +199,9 @@ export const ViewSpacePage: FC = (props) => { // use href to force full page reload (needed in order to change spaces) return ( - + @@ -216,19 +216,19 @@ export const ViewSpacePage: FC = (props) => { getUrlForApp={getUrlForApp} > - + -

{space.name}

+

{space.name}

{space.description ?? ( )} diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index a3119f775c4fc..8ec03a09c0cbd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -16,11 +16,14 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiText, EuiTitle, } from '@elastic/eui'; import type { FC } from 'react'; import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { Role } from '@kbn/security-plugin-types-common'; import type { Space } from '../../../common'; @@ -52,11 +55,15 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { const columns: Array> = [ { field: 'name', - name: 'Role', + name: i18n.translate('xpack.spaces.management.spaceDetails.roles.column.name.title', { + defaultMessage: 'Role', + }), }, { field: 'privileges', - name: 'Privileges', + name: i18n.translate('xpack.spaces.management.spaceDetails.roles.column.privileges.title', { + defaultMessage: 'Privileges', + }), render: (_value, record) => { return record.kibana.map((kibanaPrivilege) => { return kibanaPrivilege.base.join(', '); @@ -67,7 +74,12 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { name: 'Actions', actions: [ { - name: 'Remove from space', + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.column.actions.remove.title', + { + defaultMessage: 'Remove from space', + } + ), description: 'Click this action to remove the role privileges from this space.', onClick: () => { window.alert('Not yet implemented.'); @@ -103,29 +115,43 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { }} /> )} - + -

Roles that can access this space. Privileges are managed at the role level.

+ + + +

+ {i18n.translate('xpack.spaces.management.spaceDetails.roles.heading', { + defaultMessage: + 'Roles that can access this space. Privileges are managed at the role level.', + })} +

+
+
+ + { + setShowRolesPrivilegeEditor(true); + }} + > + {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign', { + defaultMessage: 'Assign role', + })} + + +
- - { - setShowRolesPrivilegeEditor(true); - }} - > - Assign role - + +
- - ); }; @@ -147,7 +173,9 @@ export const PrivilegesRolesForm: FC = (props) => { const getSaveButton = () => { return ( - Assign roles + {i18n.translate('xpack.spaces.management.spaceDetails.roles.assignRoleButton', { + defaultMessage: 'Assign roles', + })} ); }; @@ -160,10 +188,14 @@ export const PrivilegesRolesForm: FC = (props) => { -

- Roles will be granted access to the current space according to their default privileges. - Use the ‘Customize’ option to override default privileges. -

+ +

+ +

+
{getForm()}
@@ -175,7 +207,9 @@ export const PrivilegesRolesForm: FC = (props) => { flush="left" data-test-subj={'cancelRolesPrivilegeButton'} > - Cancel + {i18n.translate('xpack.spaces.management.spaceDetails.roles.cancelRoleButton', { + defaultMessage: 'Cancel', + })}
{getSaveButton()} diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 51e44f6a4b68a..07074fd498cb0 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -9,6 +9,7 @@ import { EuiNotificationBadge } from '@elastic/eui'; import React from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; +import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; @@ -33,12 +34,16 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): return [ { id: TAB_ID_CONTENT, - name: 'Content', + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.feature.heading', { + defaultMessage: 'Content', + }), content: , }, { id: TAB_ID_FEATURES, - name: 'Features', + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.feature.heading', { + defaultMessage: 'Feature visibility', + }), append: ( {enabledFeatureCount} / {totalFeatureCount} @@ -48,7 +53,9 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): }, { id: TAB_ID_ROLES, - name: 'Assigned roles', + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.roles.heading', { + defaultMessage: 'Assigned roles', + }), append: ( {roles.length} From 35a37e9c390bd9cda9e5f235c71ff243194a61e5 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 4 Jun 2024 12:39:21 +0200 Subject: [PATCH 015/129] add ftr test for space details and space switching --- .../spaces_grid/spaces_grid_page.tsx | 9 +- .../details_view/spaces_details_view.ts | 132 ++++++++++++++++++ x-pack/test/functional/apps/spaces/index.ts | 1 + .../page_objects/space_selector_page.ts | 5 + 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 0c5b360398223..49f7f13f25614 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -124,11 +124,15 @@ export class SpacesGridPage extends Component { ) : undefined} ({ + 'data-test-subj': `spacesListTableRow-${item.id}`, + })} columns={this.getColumnConfig()} pagination={true} sorting={true} @@ -255,7 +259,10 @@ export class SpacesGridPage extends Component { }), sortable: true, render: (value: string, record: Space) => ( - + {value} ), diff --git a/x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts b/x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts new file mode 100644 index 0000000000000..56fe87e6eed20 --- /dev/null +++ b/x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts @@ -0,0 +1,132 @@ +/* + * 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 crypto from 'crypto'; +import expect from '@kbn/expect'; +import { type FtrProviderContext } from '../../../ftr_provider_context'; + +export default function spaceDetailsViewFunctionalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'settings', 'spaceSelector']); + + const find = getService('find'); + const retry = getService('retry'); + const spacesServices = getService('spaces'); + const testSubjects = getService('testSubjects'); + + describe('Spaces', function () { + const testSpacesIds = [ + 'odyssey', + // this number is chosen intentionally to not exceed the default 10 items displayed by spaces table + ...Array.from(new Array(5)).map((_) => `space-${crypto.randomUUID()}`), + ]; + + before(async () => { + for (const testSpaceId of testSpacesIds) { + await spacesServices.create({ id: testSpaceId, name: `${testSpaceId}-name` }); + } + }); + + after(async () => { + for (const testSpaceId of testSpacesIds) { + await spacesServices.delete(testSpaceId); + } + }); + + describe('Space listing', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await testSubjects.existOrFail('spaces'); + }); + + beforeEach(async () => { + await PageObjects.common.navigateToUrl('management', 'kibana/spaces', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + + await testSubjects.existOrFail('spaces-grid-page'); + }); + + it('should list all the spaces populated', async () => { + const renderedSpaceRow = await find.allByCssSelector( + '[data-test-subj*=spacesListTableRow-]' + ); + + expect(renderedSpaceRow.length).to.equal(testSpacesIds.length + 1); + }); + + it('does not display the space switcher button when viewing the details page for the current selected space', async () => { + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); + + expect(currentSpaceTitle).to.equal('default'); + + await testSubjects.click('default-hyperlink'); + await testSubjects.existOrFail('spaceDetailsHeader'); + expect( + (await testSubjects.getVisibleText('spaceDetailsHeader')) + .toLowerCase() + .includes('default') + ).to.be(true); + await testSubjects.missingOrFail('spaceSwitcherButton'); + }); + + it("displays the space switcher button when viewing the details page of the space that's not the current selected one", async () => { + const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; + + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); + + expect(currentSpaceTitle).to.equal('default'); + + await testSubjects.click(`${testSpaceId}-hyperlink`); + await testSubjects.existOrFail('spaceDetailsHeader'); + expect( + (await testSubjects.getVisibleText('spaceDetailsHeader')) + .toLowerCase() + .includes(`${testSpaceId}-name`) + ).to.be(true); + await testSubjects.existOrFail('spaceSwitcherButton'); + }); + + it('switches to a new space using the space switcher button', async () => { + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); + + expect(currentSpaceTitle).to.equal('default'); + + const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; + + await testSubjects.click(`${testSpaceId}-hyperlink`); + await testSubjects.click('spaceSwitcherButton'); + + await retry.try(async () => { + const detailsTitle = ( + await testSubjects.getVisibleText('spaceDetailsHeader') + ).toLowerCase(); + + const currentSwitchSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLocaleLowerCase(); + + return ( + currentSwitchSpaceTitle && + currentSwitchSpaceTitle === `${testSpaceId}-name` && + detailsTitle.includes(currentSwitchSpaceTitle) + ); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index c951609d6a33f..0bcefafebc2d9 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -13,5 +13,6 @@ export default function spacesApp({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls/spaces_security')); loadTestFile(require.resolve('./spaces_selection')); loadTestFile(require.resolve('./enter_space')); + loadTestFile(require.resolve('./details_view/spaces_details_view')); }); } diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index 8c56dee81f435..e4879b1c3e5be 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -253,4 +253,9 @@ export class SpaceSelectorPageObject extends FtrService { ); expect(await msgElem.getVisibleText()).to.be('no spaces found'); } + + async currentSelectedSpaceTitle() { + const spacesNavSelector = await this.find.byCssSelector('[data-test-subj="spacesNavSelector"]'); + return spacesNavSelector.getAttribute('title'); + } } From 83f5dad9efc3b0549bbb38278e4e8250ec00ef8a Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 10 Jun 2024 12:06:06 +0200 Subject: [PATCH 016/129] fix failing test --- .../__snapshots__/enabled_features.test.tsx.snap | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap index 4bf010004cbef..fa2cf4f8c3f80 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap @@ -70,6 +70,15 @@ exports[`EnabledFeatures renders as expected 1`] = ` }, ] } + headerText={ + + + Feature visibility + + + } onChange={[MockFunction]} space={ Object { From dd25dd2458b9a4bd87229db82572ff86e29186e3 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 13 Jun 2024 14:34:53 +0200 Subject: [PATCH 017/129] add switch to space icon on space list table --- x-pack/plugins/spaces/common/index.ts | 6 +- .../spaces/common/lib/spaces_url_parser.ts | 19 ++++- .../spaces_grid/spaces_grid_page.tsx | 78 ++++++++++++++----- .../management/spaces_management_app.tsx | 1 + .../management/view_space/view_space.tsx | 13 ++-- 5 files changed, 87 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 4a767fb403ee2..0a0a84886647e 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -12,7 +12,11 @@ export { ENTER_SPACE_PATH, DEFAULT_SPACE_ID, } from './constants'; -export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; +export { + addSpaceIdToPath, + getSpaceIdFromPath, + getSpaceNavigationURL, +} from './lib/spaces_url_parser'; export type { Space, GetAllSpacesOptions, diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index 9b24a70792030..b847655aa9a87 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DEFAULT_SPACE_ID } from '../constants'; +import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../constants'; const spaceContextRegex = /^\/s\/([a-z0-9_\-]+)/; @@ -75,6 +75,23 @@ export function addSpaceIdToPath( return `${normalizedBasePath}${requestedPath}` || '/'; } +/** + * Builds URL that will navigate a user to the space for the spaceId provided + */ +export function getSpaceNavigationURL({ + serverBasePath, + spaceId, +}: { + serverBasePath: string; + spaceId: string; +}) { + return addSpaceIdToPath( + serverBasePath, + spaceId, + `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${spaceId}` + ); +} + function stripServerBasePath(requestBasePath: string, serverBasePath: string) { if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { return requestBasePath.substr(serverBasePath.length); diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 49f7f13f25614..5721de2d20647 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -6,6 +6,7 @@ */ import { + type EuiBasicTableColumn, EuiButton, EuiButtonIcon, EuiCallOut, @@ -30,7 +31,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; -import type { Space } from '../../../common'; +import { getSpaceNavigationURL, type Space } from '../../../common'; import { isReservedSpace } from '../../../common'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { getSpacesFeatureDescription } from '../../constants'; @@ -47,6 +48,7 @@ const LazySpaceAvatar = lazy(() => interface Props { spacesManager: SpacesManager; notifications: NotificationsStart; + serverBasePath: string; getFeatures: FeaturesPluginStart['getFeatures']; capabilities: Capabilities; history: ScopedHistory; @@ -56,6 +58,7 @@ interface Props { interface State { spaces: Space[]; + activeSpace: Space | null; features: KibanaFeature[]; loading: boolean; showConfirmDeleteModal: boolean; @@ -67,6 +70,7 @@ export class SpacesGridPage extends Component { super(props); this.state = { spaces: [], + activeSpace: null, features: [], loading: true, showConfirmDeleteModal: false, @@ -133,7 +137,7 @@ export class SpacesGridPage extends Component { rowProps={(item) => ({ 'data-test-subj': `spacesListTableRow-${item.id}`, })} - columns={this.getColumnConfig()} + columns={this.getColumnConfig({ serverBasePath: this.props.serverBasePath })} pagination={true} sorting={true} search={{ @@ -216,12 +220,18 @@ export class SpacesGridPage extends Component { }); const getSpaces = spacesManager.getSpaces(); + const getActiveSpace = spacesManager.getActiveSpace(); try { - const [spaces, features] = await Promise.all([getSpaces, getFeatures()]); + const [spaces, activeSpace, features] = await Promise.all([ + getSpaces, + getActiveSpace, + getFeatures(), + ]); this.setState({ loading: false, spaces, + activeSpace, features, }); } catch (error) { @@ -236,17 +246,23 @@ export class SpacesGridPage extends Component { } }; - public getColumnConfig() { + public getColumnConfig({ + serverBasePath, + }: { + serverBasePath: string; + }): Array> { return [ { field: 'initials', name: '', width: '50px', - render: (_value: string, record: Space) => { + render: (_value: string, rowRecord) => { return ( }> - - + + ); @@ -258,10 +274,10 @@ export class SpacesGridPage extends Component { defaultMessage: 'Space', }), sortable: true, - render: (value: string, record: Space) => ( + render: (value: string, rowRecord) => ( {value} @@ -282,8 +298,8 @@ export class SpacesGridPage extends Component { sortable: (space: Space) => { return getEnabledFeatures(this.state.features, space).length; }, - render: (_disabledFeatures: string[], record: Space) => { - const enabledFeatureCount = getEnabledFeatures(this.state.features, record).length; + render: (_disabledFeatures: string[], rowRecord) => { + const enabledFeatureCount = getEnabledFeatures(this.state.features, rowRecord).length; if (enabledFeatureCount === this.state.features.length) { return ( { }), actions: [ { - render: (record: Space) => ( + isPrimary: true, + available: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, + render: (rowRecord: Space) => { + return ( + + ); + }, + }, + { + render: (rowRecord: Space) => ( ), }, { - available: (record: Space) => !isReservedSpace(record), - render: (record: Space) => ( + available: (rowRecord: Space) => !isReservedSpace(rowRecord), + render: (rowRecord: Space) => ( this.onDeleteSpaceClick(record)} + onClick={() => this.onDeleteSpaceClick(rowRecord)} /> ), }, diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 5741006d19bd6..d068df15e6fb8 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -71,6 +71,7 @@ export const spacesManagementApp = Object.freeze({ getFeatures={features.getFeatures} notifications={notifications} spacesManager={spacesManager} + serverBasePath={http.basePath.serverBasePath} history={history} getUrlForApp={application.getUrlForApp} maxSpaces={config.maxSpaces} diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index d9bca36de4784..4491393d373e3 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -30,7 +30,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; -import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; +import { getSpaceNavigationURL, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import type { SpacesManager } from '../../spaces_manager'; @@ -187,11 +187,6 @@ export const ViewSpacePage: FC = (props) => { } const { serverBasePath } = props; - const urlToSelectedSpace = addSpaceIdToPath( - serverBasePath, - space.id, - `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${space.id}` - ); if (userActiveSpace?.id === space.id) { return null; @@ -199,7 +194,11 @@ export const ViewSpacePage: FC = (props) => { // use href to force full page reload (needed in order to change spaces) return ( - + Date: Thu, 13 Jun 2024 18:18:57 +0200 Subject: [PATCH 018/129] visual tweak for role creation page --- .../roles/edit_role/edit_role_page.tsx | 190 +++++++++++------- .../privilege_space_form.tsx | 10 +- .../space_aware_privilege_section.tsx | 2 +- 3 files changed, 125 insertions(+), 77 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index ccdc71d119f08..c69dc0d7ca821 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiForm, EuiFormRow, + EuiIconTip, EuiPanel, EuiSpacer, EuiText, @@ -555,30 +556,27 @@ export const EditRolePage: FunctionComponent = ({ const getElasticsearchPrivileges = () => { return ( -
- - -
+ ); }; @@ -586,21 +584,18 @@ export const EditRolePage: FunctionComponent = ({ const getKibanaPrivileges = () => { return ( -
- - -
+ ); }; @@ -797,44 +792,89 @@ export const EditRolePage: FunctionComponent = ({ return (
- - {getFormTitle()} - - - - - {isRoleReserved && ( - - - -

+ + + + {getFormTitle()} + + + + + + + {isRoleReserved && ( + + +

+ +

+
+
+ )} + + + {isDeprecatedRole && ( + + + + + )} + + {getRoleNameAndDescription()} + + -

- - - )} - {isDeprecatedRole && ( - - - - - )} - - {getRoleNameAndDescription()} - {getElasticsearchPrivileges()} - {getKibanaPrivileges()} - - {getFormButtons()} + } + > + {getElasticsearchPrivileges()} +
+
+ + + + + + + + } + /> + + + } + > + {getKibanaPrivileges()} + + + + {getFormButtons()} + +
); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 6abf5a04ae5c6..16f76a1e7a59d 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -105,10 +105,18 @@ export class PrivilegeSpaceForm extends Component {

+ +

+ +

+
{this.getForm()} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index f499da5c6973c..bb9430e3d873a 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -206,7 +206,7 @@ export class SpaceAwarePrivilegeSection extends Component { >
); From 31635b75e9d8fe8480137eb5a03257ba54eba1ca Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 13 Jun 2024 19:52:23 +0200 Subject: [PATCH 019/129] start work on assign to role flyout --- .../view_space/view_space_roles.tsx | 142 +++++++++++++++--- .../management/view_space/view_space_tabs.tsx | 2 +- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 8ec03a09c0cbd..9f398543b8882 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -5,35 +5,60 @@ * 2.0. */ -import type { EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; import { EuiBasicTable, EuiButton, EuiButtonEmpty, + EuiComboBox, + EuiFilterButton, + EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiForm, + EuiFormRow, + EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; +import type { + EuiBasicTableColumn, + EuiComboBoxOptionOption, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; import type { FC } from 'react'; import React, { useState } from 'react'; +import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { Role } from '@kbn/security-plugin-types-common'; import type { Space } from '../../../common'; +import { FeatureTable } from '../edit_space/enabled_features/feature_table'; interface Props { space: Space; roles: Role[]; + features: KibanaFeature[]; } -export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { +const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { + return roles.filter((role) => + role.kibana.reduce((acc, cur) => { + return ( + (cur.spaces.includes(space.name) || cur.spaces.includes('*')) && + Boolean(cur.base.length) && + acc + ); + }, true) + ); +}; + +export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const getRowProps = (item: Role) => { const { name } = item; @@ -89,12 +114,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { }, ]; - const rolesInUse = roles.filter((role) => { - const privilegesSum = role.kibana.reduce((sum, privilege) => { - return sum + privilege.base.length; - }, 0); - return privilegesSum > 0; - }); + const rolesInUse = filterRolesAssignedToSpace(roles, space); if (!rolesInUse) { return null; @@ -104,6 +124,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { <> {showRolesPrivilegeEditor && ( { @@ -143,7 +164,6 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles }) => { = ({ space, roles }) => { ); }; -interface PrivilegesRolesFormProps { - space: Space; - roles: Role[]; +interface PrivilegesRolesFormProps extends Props { closeFlyout: () => void; onSaveClick: () => void; } export const PrivilegesRolesForm: FC = (props) => { - const { space, roles, onSaveClick, closeFlyout } = props; + const { space, roles, onSaveClick, closeFlyout, features } = props; + + const [selectedRoles, setSelectedRoles] = useState>>([]); + const [spacePrivilege, setSpacePrivilege] = useState<'all' | 'read' | 'custom'>('all'); const getForm = () => { - return ; + return ( + + + ({ + label: role.name, + }))} + selectedOptions={selectedRoles} + onChange={(value) => { + setSelectedRoles(value); + }} + isClearable={true} + data-test-subj="roleSelectionComboBox" + autoFocus + fullWidth + /> + + + + setSpacePrivilege('all')} + > + + + setSpacePrivilege('read')} + > + + + setSpacePrivilege('custom')} + > + + + + + {spacePrivilege === 'custom' && ( + + <> + +

+ +

+
+ + + +
+ )} +
+ ); }; const getSaveButton = () => { @@ -186,9 +291,8 @@ export const PrivilegesRolesForm: FC = (props) => {

Assign role to {space.name}

- - - + +

= (props) => { />

- {getForm()} -
+ + {getForm()} diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 07074fd498cb0..d4cd9dc47dca2 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -61,7 +61,7 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): {roles.length}
), - content: , + content: , }, ]; }; From c16569031d04c65c1bbedec5d944940078a5a45c Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 17 Jun 2024 11:29:40 -0700 Subject: [PATCH 020/129] fix i18n --- .../spaces/public/management/view_space/view_space.tsx | 5 ++++- .../spaces/public/management/view_space/view_space_tabs.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 4491393d373e3..845b974efc570 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -175,7 +175,10 @@ export const ViewSpacePage: FC = (props) => { }} > - + ) : null; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index d4cd9dc47dca2..1118a82ed65fc 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -34,7 +34,7 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): return [ { id: TAB_ID_CONTENT, - name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.feature.heading', { + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { defaultMessage: 'Content', }), content: , From a0e4b4b168983a4abbd17670f25003c237e081dc Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 17 Jun 2024 12:10:41 -0700 Subject: [PATCH 021/129] fix ts --- .../spaces_grid/spaces_grid_page.test.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx index 59d4f1414e03a..5c785922fa012 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx @@ -58,6 +58,11 @@ featuresStart.getFeatures.mockResolvedValue([ }), ]); +const spacesGridCommonProps = { + serverBasePath: '', + maxSpaces: 1000, +}; + describe('SpacesGridPage', () => { const getUrlForApp = (appId: string) => appId; const history = scopedHistoryMock.create(); @@ -79,7 +84,7 @@ describe('SpacesGridPage', () => { catalogue: {}, spaces: { manage: true }, }} - maxSpaces={1000} + {...spacesGridCommonProps} /> ); @@ -107,7 +112,7 @@ describe('SpacesGridPage', () => { catalogue: {}, spaces: { manage: true }, }} - maxSpaces={1000} + {...spacesGridCommonProps} /> ); @@ -137,6 +142,7 @@ describe('SpacesGridPage', () => { spaces: { manage: true }, }} maxSpaces={1} + serverBasePath={spacesGridCommonProps.serverBasePath} /> ); @@ -170,7 +176,7 @@ describe('SpacesGridPage', () => { catalogue: {}, spaces: { manage: true }, }} - maxSpaces={1000} + {...spacesGridCommonProps} /> ); @@ -205,7 +211,7 @@ describe('SpacesGridPage', () => { catalogue: {}, spaces: { manage: true }, }} - maxSpaces={1000} + {...spacesGridCommonProps} /> ); From d3d6eec81f4dd51b597f7be215eb12d83d440e7e Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 21 Jun 2024 11:50:56 -0700 Subject: [PATCH 022/129] Truncate description in spaces grid page --- .../spaces/public/management/spaces_grid/spaces_grid_page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 5721de2d20647..09eb4a7d0c75c 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -289,6 +289,8 @@ export class SpacesGridPage extends Component { defaultMessage: 'Description', }), sortable: true, + truncateText: true, + width: '30%', }, { field: 'disabledFeatures', From 3b9b113b4e7eb6646eadf728df08b5daf2956b00 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 21 Jun 2024 11:53:15 -0700 Subject: [PATCH 023/129] Use subdued badge color in tabs --- .../spaces/public/management/view_space/view_space_tabs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 1118a82ed65fc..9a41d2a08bea6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -45,7 +45,7 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): defaultMessage: 'Feature visibility', }), append: ( - + {enabledFeatureCount} / {totalFeatureCount} ), @@ -57,7 +57,7 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): defaultMessage: 'Assigned roles', }), append: ( - + {roles.length} ), From 7639092f26b7378b83be5f661f22f6f0744d6ff6 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 21 Jun 2024 12:26:44 -0700 Subject: [PATCH 024/129] Update spaces grid for multiple actions --- .../spaces_grid/spaces_grid_page.tsx | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 09eb4a7d0c75c..a3e3c8da817b3 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -8,7 +8,6 @@ import { type EuiBasicTableColumn, EuiButton, - EuiButtonIcon, EuiCallOut, EuiInMemoryTable, EuiLink, @@ -352,58 +351,57 @@ export class SpacesGridPage extends Component { actions: [ { isPrimary: true, - available: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, - render: (rowRecord: Space) => { - return ( - - ); - }, + name: i18n.translate('xpack.spaces.management.spacesGridPage.editSpaceActionName', { + defaultMessage: `Edit`, + }), + description: (rowRecord) => + i18n.translate('xpack.spaces.management.spacesGridPage.editSpaceActionDescription', { + defaultMessage: `Edit {spaceName}.`, + values: { spaceName: rowRecord.name }, + }), + type: 'icon', + icon: 'pencil', + color: 'primary', + href: (rowRecord) => + reactRouterNavigate(this.props.history, this.getEditSpacePath(rowRecord)).href, + onClick: (rowRecord) => + reactRouterNavigate(this.props.history, this.getEditSpacePath(rowRecord)).onClick, + 'data-test-subj': (rowRecord) => `${rowRecord.name}-editSpace`, }, { - render: (rowRecord: Space) => ( - - ), + name: i18n.translate('xpack.spaces.management.spacesGridPage.switchSpaceActionName', { + defaultMessage: 'Switch', + }), + description: (rowRecord) => + i18n.translate( + 'xpack.spaces.management.spacesGridPage.switchSpaceActionDescription', + { + defaultMessage: 'Switch to {spaceName} space', + values: { spaceName: rowRecord.name }, + } + ), + type: 'icon', + icon: 'merge', + color: 'primary', + href: (rowRecord) => getSpaceNavigationURL({ serverBasePath, spaceId: rowRecord.id }), + available: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, + 'data-test-subj': (rowRecord) => `${rowRecord.name}-switchSpace`, }, { + name: i18n.translate('xpack.spaces.management.spacesGridPage.deleteActionName', { + defaultMessage: `Delete`, + }), + description: (rowRecord) => + i18n.translate('xpack.spaces.management.spacesGridPage.deleteActionName', { + defaultMessage: `Delete {spaceName}.`, + values: { spaceName: rowRecord.name }, + }), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (rowRecord) => this.onDeleteSpaceClick(rowRecord), available: (rowRecord: Space) => !isReservedSpace(rowRecord), - render: (rowRecord: Space) => ( - this.onDeleteSpaceClick(rowRecord)} - /> - ), + 'data-test-subj': (rowRecord) => `${rowRecord.name}-deleteSpace`, }, ], }, From 581b59f1acc007b6c07aec676f8a35a5bb0c1cc5 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 21 Jun 2024 12:38:26 -0700 Subject: [PATCH 025/129] Keep user on spaces grid page when they switch to space from there --- x-pack/plugins/spaces/common/index.ts | 6 +----- .../spaces/common/lib/spaces_url_parser.ts | 19 +------------------ .../spaces_grid/spaces_grid_page.tsx | 11 ++++++++--- .../management/view_space/view_space.tsx | 8 ++++++-- 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 0a0a84886647e..4a767fb403ee2 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -12,11 +12,7 @@ export { ENTER_SPACE_PATH, DEFAULT_SPACE_ID, } from './constants'; -export { - addSpaceIdToPath, - getSpaceIdFromPath, - getSpaceNavigationURL, -} from './lib/spaces_url_parser'; +export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; export type { Space, GetAllSpacesOptions, diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index b847655aa9a87..9b24a70792030 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../constants'; +import { DEFAULT_SPACE_ID } from '../constants'; const spaceContextRegex = /^\/s\/([a-z0-9_\-]+)/; @@ -75,23 +75,6 @@ export function addSpaceIdToPath( return `${normalizedBasePath}${requestedPath}` || '/'; } -/** - * Builds URL that will navigate a user to the space for the spaceId provided - */ -export function getSpaceNavigationURL({ - serverBasePath, - spaceId, -}: { - serverBasePath: string; - spaceId: string; -}) { - return addSpaceIdToPath( - serverBasePath, - spaceId, - `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${spaceId}` - ); -} - function stripServerBasePath(requestBasePath: string, serverBasePath: string) { if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { return requestBasePath.substr(serverBasePath.length); diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index a3e3c8da817b3..4645051cfd491 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -30,9 +30,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; -import { getSpaceNavigationURL, type Space } from '../../../common'; +import { addSpaceIdToPath, type Space } from '../../../common'; import { isReservedSpace } from '../../../common'; -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; import { getSpacesFeatureDescription } from '../../constants'; import { getSpaceAvatarComponent } from '../../space_avatar'; import type { SpacesManager } from '../../spaces_manager'; @@ -383,7 +383,12 @@ export class SpacesGridPage extends Component { type: 'icon', icon: 'merge', color: 'primary', - href: (rowRecord) => getSpaceNavigationURL({ serverBasePath, spaceId: rowRecord.id }), + href: (rowRecord) => + addSpaceIdToPath( + serverBasePath, + rowRecord.id, + `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/` + ), available: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, 'data-test-subj': (rowRecord) => `${rowRecord.name}-switchSpace`, }, diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 845b974efc570..f5b308b3f342e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -30,7 +30,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; -import { getSpaceNavigationURL, type Space } from '../../../common'; +import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import type { SpacesManager } from '../../spaces_manager'; @@ -199,7 +199,11 @@ export const ViewSpacePage: FC = (props) => { return ( Date: Fri, 21 Jun 2024 15:43:43 -0700 Subject: [PATCH 026/129] fix i18n error --- .../spaces/public/management/spaces_grid/spaces_grid_page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 4645051cfd491..9d407e60c1ca7 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -397,7 +397,7 @@ export class SpacesGridPage extends Component { defaultMessage: `Delete`, }), description: (rowRecord) => - i18n.translate('xpack.spaces.management.spacesGridPage.deleteActionName', { + i18n.translate('xpack.spaces.management.spacesGridPage.deleteActionDescription', { defaultMessage: `Delete {spaceName}.`, values: { spaceName: rowRecord.name }, }), From e7ae2f273e3fb38bc81f2d8fd995fd5309224ac4 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 1 Jul 2024 11:29:26 -0700 Subject: [PATCH 027/129] add fixme props --- .../plugins/spaces/public/management/view_space/view_space.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index f5b308b3f342e..7eab1eb458406 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -48,6 +48,8 @@ const getSelectedTabId = (selectedTabId?: string) => { interface PageProps { capabilities: Capabilities; + allowFeatureVisibility: boolean; // FIXME: handle this + solutionNavExperiment: Promise; // FIXME: handle this getFeatures: FeaturesPluginStart['getFeatures']; getUrlForApp: ApplicationStart['getUrlForApp']; navigateToUrl: ApplicationStart['navigateToUrl']; From 532a0ccd33b1219e8483a2be81aa014ea7a976a5 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 1 Jul 2024 15:57:32 -0700 Subject: [PATCH 028/129] fix ts --- .../public/management/spaces_grid/spaces_grid_page.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx index 7b5fefeaf6249..ef549496cc96b 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx @@ -143,8 +143,8 @@ describe('SpacesGridPage', () => { catalogue: {}, spaces: { manage: true }, }} - maxSpaces={1000} solutionNavExperiment={Promise.resolve(true)} + {...spacesGridCommonProps} /> ); From c7ea1bbc0d384638cb381d63b35912f8ad0f2c77 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 3 Jul 2024 14:18:30 -0700 Subject: [PATCH 029/129] Current space badge for space detail header --- .../management/view_space/view_space.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 7eab1eb458406..70b104c611c92 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -6,9 +6,9 @@ */ import { + EuiBadge, EuiButton, EuiButtonEmpty, - EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, @@ -77,7 +77,6 @@ export const ViewSpacePage: FC = (props) => { selectedTabId: _selectedTabId, } = props; - const [activeSpaceId, setActiveSpaceId] = useState(null); const selectedTabId = getSelectedTabId(_selectedTabId); const [space, setSpace] = useState(null); const [userActiveSpace, setUserActiveSpace] = useState(null); @@ -89,12 +88,6 @@ export const ViewSpacePage: FC = (props) => { const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); const { capabilities, getUrlForApp, navigateToUrl } = props; - useEffect(() => { - spacesManager.getActiveSpace().then(({ id: nextSpaceId }) => { - setActiveSpaceId(nextSpaceId); - }); - }, [spacesManager]); - useEffect(() => { if (!spaceId) { return; @@ -187,16 +180,12 @@ export const ViewSpacePage: FC = (props) => { }; const SwitchButton = () => { - if (activeSpaceId === space.id) { - return This is the current space.; - } - - const { serverBasePath } = props; - if (userActiveSpace?.id === space.id) { return null; } + const { serverBasePath } = props; + // use href to force full page reload (needed in order to change spaces) return ( = (props) => { -

{space.name}

+

+ {space.name} + {userActiveSpace?.id === space.id ? ( + <> + {' '} + + + + + ) : null} +

+

{space.description ?? ( From ac40e068bbb388e95c9f8f9033fadc81f7afb484 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 3 Jul 2024 14:56:20 -0700 Subject: [PATCH 030/129] Show selected solution in space detail --- .../management/view_space/view_space.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 70b104c611c92..57260c56c0884 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -32,6 +32,7 @@ import { useTabs } from './hooks/use_tabs'; import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; +import { SpaceSolutionBadge } from '../../space_solution_badge'; import type { SpacesManager } from '../../spaces_manager'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. @@ -49,7 +50,7 @@ const getSelectedTabId = (selectedTabId?: string) => { interface PageProps { capabilities: Capabilities; allowFeatureVisibility: boolean; // FIXME: handle this - solutionNavExperiment: Promise; // FIXME: handle this + solutionNavExperiment?: Promise; getFeatures: FeaturesPluginStart['getFeatures']; getUrlForApp: ApplicationStart['getUrlForApp']; navigateToUrl: ApplicationStart['navigateToUrl']; @@ -74,6 +75,7 @@ export const ViewSpacePage: FC = (props) => { spacesManager, history, onLoadSpace, + solutionNavExperiment, selectedTabId: _selectedTabId, } = props; @@ -87,6 +89,7 @@ export const ViewSpacePage: FC = (props) => { const [isLoadingRoles, setIsLoadingRoles] = useState(true); const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); const { capabilities, getUrlForApp, navigateToUrl } = props; + const [isSolutionNavEnabled, setIsSolutionNavEnabled] = useState(false); useEffect(() => { if (!spaceId) { @@ -134,6 +137,13 @@ export const ViewSpacePage: FC = (props) => { } }, [onLoadSpace, space]); + useEffect(() => { + solutionNavExperiment?.then((isEnabled) => { + console.log(isEnabled ? 'yeah' : 'nope'); + setIsSolutionNavEnabled(isEnabled); + }); + }, [solutionNavExperiment]); + if (!space) { return null; } @@ -221,6 +231,15 @@ export const ViewSpacePage: FC = (props) => {

{space.name} + {isSolutionNavEnabled ? ( + <> + {' '} + + + ) : null} {userActiveSpace?.id === space.id ? ( <> {' '} From fed1c856d62d36ac28993dd6e61875fa93afe764 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 3 Jul 2024 14:56:46 -0700 Subject: [PATCH 031/129] Cleanup --- .../management/view_space/view_space.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 57260c56c0884..491e5cad40961 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -77,6 +77,9 @@ export const ViewSpacePage: FC = (props) => { onLoadSpace, solutionNavExperiment, selectedTabId: _selectedTabId, + capabilities, + getUrlForApp, + navigateToUrl, } = props; const selectedTabId = getSelectedTabId(_selectedTabId); @@ -88,7 +91,6 @@ export const ViewSpacePage: FC = (props) => { const [isLoadingFeatures, setIsLoadingFeatures] = useState(true); const [isLoadingRoles, setIsLoadingRoles] = useState(true); const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); - const { capabilities, getUrlForApp, navigateToUrl } = props; const [isSolutionNavEnabled, setIsSolutionNavEnabled] = useState(false); useEffect(() => { @@ -111,15 +113,17 @@ export const ViewSpacePage: FC = (props) => { }, [spaceId, spacesManager]); useEffect(() => { - if (spaceId) { - const getRoles = async () => { - const result = await spacesManager.getRolesForSpace(spaceId); - setRoles(result); - setIsLoadingRoles(false); - }; - - getRoles().catch(handleApiError); + if (!spaceId) { + return; } + + const getRoles = async () => { + const result = await spacesManager.getRolesForSpace(spaceId); + setRoles(result); + setIsLoadingRoles(false); + }; + + getRoles().catch(handleApiError); }, [spaceId, spacesManager]); useEffect(() => { From 2a16ec9c8123bb5e4f351a1680338e9a2c5e864f Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 8 Jul 2024 12:41:08 -0700 Subject: [PATCH 032/129] Minor feedback updates --- .../public/management/spaces_grid/spaces_grid_page.tsx | 4 ++-- .../spaces/public/management/view_space/view_space.tsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 07c0e9ba8ab32..1629a749b3067 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -298,7 +298,7 @@ export class SpacesGridPage extends Component { { field: 'disabledFeatures', name: i18n.translate('xpack.spaces.management.spacesGridPage.featuresColumnName', { - defaultMessage: 'Features', + defaultMessage: 'Features visible', }), sortable: (space: Space) => { return getEnabledFeatures(this.state.features, space).length; @@ -326,7 +326,7 @@ export class SpacesGridPage extends Component { return ( = (props) => { useEffect(() => { solutionNavExperiment?.then((isEnabled) => { - console.log(isEnabled ? 'yeah' : 'nope'); setIsSolutionNavEnabled(isEnabled); }); }, [solutionNavExperiment]); @@ -250,8 +249,8 @@ export const ViewSpacePage: FC = (props) => { From 19dd7ff3d399ff218b798dc3c1e6dd1458067f92 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 8 Jul 2024 12:59:59 -0700 Subject: [PATCH 033/129] Make action icons for the space consistent --- .../spaces_grid/spaces_grid_page.tsx | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 1629a749b3067..64f5765938584 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -388,14 +388,26 @@ export class SpacesGridPage extends Component { 'data-test-subj': (rowRecord) => `${rowRecord.name}-editSpace`, }, { + isPrimary: true, name: i18n.translate('xpack.spaces.management.spacesGridPage.switchSpaceActionName', { defaultMessage: 'Switch', }), description: (rowRecord) => - i18n.translate('xpack.spaces.management.spacesGridPage.switchSpaceActionDescription', { - defaultMessage: 'Switch to {spaceName} space', - values: { spaceName: rowRecord.name }, - }), + this.state.activeSpace?.name !== rowRecord.name + ? i18n.translate( + 'xpack.spaces.management.spacesGridPage.switchSpaceActionDescription', + { + defaultMessage: 'Switch to {spaceName}', + values: { spaceName: rowRecord.name }, + } + ) + : i18n.translate( + 'xpack.spaces.management.spacesGridPage.switchSpaceActionDisabledDescription', + { + defaultMessage: '{spaceName} is the current space', + values: { spaceName: rowRecord.name }, + } + ), type: 'icon', icon: 'merge', color: 'primary', @@ -405,7 +417,7 @@ export class SpacesGridPage extends Component { rowRecord.id, `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/` ), - available: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, + enabled: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, 'data-test-subj': (rowRecord) => `${rowRecord.name}-switchSpace`, }, { @@ -413,15 +425,23 @@ export class SpacesGridPage extends Component { defaultMessage: `Delete`, }), description: (rowRecord) => - i18n.translate('xpack.spaces.management.spacesGridPage.deleteActionDescription', { - defaultMessage: `Delete {spaceName}.`, - values: { spaceName: rowRecord.name }, - }), + isReservedSpace(rowRecord) + ? i18n.translate( + 'xpack.spaces.management.spacesGridPage.deleteActionDisabledDescription', + { + defaultMessage: `{spaceName} is reserved`, + values: { spaceName: rowRecord.name }, + } + ) + : i18n.translate('xpack.spaces.management.spacesGridPage.deleteActionDescription', { + defaultMessage: `Delete {spaceName}`, + values: { spaceName: rowRecord.name }, + }), type: 'icon', icon: 'trash', color: 'danger', onClick: (rowRecord) => this.onDeleteSpaceClick(rowRecord), - available: (rowRecord: Space) => !isReservedSpace(rowRecord), + enabled: (rowRecord: Space) => !isReservedSpace(rowRecord), 'data-test-subj': (rowRecord) => `${rowRecord.name}-deleteSpace`, }, ], From 6e00210a4a776ceb184cc7560235b1e6292e4b3b Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 2 Jul 2024 15:16:49 +0200 Subject: [PATCH 034/129] include roleAPIClient --- .../management/spaces_management_app.tsx | 9 +++++- .../hooks/view_space_context_provider.tsx | 4 ++- .../management/view_space/view_space.tsx | 23 +++++++-------- .../view_space/view_space_roles.tsx | 28 +++++++++++++++++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 24792a1ef26b7..c5c0d9179f059 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -33,7 +33,13 @@ interface CreateParams { export const spacesManagementApp = Object.freeze({ id: 'spaces', - create({ getStartServices, spacesManager, config, solutionNavExperiment }: CreateParams) { + create({ + getStartServices, + spacesManager, + config, + solutionNavExperiment, + getRolesAPIClient, + }: CreateParams) { const title = i18n.translate('xpack.spaces.displayName', { defaultMessage: 'Spaces', }); @@ -160,6 +166,7 @@ export const spacesManagementApp = Object.freeze({ onLoadSpace={onLoadSpace} spaceId={spaceId} selectedTabId={selectedTabId} + getRolesAPIClient={getRolesAPIClient} /> ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index c37b9329f942e..77f246d570948 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -9,14 +9,16 @@ import type { FC, PropsWithChildren } from 'react'; import React, { createContext, useContext } from 'react'; import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { RolesAPIClient } from '@kbn/security-plugin-types-public'; import type { SpacesManager } from '../../../spaces_manager'; -interface ViewSpaceServices { +export interface ViewSpaceServices { serverBasePath: string; getUrlForApp: ApplicationStart['getUrlForApp']; navigateToUrl: ApplicationStart['navigateToUrl']; spacesManager: SpacesManager; + getRolesAPIClient: () => Promise; } const ViewSpaceContext = createContext(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 452ca57b3163f..8edccacb91110 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -21,7 +21,7 @@ import { import React, { lazy, Suspense, useEffect, useState } from 'react'; import type { FC } from 'react'; -import type { ApplicationStart, Capabilities, ScopedHistory } from '@kbn/core/public'; +import type { Capabilities, ScopedHistory } from '@kbn/core/public'; import type { FeaturesPluginStart, KibanaFeature } from '@kbn/features-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; @@ -29,11 +29,13 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; -import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; +import { + ViewSpaceContextProvider, + type ViewSpaceServices, +} from './hooks/view_space_context_provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import { SpaceSolutionBadge } from '../../space_solution_badge'; -import type { SpacesManager } from '../../spaces_manager'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. const LazySpaceAvatar = lazy(() => @@ -47,19 +49,15 @@ const getSelectedTabId = (selectedTabId?: string) => { : TAB_ID_CONTENT; }; -interface PageProps { +interface PageProps extends ViewSpaceServices { + spaceId?: string; + history: ScopedHistory; + selectedTabId?: string; capabilities: Capabilities; allowFeatureVisibility: boolean; // FIXME: handle this solutionNavExperiment?: Promise; getFeatures: FeaturesPluginStart['getFeatures']; - getUrlForApp: ApplicationStart['getUrlForApp']; - navigateToUrl: ApplicationStart['navigateToUrl']; - serverBasePath: string; - spacesManager: SpacesManager; - history: ScopedHistory; onLoadSpace: (space: Space) => void; - spaceId?: string; - selectedTabId?: string; } const handleApiError = (error: Error) => { @@ -80,6 +78,7 @@ export const ViewSpacePage: FC = (props) => { capabilities, getUrlForApp, navigateToUrl, + getRolesAPIClient, } = props; const selectedTabId = getSelectedTabId(_selectedTabId); @@ -123,6 +122,7 @@ export const ViewSpacePage: FC = (props) => { setIsLoadingRoles(false); }; + // maybe we do not make this call if user can't view roles? 🤔 getRoles().catch(handleApiError); }, [spaceId, spacesManager]); @@ -220,6 +220,7 @@ export const ViewSpacePage: FC = (props) => { return ( extends Promise + ? R + : never; + interface Props { space: Space; roles: Role[]; @@ -60,6 +65,19 @@ const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); + + const rolesAPIClient = useRef(); + + const { getRolesAPIClient } = useViewSpaceServices(); + + useEffect(() => { + async function resolveRolesAPIClient() { + rolesAPIClient.current = await getRolesAPIClient(); + } + + resolveRolesAPIClient(); + }, [getRolesAPIClient]); + const getRowProps = (item: Role) => { const { name } = item; return { @@ -134,6 +152,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => window.alert('your wish is granted'); setShowRolesPrivilegeEditor(false); }} + roleAPIClient={rolesAPIClient.current} /> )} @@ -179,10 +198,13 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => interface PrivilegesRolesFormProps extends Props { closeFlyout: () => void; onSaveClick: () => void; + roleAPIClient: RolesAPIClient; } export const PrivilegesRolesForm: FC = (props) => { - const { space, roles, onSaveClick, closeFlyout, features } = props; + const { roles, onSaveClick, closeFlyout, features, roleAPIClient } = props; + + const [space, setSpaceState] = useState(props.space); const [selectedRoles, setSelectedRoles] = useState>>([]); const [spacePrivilege, setSpacePrivilege] = useState<'all' | 'read' | 'custom'>('all'); @@ -286,7 +308,7 @@ export const PrivilegesRolesForm: FC = (props) => { }; return ( - +

Assign role to {space.name}

From fa0a086b9a92f5da7ea6eaa79fd6d83e80c77b64 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 2 Jul 2024 15:18:07 +0200 Subject: [PATCH 035/129] conditionally render role action based on user capabilities --- .../management/view_space/hooks/use_tabs.ts | 29 ++++---- .../management/view_space/view_space.tsx | 17 +++-- .../view_space/view_space_roles.tsx | 69 ++++++++++++------- .../management/view_space/view_space_tabs.tsx | 44 ++++++++++-- 4 files changed, 112 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index 3c518c7250dc6..176d4be754458 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -8,25 +8,30 @@ import { useMemo } from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/public'; -import type { Role } from '@kbn/security-plugin-types-common'; import type { Space } from '../../../../common'; -import type { ViewSpaceTab } from '../view_space_tabs'; -import { getTabs } from '../view_space_tabs'; +import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs'; -export const useTabs = ( - space: Space | null, - features: KibanaFeature[] | null, - roles: Role[], - currentSelectedTabId: string -): [ViewSpaceTab[], JSX.Element | undefined] => { +type UseTabsProps = Omit & { + space: Space | null; + features: KibanaFeature[] | null; + currentSelectedTabId: string; +}; + +export const useTabs = ({ + space, + features, + currentSelectedTabId, + ...getTabsArgs +}: UseTabsProps): [ViewSpaceTab[], JSX.Element | undefined] => { const [tabs, selectedTabContent] = useMemo(() => { - if (space == null || features == null) { + if (space === null || features === null) { return [[]]; } - const _tabs = space != null ? getTabs(space, features, roles) : []; + + const _tabs = space != null ? getTabs({ space, features, ...getTabsArgs }) : []; return [_tabs, _tabs.find((obj) => obj.id === currentSelectedTabId)?.content]; - }, [space, currentSelectedTabId, features, roles]); + }, [space, features, getTabsArgs, currentSelectedTabId]); return [tabs, selectedTabContent]; }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 8edccacb91110..6cf2cbe4d4d8a 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -42,9 +42,12 @@ const LazySpaceAvatar = lazy(() => getSpaceAvatarComponent().then((component) => ({ default: component })) ); -const getSelectedTabId = (selectedTabId?: string) => { +const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) => { // Validation of the selectedTabId routing parameter, default to the Content tab - return selectedTabId && [TAB_ID_FEATURES, TAB_ID_ROLES].includes(selectedTabId) + return selectedTabId && + [TAB_ID_FEATURES, canUserViewRoles ? TAB_ID_ROLES : null] + .filter(Boolean) + .includes(selectedTabId) ? selectedTabId : TAB_ID_CONTENT; }; @@ -81,7 +84,6 @@ export const ViewSpacePage: FC = (props) => { getRolesAPIClient, } = props; - const selectedTabId = getSelectedTabId(_selectedTabId); const [space, setSpace] = useState(null); const [userActiveSpace, setUserActiveSpace] = useState(null); const [features, setFeatures] = useState(null); @@ -89,8 +91,15 @@ export const ViewSpacePage: FC = (props) => { const [isLoadingSpace, setIsLoadingSpace] = useState(true); const [isLoadingFeatures, setIsLoadingFeatures] = useState(true); const [isLoadingRoles, setIsLoadingRoles] = useState(true); - const [tabs, selectedTabContent] = useTabs(space, features, roles, selectedTabId); const [isSolutionNavEnabled, setIsSolutionNavEnabled] = useState(false); + const selectedTabId = getSelectedTabId(Boolean(capabilities?.roles?.view), _selectedTabId); + const [tabs, selectedTabContent] = useTabs({ + space, + features, + roles, + capabilities, + currentSelectedTabId: selectedTabId, + }); useEffect(() => { if (!spaceId) { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 7931e2ff82dca..c3cb560f2caca 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -49,6 +49,7 @@ interface Props { space: Space; roles: Role[]; features: KibanaFeature[]; + isReadOnly: boolean; } const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { @@ -63,7 +64,7 @@ const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { ); }; -export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => { +export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const rolesAPIClient = useRef(); @@ -75,8 +76,10 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => rolesAPIClient.current = await getRolesAPIClient(); } - resolveRolesAPIClient(); - }, [getRolesAPIClient]); + if (!isReadOnly) { + resolveRolesAPIClient(); + } + }, [getRolesAPIClient, isReadOnly]); const getRowProps = (item: Role) => { const { name } = item; @@ -113,7 +116,10 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => }); }, }, - { + ]; + + if (!isReadOnly) { + columns.push({ name: 'Actions', actions: [ { @@ -129,8 +135,8 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => }, }, ], - }, - ]; + }); + } const rolesInUse = filterRolesAssignedToSpace(roles, space); @@ -168,17 +174,19 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) =>

- - { - setShowRolesPrivilegeEditor(true); - }} - > - {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign', { - defaultMessage: 'Assign role', - })} - - + {!isReadOnly && ( + + { + setShowRolesPrivilegeEditor(true); + }} + > + {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign', { + defaultMessage: 'Assign role', + })} + + + )}
@@ -195,20 +203,31 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features }) => ); }; -interface PrivilegesRolesFormProps extends Props { +interface PrivilegesRolesFormProps extends Omit { closeFlyout: () => void; onSaveClick: () => void; roleAPIClient: RolesAPIClient; } +const createRolesComboBoxOptions = (roles: Role[]): Array> => + roles.map((role) => ({ + label: role.name, + })); + export const PrivilegesRolesForm: FC = (props) => { const { roles, onSaveClick, closeFlyout, features, roleAPIClient } = props; const [space, setSpaceState] = useState(props.space); + const [selectedRoles, setSelectedRoles] = useState( + createRolesComboBoxOptions(filterRolesAssignedToSpace(roles, space)) + ); - const [selectedRoles, setSelectedRoles] = useState>>([]); const [spacePrivilege, setSpacePrivilege] = useState<'all' | 'read' | 'custom'>('all'); + const assignRolesToSpace = useCallback(() => { + onSaveClick(); + }, [onSaveClick]); + const getForm = () => { return ( @@ -219,9 +238,7 @@ export const PrivilegesRolesForm: FC = (props) => { values: { spaceName: space.name }, })} placeholder="Select roles" - options={roles.map((role) => ({ - label: role.name, - }))} + options={createRolesComboBoxOptions(roles)} selectedOptions={selectedRoles} onChange={(value) => { setSelectedRoles(value); @@ -289,7 +306,7 @@ export const PrivilegesRolesForm: FC = (props) => {

- + )} @@ -299,7 +316,11 @@ export const PrivilegesRolesForm: FC = (props) => { const getSaveButton = () => { return ( - + assignRolesToSpace()} + data-test-subj={'createRolesPrivilegeButton'} + > {i18n.translate('xpack.spaces.management.spaceDetails.roles.assignRoleButton', { defaultMessage: 'Assign roles', })} diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 9a41d2a08bea6..19f165bb558ca 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -8,6 +8,7 @@ import { EuiNotificationBadge } from '@elastic/eui'; import React from 'react'; +import type { Capabilities } from '@kbn/core/public'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; @@ -27,11 +28,28 @@ export interface ViewSpaceTab { href?: string; } -export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): ViewSpaceTab[] => { +export interface GetTabsProps { + space: Space; + roles: Role[]; + features: KibanaFeature[]; + capabilities: Capabilities & { + roles?: { view: boolean; save: boolean }; + }; +} + +export const getTabs = ({ + space, + features, + capabilities, + ...rest +}: GetTabsProps): ViewSpaceTab[] => { const enabledFeatureCount = getEnabledFeatures(features, space).length; const totalFeatureCount = features.length; - return [ + const canUserViewRoles = Boolean(capabilities?.roles?.view); + const canUserModifyRoles = Boolean(capabilities?.roles?.save); + + const tabsDefinition = [ { id: TAB_ID_CONTENT, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { @@ -51,17 +69,29 @@ export const getTabs = (space: Space, features: KibanaFeature[], roles: Role[]): ), content: , }, - { + ]; + + if (canUserViewRoles) { + tabsDefinition.push({ id: TAB_ID_ROLES, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.roles.heading', { defaultMessage: 'Assigned roles', }), append: ( - {roles.length} + {rest.roles.length} ), - content: , - }, - ]; + content: ( + + ), + }); + } + + return tabsDefinition; }; From fbfa5c2fb24a00faf61ee5e4aa71b28fd43ae6b9 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 2 Jul 2024 16:25:35 +0200 Subject: [PATCH 036/129] start on assigning space to selected roles --- .../view_space/view_space_roles.tsx | 169 ++++++++++++------ .../management/view_space/view_space_tabs.tsx | 2 +- 2 files changed, 117 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index c3cb560f2caca..26674d403efb8 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -9,9 +9,8 @@ import { EuiBasicTable, EuiButton, EuiButtonEmpty, + EuiButtonGroup, EuiComboBox, - EuiFilterButton, - EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -45,9 +44,11 @@ type RolesAPIClient = ReturnType extends ? R : never; +type KibanaPrivilegeBase = 'all' | 'read'; + interface Props { space: Space; - roles: Role[]; + spaceRoles: Role[]; features: KibanaFeature[]; isReadOnly: boolean; } @@ -64,22 +65,39 @@ const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { ); }; -export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { +export const ViewSpaceAssignedRoles: FC = ({ space, spaceRoles, features, isReadOnly }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); + const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); + const [systemRoles, setSystemRoles] = useState([]); const rolesAPIClient = useRef(); const { getRolesAPIClient } = useViewSpaceServices(); - useEffect(() => { - async function resolveRolesAPIClient() { + const resolveRolesAPIClient = useCallback(async () => { + try { rolesAPIClient.current = await getRolesAPIClient(); + setRoleAPIClientInitialized(true); + } catch { + // } + }, [getRolesAPIClient]); + useEffect(() => { if (!isReadOnly) { resolveRolesAPIClient(); } - }, [getRolesAPIClient, isReadOnly]); + }, [isReadOnly, resolveRolesAPIClient]); + + useEffect(() => { + async function fetchAllSystemRoles() { + setSystemRoles((await rolesAPIClient.current?.getRoles()) ?? []); + } + + if (roleAPIClientInitialized) { + fetchAllSystemRoles?.(); + } + }, [roleAPIClientInitialized]); const getRowProps = (item: Role) => { const { name } = item; @@ -138,7 +156,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe }); } - const rolesInUse = filterRolesAssignedToSpace(roles, space); + const rolesInUse = filterRolesAssignedToSpace(spaceRoles, space); if (!rolesInUse) { return null; @@ -150,15 +168,16 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe { setShowRolesPrivilegeEditor(false); }} onSaveClick={() => { - window.alert('your wish is granted'); setShowRolesPrivilegeEditor(false); }} - roleAPIClient={rolesAPIClient.current} + systemRoles={systemRoles} + // rolesAPIClient would have been initialized before the privilege editor is displayed + roleAPIClient={rolesAPIClient.current!} /> )} @@ -177,7 +196,10 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe {!isReadOnly && ( { + onClick={async () => { + if (!roleAPIClientInitialized) { + await resolveRolesAPIClient(); + } setShowRolesPrivilegeEditor(true); }} > @@ -206,46 +228,78 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe interface PrivilegesRolesFormProps extends Omit { closeFlyout: () => void; onSaveClick: () => void; + systemRoles: Role[]; roleAPIClient: RolesAPIClient; } -const createRolesComboBoxOptions = (roles: Role[]): Array> => +const createRolesComboBoxOptions = (roles: Role[]): Array> => roles.map((role) => ({ label: role.name, + value: role, })); export const PrivilegesRolesForm: FC = (props) => { - const { roles, onSaveClick, closeFlyout, features, roleAPIClient } = props; + const { spaceRoles, onSaveClick, closeFlyout, features, roleAPIClient, systemRoles } = props; const [space, setSpaceState] = useState(props.space); const [selectedRoles, setSelectedRoles] = useState( - createRolesComboBoxOptions(filterRolesAssignedToSpace(roles, space)) + createRolesComboBoxOptions(filterRolesAssignedToSpace(spaceRoles, space)) ); - const [spacePrivilege, setSpacePrivilege] = useState<'all' | 'read' | 'custom'>('all'); + const [spacePrivilege, setSpacePrivilege] = useState( + space.disabledFeatures.length ? 'custom' : 'all' + ); + + const [assigningToRole, setAssigningToRole] = useState(false); + + const assignRolesToSpace = useCallback(async () => { + try { + setAssigningToRole(true); - const assignRolesToSpace = useCallback(() => { - onSaveClick(); - }, [onSaveClick]); + await Promise.all( + selectedRoles.map((selectedRole) => { + roleAPIClient.saveRole({ role: selectedRole.value! }); + }) + ).then(setAssigningToRole.bind(null, false)); + + onSaveClick(); + } catch { + // Handle resulting error + } + }, [onSaveClick, roleAPIClient, selectedRoles]); const getForm = () => { return ( { - setSelectedRoles(value); + setSelectedRoles((prevRoles) => { + if (prevRoles.length < value.length) { + const newlyAdded = value[value.length - 1]; + + // Add kibana space privilege definition to role + newlyAdded.value!.kibana.push({ + spaces: [space.name], + base: spacePrivilege === 'custom' ? [] : [spacePrivilege], + feature: {}, + }); + + return prevRoles.concat(newlyAdded); + } else { + // TODO: handle from role from space + return value; + } + }); }} - isClearable={true} - data-test-subj="roleSelectionComboBox" - autoFocus fullWidth /> @@ -258,35 +312,43 @@ export const PrivilegesRolesForm: FC = (props) => { } )} > - - setSpacePrivilege('all')} - > - - - setSpacePrivilege('read')} - > - - - setSpacePrivilege('custom')} - > - - - + ({ + ...privilege, + 'data-test-subj': `${privilege.id}-privilege-button`, + }))} + color="primary" + idSelected={spacePrivilege} + onChange={(id) => setSpacePrivilege(id)} + buttonSize="compressed" + isFullWidth + /> {spacePrivilege === 'custom' && ( = (props) => { return ( assignRolesToSpace()} data-test-subj={'createRolesPrivilegeButton'} > diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 19f165bb558ca..e26d87126c514 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -85,7 +85,7 @@ export const getTabs = ({ content: ( From b92d1e5ab2bfd41a24093b7bda0e3da94c1f7532 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Wed, 3 Jul 2024 17:04:48 +0200 Subject: [PATCH 037/129] switch to leveraging predefined privilege --- .../view_space/view_space_roles.tsx | 25 ++++++++----------- .../management/view_space/view_space_tabs.tsx | 2 +- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 26674d403efb8..8909bd175bcc6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -31,7 +31,7 @@ import type { import type { FC } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import type { KibanaFeature } from '@kbn/features-plugin/common'; +import type { KibanaFeature, KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { Role } from '@kbn/security-plugin-types-common'; @@ -44,11 +44,11 @@ type RolesAPIClient = ReturnType extends ? R : never; -type KibanaPrivilegeBase = 'all' | 'read'; +type KibanaPrivilegeBase = keyof NonNullable; interface Props { space: Space; - spaceRoles: Role[]; + roles: Role[]; features: KibanaFeature[]; isReadOnly: boolean; } @@ -65,7 +65,7 @@ const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { ); }; -export const ViewSpaceAssignedRoles: FC = ({ space, spaceRoles, features, isReadOnly }) => { +export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); const [systemRoles, setSystemRoles] = useState([]); @@ -156,7 +156,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, spaceRoles, features, }); } - const rolesInUse = filterRolesAssignedToSpace(spaceRoles, space); + const rolesInUse = filterRolesAssignedToSpace(roles, space); if (!rolesInUse) { return null; @@ -168,7 +168,6 @@ export const ViewSpaceAssignedRoles: FC = ({ space, spaceRoles, features, { setShowRolesPrivilegeEditor(false); }} @@ -225,7 +224,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, spaceRoles, features, ); }; -interface PrivilegesRolesFormProps extends Omit { +interface PrivilegesRolesFormProps extends Omit { closeFlyout: () => void; onSaveClick: () => void; systemRoles: Role[]; @@ -239,15 +238,12 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { - const { spaceRoles, onSaveClick, closeFlyout, features, roleAPIClient, systemRoles } = props; + const { onSaveClick, closeFlyout, features, roleAPIClient, systemRoles } = props; const [space, setSpaceState] = useState(props.space); - const [selectedRoles, setSelectedRoles] = useState( - createRolesComboBoxOptions(filterRolesAssignedToSpace(spaceRoles, space)) - ); - - const [spacePrivilege, setSpacePrivilege] = useState( - space.disabledFeatures.length ? 'custom' : 'all' + const [spacePrivilege, setSpacePrivilege] = useState('all'); + const [selectedRoles, setSelectedRoles] = useState>( + [] ); const [assigningToRole, setAssigningToRole] = useState(false); @@ -295,7 +291,6 @@ export const PrivilegesRolesForm: FC = (props) => { return prevRoles.concat(newlyAdded); } else { - // TODO: handle from role from space return value; } }); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index e26d87126c514..19f165bb558ca 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -85,7 +85,7 @@ export const getTabs = ({ content: ( From e5f24acfd67524166984313f4803005f77b9ab59 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 9 Jul 2024 15:52:35 +0200 Subject: [PATCH 038/129] Design feedback --- .../spaces_grid/spaces_grid_page.tsx | 28 +++++++--- .../management/view_space/view_space.tsx | 51 ++++++++----------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 64f5765938584..3103e1ad46f9e 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -6,9 +6,12 @@ */ import { + EuiBadge, type EuiBasicTableColumn, EuiButton, EuiCallOut, + EuiFlexGroup, + EuiFlexItem, EuiInMemoryTable, EuiLink, EuiLoadingSpinner, @@ -278,12 +281,25 @@ export class SpacesGridPage extends Component { }), sortable: true, render: (value: string, rowRecord) => ( - - {value} - + + + + {value} + + + {this.state.activeSpace?.name === rowRecord.name && ( + + + {i18n.translate('xpack.spaces.management.spacesGridPage.currentSpaceMarkerText', { + defaultMessage: 'current', + })} + + + )} + ), }, { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 6cf2cbe4d4d8a..1578cf6c19b82 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -201,32 +201,6 @@ export const ViewSpacePage: FC = (props) => { ) : null; }; - const SwitchButton = () => { - if (userActiveSpace?.id === space.id) { - return null; - } - - const { serverBasePath } = props; - - // use href to force full page reload (needed in order to change spaces) - return ( - - - - ); - }; - return ( = (props) => { getUrlForApp={getUrlForApp} > - + @@ -280,13 +254,28 @@ export const ViewSpacePage: FC = (props) => { - + - - - + {userActiveSpace?.id !== space.id ? ( + + + + + + ) : null} From 07b4e5b6511d1f4ca6bb6fe74e5c6e31c2a4ff00 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 12 Jul 2024 14:56:36 -0700 Subject: [PATCH 039/129] fix jest test --- .../spaces/public/management/spaces_management_app.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index e953c324be285..7722253b46cb1 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -100,7 +100,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"maxSpaces":1000,"solutionNavExperiment":{}} + Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"serverBasePath":"","history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"maxSpaces":1000,"solutionNavExperiment":{}}

`); @@ -150,7 +150,7 @@ describe('spacesManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledTimes(1); expect(setBreadcrumbs).toHaveBeenCalledWith([ { href: `/`, text: 'Spaces' }, - { text: `space with id some-space` }, + { text: `Edit space with id some-space` }, ]); expect(docTitle.change).toHaveBeenCalledWith('Spaces'); expect(docTitle.reset).not.toHaveBeenCalled(); From 12995c120567a4691469889ef12e05c63411423e Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 12 Jul 2024 15:23:42 -0700 Subject: [PATCH 040/129] fix fn test --- x-pack/test/functional/page_objects/space_selector_page.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index 857e4493b1399..94a9f9e033e5a 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -213,6 +213,8 @@ export class SpaceSelectorPageObject extends FtrService { } async clickOnDeleteSpaceButton(spaceName: string) { + const collapsedButtonSelector = '[data-test-subj=euiCollapsedItemActionsButton]'; + await this.find.clickByCssSelector(`#${spaceName}-actions ${collapsedButtonSelector}`); await this.testSubjects.click(`${spaceName}-deleteSpace`); } From 5f851b30876c58e2d576a2b30de298f3e48c1802 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 17 Jul 2024 16:06:00 -0700 Subject: [PATCH 041/129] File rename --- ...ace_enabled_features_tab.tsx => view_space_features_tab.tsx} | 0 .../spaces/public/management/view_space/view_space_tabs.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename x-pack/plugins/spaces/public/management/view_space/{view_space_enabled_features_tab.tsx => view_space_features_tab.tsx} (100%) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/view_space/view_space_enabled_features_tab.tsx rename to x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 19f165bb558ca..ff604e888bba6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -15,7 +15,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; import { ViewSpaceContent } from './view_space_content_tab'; -import { ViewSpaceEnabledFeatures } from './view_space_enabled_features_tab'; +import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import { ViewSpaceAssignedRoles } from './view_space_roles'; import type { Space } from '../../../common'; import { getEnabledFeatures } from '../lib/feature_utils'; From 0d227e597335325f35c22a8bd698c0442895699c Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 17 Jul 2024 17:21:08 -0700 Subject: [PATCH 042/129] Replace Omit with Pick --- .../spaces/public/management/view_space/hooks/use_tabs.ts | 2 +- .../spaces/public/management/view_space/view_space_roles.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index 176d4be754458..da89a2ece6bb9 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -12,7 +12,7 @@ import type { KibanaFeature } from '@kbn/features-plugin/public'; import type { Space } from '../../../../common'; import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs'; -type UseTabsProps = Omit & { +type UseTabsProps = Pick & { space: Space | null; features: KibanaFeature[] | null; currentSelectedTabId: string; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 8909bd175bcc6..0560812165be5 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -308,7 +308,6 @@ export const PrivilegesRolesForm: FC = (props) => { )} > Date: Wed, 17 Jul 2024 17:21:15 -0700 Subject: [PATCH 043/129] Add General tab --- .../public/management/view_space/constants.ts | 1 + .../management/view_space/view_space.tsx | 4 +-- .../view_space/view_space_general_tab.tsx | 31 +++++++++++++++++++ .../management/view_space/view_space_tabs.tsx | 11 ++++++- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/constants.ts b/x-pack/plugins/spaces/public/management/view_space/constants.ts index 460bb8c5c4b3d..258a98c71952c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/constants.ts +++ b/x-pack/plugins/spaces/public/management/view_space/constants.ts @@ -8,3 +8,4 @@ export const TAB_ID_CONTENT = 'content'; export const TAB_ID_FEATURES = 'features'; export const TAB_ID_ROLES = 'roles'; +export const TAB_ID_GENERAL = 'general'; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 1578cf6c19b82..09909e5bd07aa 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -27,7 +27,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import type { Role } from '@kbn/security-plugin-types-common'; -import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; +import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; import { ViewSpaceContextProvider, @@ -45,7 +45,7 @@ const LazySpaceAvatar = lazy(() => const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) => { // Validation of the selectedTabId routing parameter, default to the Content tab return selectedTabId && - [TAB_ID_FEATURES, canUserViewRoles ? TAB_ID_ROLES : null] + [TAB_ID_FEATURES, TAB_ID_GENERAL, canUserViewRoles ? TAB_ID_ROLES : null] .filter(Boolean) .includes(selectedTabId) ? selectedTabId diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx new file mode 100644 index 0000000000000..dfc2ceae61c5d --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -0,0 +1,31 @@ +/* + * 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 type { Space } from '../../../common'; +import { CustomizeSpace } from '../edit_space/customize_space'; +import { SpaceValidator } from '../lib'; + +interface Props { + space: Space; + isReadOnly: boolean; +} + +export const ViewSpaceGeneral: React.FC = (props) => { + const onChange = () => {}; + const validator = new SpaceValidator(); + + return ( + + ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index ff604e888bba6..fe47b5b7775ec 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -13,9 +13,10 @@ import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; -import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_ROLES } from './constants'; +import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { ViewSpaceContent } from './view_space_content_tab'; import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; +import { ViewSpaceGeneral } from './view_space_general_tab'; import { ViewSpaceAssignedRoles } from './view_space_roles'; import type { Space } from '../../../common'; import { getEnabledFeatures } from '../lib/feature_utils'; @@ -93,5 +94,13 @@ export const getTabs = ({ }); } + tabsDefinition.push({ + id: TAB_ID_GENERAL, + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { + defaultMessage: 'General settings', + }), + content: , + }); + return tabsDefinition; }; From af8121bff25a93fc70f2d961bf70de169efd2ab2 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 17 Jul 2024 18:20:20 -0700 Subject: [PATCH 044/129] Add Solution View to features tab --- .../management/view_space/hooks/use_tabs.ts | 1 + .../view_space/view_space_features_tab.tsx | 107 +++++++++++------- .../management/view_space/view_space_tabs.tsx | 16 ++- 3 files changed, 76 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index da89a2ece6bb9..6ed8650675e81 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -16,6 +16,7 @@ type UseTabsProps = Pick & { space: Space | null; features: KibanaFeature[] | null; currentSelectedTabId: string; + isSolutionNavEnabled: boolean; }; export const useTabs = ({ diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index b4b89d0a20145..feb0bdf455270 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -10,18 +10,22 @@ import type { FC } from 'react'; import React from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Space } from '../../../common'; import { FeatureTable } from '../edit_space/enabled_features/feature_table'; +import { SectionPanel } from '../edit_space/section_panel'; +import { SolutionView } from '../edit_space/solution_view'; interface Props { space: Space; features: KibanaFeature[]; + isSolutionNavEnabled: boolean; } -export const ViewSpaceEnabledFeatures: FC = ({ features, space }) => { +export const ViewSpaceEnabledFeatures: FC = ({ features, space, isSolutionNavEnabled }) => { const { services } = useKibana(); if (!features) { @@ -31,48 +35,63 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space }) => { const canManageRoles = services.application?.capabilities.management?.security?.roles === true; return ( - - - -

- -

-
- - -

- - - - ) : ( - - ), - }} - /> -

-
-
- - - -
+ <> + {isSolutionNavEnabled && ( + <> + {}} /> + + + )} + + + + +

+ +

+
+ + +

+ + + + ) : ( + + ), + }} + /> +

+
+
+ + + +
+
+ ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index fe47b5b7775ec..5ab99633ef3bf 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -36,13 +36,15 @@ export interface GetTabsProps { capabilities: Capabilities & { roles?: { view: boolean; save: boolean }; }; + isSolutionNavEnabled: boolean; } export const getTabs = ({ space, features, capabilities, - ...rest + roles, + isSolutionNavEnabled, }: GetTabsProps): ViewSpaceTab[] => { const enabledFeatureCount = getEnabledFeatures(features, space).length; const totalFeatureCount = features.length; @@ -68,7 +70,13 @@ export const getTabs = ({ {enabledFeatureCount} / {totalFeatureCount} ), - content: , + content: ( + + ), }, ]; @@ -80,13 +88,13 @@ export const getTabs = ({ }), append: ( - {rest.roles.length} + {roles.length} ), content: ( From 83c0a916b429e9ec7f1f7e398b310d63e84a5b7e Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 18 Jul 2024 12:04:09 -0700 Subject: [PATCH 045/129] fix solution view in features tab --- .../plugins/spaces/public/management/view_space/view_space.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 09909e5bd07aa..101c97c480ae6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -99,6 +99,7 @@ export const ViewSpacePage: FC = (props) => { roles, capabilities, currentSelectedTabId: selectedTabId, + isSolutionNavEnabled, }); useEffect(() => { From b9c8ff03e63969dc7ec5c1e303ef0d0709e17502 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 18 Jul 2024 12:53:15 -0700 Subject: [PATCH 046/129] Clean up args --- .../public/management/spaces_grid/spaces_grid_page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 3103e1ad46f9e..d99157faf7934 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -138,7 +138,7 @@ export class SpacesGridPage extends Component { ) : undefined} { rowProps={(item) => ({ 'data-test-subj': `spacesListTableRow-${item.id}`, })} - columns={this.getColumnConfig({ serverBasePath: this.props.serverBasePath })} + columns={this.getColumnConfig()} pagination={true} sorting={true} search={{ @@ -256,7 +256,7 @@ export class SpacesGridPage extends Component { } }; - public getColumnConfig({ serverBasePath }: { serverBasePath: string }) { + public getColumnConfig() { const config: Array> = [ { field: 'initials', @@ -429,7 +429,7 @@ export class SpacesGridPage extends Component { color: 'primary', href: (rowRecord) => addSpaceIdToPath( - serverBasePath, + this.props.serverBasePath, rowRecord.id, `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/` ), From 17a66efc184d9378b0c3bf42b11d20f05335ea56 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 18 Jul 2024 13:42:28 -0700 Subject: [PATCH 047/129] Avoid `useKibana` --- .../view_space/hooks/view_space_context_provider.tsx | 3 ++- .../public/management/view_space/view_space.tsx | 1 + .../view_space/view_space_features_tab.tsx | 12 ++++-------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index 77f246d570948..fe98bb71345db 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -14,9 +14,10 @@ import type { RolesAPIClient } from '@kbn/security-plugin-types-public'; import type { SpacesManager } from '../../../spaces_manager'; export interface ViewSpaceServices { - serverBasePath: string; + capabilities: ApplicationStart['capabilities']; getUrlForApp: ApplicationStart['getUrlForApp']; navigateToUrl: ApplicationStart['navigateToUrl']; + serverBasePath: string; spacesManager: SpacesManager; getRolesAPIClient: () => Promise; } diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 101c97c480ae6..f059fcb2f5c39 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -204,6 +204,7 @@ export const ViewSpacePage: FC = (props) => { return ( = ({ features, space, isSolutionNavEnabled }) => { - const { services } = useKibana(); + const { capabilities, getUrlForApp } = useViewSpaceServices(); if (!features) { return null; } - const canManageRoles = services.application?.capabilities.management?.security?.roles === true; + const canManageRoles = capabilities.management?.security?.roles === true; return ( <> @@ -66,11 +66,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, isSolutio defaultMessage="Hidden features are removed from the user interface, but not disabled. To secure access to features, {manageRolesLink}." values={{ manageRolesLink: canManageRoles ? ( - + Date: Thu, 18 Jul 2024 12:53:25 -0700 Subject: [PATCH 048/129] Wip - make editable --- .../enabled_features.test.tsx.snap | 9 --- .../enabled_features/enabled_features.tsx | 17 +---- .../enabled_features/feature_table.tsx | 76 +++++++++---------- .../view_space/view_space_features_tab.tsx | 65 +++++++++++++++- .../view_space/view_space_general_tab.tsx | 54 ++++++++++--- .../management/view_space/view_space_tabs.tsx | 4 +- 6 files changed, 147 insertions(+), 78 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap index fa2cf4f8c3f80..4bf010004cbef 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap @@ -70,15 +70,6 @@ exports[`EnabledFeatures renders as expected 1`] = ` }, ] } - headerText={ - - - Feature visibility - - - } onChange={[MockFunction]} space={ Object { diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 93e37b4d68a77..36d0694953242 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -21,7 +21,7 @@ import { SectionPanel } from '../section_panel'; interface Props { space: Partial; features: KibanaFeatureConfig[]; - onChange?: (space: Partial) => void; + onChange: (space: Partial) => void; } export const EnabledFeatures: FunctionComponent = (props) => { @@ -75,20 +75,7 @@ export const EnabledFeatures: FunctionComponent = (props) => { - - - {i18n.translate('xpack.spaces.management.featureVisibilityTitle', { - defaultMessage: 'Feature visibility', - })} - - - } - /> + diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index 1fec0ad8d3f2f..bc4b586c6cd1d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -31,20 +31,16 @@ import type { Space } from '../../../../common'; import { getEnabledFeatures } from '../../lib/feature_utils'; interface Props { - headerText?: JSX.Element; space: Partial; features: KibanaFeatureConfig[]; - onChange?: (space: Partial) => void; + onChange: (space: Partial) => void; } export class FeatureTable extends Component { private featureCategories: Map = new Map(); - private isReadOnly: boolean; constructor(props: Props) { super(props); - this.isReadOnly = props.onChange == null; - // features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner props.features.forEach((feature) => { if (!this.featureCategories.has(feature.category.id)) { @@ -70,8 +66,6 @@ export class FeatureTable extends Component { id: `featureCategoryCheckbox_${category.id}`, indeterminate: enabledCount > 0 && enabledCount < featureCount, checked: featureCount === enabledCount, - readOnly: this.isReadOnly, - disabled: this.isReadOnly, ['aria-label']: i18n.translate( 'xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel', { defaultMessage: 'Category toggle' } @@ -168,8 +162,6 @@ export class FeatureTable extends Component { id={`featureCheckbox_${feature.id}`} data-test-subj={`featureCheckbox_${feature.id}`} checked={featureChecked} - readOnly={this.isReadOnly} - disabled={this.isReadOnly} onChange={this.onChange(feature.id) as any} label={feature.name} /> @@ -193,39 +185,45 @@ export class FeatureTable extends Component { const featureCount = this.props.features.length; const enabledCount = getEnabledFeatures(this.props.features, this.props.space).length; const controls = []; - if (this.props.onChange) { - if (enabledCount < featureCount) { - controls.push( - this.showAll()} - size="xs" - data-test-subj="showAllFeaturesLink" - > - {i18n.translate('xpack.spaces.management.selectAllFeaturesLink', { - defaultMessage: 'Show all', - })} - - ); - } - if (enabledCount > 0) { - controls.push( - this.hideAll()} - size="xs" - data-test-subj="hideAllFeaturesLink" - > - {i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', { - defaultMessage: 'Hide all', - })} - - ); - } + if (enabledCount < featureCount) { + controls.push( + this.showAll()} + size="xs" + data-test-subj="showAllFeaturesLink" + > + {i18n.translate('xpack.spaces.management.selectAllFeaturesLink', { + defaultMessage: 'Show all', + })} + + ); + } + if (enabledCount > 0) { + controls.push( + this.hideAll()} + size="xs" + data-test-subj="hideAllFeaturesLink" + > + {i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', { + defaultMessage: 'Hide all', + })} + + ); } return (
- {this.props.headerText} + + + + {i18n.translate('xpack.spaces.management.featureVisibilityTitle', { + defaultMessage: 'Feature visibility', + })} + + + {controls.map((control, idx) => ( {control} @@ -256,7 +254,7 @@ export class FeatureTable extends Component { } updatedSpace.disabledFeatures = disabledFeatures; - this.props.onChange?.(updatedSpace); + this.props.onChange(updatedSpace); }; private getAllFeatureIds = () => @@ -285,7 +283,7 @@ export class FeatureTable extends Component { ); } - this.props.onChange?.(updatedSpace); + this.props.onChange(updatedSpace); }; private getCategoryHelpText = (category: AppCategory) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index 82ffb60dfcc78..cc9334bc2c681 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -5,9 +5,18 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; import type { FC } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; @@ -28,20 +37,47 @@ interface Props { export const ViewSpaceEnabledFeatures: FC = ({ features, space, isSolutionNavEnabled }) => { const { capabilities, getUrlForApp } = useViewSpaceServices(); + const [spaceNavigation, setSpaceNavigation] = useState>(space); // space details as seen in the Solution View UI, possibly with unsaved changes + const [spaceFeatures, setSpaceFeatures] = useState>(space); // space details as seen in the Feature Visibility UI, possibly with unsaved changes + const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made + if (!features) { return null; } const canManageRoles = capabilities.management?.security?.roles === true; + const onChangeSpaceNavigation = (updatedSpace: Partial) => { + setIsDirty(true); + setSpaceNavigation(updatedSpace); + console.log('updatedSpace-solutionView', updatedSpace); + }; + + const onChangeSpaceFeatures = (updatedSpace: Partial) => { + setIsDirty(true); + setSpaceFeatures({ ...updatedSpace, id: space.id }); + console.log('updatedSpace-featuresTable', updatedSpace); + }; + + const onUpdateSpace = () => { + window.alert('not yet implemented'); + }; + + const onCancel = () => { + setSpaceNavigation(space); + setSpaceFeatures(space); + setIsDirty(false); + }; + return ( <> {isSolutionNavEnabled && ( <> - {}} /> + )} + = ({ features, space, isSolutio - + + + {isDirty && ( + <> + +

+ + Changes will impact all users in the Space. The page will be reloaded. + +

+

+ + Update Space + + Cancel +

+ + )} ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index dfc2ceae61c5d..b3abce9fb6bd9 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React from 'react'; +import { EuiButton, EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { useState } from 'react'; import type { Space } from '../../../common'; import { CustomizeSpace } from '../edit_space/customize_space'; @@ -16,16 +17,51 @@ interface Props { isReadOnly: boolean; } -export const ViewSpaceGeneral: React.FC = (props) => { - const onChange = () => {}; +export const ViewSpaceSettings: React.FC = ({ space }) => { + const [spaceSettings, setSpaceSettings] = useState>(space); + const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made + const validator = new SpaceValidator(); + const onChangeSpaceSettings = (updatedSpace: Partial) => { + setSpaceSettings(updatedSpace); + setIsDirty(true); + console.log('value', updatedSpace); + }; + + const onUpdateSpace = () => { + window.alert('not yet implemented'); + }; + + const onCancel = () => { + setSpaceSettings(space); + setIsDirty(false); + }; + return ( - + <> + + {isDirty && ( + <> + +

+ + Changes will impact all users in the Space. The page will be reloaded. + +

+

+ + Update Space + + Cancel +

+ + )} + ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 5ab99633ef3bf..e6e1a3de13f1b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -16,7 +16,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { ViewSpaceContent } from './view_space_content_tab'; import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; -import { ViewSpaceGeneral } from './view_space_general_tab'; +import { ViewSpaceSettings } from './view_space_general_tab'; import { ViewSpaceAssignedRoles } from './view_space_roles'; import type { Space } from '../../../common'; import { getEnabledFeatures } from '../lib/feature_utils'; @@ -107,7 +107,7 @@ export const getTabs = ({ name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { defaultMessage: 'General settings', }), - content: , + content: , }); return tabsDefinition; From 278dee4347776c71a2a95d844159fc1793733b5a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 22 Jul 2024 14:48:41 -0700 Subject: [PATCH 049/129] Fix ts in Roles --- .../public/management/view_space/view_space_roles.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 0560812165be5..ecc55ef4ebd7c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -240,7 +240,7 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { const { onSaveClick, closeFlyout, features, roleAPIClient, systemRoles } = props; - const [space, setSpaceState] = useState(props.space); + const [space, setSpaceState] = useState>(props.space); const [spacePrivilege, setSpacePrivilege] = useState('all'); const [selectedRoles, setSelectedRoles] = useState>( [] @@ -282,9 +282,14 @@ export const PrivilegesRolesForm: FC = (props) => { if (prevRoles.length < value.length) { const newlyAdded = value[value.length - 1]; + const { name: spaceName } = space; + if (!spaceName) { + throw new Error('space state requires name!'); + } + // Add kibana space privilege definition to role newlyAdded.value!.kibana.push({ - spaces: [space.name], + spaces: [spaceName], base: spacePrivilege === 'custom' ? [] : [spacePrivilege], feature: {}, }); @@ -339,7 +344,7 @@ export const PrivilegesRolesForm: FC = (props) => { }))} color="primary" idSelected={spacePrivilege} - onChange={(id) => setSpacePrivilege(id)} + onChange={(id) => setSpacePrivilege(id as KibanaPrivilegeBase | 'custom')} buttonSize="compressed" isFullWidth /> From 5e064f73f53a5bffa72993e2b6db1f7f14fa6688 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 22 Jul 2024 15:07:19 -0700 Subject: [PATCH 050/129] Unsaved changes prompt --- .../management/spaces_management_app.tsx | 4 +++- .../management/view_space/hooks/use_tabs.ts | 2 ++ .../hooks/view_space_context_provider.tsx | 4 ++++ .../management/view_space/view_space.tsx | 9 ++++---- .../view_space/view_space_features_tab.tsx | 21 ++++++++++++++++--- .../management/view_space/view_space_tabs.tsx | 5 ++++- x-pack/plugins/spaces/tsconfig.json | 1 + 7 files changed, 37 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index c5c0d9179f059..c13a5d64d1bb0 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -66,7 +66,7 @@ export const spacesManagementApp = Object.freeze({ text: title, href: `/`, }; - const { notifications, application, chrome, http } = coreStart; + const { notifications, application, chrome, http, overlays } = coreStart; chrome.docTitle.change(title); @@ -167,6 +167,8 @@ export const spacesManagementApp = Object.freeze({ spaceId={spaceId} selectedTabId={selectedTabId} getRolesAPIClient={getRolesAPIClient} + http={http} + overlays={overlays} /> ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index 6ed8650675e81..8849d05c9021d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -7,6 +7,7 @@ import { useMemo } from 'react'; +import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/public'; import type { Space } from '../../../../common'; @@ -16,6 +17,7 @@ type UseTabsProps = Pick & { space: Space | null; features: KibanaFeature[] | null; currentSelectedTabId: string; + history: ScopedHistory; isSolutionNavEnabled: boolean; }; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index fe98bb71345db..46d07b10bda31 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -9,6 +9,8 @@ import type { FC, PropsWithChildren } from 'react'; import React, { createContext, useContext } from 'react'; import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { OverlayStart } from '@kbn/core-overlays-browser'; import type { RolesAPIClient } from '@kbn/security-plugin-types-public'; import type { SpacesManager } from '../../../spaces_manager'; @@ -20,6 +22,8 @@ export interface ViewSpaceServices { serverBasePath: string; spacesManager: SpacesManager; getRolesAPIClient: () => Promise; + http: HttpStart; + overlays: OverlayStart; } const ViewSpaceContext = createContext(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index f059fcb2f5c39..c36b6704e3434 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -57,7 +57,7 @@ interface PageProps extends ViewSpaceServices { history: ScopedHistory; selectedTabId?: string; capabilities: Capabilities; - allowFeatureVisibility: boolean; // FIXME: handle this + allowFeatureVisibility: boolean; solutionNavExperiment?: Promise; getFeatures: FeaturesPluginStart['getFeatures']; onLoadSpace: (space: Space) => void; @@ -79,9 +79,10 @@ export const ViewSpacePage: FC = (props) => { solutionNavExperiment, selectedTabId: _selectedTabId, capabilities, + allowFeatureVisibility, // FIXME: handle this getUrlForApp, navigateToUrl, - getRolesAPIClient, + ...viewSpaceServices } = props; const [space, setSpace] = useState(null); @@ -98,6 +99,7 @@ export const ViewSpacePage: FC = (props) => { features, roles, capabilities, + history, currentSelectedTabId: selectedTabId, isSolutionNavEnabled, }); @@ -205,11 +207,10 @@ export const ViewSpacePage: FC = (props) => { return ( diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index cc9334bc2c681..0de22686b3e60 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -18,9 +18,11 @@ import { import type { FC } from 'react'; import React, { useState } from 'react'; +import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; @@ -32,15 +34,28 @@ interface Props { space: Space; features: KibanaFeature[]; isSolutionNavEnabled: boolean; + history: ScopedHistory; } -export const ViewSpaceEnabledFeatures: FC = ({ features, space, isSolutionNavEnabled }) => { - const { capabilities, getUrlForApp } = useViewSpaceServices(); - +export const ViewSpaceEnabledFeatures: FC = ({ + features, + space, + isSolutionNavEnabled, + ...props +}) => { const [spaceNavigation, setSpaceNavigation] = useState>(space); // space details as seen in the Solution View UI, possibly with unsaved changes const [spaceFeatures, setSpaceFeatures] = useState>(space); // space details as seen in the Feature Visibility UI, possibly with unsaved changes const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made + const { capabilities, getUrlForApp, http, overlays, navigateToUrl } = useViewSpaceServices(); + useUnsavedChangesPrompt({ + hasUnsavedChanges: isDirty, + http, + openConfirm: overlays.openConfirm, + navigateToUrl, + history: props.history, + }); + if (!features) { return null; } diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index e6e1a3de13f1b..ec8217ea7df17 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -8,7 +8,7 @@ import { EuiNotificationBadge } from '@elastic/eui'; import React from 'react'; -import type { Capabilities } from '@kbn/core/public'; +import type { Capabilities, ScopedHistory } from '@kbn/core/public'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; @@ -33,6 +33,7 @@ export interface GetTabsProps { space: Space; roles: Role[]; features: KibanaFeature[]; + history: ScopedHistory; capabilities: Capabilities & { roles?: { view: boolean; save: boolean }; }; @@ -42,6 +43,7 @@ export interface GetTabsProps { export const getTabs = ({ space, features, + history, capabilities, roles, isSolutionNavEnabled, @@ -73,6 +75,7 @@ export const getTabs = ({ content: ( diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index c2213dbc316bc..a98e25176f14e 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -40,6 +40,7 @@ "@kbn/cloud-experiments-plugin", "@kbn/security-plugin-types-common", "@kbn/core-application-browser", + "@kbn/unsaved-changes-prompt", ], "exclude": [ "target/**/*", From 1cc5125e1ad7250c5e823fbe660c3aa5c481d7b4 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 22 Jul 2024 16:11:09 -0700 Subject: [PATCH 051/129] quick checks and consistency --- .../management/view_space/view_space_content_tab.tsx | 9 +++++++-- .../management/view_space/view_space_features_tab.tsx | 4 +--- .../management/view_space/view_space_general_tab.tsx | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx index eae31edb8e2e2..6e256a14330d0 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -22,6 +22,12 @@ import { useViewSpaceServices } from './hooks/view_space_context_provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import type { SpaceContentTypeSummaryItem } from '../../types'; +const handleApiError = (error: Error) => { + // eslint-disable-next-line no-console + console.error(error); + throw error; +}; + export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { const { id: spaceId } = space; const { spacesManager, serverBasePath } = useViewSpaceServices(); @@ -89,8 +95,7 @@ export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { setIsLoading(false); }; - // eslint-disable-next-line no-console - getItems().catch(console.error); + getItems().catch(handleApiError); }, [spaceId, spacesManager]); if (isLoading) { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index 0de22686b3e60..ad831a506f466 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -65,17 +65,15 @@ export const ViewSpaceEnabledFeatures: FC = ({ const onChangeSpaceNavigation = (updatedSpace: Partial) => { setIsDirty(true); setSpaceNavigation(updatedSpace); - console.log('updatedSpace-solutionView', updatedSpace); }; const onChangeSpaceFeatures = (updatedSpace: Partial) => { setIsDirty(true); setSpaceFeatures({ ...updatedSpace, id: space.id }); - console.log('updatedSpace-featuresTable', updatedSpace); }; const onUpdateSpace = () => { - window.alert('not yet implemented'); + window.alert('not yet implemented'); // FIXME }; const onCancel = () => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index b3abce9fb6bd9..68d81ce99c121 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -26,11 +26,10 @@ export const ViewSpaceSettings: React.FC = ({ space }) => { const onChangeSpaceSettings = (updatedSpace: Partial) => { setSpaceSettings(updatedSpace); setIsDirty(true); - console.log('value', updatedSpace); }; const onUpdateSpace = () => { - window.alert('not yet implemented'); + window.alert('not yet implemented'); // FIXME }; const onCancel = () => { From 79afa3a35e988be8771887cfb22aa4114b18bcc2 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:21:37 +0000 Subject: [PATCH 052/129] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/spaces/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index a98e25176f14e..7e3b25ee1c3b7 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -41,6 +41,8 @@ "@kbn/security-plugin-types-common", "@kbn/core-application-browser", "@kbn/unsaved-changes-prompt", + "@kbn/core-http-browser", + "@kbn/core-overlays-browser", ], "exclude": [ "target/**/*", From 0f2c78c0690d2f5f5fe6a06c54ed0f397406aebd Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 24 Jul 2024 15:27:24 -0700 Subject: [PATCH 053/129] Optional title for SectionPanel to lessen headers for View Space tabs --- .../edit_space/customize_space/customize_space.tsx | 8 +++----- .../management/edit_space/manage_space_page.tsx | 12 +++++++++++- .../edit_space/section_panel/section_panel.tsx | 8 ++++++-- .../edit_space/solution_view/solution_view.tsx | 10 +++------- .../view_space/view_space_features_tab.tsx | 12 +++--------- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx b/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx index 1de97136e9104..33113f3338960 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx @@ -37,6 +37,7 @@ interface Props { space: FormValues; editingExistingSpace: boolean; onChange: (space: FormValues) => void; + title?: string; } interface State { @@ -51,14 +52,11 @@ export class CustomizeSpace extends Component { }; public render() { - const { validator, editingExistingSpace, space } = this.props; + const { validator, editingExistingSpace, space, title } = this.props; const { name = '', description = '' } = space; - const panelTitle = i18n.translate('xpack.spaces.management.manageSpacePage.generalTitle', { - defaultMessage: 'General', - }); return ( - + diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 7594778062857..1edfb7d22fce0 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -193,6 +193,9 @@ export class ManageSpacePage extends Component { return (
{ {this.state.isSolutionNavEnabled && ( <> - + )} diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx b/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx index 3d07aaabfd6b3..8024df2d6e85f 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx @@ -12,7 +12,7 @@ import React, { Component, Fragment } from 'react'; interface Props { iconType?: IconType; - title: string | ReactNode; + title?: string | ReactNode; dataTestSubj?: string; } @@ -27,6 +27,10 @@ export class SectionPanel extends Component { } public getTitle = () => { + if (!this.props.title) { + return null; + } + return ( @@ -52,7 +56,7 @@ export class SectionPanel extends Component { public getForm = () => { return ( - + {this.props.title ? : null} {this.props.children} ); diff --git a/x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx b/x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx index 0a6dc317161f2..608454a75600b 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx @@ -98,18 +98,14 @@ const getOptions = ({ size }: EuiThemeComputed): Array; onChange: (space: Partial) => void; + sectionTitle?: string; } -export const SolutionView: FunctionComponent = ({ space, onChange }) => { +export const SolutionView: FunctionComponent = ({ space, onChange, sectionTitle }) => { const { euiTheme } = useEuiTheme(); return ( - + diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index ad831a506f466..dcac61f9408fe 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -20,7 +20,6 @@ import React, { useState } from 'react'; import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/common'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; @@ -91,19 +90,14 @@ export const ViewSpaceEnabledFeatures: FC = ({ )} - +

From dbdbaa550e9ab08bc0d9c21e4d081bf1ea27cfd8 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 24 Jul 2024 16:36:47 -0700 Subject: [PATCH 054/129] Handle allowFeatureVisibility --- .../public/management/view_space/hooks/use_tabs.ts | 1 + .../public/management/view_space/view_space.tsx | 3 ++- .../management/view_space/view_space_tabs.tsx | 13 +++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index 8849d05c9021d..70e9c29e1bade 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -19,6 +19,7 @@ type UseTabsProps = Pick & { currentSelectedTabId: string; history: ScopedHistory; isSolutionNavEnabled: boolean; + allowFeatureVisibility: boolean; }; export const useTabs = ({ diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index c36b6704e3434..26106ad18e53e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -79,7 +79,7 @@ export const ViewSpacePage: FC = (props) => { solutionNavExperiment, selectedTabId: _selectedTabId, capabilities, - allowFeatureVisibility, // FIXME: handle this + allowFeatureVisibility, getUrlForApp, navigateToUrl, ...viewSpaceServices @@ -102,6 +102,7 @@ export const ViewSpacePage: FC = (props) => { history, currentSelectedTabId: selectedTabId, isSolutionNavEnabled, + allowFeatureVisibility, }); useEffect(() => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index ec8217ea7df17..1bebbeb71de4e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -38,6 +38,7 @@ export interface GetTabsProps { roles?: { view: boolean; save: boolean }; }; isSolutionNavEnabled: boolean; + allowFeatureVisibility: boolean; } export const getTabs = ({ @@ -47,6 +48,7 @@ export const getTabs = ({ capabilities, roles, isSolutionNavEnabled, + allowFeatureVisibility, }: GetTabsProps): ViewSpaceTab[] => { const enabledFeatureCount = getEnabledFeatures(features, space).length; const totalFeatureCount = features.length; @@ -54,7 +56,7 @@ export const getTabs = ({ const canUserViewRoles = Boolean(capabilities?.roles?.view); const canUserModifyRoles = Boolean(capabilities?.roles?.save); - const tabsDefinition = [ + const tabsDefinition: ViewSpaceTab[] = [ { id: TAB_ID_CONTENT, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { @@ -62,7 +64,10 @@ export const getTabs = ({ }), content: , }, - { + ]; + + if (allowFeatureVisibility) { + tabsDefinition.push({ id: TAB_ID_FEATURES, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.feature.heading', { defaultMessage: 'Feature visibility', @@ -80,8 +85,8 @@ export const getTabs = ({ isSolutionNavEnabled={isSolutionNavEnabled} /> ), - }, - ]; + }); + } if (canUserViewRoles) { tabsDefinition.push({ From cc06372a4cfbd991533c44611cabce9b8c5c6756 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 24 Jul 2024 17:10:26 -0700 Subject: [PATCH 055/129] Implement save space features and space settings --- .../view_space/view_space_features_tab.tsx | 27 ++++++++++++++++--- .../view_space/view_space_general_tab.tsx | 26 ++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index dcac61f9408fe..e971906642371 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -46,7 +46,9 @@ export const ViewSpaceEnabledFeatures: FC = ({ const [spaceFeatures, setSpaceFeatures] = useState>(space); // space details as seen in the Feature Visibility UI, possibly with unsaved changes const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made - const { capabilities, getUrlForApp, http, overlays, navigateToUrl } = useViewSpaceServices(); + const { capabilities, getUrlForApp, http, overlays, navigateToUrl, spacesManager } = + useViewSpaceServices(); + useUnsavedChangesPrompt({ hasUnsavedChanges: isDirty, http, @@ -71,8 +73,27 @@ export const ViewSpaceEnabledFeatures: FC = ({ setSpaceFeatures({ ...updatedSpace, id: space.id }); }; - const onUpdateSpace = () => { - window.alert('not yet implemented'); // FIXME + // TODO handle create space + + const onUpdateSpace = async () => { + const { id, name, disabledFeatures } = spaceFeatures; + if (!id) { + throw new Error(`Can not update space without id field!`); + } + if (!name) { + throw new Error(`Can not update space without name field!`); + } + + // TODO cancel previous request, if there is one pending + await spacesManager.updateSpace({ + id, + name, + disabledFeatures: disabledFeatures ?? [], + ...spaceFeatures, + }); + + // TODO error handling + setIsDirty(false); }; const onCancel = () => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 68d81ce99c121..db17738008bc0 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useState } from 'react'; +import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; import { CustomizeSpace } from '../edit_space/customize_space'; import { SpaceValidator } from '../lib'; @@ -21,6 +22,8 @@ export const ViewSpaceSettings: React.FC = ({ space }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made + const { spacesManager } = useViewSpaceServices(); + const validator = new SpaceValidator(); const onChangeSpaceSettings = (updatedSpace: Partial) => { @@ -28,8 +31,27 @@ export const ViewSpaceSettings: React.FC = ({ space }) => { setIsDirty(true); }; - const onUpdateSpace = () => { - window.alert('not yet implemented'); // FIXME + // TODO handle create space + + const onUpdateSpace = async () => { + const { id, name, disabledFeatures } = spaceSettings; + if (!id) { + throw new Error(`Can not update space without id field!`); + } + if (!name) { + throw new Error(`Can not update space without name field!`); + } + + // TODO cancel previous request, if there is one pending + await spacesManager.updateSpace({ + id, + name, + disabledFeatures: disabledFeatures ?? [], + ...spaceSettings, + }); + + // TODO error handling + setIsDirty(false); }; const onCancel = () => { From 99802b099c8b9add66b16cd89def5edd88d66c3a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 26 Jul 2024 16:14:04 -0700 Subject: [PATCH 056/129] Use new UI for Edit link --- .../spaces_grid/spaces_grid_page.tsx | 6 +-- .../management/spaces_management_app.tsx | 46 +++++-------------- .../management/view_space/view_space.tsx | 4 +- 3 files changed, 15 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index d99157faf7934..e68ce8bcb4d6a 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -266,7 +266,7 @@ export class SpacesGridPage extends Component { return ( }> @@ -284,7 +284,7 @@ export class SpacesGridPage extends Component { {value} @@ -466,8 +466,6 @@ export class SpacesGridPage extends Component { return config; } - private getViewSpacePath = (space: Space) => `view/${encodeURIComponent(space.id)}`; - private getEditSpacePath = (space: Space) => `edit/${encodeURIComponent(space.id)}`; private onDeleteSpaceClick = (space: Space) => { diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index c13a5d64d1bb0..afe7aae392cf7 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -53,8 +53,8 @@ export const spacesManagementApp = Object.freeze({ const [ [coreStart, { features }], { SpacesGridPage }, - { ManageSpacePage }, - { ViewSpacePage }, + { ManageSpacePage: CreateSpacePage }, + { ViewSpacePage: EditSpacePage }, ] = await Promise.all([ getStartServices(), import('./spaces_grid'), @@ -98,7 +98,7 @@ export const spacesManagementApp = Object.freeze({ ]); return ( - { + const EditSpacePageWithBreadcrumbs = () => { const { spaceId, selectedTabId } = useParams<{ spaceId: string; selectedTabId?: string; }>(); const breadcrumbText = (space: Space) => - context === 'edit' - ? i18n.translate('xpack.spaces.management.editSpaceBreadcrumb', { - defaultMessage: 'Edit {space}', - values: { space: space.name }, - }) - : i18n.translate('xpack.spaces.management.viewSpaceBreadcrumb', { - defaultMessage: 'View {space}', - values: { space: space.name }, - }); + i18n.translate('xpack.spaces.management.editSpaceBreadcrumb', { + defaultMessage: 'Edit {space}', + values: { space: space.name }, + }); const onLoadSpace = (space: Space) => { setBreadcrumbs([ @@ -136,24 +131,8 @@ export const spacesManagementApp = Object.freeze({ ]); }; - if (context === 'edit') { - return ( - - ); - } - return ( - - - - - - + + diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 26106ad18e53e..d07636ca96f65 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -269,7 +269,7 @@ export const ViewSpacePage: FC = (props) => { href={addSpaceIdToPath( props.serverBasePath, space.id, - `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/view/${space.id}` + `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/edit/${space.id}` )} data-test-subj="spaceSwitcherButton" > @@ -296,7 +296,7 @@ export const ViewSpacePage: FC = (props) => { append={tab.append} {...reactRouterNavigate( history, - `/view/${encodeURIComponent(space.id)}/${tab.id}` + `/edit/${encodeURIComponent(space.id)}/${tab.id}` )} > {tab.name} From abddadee6880b3ea5e8efb6a1bf3d4af6d451820 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 29 Jul 2024 16:26:37 -0700 Subject: [PATCH 057/129] --wip-- [skip ci] --- .../public/management/view_space/view_space_features_tab.tsx | 2 -- .../public/management/view_space/view_space_general_tab.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index e971906642371..0fca543bdcea9 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -73,8 +73,6 @@ export const ViewSpaceEnabledFeatures: FC = ({ setSpaceFeatures({ ...updatedSpace, id: space.id }); }; - // TODO handle create space - const onUpdateSpace = async () => { const { id, name, disabledFeatures } = spaceFeatures; if (!id) { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index db17738008bc0..db4eeecadf6d8 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -31,8 +31,6 @@ export const ViewSpaceSettings: React.FC = ({ space }) => { setIsDirty(true); }; - // TODO handle create space - const onUpdateSpace = async () => { const { id, name, disabledFeatures } = spaceSettings; if (!id) { From e7a2c96468fd650ab1698cf6e706d5fb9f306478 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 29 Jul 2024 17:34:55 -0700 Subject: [PATCH 058/129] Footer controls --- .../public/management/view_space/footer.tsx | 84 +++++++++++++++++++ .../view_space/view_space_features_tab.tsx | 36 +++----- .../view_space/view_space_general_tab.tsx | 44 +++++----- .../management/view_space/view_space_tabs.tsx | 2 +- 4 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/footer.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx new file mode 100644 index 0000000000000..fed750c1a968d --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -0,0 +1,84 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; + +interface Props { + isDirty: boolean; + isLoading: boolean; + setIsLoading: (value: boolean) => void; + onCancel: () => void; + onUpdateSpace: () => Promise; +} + +export const ViewSpaceTabFooter: React.FC = ({ + isDirty, + isLoading, + setIsLoading, + onCancel, + onUpdateSpace, +}) => { + const onUpdateSpaceWrapper = async () => { + setIsLoading(true); + await onUpdateSpace(); + window.location.reload(); + setIsLoading(false); // in case reload fails + }; + + return ( + <> + + + Changes will impact all users in the Space. The page will be reloaded. + + + {isLoading && ( + + + + + + )} + {!isLoading && ( + + + + Delete space + + + + + {isDirty && ( + <> + + Cancel + + + + Update space + + + + )} + + )} + + ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index 0fca543bdcea9..28f475b182ef0 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -5,16 +5,7 @@ * 2.0. */ -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import type { FC } from 'react'; import React, { useState } from 'react'; @@ -23,6 +14,7 @@ import type { KibanaFeature } from '@kbn/features-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; +import { ViewSpaceTabFooter } from './footer'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; import { FeatureTable } from '../edit_space/enabled_features/feature_table'; @@ -45,6 +37,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ const [spaceNavigation, setSpaceNavigation] = useState>(space); // space details as seen in the Solution View UI, possibly with unsaved changes const [spaceFeatures, setSpaceFeatures] = useState>(space); // space details as seen in the Feature Visibility UI, possibly with unsaved changes const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made + const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button const { capabilities, getUrlForApp, http, overlays, navigateToUrl, spacesManager } = useViewSpaceServices(); @@ -155,22 +148,13 @@ export const ViewSpaceEnabledFeatures: FC = ({
- {isDirty && ( - <> - -

- - Changes will impact all users in the Space. The page will be reloaded. - -

-

- - Update Space - - Cancel -

- - )} + ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index db4eeecadf6d8..2a04119167b3b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -5,9 +5,12 @@ * 2.0. */ -import { EuiButton, EuiButtonEmpty, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useState } from 'react'; +import type { ScopedHistory } from '@kbn/core-application-browser'; +import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; + +import { ViewSpaceTabFooter } from './footer'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; import { CustomizeSpace } from '../edit_space/customize_space'; @@ -15,14 +18,23 @@ import { SpaceValidator } from '../lib'; interface Props { space: Space; - isReadOnly: boolean; + history: ScopedHistory; } -export const ViewSpaceSettings: React.FC = ({ space }) => { +export const ViewSpaceSettings: React.FC = ({ space, ...props }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made + const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button + + const { http, overlays, navigateToUrl, spacesManager } = useViewSpaceServices(); - const { spacesManager } = useViewSpaceServices(); + useUnsavedChangesPrompt({ + hasUnsavedChanges: isDirty, + http, + openConfirm: overlays.openConfirm, + navigateToUrl, + history: props.history, + }); const validator = new SpaceValidator(); @@ -65,22 +77,14 @@ export const ViewSpaceSettings: React.FC = ({ space }) => { editingExistingSpace={true} validator={validator} /> - {isDirty && ( - <> - -

- - Changes will impact all users in the Space. The page will be reloaded. - -

-

- - Update Space - - Cancel -

- - )} + + ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 1bebbeb71de4e..31d12d27ca0ba 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -115,7 +115,7 @@ export const getTabs = ({ name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { defaultMessage: 'General settings', }), - content: , + content: , }); return tabsDefinition; From aa27b32df665d2004d50b0aa157d03f9704fa94c Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 30 Jul 2024 16:16:36 -0700 Subject: [PATCH 059/129] Remove Settings button --- .../management/view_space/view_space.tsx | 69 +++++-------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index d07636ca96f65..60f1113f07153 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -8,7 +8,6 @@ import { EuiBadge, EuiButton, - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, @@ -182,29 +181,6 @@ export const ViewSpacePage: FC = (props) => { ); }; - const SettingsButton = () => { - const href = getUrlForApp('management', { - path: `/kibana/spaces/edit/${space.id}`, - }); - - return capabilities.spaces.manage ? ( - { - event.preventDefault(); - navigateToUrl(href); - }} - > - - - - - ) : null; - }; - return ( = (props) => { - +

{space.name} @@ -257,31 +233,24 @@ export const ViewSpacePage: FC = (props) => {

- - - - - - {userActiveSpace?.id !== space.id ? ( - - - - - - ) : null} - - + {userActiveSpace?.id !== space.id ? ( + + + + + + ) : null} From b8b1b52c748af355829703745ad9c7353961c42d Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 30 Jul 2024 16:27:48 -0700 Subject: [PATCH 060/129] wip: assign roles from create form --- .../public/management/edit_space/manage_space_page.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 1edfb7d22fce0..058adc1326d7a 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -30,6 +30,7 @@ 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 { SectionPanel } from './section_panel'; import { SolutionView } from './solution_view'; import type { Space } from '../../../common'; import { isReservedSpace } from '../../../common'; @@ -216,6 +217,11 @@ export class ManageSpacePage extends Component { )} + + +

WIP

+
+ {this.props.allowFeatureVisibility && ( <> From 1269674bdb472fb5378c859fba5d5833a9f8c3fd Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 2 Aug 2024 14:02:01 -0700 Subject: [PATCH 061/129] fix unit test --- .../customize_space/__snapshots__/customize_space.test.tsx.snap | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap index 31de1c9ea55e9..66da6992614bc 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap @@ -3,7 +3,6 @@ exports[`renders correctly 1`] = ` Date: Fri, 2 Aug 2024 14:17:25 -0700 Subject: [PATCH 062/129] Remove features column from table for non-classic --- .../spaces_grid/spaces_grid_page.tsx | 52 +++++++++++-------- .../management/view_space/view_space_tabs.tsx | 33 +----------- 2 files changed, 32 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 95ef9a563162f..511560339988b 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -251,6 +251,9 @@ export class SpacesGridPage extends Component { }; public getColumnConfig() { + const { activeSpace, features } = this.state; + const { solution: activeSolution } = activeSpace ?? {}; + const config: Array> = [ { field: 'initials', @@ -284,7 +287,7 @@ export class SpacesGridPage extends Component { {value} - {this.state.activeSpace?.name === rowRecord.name && ( + {activeSpace?.name === rowRecord.name && ( {i18n.translate('xpack.spaces.management.spacesGridPage.currentSpaceMarkerText', { @@ -306,17 +309,21 @@ export class SpacesGridPage extends Component { truncateText: true, width: '30%', }, - { + ]; + + const shouldShowFeaturesColumn = !activeSolution || activeSolution === 'classic'; + if (shouldShowFeaturesColumn) { + config.push({ field: 'disabledFeatures', name: i18n.translate('xpack.spaces.management.spacesGridPage.featuresColumnName', { defaultMessage: 'Features visible', }), sortable: (space: Space) => { - return getEnabledFeatures(this.state.features, space).length; + return getEnabledFeatures(features, space).length; }, render: (_disabledFeatures: string[], rowRecord: Space) => { - const enabledFeatureCount = getEnabledFeatures(this.state.features, rowRecord).length; - if (enabledFeatureCount === this.state.features.length) { + const enabledFeatureCount = getEnabledFeatures(features, rowRecord).length; + if (enabledFeatureCount === features.length) { return ( { defaultMessage="{enabledFeatureCount} / {totalFeatureCount}" values={{ enabledFeatureCount, - totalFeatureCount: this.state.features.length, + totalFeatureCount: features.length, }} /> ); }, + }); + } + + config.push({ + field: 'id', + name: i18n.translate('xpack.spaces.management.spacesGridPage.identifierColumnName', { + defaultMessage: 'Identifier', + }), + sortable: true, + render(id: string) { + if (id === DEFAULT_SPACE_ID) { + return ''; + } + return id; }, - { - field: 'id', - name: i18n.translate('xpack.spaces.management.spacesGridPage.identifierColumnName', { - defaultMessage: 'Identifier', - }), - sortable: true, - render(id: string) { - if (id === DEFAULT_SPACE_ID) { - return ''; - } - return id; - }, - }, - ]; + }); if (this.props.allowSolutionVisibility) { config.push({ @@ -404,7 +412,7 @@ export class SpacesGridPage extends Component { defaultMessage: 'Switch', }), description: (rowRecord) => - this.state.activeSpace?.name !== rowRecord.name + activeSpace?.name !== rowRecord.name ? i18n.translate( 'xpack.spaces.management.spacesGridPage.switchSpaceActionDescription', { @@ -428,7 +436,7 @@ export class SpacesGridPage extends Component { rowRecord.id, `${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/` ), - enabled: (rowRecord) => this.state.activeSpace?.name !== rowRecord.name, + enabled: (rowRecord) => activeSpace?.name !== rowRecord.name, 'data-test-subj': (rowRecord) => `${rowRecord.name}-switchSpace`, }, { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 31d12d27ca0ba..a67cf48fe1d14 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -13,13 +13,11 @@ import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; -import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; +import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { ViewSpaceContent } from './view_space_content_tab'; -import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import { ViewSpaceSettings } from './view_space_general_tab'; import { ViewSpaceAssignedRoles } from './view_space_roles'; import type { Space } from '../../../common'; -import { getEnabledFeatures } from '../lib/feature_utils'; export interface ViewSpaceTab { id: string; @@ -38,7 +36,7 @@ export interface GetTabsProps { roles?: { view: boolean; save: boolean }; }; isSolutionNavEnabled: boolean; - allowFeatureVisibility: boolean; + allowFeatureVisibility: boolean; // FIXME: not for tab } export const getTabs = ({ @@ -47,12 +45,7 @@ export const getTabs = ({ history, capabilities, roles, - isSolutionNavEnabled, - allowFeatureVisibility, }: GetTabsProps): ViewSpaceTab[] => { - const enabledFeatureCount = getEnabledFeatures(features, space).length; - const totalFeatureCount = features.length; - const canUserViewRoles = Boolean(capabilities?.roles?.view); const canUserModifyRoles = Boolean(capabilities?.roles?.save); @@ -66,28 +59,6 @@ export const getTabs = ({ }, ]; - if (allowFeatureVisibility) { - tabsDefinition.push({ - id: TAB_ID_FEATURES, - name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.feature.heading', { - defaultMessage: 'Feature visibility', - }), - append: ( - - {enabledFeatureCount} / {totalFeatureCount} - - ), - content: ( - - ), - }); - } - if (canUserViewRoles) { tabsDefinition.push({ id: TAB_ID_ROLES, From adaea5fdfc475a842416a03a7b855ea44f194851 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 2 Aug 2024 15:36:12 -0700 Subject: [PATCH 063/129] Move features table to settings tab for non-classic --- .../public/management/view_space/footer.tsx | 1 - .../view_space/view_space_features_tab.tsx | 149 ++++++------------ .../view_space/view_space_general_tab.tsx | 27 +++- .../management/view_space/view_space_tabs.tsx | 2 +- 4 files changed, 68 insertions(+), 111 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index fed750c1a968d..5c507e1652944 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -40,7 +40,6 @@ export const ViewSpaceTabFooter: React.FC = ({ return ( <> - = ({ - features, - space, - isSolutionNavEnabled, - ...props -}) => { - const [spaceNavigation, setSpaceNavigation] = useState>(space); // space details as seen in the Solution View UI, possibly with unsaved changes +export const ViewSpaceEnabledFeatures: FC = ({ features, space, ...props }) => { const [spaceFeatures, setSpaceFeatures] = useState>(space); // space details as seen in the Feature Visibility UI, possibly with unsaved changes const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made - const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button - const { capabilities, getUrlForApp, http, overlays, navigateToUrl, spacesManager } = - useViewSpaceServices(); + const { capabilities, getUrlForApp, http, overlays, navigateToUrl } = useViewSpaceServices(); + const canManageRoles = capabilities.management?.security?.roles === true; useUnsavedChangesPrompt({ hasUnsavedChanges: isDirty, @@ -54,107 +44,56 @@ export const ViewSpaceEnabledFeatures: FC = ({ return null; } - const canManageRoles = capabilities.management?.security?.roles === true; - - const onChangeSpaceNavigation = (updatedSpace: Partial) => { - setIsDirty(true); - setSpaceNavigation(updatedSpace); - }; - const onChangeSpaceFeatures = (updatedSpace: Partial) => { setIsDirty(true); setSpaceFeatures({ ...updatedSpace, id: space.id }); }; - const onUpdateSpace = async () => { - const { id, name, disabledFeatures } = spaceFeatures; - if (!id) { - throw new Error(`Can not update space without id field!`); - } - if (!name) { - throw new Error(`Can not update space without name field!`); - } - - // TODO cancel previous request, if there is one pending - await spacesManager.updateSpace({ - id, - name, - disabledFeatures: disabledFeatures ?? [], - ...spaceFeatures, - }); - - // TODO error handling - setIsDirty(false); - }; - - const onCancel = () => { - setSpaceNavigation(space); - setSpaceFeatures(space); - setIsDirty(false); - }; - return ( - <> - {isSolutionNavEnabled && ( - <> - - - - )} - - - - - -

- -

-
- - -

- - - - ) : ( + + + + +

+ +

+ + + +

+ - ), - }} - /> -

-
-
- - - -
-
- - - + + ) : ( + + ), + }} + /> +

+ +
+ + + + +
); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 2a04119167b3b..77dac5241ca38 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -5,39 +5,47 @@ * 2.0. */ +import { EuiSpacer } from '@elastic/eui'; import React, { useState } from 'react'; import type { ScopedHistory } from '@kbn/core-application-browser'; +import type { KibanaFeature } from '@kbn/features-plugin/common'; import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; import { ViewSpaceTabFooter } from './footer'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; +import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import type { Space } from '../../../common'; import { CustomizeSpace } from '../edit_space/customize_space'; +import { SolutionView } from '../edit_space/solution_view'; import { SpaceValidator } from '../lib'; interface Props { space: Space; history: ScopedHistory; + features: KibanaFeature[]; } -export const ViewSpaceSettings: React.FC = ({ space, ...props }) => { +export const ViewSpaceSettings: React.FC = ({ space, features, history }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button const { http, overlays, navigateToUrl, spacesManager } = useViewSpaceServices(); + const { solution } = space; + const shouldShowFeaturesVisibility = !solution || solution === 'classic'; + + const validator = new SpaceValidator(); + useUnsavedChangesPrompt({ hasUnsavedChanges: isDirty, http, openConfirm: overlays.openConfirm, navigateToUrl, - history: props.history, + history, }); - const validator = new SpaceValidator(); - const onChangeSpaceSettings = (updatedSpace: Partial) => { setSpaceSettings(updatedSpace); setIsDirty(true); @@ -78,6 +86,17 @@ export const ViewSpaceSettings: React.FC = ({ space, ...props }) => { validator={validator} /> + + + + {shouldShowFeaturesVisibility ? ( + <> + + + + ) : null} + + , + content: , }); return tabsDefinition; From 2d4ec5687fe6415c1e6944a83aedfe1f705d7d96 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 2 Aug 2024 14:02:01 -0700 Subject: [PATCH 064/129] fix unit test --- .../__snapshots__/customize_space.test.tsx.snap | 1 - .../public/management/spaces_management_app.test.tsx | 11 ++++++++++- .../public/spaces_manager/spaces_manager.mock.ts | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap index 31de1c9ea55e9..66da6992614bc 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap @@ -3,7 +3,6 @@ exports[`renders correctly 1`] = ` ({ }, })); +jest.mock('./view_space', () => ({ + ViewSpacePage: (props: any) => { + if (props.spacesManager && props.onLoadSpace) { + props.spacesManager.getSpace().then((space: any) => props.onLoadSpace(space)); + } + return `Spaces View Page: ${JSON.stringify(props)}`; + }, +})); + import { coreMock, scopedHistoryMock, themeServiceMock } from '@kbn/core/public/mocks'; import { featuresPluginMock } from '@kbn/features-plugin/public/mocks'; @@ -164,7 +173,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}} + Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"spaceId":"some-space","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}}}

`); diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 61ac8da35d3ae..e4194ccdb8291 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -26,6 +26,8 @@ function createSpacesManagerMock() { updateSavedObjectsSpaces: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), getShareSavedObjectPermissions: jest.fn().mockResolvedValue(undefined), + getContentForSpace: jest.fn().mockResolvedValue([]), + getRolesForSpace: jest.fn().mockResolvedValue([]), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), } as unknown as jest.Mocked; } From 67146fbbe590157442bf399078913564e952fd85 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 2 Aug 2024 16:07:14 -0700 Subject: [PATCH 065/129] show space badge if non-classic --- .../management/view_space/view_space.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index efccf2105b8ef..ba04a56339abf 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -182,6 +182,10 @@ export const ViewSpacePage: FC = (props) => { ); }; + const { id, solution: spaceSolution } = space; + const solution = spaceSolution ?? 'classic'; + const shouldShowSolutionBadge = isSolutionNavEnabled || solution !== 'classic'; + return ( = (props) => {

{space.name} - {isSolutionNavEnabled ? ( + {shouldShowSolutionBadge ? ( <> {' '} ) : null} - {userActiveSpace?.id === space.id ? ( + {userActiveSpace?.id === id ? ( <> {' '} @@ -234,14 +238,14 @@ export const ViewSpacePage: FC = (props) => {

- {userActiveSpace?.id !== space.id ? ( + {userActiveSpace?.id !== id ? ( @@ -264,10 +268,7 @@ export const ViewSpacePage: FC = (props) => { key={index} isSelected={tab.id === selectedTabId} append={tab.append} - {...reactRouterNavigate( - history, - `/edit/${encodeURIComponent(space.id)}/${tab.id}` - )} + {...reactRouterNavigate(history, `/edit/${encodeURIComponent(id)}/${tab.id}`)} > {tab.name} From 54028cfb4d648e1ee8806e2d91a8e158156f0326 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:57:58 +0000 Subject: [PATCH 066/129] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/spaces/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index b625fa5b2cf32..f541cb07d01b0 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -38,7 +38,6 @@ "@kbn/security-plugin-types-public", "@kbn/cloud-plugin", "@kbn/core-analytics-browser", - "@kbn/cloud-experiments-plugin", "@kbn/core-analytics-browser", "@kbn/security-plugin-types-common", "@kbn/core-application-browser", From e53ce6d139325beb302cf5d3c62448fced9e644a Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 13:13:04 -0700 Subject: [PATCH 067/129] fix functional test --- x-pack/test/functional/page_objects/space_selector_page.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index ea9a7948410f3..a38ccb91cd6be 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -288,4 +288,9 @@ export class SpaceSelectorPageObject extends FtrService { ); expect(await msgElem.getVisibleText()).to.be('no spaces found'); } + + async currentSelectedSpaceTitle() { + const spacesNavSelector = await this.find.byCssSelector('[data-test-subj="spacesNavSelector"]'); + return spacesNavSelector.getAttribute('title'); + } } From ad8c70281e1b8500fab0dd8d662a41aa12baf4ca Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 13:49:31 -0700 Subject: [PATCH 068/129] wip fix functional tests for new tab design --- .../public/management/view_space/constants.ts | 1 - .../public/management/view_space/footer.tsx | 7 +- .../management/view_space/view_space.tsx | 182 +++++++++--------- .../management/view_space/view_space_tabs.tsx | 16 +- .../create_edit_space.ts | 9 +- 5 files changed, 110 insertions(+), 105 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/constants.ts b/x-pack/plugins/spaces/public/management/view_space/constants.ts index 258a98c71952c..21e10c547800f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/constants.ts +++ b/x-pack/plugins/spaces/public/management/view_space/constants.ts @@ -6,6 +6,5 @@ */ export const TAB_ID_CONTENT = 'content'; -export const TAB_ID_FEATURES = 'features'; export const TAB_ID_ROLES = 'roles'; export const TAB_ID_GENERAL = 'general'; diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 5c507e1652944..3c33e62111360 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -70,7 +70,12 @@ export const ViewSpaceTabFooter: React.FC = ({ Cancel - + Update space diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index ba04a56339abf..a23f1ae194d8d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import type { Role } from '@kbn/security-plugin-types-common'; -import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; +import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; import { ViewSpaceContextProvider, @@ -44,11 +44,9 @@ const LazySpaceAvatar = lazy(() => const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) => { // Validation of the selectedTabId routing parameter, default to the Content tab return selectedTabId && - [TAB_ID_FEATURES, TAB_ID_GENERAL, canUserViewRoles ? TAB_ID_ROLES : null] - .filter(Boolean) - .includes(selectedTabId) + [TAB_ID_CONTENT, canUserViewRoles ? TAB_ID_ROLES : null].filter(Boolean).includes(selectedTabId) ? selectedTabId - : TAB_ID_CONTENT; + : TAB_ID_GENERAL; }; interface PageProps extends ViewSpaceServices { @@ -187,97 +185,99 @@ export const ViewSpacePage: FC = (props) => { const shouldShowSolutionBadge = isSolutionNavEnabled || solution !== 'classic'; return ( - - - - - - - - -

- {space.name} - {shouldShowSolutionBadge ? ( - <> - {' '} - - - ) : null} - {userActiveSpace?.id === id ? ( - <> - {' '} - - + + + + + + + + +

+ {space.name} + {shouldShowSolutionBadge ? ( + <> + {' '} + - - - ) : null} -

-
+ + ) : null} + {userActiveSpace?.id === id ? ( + <> + {' '} + + + + + ) : null} +

+
- -

- {space.description ?? ( + +

+ {space.description ?? ( + + )} +

+
+
+ {userActiveSpace?.id !== id ? ( + + - )} -

-
- - {userActiveSpace?.id !== id ? ( - - - - - - ) : null} - + + + ) : null} + - + - - - - {tabs.map((tab, index) => ( - - {tab.name} - - ))} - - - {selectedTabContent ?? null} - - -
+ + + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + {selectedTabContent ?? null} + + + + ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index a78d33a2c595b..a62e3a9cdc77c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -51,11 +51,11 @@ export const getTabs = ({ const tabsDefinition: ViewSpaceTab[] = [ { - id: TAB_ID_CONTENT, - name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { - defaultMessage: 'Content', + id: TAB_ID_GENERAL, + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { + defaultMessage: 'General settings', }), - content: , + content: , }, ]; @@ -82,11 +82,11 @@ export const getTabs = ({ } tabsDefinition.push({ - id: TAB_ID_GENERAL, - name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { - defaultMessage: 'General settings', + id: TAB_ID_CONTENT, + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { + defaultMessage: 'Content', }), - content: , + content: , }); return tabsDefinition; diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts index 3f00dda32c878..1bee30277566e 100644 --- a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts @@ -28,12 +28,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('spaces-edit-page'); - await testSubjects.existOrFail('spaces-edit-page > generalPanel'); - await testSubjects.existOrFail('spaces-edit-page > navigationPanel'); + await testSubjects.existOrFail('spaces-view-page'); + await testSubjects.existOrFail('spaces-view-page > generalPanel'); + await testSubjects.existOrFail('spaces-view-page > navigationPanel'); }); - it('changes the space solution and updates the side navigation', async () => { + // FIXME + it.skip('changes the space solution and updates the side navigation', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/spaces/edit/default', { shouldUseHashForSubUrl: false, }); From 24653256dae54b4e48225bd6cbb9fcdc27be26e9 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 13:49:31 -0700 Subject: [PATCH 069/129] wip fix functional tests for new tab design --- .../public/management/view_space/constants.ts | 1 - .../public/management/view_space/footer.tsx | 18 +- .../management/view_space/view_space.tsx | 182 +++++++++--------- .../view_space/view_space_general_tab.tsx | 34 +++- .../management/view_space/view_space_tabs.tsx | 16 +- .../create_edit_space.ts | 6 +- 6 files changed, 139 insertions(+), 118 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/constants.ts b/x-pack/plugins/spaces/public/management/view_space/constants.ts index 258a98c71952c..21e10c547800f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/constants.ts +++ b/x-pack/plugins/spaces/public/management/view_space/constants.ts @@ -6,6 +6,5 @@ */ export const TAB_ID_CONTENT = 'content'; -export const TAB_ID_FEATURES = 'features'; export const TAB_ID_ROLES = 'roles'; export const TAB_ID_GENERAL = 'general'; diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 5c507e1652944..0038e838a94f6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -19,25 +19,16 @@ import React from 'react'; interface Props { isDirty: boolean; isLoading: boolean; - setIsLoading: (value: boolean) => void; onCancel: () => void; - onUpdateSpace: () => Promise; + onUpdateSpace: () => void; } export const ViewSpaceTabFooter: React.FC = ({ isDirty, isLoading, - setIsLoading, onCancel, onUpdateSpace, }) => { - const onUpdateSpaceWrapper = async () => { - setIsLoading(true); - await onUpdateSpace(); - window.location.reload(); - setIsLoading(false); // in case reload fails - }; - return ( <> = ({ Cancel - + Update space diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index ba04a56339abf..a23f1ae194d8d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import type { Role } from '@kbn/security-plugin-types-common'; -import { TAB_ID_CONTENT, TAB_ID_FEATURES, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; +import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; import { ViewSpaceContextProvider, @@ -44,11 +44,9 @@ const LazySpaceAvatar = lazy(() => const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) => { // Validation of the selectedTabId routing parameter, default to the Content tab return selectedTabId && - [TAB_ID_FEATURES, TAB_ID_GENERAL, canUserViewRoles ? TAB_ID_ROLES : null] - .filter(Boolean) - .includes(selectedTabId) + [TAB_ID_CONTENT, canUserViewRoles ? TAB_ID_ROLES : null].filter(Boolean).includes(selectedTabId) ? selectedTabId - : TAB_ID_CONTENT; + : TAB_ID_GENERAL; }; interface PageProps extends ViewSpaceServices { @@ -187,97 +185,99 @@ export const ViewSpacePage: FC = (props) => { const shouldShowSolutionBadge = isSolutionNavEnabled || solution !== 'classic'; return ( - - - - - - - - -

- {space.name} - {shouldShowSolutionBadge ? ( - <> - {' '} - - - ) : null} - {userActiveSpace?.id === id ? ( - <> - {' '} - - + + + + + + + + +

+ {space.name} + {shouldShowSolutionBadge ? ( + <> + {' '} + - - - ) : null} -

-
+ + ) : null} + {userActiveSpace?.id === id ? ( + <> + {' '} + + + + + ) : null} +

+
- -

- {space.description ?? ( + +

+ {space.description ?? ( + + )} +

+
+
+ {userActiveSpace?.id !== id ? ( + + - )} -

-
- - {userActiveSpace?.id !== id ? ( - - - - - - ) : null} - + + + ) : null} + - + - - - - {tabs.map((tab, index) => ( - - {tab.name} - - ))} - - - {selectedTabContent ?? null} - - -
+ + + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + {selectedTabContent ?? null} + + + + ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 77dac5241ca38..cca93d3ceb47f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -16,6 +16,7 @@ import { ViewSpaceTabFooter } from './footer'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import type { Space } from '../../../common'; +import { ConfirmAlterActiveSpaceModal } from '../edit_space/confirm_alter_active_space_modal'; import { CustomizeSpace } from '../edit_space/customize_space'; import { SolutionView } from '../edit_space/solution_view'; import { SpaceValidator } from '../lib'; @@ -30,6 +31,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button + const [showAlteringActiveSpaceDialog, setShowAlteringActiveSpaceDialog] = useState(false); const { http, overlays, navigateToUrl, spacesManager } = useViewSpaceServices(); @@ -51,7 +53,10 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) setIsDirty(true); }; - const onUpdateSpace = async () => { + // TODO cancel previous request, if there is one pending + // TODO flush analytics + // TODO error handling + const performSave = async ({ requiresReload = false }) => { const { id, name, disabledFeatures } = spaceSettings; if (!id) { throw new Error(`Can not update space without id field!`); @@ -60,7 +65,8 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) throw new Error(`Can not update space without name field!`); } - // TODO cancel previous request, if there is one pending + setIsLoading(true); + await spacesManager.updateSpace({ id, name, @@ -68,8 +74,20 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) ...spaceSettings, }); - // TODO error handling setIsDirty(false); + + if (requiresReload) { + window.location.reload(); + } + + setIsLoading(false); + }; + + const onUpdateSpace = () => { + setShowAlteringActiveSpaceDialog(true); + + // FIXME if user did not modify visible features, no reload is required + // performSave({ requiresReload: false }); }; const onCancel = () => { @@ -79,6 +97,15 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) return ( <> + {showAlteringActiveSpaceDialog && ( + performSave({ requiresReload: true })} + onCancel={() => { + setShowAlteringActiveSpaceDialog(false); + }} + /> + )} + = ({ space, features, history }) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index a78d33a2c595b..a62e3a9cdc77c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -51,11 +51,11 @@ export const getTabs = ({ const tabsDefinition: ViewSpaceTab[] = [ { - id: TAB_ID_CONTENT, - name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { - defaultMessage: 'Content', + id: TAB_ID_GENERAL, + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { + defaultMessage: 'General settings', }), - content: , + content: , }, ]; @@ -82,11 +82,11 @@ export const getTabs = ({ } tabsDefinition.push({ - id: TAB_ID_GENERAL, - name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { - defaultMessage: 'General settings', + id: TAB_ID_CONTENT, + name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.content.heading', { + defaultMessage: 'Content', }), - content: , + content: , }); return tabsDefinition; diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts index 3f00dda32c878..6a4b001e8ad7f 100644 --- a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts @@ -28,9 +28,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('spaces-edit-page'); - await testSubjects.existOrFail('spaces-edit-page > generalPanel'); - await testSubjects.existOrFail('spaces-edit-page > navigationPanel'); + await testSubjects.existOrFail('spaces-view-page'); + await testSubjects.existOrFail('spaces-view-page > generalPanel'); + await testSubjects.existOrFail('spaces-view-page > navigationPanel'); }); it('changes the space solution and updates the side navigation', async () => { From 60d79cf34357fa5c4b98bdca46ae918f61038be9 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 15:46:15 -0700 Subject: [PATCH 070/129] update user impact warning --- .../public/management/view_space/footer.tsx | 17 +++++++++++++---- .../create_edit_space.ts | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 0038e838a94f6..659bd5a1e38b4 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -29,15 +29,24 @@ export const ViewSpaceTabFooter: React.FC = ({ onCancel, onUpdateSpace, }) => { - return ( - <> + // FIXME show if disable features have changed, or if solution view has changed + const showUserImpactWarning = () => { + return ( - Changes will impact all users in the Space. The page will be reloaded. + {' '} + The changes made will impact all users in the space.{' '} + ); + }; + + return ( + <> + {showUserImpactWarning()} {isLoading && ( diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts index 6a4b001e8ad7f..3a4470fb983fd 100644 --- a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts @@ -58,7 +58,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - await testSubjects.missingOrFail('userImpactWarning'); + // FIXME + // await testSubjects.missingOrFail('userImpactWarning'); await PageObjects.spaceSelector.changeSolutionView('classic'); await testSubjects.existOrFail('userImpactWarning'); // Warn that the change will impact other users From f3224c89127f94a79a60a02654511f51d1770742 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 16:36:52 -0700 Subject: [PATCH 071/129] Fix edit space confirm modals --- .../public/management/view_space/footer.tsx | 30 +----- .../view_space/view_space_features_tab.tsx | 34 ++----- .../view_space/view_space_general_tab.tsx | 91 ++++++++++++++----- 3 files changed, 77 insertions(+), 78 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 659bd5a1e38b4..2bd5983fa0460 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -8,11 +8,9 @@ import { EuiButton, EuiButtonEmpty, - EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, - EuiSpacer, } from '@elastic/eui'; import React from 'react'; @@ -20,34 +18,12 @@ interface Props { isDirty: boolean; isLoading: boolean; onCancel: () => void; - onUpdateSpace: () => void; + onSubmit: () => void; } -export const ViewSpaceTabFooter: React.FC = ({ - isDirty, - isLoading, - onCancel, - onUpdateSpace, -}) => { - // FIXME show if disable features have changed, or if solution view has changed - const showUserImpactWarning = () => { - return ( - - {' '} - The changes made will impact all users in the space.{' '} - - ); - }; - +export const ViewSpaceTabFooter: React.FC = ({ isDirty, isLoading, onCancel, onSubmit }) => { return ( <> - {showUserImpactWarning()} - {isLoading && ( @@ -73,7 +49,7 @@ export const ViewSpaceTabFooter: React.FC = ({ Update space diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index ae9d1d0676b98..5f510461be94c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -7,12 +7,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import type { FC } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; -import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; @@ -20,35 +18,19 @@ import { FeatureTable } from '../edit_space/enabled_features/feature_table'; import { SectionPanel } from '../edit_space/section_panel'; interface Props { - space: Space; + space: Partial; features: KibanaFeature[]; - history: ScopedHistory; + onChange: (updatedSpace: Partial) => void; } -export const ViewSpaceEnabledFeatures: FC = ({ features, space, ...props }) => { - const [spaceFeatures, setSpaceFeatures] = useState>(space); // space details as seen in the Feature Visibility UI, possibly with unsaved changes - const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made - - const { capabilities, getUrlForApp, http, overlays, navigateToUrl } = useViewSpaceServices(); +export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { + const { capabilities, getUrlForApp } = useViewSpaceServices(); const canManageRoles = capabilities.management?.security?.roles === true; - useUnsavedChangesPrompt({ - hasUnsavedChanges: isDirty, - http, - openConfirm: overlays.openConfirm, - navigateToUrl, - history: props.history, - }); - if (!features) { return null; } - const onChangeSpaceFeatures = (updatedSpace: Partial) => { - setIsDirty(true); - setSpaceFeatures({ ...updatedSpace, id: space.id }); - }; - return ( @@ -87,11 +69,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, ...props - + diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index cca93d3ceb47f..06eed079f6c06 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React, { useState } from 'react'; import type { ScopedHistory } from '@kbn/core-application-browser'; @@ -31,7 +31,9 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button - const [showAlteringActiveSpaceDialog, setShowAlteringActiveSpaceDialog] = useState(false); + const [shouldShowUserImpactWarning, setShouldShowUserImpactWarning] = useState(false); + const [shouldShowAlteringActiveSpaceDialog, setShouldShowAlteringActiveSpaceDialog] = + useState(false); const { http, overlays, navigateToUrl, spacesManager } = useViewSpaceServices(); @@ -53,6 +55,28 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) setIsDirty(true); }; + const onChangeFeatures = (updatedSpace: Partial) => { + setSpaceSettings(updatedSpace); + setIsDirty(true); + setShouldShowUserImpactWarning(true); + }; + + const onSubmit = () => { + if (shouldShowUserImpactWarning) { + setShouldShowAlteringActiveSpaceDialog(true); + } else { + performSave({ requiresReload: false }); + } + }; + + const onCancel = () => { + setSpaceSettings(space); + setShouldShowAlteringActiveSpaceDialog(false); + setShouldShowUserImpactWarning(false); + setIsDirty(false); + setIsLoading(false); + }; + // TODO cancel previous request, if there is one pending // TODO flush analytics // TODO error handling @@ -83,28 +107,43 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) setIsLoading(false); }; - const onUpdateSpace = () => { - setShowAlteringActiveSpaceDialog(true); - - // FIXME if user did not modify visible features, no reload is required - // performSave({ requiresReload: false }); + const doShowAlteringActiveSpaceDialog = () => { + return ( + shouldShowAlteringActiveSpaceDialog && ( + performSave({ requiresReload: true })} + onCancel={() => { + setShouldShowAlteringActiveSpaceDialog(false); + }} + /> + ) + ); }; - const onCancel = () => { - setSpaceSettings(space); - setIsDirty(false); + // Show if user has changed disabled features + // Show if user has changed solution view + const doShowUserImpactWarning = () => { + return ( + shouldShowUserImpactWarning && ( + <> + + + {' '} + The changes made will impact all users in the space.{' '} + + + ) + ); }; return ( <> - {showAlteringActiveSpaceDialog && ( - performSave({ requiresReload: true })} - onCancel={() => { - setShowAlteringActiveSpaceDialog(false); - }} - /> - )} + {doShowAlteringActiveSpaceDialog()} = ({ space, features, history }) /> - + - {shouldShowFeaturesVisibility ? ( + {shouldShowFeaturesVisibility && ( <> - + - ) : null} + )} + + {doShowUserImpactWarning()} ); From 4d013a4e13acf3ca4e7dae3ea7235812e4c3ce98 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 16:37:00 -0700 Subject: [PATCH 072/129] Fix functional tests --- .../spaces/solution_view_flag_enabled/create_edit_space.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts index 7378f11a41c55..6a4b001e8ad7f 100644 --- a/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/solution_view_flag_enabled/create_edit_space.ts @@ -33,8 +33,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('spaces-view-page > navigationPanel'); }); - // FIXME - it.skip('changes the space solution and updates the side navigation', async () => { + it('changes the space solution and updates the side navigation', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/spaces/edit/default', { shouldUseHashForSubUrl: false, }); @@ -59,8 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - // FIXME - // await testSubjects.missingOrFail('userImpactWarning'); + await testSubjects.missingOrFail('userImpactWarning'); await PageObjects.spaceSelector.changeSolutionView('classic'); await testSubjects.existOrFail('userImpactWarning'); // Warn that the change will impact other users From 78a07e535e627b4944bf8edb378e6d519ac2a6b8 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 5 Aug 2024 21:25:43 -0700 Subject: [PATCH 073/129] skip failing tests --- x-pack/test/accessibility/apps/group1/spaces.ts | 3 ++- .../functional/apps/spaces/feature_controls/spaces_security.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/group1/spaces.ts b/x-pack/test/accessibility/apps/group1/spaces.ts index 5ec4b7c1ee644..1fa15708e1123 100644 --- a/x-pack/test/accessibility/apps/group1/spaces.ts +++ b/x-pack/test/accessibility/apps/group1/spaces.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const toasts = getService('toasts'); const kibanaServer = getService('kibanaServer'); - describe('Kibana Spaces Accessibility', () => { + // FIXME + describe.skip('Kibana Spaces Accessibility', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await PageObjects.common.navigateToApp('home'); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index be03af7c896a0..00ea29c528c4a 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -31,7 +31,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.savedObjects.cleanStandardList(); }); - describe('global all base privilege', () => { + // FIXME + describe.skip('global all base privilege', () => { before(async () => { await security.role.create('global_all_role', { kibana: [ From 801a52c7e780b6e0c39ec58c7d05f51cbf8c81e0 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 6 Aug 2024 12:23:09 -0700 Subject: [PATCH 074/129] Fix update avatar initials --- .../management/view_space/view_space_general_tab.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 06eed079f6c06..df0c260eb6931 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -18,6 +18,7 @@ import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import type { Space } from '../../../common'; import { ConfirmAlterActiveSpaceModal } from '../edit_space/confirm_alter_active_space_modal'; import { CustomizeSpace } from '../edit_space/customize_space'; +import type { FormValues } from '../edit_space/manage_space_page'; import { SolutionView } from '../edit_space/solution_view'; import { SpaceValidator } from '../lib'; @@ -50,7 +51,14 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) history, }); - const onChangeSpaceSettings = (updatedSpace: Partial) => { + const onChangeSpaceSettings = (formValues: FormValues & Partial) => { + const { + customIdentifier, + avatarType, + customAvatarInitials, + customAvatarColor, + ...updatedSpace + } = formValues; setSpaceSettings(updatedSpace); setIsDirty(true); }; From 2c70b0593d1c8b964d73e9c944ccc0d83a5c2c6b Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 6 Aug 2024 13:04:11 -0700 Subject: [PATCH 075/129] Apply notifications when save or cancel --- .../management/spaces_management_app.test.tsx | 2 +- .../management/spaces_management_app.tsx | 1 + .../hooks/view_space_context_provider.tsx | 2 + .../view_space/view_space_general_tab.tsx | 62 +++++++++++++------ 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index e897d40054235..6785427b167db 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -173,7 +173,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"spaceId":"some-space","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}}} + Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"spaceId":"some-space","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"notifications":{"toasts":{}},"overlays":{"banners":{}}} `); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 5b207cad764a9..c12700a82fdd9 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -148,6 +148,7 @@ export const spacesManagementApp = Object.freeze({ selectedTabId={selectedTabId} getRolesAPIClient={getRolesAPIClient} http={http} + notifications={notifications} overlays={overlays} /> ); diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index 46d07b10bda31..3e64cc7fc0934 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -10,6 +10,7 @@ import React, { createContext, useContext } from 'react'; import type { ApplicationStart } from '@kbn/core-application-browser'; import type { HttpStart } from '@kbn/core-http-browser'; +import type { NotificationsStart } from '@kbn/core-notifications-browser'; import type { OverlayStart } from '@kbn/core-overlays-browser'; import type { RolesAPIClient } from '@kbn/security-plugin-types-public'; @@ -24,6 +25,7 @@ export interface ViewSpaceServices { getRolesAPIClient: () => Promise; http: HttpStart; overlays: OverlayStart; + notifications: NotificationsStart; } const ViewSpaceContext = createContext(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index df0c260eb6931..32a25502c6193 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -10,6 +10,7 @@ import React, { useState } from 'react'; import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/common'; +import { i18n } from '@kbn/i18n'; import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; import { ViewSpaceTabFooter } from './footer'; @@ -36,7 +37,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const [shouldShowAlteringActiveSpaceDialog, setShouldShowAlteringActiveSpaceDialog] = useState(false); - const { http, overlays, navigateToUrl, spacesManager } = useViewSpaceServices(); + const { http, overlays, notifications, navigateToUrl, spacesManager } = useViewSpaceServices(); const { solution } = space; const shouldShowFeaturesVisibility = !solution || solution === 'classic'; @@ -59,12 +60,12 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) customAvatarColor, ...updatedSpace } = formValues; - setSpaceSettings(updatedSpace); + setSpaceSettings({ ...spaceSettings, ...updatedSpace }); setIsDirty(true); }; const onChangeFeatures = (updatedSpace: Partial) => { - setSpaceSettings(updatedSpace); + setSpaceSettings({ ...spaceSettings, ...updatedSpace }); setIsDirty(true); setShouldShowUserImpactWarning(true); }; @@ -77,17 +78,18 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) } }; + const backToSpacesList = () => { + history.push('/'); + }; + const onCancel = () => { - setSpaceSettings(space); setShouldShowAlteringActiveSpaceDialog(false); setShouldShowUserImpactWarning(false); - setIsDirty(false); - setIsLoading(false); + backToSpacesList(); }; // TODO cancel previous request, if there is one pending // TODO flush analytics - // TODO error handling const performSave = async ({ requiresReload = false }) => { const { id, name, disabledFeatures } = spaceSettings; if (!id) { @@ -99,20 +101,40 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) setIsLoading(true); - await spacesManager.updateSpace({ - id, - name, - disabledFeatures: disabledFeatures ?? [], - ...spaceSettings, - }); - - setIsDirty(false); - - if (requiresReload) { - window.location.reload(); + try { + await spacesManager.updateSpace({ + id, + name, + disabledFeatures: disabledFeatures ?? [], + ...spaceSettings, + }); + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.spaces.management.spaceDetails.spaceSuccessfullySavedNotificationMessage', + { + defaultMessage: `Space {name} was saved.`, + values: { name: `'${name}'` }, + } + ) + ); + + setIsDirty(false); + backToSpacesList(); + if (requiresReload) { + window.location.reload(); + } + } catch (error) { + const message = error?.body?.message ?? ''; + notifications.toasts.addDanger( + i18n.translate('xpack.spaces.management.spaceDetails.errorSavingSpaceTitle', { + defaultMessage: 'Error saving space: {message}', + values: { message }, + }) + ); + } finally { + setIsLoading(false); } - - setIsLoading(false); }; const doShowAlteringActiveSpaceDialog = () => { From f2cae837c4cf004c5af6baaf3d99492385d5b8a9 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 6 Aug 2024 13:23:47 -0700 Subject: [PATCH 076/129] fix font size for "No features visible" --- .../spaces/public/management/spaces_grid/spaces_grid_page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 511560339988b..f85fef1d04a6d 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -333,7 +333,7 @@ export class SpacesGridPage extends Component { } if (enabledFeatureCount === 0) { return ( - + Date: Tue, 6 Aug 2024 14:40:02 -0700 Subject: [PATCH 077/129] handle error when user has no privilege to view roles --- .../management/view_space/view_space.tsx | 21 ++++++++++++++++++- .../view_space/view_space_general_tab.tsx | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index a23f1ae194d8d..7a5b75818c6c5 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -128,7 +128,26 @@ export const ViewSpacePage: FC = (props) => { } const getRoles = async () => { - const result = await spacesManager.getRolesForSpace(spaceId); + let result: Role[] = []; + try { + result = await spacesManager.getRolesForSpace(spaceId); + } catch (error) { + const message = error?.body?.message ?? error.toString(); + const statusCode = error?.body?.statusCode ?? null; + if (statusCode === 403) { + // eslint-disable-next-line no-console + console.log('Insufficient permissions to get list of roles for the space'); + // eslint-disable-next-line no-console + console.log(message); + } else { + // eslint-disable-next-line no-console + console.error('Encountered error while getting list of roles for space!'); + // eslint-disable-next-line no-console + console.error(error); + throw error; + } + } + setRoles(result); setIsLoadingRoles(false); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 32a25502c6193..61e09228e6c85 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -125,7 +125,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) window.location.reload(); } } catch (error) { - const message = error?.body?.message ?? ''; + const message = error?.body?.message ?? error.toString(); notifications.toasts.addDanger( i18n.translate('xpack.spaces.management.spaceDetails.errorSavingSpaceTitle', { defaultMessage: 'Error saving space: {message}', From 22b98408905bde75918f754d1386cec125894c80 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 6 Aug 2024 13:48:31 -0700 Subject: [PATCH 078/129] fix fn tests --- x-pack/test/accessibility/apps/group1/spaces.ts | 3 +-- x-pack/test/functional/apps/spaces/create_edit_space.ts | 9 +++++---- .../apps/spaces/feature_controls/spaces_security.ts | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/test/accessibility/apps/group1/spaces.ts b/x-pack/test/accessibility/apps/group1/spaces.ts index 1fa15708e1123..5ec4b7c1ee644 100644 --- a/x-pack/test/accessibility/apps/group1/spaces.ts +++ b/x-pack/test/accessibility/apps/group1/spaces.ts @@ -20,8 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const toasts = getService('toasts'); const kibanaServer = getService('kibanaServer'); - // FIXME - describe.skip('Kibana Spaces Accessibility', () => { + describe('Kibana Spaces Accessibility', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await PageObjects.common.navigateToApp('home'); diff --git a/x-pack/test/functional/apps/spaces/create_edit_space.ts b/x-pack/test/functional/apps/spaces/create_edit_space.ts index cfffc752cca0c..628f591221199 100644 --- a/x-pack/test/functional/apps/spaces/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/create_edit_space.ts @@ -22,14 +22,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('solution view', () => { - it('does not show solution view panel', async () => { + // FIXME: no longer a valid test? + it.skip('does not show solution view panel', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/spaces/edit/default', { shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('spaces-edit-page'); - await testSubjects.existOrFail('spaces-edit-page > generalPanel'); - await testSubjects.missingOrFail('spaces-edit-page > navigationPanel'); + await testSubjects.existOrFail('spaces-view-page'); + await testSubjects.existOrFail('spaces-view-page > generalPanel'); + await testSubjects.missingOrFail('spaces-view-page > navigationPanel'); }); }); }); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 00ea29c528c4a..ee4fab8458b78 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -31,8 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.savedObjects.cleanStandardList(); }); - // FIXME - describe.skip('global all base privilege', () => { + describe('global all base privilege', () => { before(async () => { await security.role.create('global_all_role', { kibana: [ @@ -103,7 +102,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('spaces-edit-page'); + await testSubjects.existOrFail('spaces-view-page'); }); }); From fbec082bab8539519d669aef1611d8dcd4de0e85 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 9 Aug 2024 11:57:00 -0700 Subject: [PATCH 079/129] Clean up unused prop --- .../public/management/spaces_management_app.tsx | 13 ++++++------- .../public/management/view_space/hooks/use_tabs.ts | 1 - .../public/management/view_space/view_space.tsx | 3 --- .../management/view_space/view_space_tabs.tsx | 1 - 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index c12700a82fdd9..a217542ee34a8 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -136,20 +136,19 @@ export const spacesManagementApp = Object.freeze({ return ( ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index 70e9c29e1bade..8849d05c9021d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -19,7 +19,6 @@ type UseTabsProps = Pick & { currentSelectedTabId: string; history: ScopedHistory; isSolutionNavEnabled: boolean; - allowFeatureVisibility: boolean; }; export const useTabs = ({ diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 7a5b75818c6c5..67a169c4e8046 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -54,7 +54,6 @@ interface PageProps extends ViewSpaceServices { history: ScopedHistory; selectedTabId?: string; capabilities: Capabilities; - allowFeatureVisibility: boolean; solutionNavExperiment?: Promise; getFeatures: FeaturesPluginStart['getFeatures']; onLoadSpace: (space: Space) => void; @@ -77,7 +76,6 @@ export const ViewSpacePage: FC = (props) => { solutionNavExperiment, selectedTabId: _selectedTabId, capabilities, - allowFeatureVisibility, getUrlForApp, navigateToUrl, ...viewSpaceServices @@ -100,7 +98,6 @@ export const ViewSpacePage: FC = (props) => { history, currentSelectedTabId: selectedTabId, isSolutionNavEnabled, - allowFeatureVisibility, }); useEffect(() => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index a62e3a9cdc77c..10a66843128dc 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -36,7 +36,6 @@ export interface GetTabsProps { roles?: { view: boolean; save: boolean }; }; isSolutionNavEnabled: boolean; - allowFeatureVisibility: boolean; // FIXME: not for tab } export const getTabs = ({ From d3abbe1873480f598a112b55d93964dea048fb53 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 9 Aug 2024 12:01:13 -0700 Subject: [PATCH 080/129] Remove stray WIP --- .../public/management/edit_space/manage_space_page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 0b61a6c0c6774..b34d2eec88e48 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -30,7 +30,6 @@ 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 { SectionPanel } from './section_panel'; import { SolutionView } from './solution_view'; import type { Space } from '../../../common'; import { isReservedSpace } from '../../../common'; @@ -213,11 +212,6 @@ export class ManageSpacePage extends Component { )} - - -

WIP

-
- {this.props.allowFeatureVisibility && ( <> From 04f1cb530c4c292697e31319f5a589f4e219612e Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 9 Aug 2024 12:32:22 -0700 Subject: [PATCH 081/129] lazy load tab content --- .../management/view_space/view_space_tabs.tsx | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 10a66843128dc..b219425c2c2f4 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -12,11 +12,9 @@ import type { Capabilities, ScopedHistory } from '@kbn/core/public'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; +import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; -import { ViewSpaceContent } from './view_space_content_tab'; -import { ViewSpaceSettings } from './view_space_general_tab'; -import { ViewSpaceAssignedRoles } from './view_space_roles'; import type { Space } from '../../../common'; export interface ViewSpaceTab { @@ -38,6 +36,30 @@ export interface GetTabsProps { isSolutionNavEnabled: boolean; } +const SuspenseViewSpaceSettings = withSuspense( + React.lazy(() => + import('./view_space_general_tab').then(({ ViewSpaceSettings }) => ({ + default: ViewSpaceSettings, + })) + ) +); + +const SuspenseViewSpaceAssignedRoles = withSuspense( + React.lazy(() => + import('./view_space_roles').then(({ ViewSpaceAssignedRoles }) => ({ + default: ViewSpaceAssignedRoles, + })) + ) +); + +const SuspenseViewSpaceContent = withSuspense( + React.lazy(() => + import('./view_space_content_tab').then(({ ViewSpaceContent }) => ({ + default: ViewSpaceContent, + })) + ) +); + export const getTabs = ({ space, features, @@ -54,7 +76,7 @@ export const getTabs = ({ name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { defaultMessage: 'General settings', }), - content: , + content: , }, ]; @@ -70,7 +92,7 @@ export const getTabs = ({ ), content: ( - , + content: , }); return tabsDefinition; From e732902bb729470899787f19b38bffba89f47e94 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 9 Aug 2024 12:58:09 -0700 Subject: [PATCH 082/129] Fix delete space --- .../public/management/view_space/footer.tsx | 19 ++++-- .../view_space/view_space_general_tab.tsx | 59 +++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 2bd5983fa0460..35e2d3f26f6e2 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -17,11 +17,18 @@ import React from 'react'; interface Props { isDirty: boolean; isLoading: boolean; - onCancel: () => void; - onSubmit: () => void; + onClickCancel: () => void; + onClickSubmit: () => void; + onClickDeleteSpace: () => void; } -export const ViewSpaceTabFooter: React.FC = ({ isDirty, isLoading, onCancel, onSubmit }) => { +export const ViewSpaceTabFooter: React.FC = ({ + isDirty, + isLoading, + onClickCancel, + onClickSubmit, + onClickDeleteSpace, +}) => { return ( <> {isLoading && ( @@ -34,7 +41,7 @@ export const ViewSpaceTabFooter: React.FC = ({ isDirty, isLoading, onCanc {!isLoading && ( - + Delete space @@ -43,13 +50,13 @@ export const ViewSpaceTabFooter: React.FC = ({ isDirty, isLoading, onCanc {isDirty && ( <> - Cancel + Cancel Update space diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 61e09228e6c85..c65f845a2980b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -17,6 +17,7 @@ import { ViewSpaceTabFooter } from './footer'; import { useViewSpaceServices } from './hooks/view_space_context_provider'; import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import type { Space } from '../../../common'; +import { ConfirmDeleteModal } from '../components'; import { ConfirmAlterActiveSpaceModal } from '../edit_space/confirm_alter_active_space_modal'; import { CustomizeSpace } from '../edit_space/customize_space'; import type { FormValues } from '../edit_space/manage_space_page'; @@ -33,9 +34,9 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button - const [shouldShowUserImpactWarning, setShouldShowUserImpactWarning] = useState(false); - const [shouldShowAlteringActiveSpaceDialog, setShouldShowAlteringActiveSpaceDialog] = - useState(false); + const [showUserImpactWarning, setShowUserImpactWarning] = useState(false); + const [showAlteringActiveSpaceDialog, setShowAlteringActiveSpaceDialog] = useState(false); + const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); const { http, overlays, notifications, navigateToUrl, spacesManager } = useViewSpaceServices(); @@ -67,12 +68,12 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const onChangeFeatures = (updatedSpace: Partial) => { setSpaceSettings({ ...spaceSettings, ...updatedSpace }); setIsDirty(true); - setShouldShowUserImpactWarning(true); + setShowUserImpactWarning(true); }; - const onSubmit = () => { - if (shouldShowUserImpactWarning) { - setShouldShowAlteringActiveSpaceDialog(true); + const onClickSubmit = () => { + if (showUserImpactWarning) { + setShowAlteringActiveSpaceDialog(true); } else { performSave({ requiresReload: false }); } @@ -82,12 +83,16 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) history.push('/'); }; - const onCancel = () => { - setShouldShowAlteringActiveSpaceDialog(false); - setShouldShowUserImpactWarning(false); + const onClickCancel = () => { + setShowAlteringActiveSpaceDialog(false); + setShowUserImpactWarning(false); backToSpacesList(); }; + const onClickDeleteSpace = () => { + setShowConfirmDeleteModal(true); + }; + // TODO cancel previous request, if there is one pending // TODO flush analytics const performSave = async ({ requiresReload = false }) => { @@ -113,8 +118,8 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) i18n.translate( 'xpack.spaces.management.spaceDetails.spaceSuccessfullySavedNotificationMessage', { - defaultMessage: `Space {name} was saved.`, - values: { name: `'${name}'` }, + defaultMessage: `Space '{name}' was saved.`, + values: { name }, } ) ); @@ -139,11 +144,29 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const doShowAlteringActiveSpaceDialog = () => { return ( - shouldShowAlteringActiveSpaceDialog && ( + showAlteringActiveSpaceDialog && ( performSave({ requiresReload: true })} onCancel={() => { - setShouldShowAlteringActiveSpaceDialog(false); + setShowAlteringActiveSpaceDialog(false); + }} + /> + ) + ); + }; + + const doShowConfirmDeleteSpaceDialog = () => { + return ( + showConfirmDeleteModal && ( + { + setShowConfirmDeleteModal(false); + }} + onSuccess={() => { + setShowConfirmDeleteModal(false); + backToSpacesList(); }} /> ) @@ -154,7 +177,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) // Show if user has changed solution view const doShowUserImpactWarning = () => { return ( - shouldShowUserImpactWarning && ( + showUserImpactWarning && ( <> = ({ space, features, history }) return ( <> {doShowAlteringActiveSpaceDialog()} + {doShowConfirmDeleteSpaceDialog()} = ({ space, features, history }) ); From 0fd7a06949a5663582d7f11486562946e9bc32db Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 9 Aug 2024 15:54:53 -0700 Subject: [PATCH 083/129] consolidate ftr tests for listing of spaces --- .../details_view/spaces_details_view.ts | 132 ----------------- x-pack/test/functional/apps/spaces/index.ts | 1 - .../functional/apps/spaces/spaces_grid.ts | 133 ++++++++++++++---- 3 files changed, 109 insertions(+), 157 deletions(-) delete mode 100644 x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts diff --git a/x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts b/x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts deleted file mode 100644 index 56fe87e6eed20..0000000000000 --- a/x-pack/test/functional/apps/spaces/details_view/spaces_details_view.ts +++ /dev/null @@ -1,132 +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 crypto from 'crypto'; -import expect from '@kbn/expect'; -import { type FtrProviderContext } from '../../../ftr_provider_context'; - -export default function spaceDetailsViewFunctionalTests({ - getService, - getPageObjects, -}: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'settings', 'spaceSelector']); - - const find = getService('find'); - const retry = getService('retry'); - const spacesServices = getService('spaces'); - const testSubjects = getService('testSubjects'); - - describe('Spaces', function () { - const testSpacesIds = [ - 'odyssey', - // this number is chosen intentionally to not exceed the default 10 items displayed by spaces table - ...Array.from(new Array(5)).map((_) => `space-${crypto.randomUUID()}`), - ]; - - before(async () => { - for (const testSpaceId of testSpacesIds) { - await spacesServices.create({ id: testSpaceId, name: `${testSpaceId}-name` }); - } - }); - - after(async () => { - for (const testSpaceId of testSpacesIds) { - await spacesServices.delete(testSpaceId); - } - }); - - describe('Space listing', () => { - before(async () => { - await PageObjects.settings.navigateTo(); - await testSubjects.existOrFail('spaces'); - }); - - beforeEach(async () => { - await PageObjects.common.navigateToUrl('management', 'kibana/spaces', { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - }); - - await testSubjects.existOrFail('spaces-grid-page'); - }); - - it('should list all the spaces populated', async () => { - const renderedSpaceRow = await find.allByCssSelector( - '[data-test-subj*=spacesListTableRow-]' - ); - - expect(renderedSpaceRow.length).to.equal(testSpacesIds.length + 1); - }); - - it('does not display the space switcher button when viewing the details page for the current selected space', async () => { - const currentSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLowerCase(); - - expect(currentSpaceTitle).to.equal('default'); - - await testSubjects.click('default-hyperlink'); - await testSubjects.existOrFail('spaceDetailsHeader'); - expect( - (await testSubjects.getVisibleText('spaceDetailsHeader')) - .toLowerCase() - .includes('default') - ).to.be(true); - await testSubjects.missingOrFail('spaceSwitcherButton'); - }); - - it("displays the space switcher button when viewing the details page of the space that's not the current selected one", async () => { - const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; - - const currentSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLowerCase(); - - expect(currentSpaceTitle).to.equal('default'); - - await testSubjects.click(`${testSpaceId}-hyperlink`); - await testSubjects.existOrFail('spaceDetailsHeader'); - expect( - (await testSubjects.getVisibleText('spaceDetailsHeader')) - .toLowerCase() - .includes(`${testSpaceId}-name`) - ).to.be(true); - await testSubjects.existOrFail('spaceSwitcherButton'); - }); - - it('switches to a new space using the space switcher button', async () => { - const currentSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLowerCase(); - - expect(currentSpaceTitle).to.equal('default'); - - const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; - - await testSubjects.click(`${testSpaceId}-hyperlink`); - await testSubjects.click('spaceSwitcherButton'); - - await retry.try(async () => { - const detailsTitle = ( - await testSubjects.getVisibleText('spaceDetailsHeader') - ).toLowerCase(); - - const currentSwitchSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLocaleLowerCase(); - - return ( - currentSwitchSpaceTitle && - currentSwitchSpaceTitle === `${testSpaceId}-name` && - detailsTitle.includes(currentSwitchSpaceTitle) - ); - }); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index f96f8b9c58b64..3fe77a1a4528b 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -14,7 +14,6 @@ export default function spacesApp({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./spaces_selection')); loadTestFile(require.resolve('./enter_space')); loadTestFile(require.resolve('./create_edit_space')); - loadTestFile(require.resolve('./details_view/spaces_details_view')); loadTestFile(require.resolve('./spaces_grid')); }); } diff --git a/x-pack/test/functional/apps/spaces/spaces_grid.ts b/x-pack/test/functional/apps/spaces/spaces_grid.ts index 62363802db98a..8b9d9f8834125 100644 --- a/x-pack/test/functional/apps/spaces/spaces_grid.ts +++ b/x-pack/test/functional/apps/spaces/spaces_grid.ts @@ -5,43 +5,128 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import crypto from 'crypto'; +import expect from '@kbn/expect'; +import { type FtrProviderContext } from '../../ftr_provider_context'; -export default function enterSpaceFunctionalTests({ +export default function spaceDetailsViewFunctionalTests({ getService, getPageObjects, }: FtrProviderContext) { - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['security', 'spaceSelector', 'common']); - const spacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'settings', 'spaceSelector']); + + const find = getService('find'); + const retry = getService('retry'); + const spacesServices = getService('spaces'); const testSubjects = getService('testSubjects'); - const anotherSpace = { - id: 'space2', - name: 'space2', - disabledFeatures: [], - }; + describe('Spaces', function () { + const testSpacesIds = [ + 'odyssey', + // this number is chosen intentionally to not exceed the default 10 items displayed by spaces table + ...Array.from(new Array(5)).map((_) => `space-${crypto.randomUUID()}`), + ]; - describe('Spaces grid', function () { before(async () => { - await spacesService.create(anotherSpace); - - await PageObjects.common.navigateToApp('spacesManagement'); - await testSubjects.existOrFail('spaces-grid-page'); + for (const testSpaceId of testSpacesIds) { + await spacesServices.create({ id: testSpaceId, name: `${testSpaceId}-name` }); + } }); after(async () => { - await spacesService.delete('another-space'); - await kibanaServer.savedObjects.cleanStandardList(); + for (const testSpaceId of testSpacesIds) { + await spacesServices.delete(testSpaceId); + } }); - it('can switch to a space from the row in the grid', async () => { - // use the "current" badge confirm that Default is the current space - await testSubjects.existOrFail('spacesListCurrentBadge-default'); - // click the switch button of "another space" - await PageObjects.spaceSelector.clickSwitchSpaceButton('space2'); - // use the "current" badge confirm that "Another Space" is now the current space - await testSubjects.existOrFail('spacesListCurrentBadge-space2'); + describe('Space listing', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await testSubjects.existOrFail('spaces'); + }); + + beforeEach(async () => { + await PageObjects.common.navigateToUrl('management', 'kibana/spaces', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + + await testSubjects.existOrFail('spaces-grid-page'); + }); + + it('should list all the spaces populated', async () => { + const renderedSpaceRow = await find.allByCssSelector( + '[data-test-subj*=spacesListTableRow-]' + ); + + expect(renderedSpaceRow.length).to.equal(testSpacesIds.length + 1); + }); + + it('does not display the space switcher button when viewing the details page for the current selected space', async () => { + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); + + expect(currentSpaceTitle).to.equal('default'); + + await testSubjects.click('default-hyperlink'); + await testSubjects.existOrFail('spaceDetailsHeader'); + expect( + (await testSubjects.getVisibleText('spaceDetailsHeader')) + .toLowerCase() + .includes('default') + ).to.be(true); + await testSubjects.missingOrFail('spaceSwitcherButton'); + }); + + it("displays the space switcher button when viewing the details page of the space that's not the current selected one", async () => { + const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; + + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); + + expect(currentSpaceTitle).to.equal('default'); + + await testSubjects.click(`${testSpaceId}-hyperlink`); + await testSubjects.existOrFail('spaceDetailsHeader'); + expect( + (await testSubjects.getVisibleText('spaceDetailsHeader')) + .toLowerCase() + .includes(`${testSpaceId}-name`) + ).to.be(true); + await testSubjects.existOrFail('spaceSwitcherButton'); + }); + + it('switches to a new space using the space switcher button', async () => { + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); + + expect(currentSpaceTitle).to.equal('default'); + + const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; + + await testSubjects.click(`${testSpaceId}-hyperlink`); + await testSubjects.click('spaceSwitcherButton'); + + await retry.try(async () => { + const detailsTitle = ( + await testSubjects.getVisibleText('spaceDetailsHeader') + ).toLowerCase(); + + const currentSwitchSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLocaleLowerCase(); + + return ( + currentSwitchSpaceTitle && + currentSwitchSpaceTitle === `${testSpaceId}-name` && + detailsTitle.includes(currentSwitchSpaceTitle) + ); + }); + }); }); }); } From 2ebb3f6487e98168ba29569230aa50a3704e9b19 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 9 Aug 2024 17:06:03 -0700 Subject: [PATCH 084/129] start ftr test for create space --- .../apps/spaces/create_edit_space.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/spaces/create_edit_space.ts b/x-pack/test/functional/apps/spaces/create_edit_space.ts index 628f591221199..ee7e199395c58 100644 --- a/x-pack/test/functional/apps/spaces/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/create_edit_space.ts @@ -5,14 +5,17 @@ * 2.0. */ +import expect from '@kbn/expect'; +import { faker } from '@faker-js/faker'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); + const log = getService('log'); - describe('edit space', () => { + describe('create and edit space', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); }); @@ -21,6 +24,39 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await kibanaServer.savedObjects.cleanStandardList(); }); + describe('create space', () => { + const newSpaceName = faker.word.adjective() + ' space'; + log.debug(`new space name: ${newSpaceName}`); + const newSpaceInitials = faker.random.alpha(2); + log.debug(`new space initials: ${newSpaceInitials}`); + let newSpaceIdentifier: string; + + it('create a space with a given name', async () => { + await PageObjects.common.navigateToApp('spacesManagement'); + await testSubjects.existOrFail('spaces-grid-page'); + + await PageObjects.spaceSelector.clickCreateSpace(); + await testSubjects.existOrFail('spaces-edit-page'); + + await PageObjects.spaceSelector.addSpaceName(newSpaceName); + await PageObjects.spaceSelector.addSpaceInitials(newSpaceInitials); + newSpaceIdentifier = await testSubjects.getVisibleText('spaceURLDisplay'); + expect(newSpaceIdentifier).not.to.be.empty(); + log.debug(`new space identifier: ${newSpaceIdentifier}`); + await PageObjects.spaceSelector.clickSaveSpaceCreation(); + + await testSubjects.existOrFail('spaces-grid-page'); + await testSubjects.existOrFail(`spacesListTableRow-${newSpaceIdentifier}`); + }); + }); + + describe('manage general settings', () => { + it('lalala', async () => { + await PageObjects.common.navigateToApp('spacesManagement'); + await testSubjects.existOrFail('spaces-grid-page'); + }); + }); + describe('solution view', () => { // FIXME: no longer a valid test? it.skip('does not show solution view panel', async () => { From ccddaaf176e8f498ae234f5581245ab991445ea0 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 14 Aug 2024 16:24:07 -0700 Subject: [PATCH 085/129] Test for editing space initials --- .../apps/spaces/create_edit_space.ts | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/create_edit_space.ts b/x-pack/test/functional/apps/spaces/create_edit_space.ts index ee7e199395c58..26ea85ae58791 100644 --- a/x-pack/test/functional/apps/spaces/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/create_edit_space.ts @@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); + const spacesServices = getService('spaces'); const log = getService('log'); describe('create and edit space', () => { @@ -25,35 +26,61 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('create space', () => { - const newSpaceName = faker.word.adjective() + ' space'; - log.debug(`new space name: ${newSpaceName}`); - const newSpaceInitials = faker.random.alpha(2); - log.debug(`new space initials: ${newSpaceInitials}`); - let newSpaceIdentifier: string; - it('create a space with a given name', async () => { + const spaceName = faker.word.adjective() + ' space'; + log.debug(`new space name: ${spaceName}`); + await PageObjects.common.navigateToApp('spacesManagement'); await testSubjects.existOrFail('spaces-grid-page'); await PageObjects.spaceSelector.clickCreateSpace(); await testSubjects.existOrFail('spaces-edit-page'); - await PageObjects.spaceSelector.addSpaceName(newSpaceName); - await PageObjects.spaceSelector.addSpaceInitials(newSpaceInitials); - newSpaceIdentifier = await testSubjects.getVisibleText('spaceURLDisplay'); - expect(newSpaceIdentifier).not.to.be.empty(); - log.debug(`new space identifier: ${newSpaceIdentifier}`); + await PageObjects.spaceSelector.addSpaceName(spaceName); + const spaceUrlDisplay = await testSubjects.find('spaceURLDisplay'); + const spaceId = (await spaceUrlDisplay.getAttribute('value')) as string; + expect(spaceId).not.to.be.empty(); + log.debug(`new space identifier: ${spaceId}`); await PageObjects.spaceSelector.clickSaveSpaceCreation(); await testSubjects.existOrFail('spaces-grid-page'); - await testSubjects.existOrFail(`spacesListTableRow-${newSpaceIdentifier}`); + await testSubjects.existOrFail(`spacesListTableRow-${spaceId}`); + + await spacesServices.delete(spaceId); }); }); - describe('manage general settings', () => { - it('lalala', async () => { + describe('edit space', () => { + const spaceName = faker.word.adjective() + ' space'; + const spaceId = spaceName.replace(' ', '-'); + + before(async () => { + await spacesServices.create({ + id: spaceId, + name: spaceName, + disabledFeatures: [], + color: '#AABBCC', + }); + }); + + it('allows changing space initials', async () => { await PageObjects.common.navigateToApp('spacesManagement'); await testSubjects.existOrFail('spaces-grid-page'); + await testSubjects.click(`${spaceId}-hyperlink`); + + const spaceInitials = faker.string.alpha(2); + + // navigated to edit space page + await testSubjects.existOrFail('spaces-view-page > generalPanel'); + await testSubjects.setValue('spaceLetterInitial', spaceInitials); + await testSubjects.click('save-space-button'); + + // navigated back to space grid + await testSubjects.existOrFail('spaces-grid-page'); + await testSubjects.existOrFail(`space-avatar-${spaceId}`); + expect(await testSubjects.getVisibleText(`space-avatar-${spaceId}`)).to.be(spaceInitials); + + await spacesServices.delete(spaceId); }); }); From 5a7168736908fec39dbcaa710324127fa0bb3a0c Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 15 Aug 2024 10:56:53 -0700 Subject: [PATCH 086/129] functional tests cleanup --- .../apps/spaces/create_edit_space.ts | 44 +++-- .../functional/apps/spaces/spaces_grid.ts | 159 +++++++++--------- 2 files changed, 96 insertions(+), 107 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/create_edit_space.ts b/x-pack/test/functional/apps/spaces/create_edit_space.ts index 26ea85ae58791..6ef6588629cbd 100644 --- a/x-pack/test/functional/apps/spaces/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/create_edit_space.ts @@ -16,7 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesServices = getService('spaces'); const log = getService('log'); - describe('create and edit space', () => { + describe('Create and edit Space', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); }); @@ -26,61 +26,59 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('create space', () => { - it('create a space with a given name', async () => { - const spaceName = faker.word.adjective() + ' space'; - log.debug(`new space name: ${spaceName}`); + const spaceName = `${faker.word.adjective()} space`; + const spaceId = spaceName.replace(' ', '-'); + before(async () => { await PageObjects.common.navigateToApp('spacesManagement'); await testSubjects.existOrFail('spaces-grid-page'); await PageObjects.spaceSelector.clickCreateSpace(); await testSubjects.existOrFail('spaces-edit-page'); + }); + + after(async () => { + await spacesServices.delete(spaceId); + }); + it('create a space with a given name', async () => { await PageObjects.spaceSelector.addSpaceName(spaceName); - const spaceUrlDisplay = await testSubjects.find('spaceURLDisplay'); - const spaceId = (await spaceUrlDisplay.getAttribute('value')) as string; - expect(spaceId).not.to.be.empty(); - log.debug(`new space identifier: ${spaceId}`); await PageObjects.spaceSelector.clickSaveSpaceCreation(); - - await testSubjects.existOrFail('spaces-grid-page'); await testSubjects.existOrFail(`spacesListTableRow-${spaceId}`); - - await spacesServices.delete(spaceId); }); }); describe('edit space', () => { - const spaceName = faker.word.adjective() + ' space'; + const spaceName = `${faker.word.adjective()} space`; const spaceId = spaceName.replace(' ', '-'); before(async () => { + log.debug(`Creating space named "${spaceName}" with ID "${spaceId}"`); + await spacesServices.create({ id: spaceId, name: spaceName, disabledFeatures: [], color: '#AABBCC', }); - }); - it('allows changing space initials', async () => { await PageObjects.common.navigateToApp('spacesManagement'); await testSubjects.existOrFail('spaces-grid-page'); await testSubjects.click(`${spaceId}-hyperlink`); + await testSubjects.existOrFail('spaces-view-page > generalPanel'); + }); - const spaceInitials = faker.string.alpha(2); + after(async () => { + await spacesServices.delete(spaceId); + }); - // navigated to edit space page - await testSubjects.existOrFail('spaces-view-page > generalPanel'); + it('allows changing space initials', async () => { + const spaceInitials = faker.string.alpha(2); await testSubjects.setValue('spaceLetterInitial', spaceInitials); await testSubjects.click('save-space-button'); - - // navigated back to space grid - await testSubjects.existOrFail('spaces-grid-page'); + await testSubjects.existOrFail('spaces-grid-page'); // wait for grid page to reload await testSubjects.existOrFail(`space-avatar-${spaceId}`); expect(await testSubjects.getVisibleText(`space-avatar-${spaceId}`)).to.be(spaceInitials); - - await spacesServices.delete(spaceId); }); }); diff --git a/x-pack/test/functional/apps/spaces/spaces_grid.ts b/x-pack/test/functional/apps/spaces/spaces_grid.ts index 8b9d9f8834125..c5e8ef6e54087 100644 --- a/x-pack/test/functional/apps/spaces/spaces_grid.ts +++ b/x-pack/test/functional/apps/spaces/spaces_grid.ts @@ -14,118 +14,109 @@ export default function spaceDetailsViewFunctionalTests({ getPageObjects, }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'spaceSelector']); - + const spacesService = getService('spaces'); + const testSubjects = getService('testSubjects'); const find = getService('find'); const retry = getService('retry'); - const spacesServices = getService('spaces'); - const testSubjects = getService('testSubjects'); - describe('Spaces', function () { - const testSpacesIds = [ - 'odyssey', - // this number is chosen intentionally to not exceed the default 10 items displayed by spaces table - ...Array.from(new Array(5)).map((_) => `space-${crypto.randomUUID()}`), - ]; + const testSpacesIds = [ + 'odyssey', + // this number is chosen intentionally to not exceed the default 10 items displayed by spaces table + ...Array.from(new Array(5)).map((_) => `space-${crypto.randomUUID()}`), + ]; + describe('Spaces grid', function () { before(async () => { for (const testSpaceId of testSpacesIds) { - await spacesServices.create({ id: testSpaceId, name: `${testSpaceId}-name` }); + await spacesService.create({ id: testSpaceId, name: `${testSpaceId}-name` }); } + + await PageObjects.settings.navigateTo(); + await testSubjects.existOrFail('spaces'); + }); + + beforeEach(async () => { + await PageObjects.common.navigateToUrl('management', 'kibana/spaces', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + }); + + await testSubjects.existOrFail('spaces-grid-page'); }); after(async () => { for (const testSpaceId of testSpacesIds) { - await spacesServices.delete(testSpaceId); + await spacesService.delete(testSpaceId); } }); - describe('Space listing', () => { - before(async () => { - await PageObjects.settings.navigateTo(); - await testSubjects.existOrFail('spaces'); - }); + it('should list all the spaces populated', async () => { + const renderedSpaceRow = await find.allByCssSelector('[data-test-subj*=spacesListTableRow-]'); - beforeEach(async () => { - await PageObjects.common.navigateToUrl('management', 'kibana/spaces', { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - }); + expect(renderedSpaceRow.length).to.equal(testSpacesIds.length + 1); + }); - await testSubjects.existOrFail('spaces-grid-page'); - }); + it('does not display the space switcher button when viewing the details page for the current selected space', async () => { + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); - it('should list all the spaces populated', async () => { - const renderedSpaceRow = await find.allByCssSelector( - '[data-test-subj*=spacesListTableRow-]' - ); + expect(currentSpaceTitle).to.equal('default'); - expect(renderedSpaceRow.length).to.equal(testSpacesIds.length + 1); - }); + await testSubjects.click('default-hyperlink'); + await testSubjects.existOrFail('spaceDetailsHeader'); + expect( + (await testSubjects.getVisibleText('spaceDetailsHeader')).toLowerCase().includes('default') + ).to.be(true); + await testSubjects.missingOrFail('spaceSwitcherButton'); + }); - it('does not display the space switcher button when viewing the details page for the current selected space', async () => { - const currentSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLowerCase(); - - expect(currentSpaceTitle).to.equal('default'); - - await testSubjects.click('default-hyperlink'); - await testSubjects.existOrFail('spaceDetailsHeader'); - expect( - (await testSubjects.getVisibleText('spaceDetailsHeader')) - .toLowerCase() - .includes('default') - ).to.be(true); - await testSubjects.missingOrFail('spaceSwitcherButton'); - }); + it("displays the space switcher button when viewing the details page of the space that's not the current selected one", async () => { + const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; - it("displays the space switcher button when viewing the details page of the space that's not the current selected one", async () => { - const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); - const currentSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLowerCase(); - - expect(currentSpaceTitle).to.equal('default'); - - await testSubjects.click(`${testSpaceId}-hyperlink`); - await testSubjects.existOrFail('spaceDetailsHeader'); - expect( - (await testSubjects.getVisibleText('spaceDetailsHeader')) - .toLowerCase() - .includes(`${testSpaceId}-name`) - ).to.be(true); - await testSubjects.existOrFail('spaceSwitcherButton'); - }); + expect(currentSpaceTitle).to.equal('default'); - it('switches to a new space using the space switcher button', async () => { - const currentSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLowerCase(); + await testSubjects.click(`${testSpaceId}-hyperlink`); + await testSubjects.existOrFail('spaceDetailsHeader'); + expect( + (await testSubjects.getVisibleText('spaceDetailsHeader')) + .toLowerCase() + .includes(`${testSpaceId}-name`) + ).to.be(true); + await testSubjects.existOrFail('spaceSwitcherButton'); + }); - expect(currentSpaceTitle).to.equal('default'); + it('switches to a new space using the space switcher button', async () => { + const currentSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLowerCase(); - const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; + expect(currentSpaceTitle).to.equal('default'); - await testSubjects.click(`${testSpaceId}-hyperlink`); - await testSubjects.click('spaceSwitcherButton'); + const testSpaceId = testSpacesIds[Math.floor(Math.random() * testSpacesIds.length)]; - await retry.try(async () => { - const detailsTitle = ( - await testSubjects.getVisibleText('spaceDetailsHeader') - ).toLowerCase(); + await testSubjects.click(`${testSpaceId}-hyperlink`); + await testSubjects.click('spaceSwitcherButton'); - const currentSwitchSpaceTitle = ( - await PageObjects.spaceSelector.currentSelectedSpaceTitle() - )?.toLocaleLowerCase(); + await retry.try(async () => { + const detailsTitle = ( + await testSubjects.getVisibleText('spaceDetailsHeader') + ).toLowerCase(); - return ( - currentSwitchSpaceTitle && - currentSwitchSpaceTitle === `${testSpaceId}-name` && - detailsTitle.includes(currentSwitchSpaceTitle) - ); - }); + const currentSwitchSpaceTitle = ( + await PageObjects.spaceSelector.currentSelectedSpaceTitle() + )?.toLocaleLowerCase(); + + return ( + currentSwitchSpaceTitle && + currentSwitchSpaceTitle === `${testSpaceId}-name` && + detailsTitle.includes(currentSwitchSpaceTitle) + ); }); }); }); From eb4a5f500b08e62a00bcf1a8430d83355faccbaa Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:31:44 +0000 Subject: [PATCH 087/129] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/spaces/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index f541cb07d01b0..67f6258b5c001 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -44,6 +44,8 @@ "@kbn/unsaved-changes-prompt", "@kbn/core-http-browser", "@kbn/core-overlays-browser", + "@kbn/core-notifications-browser", + "@kbn/shared-ux-utility", ], "exclude": [ "target/**/*", From 24c014bff45cf8dddc34c90d523a790975052e71 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 15 Aug 2024 17:51:00 -0700 Subject: [PATCH 088/129] fix i18n check --- .../public/management/view_space/view_space_general_tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index c65f845a2980b..f93b30df94a52 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -118,7 +118,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) i18n.translate( 'xpack.spaces.management.spaceDetails.spaceSuccessfullySavedNotificationMessage', { - defaultMessage: `Space '{name}' was saved.`, + defaultMessage: 'Space "{name}" was saved.', values: { name }, } ) From d7f9a71eb95a9b94272549a464bccc4270fe7cdc Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 15 Aug 2024 21:10:38 -0700 Subject: [PATCH 089/129] Show/hide feature visibility picker --- .../management/edit_space/manage_space_page.tsx | 16 +++++++++++++--- .../view_space/view_space_general_tab.tsx | 17 ++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index b34d2eec88e48..7fe813eef4f2b 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -68,6 +68,7 @@ interface State { showAlteringActiveSpaceDialog: boolean; haveDisabledFeaturesChanged: boolean; hasSolutionViewChanged: boolean; + allowFeatureVisibility: boolean; isLoading: boolean; saveInProgress: boolean; formError?: { @@ -90,6 +91,7 @@ export class ManageSpacePage extends Component { color: getSpaceColor({}), }, features: [], + allowFeatureVisibility: true, haveDisabledFeaturesChanged: false, hasSolutionViewChanged: false, }; @@ -185,7 +187,6 @@ export class ManageSpacePage extends Component { public getForm = () => { const { showAlteringActiveSpaceDialog } = this.state; - return (
{ { )} - {this.props.allowFeatureVisibility && ( + {this.state.allowFeatureVisibility && ( <> { return null; }; + private onSolutionViewChange = (space: Partial) => { + let allowFeatureVisibility = false; + if (space.solution === 'classic' || space.solution == null) { + allowFeatureVisibility = true; + } + this.setState((state) => ({ ...state, allowFeatureVisibility })); + this.onSpaceChange(space); + }; + public onSpaceChange = (updatedSpace: FormValues) => { this.setState({ space: updatedSpace, diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index f93b30df94a52..2091e73bfcfe4 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -37,13 +37,9 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) const [showUserImpactWarning, setShowUserImpactWarning] = useState(false); const [showAlteringActiveSpaceDialog, setShowAlteringActiveSpaceDialog] = useState(false); const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); - const { http, overlays, notifications, navigateToUrl, spacesManager } = useViewSpaceServices(); - const { solution } = space; - const shouldShowFeaturesVisibility = !solution || solution === 'classic'; - - const validator = new SpaceValidator(); + const [solution, setSolution] = useState(space.solution); useUnsavedChangesPrompt({ hasUnsavedChanges: isDirty, @@ -71,6 +67,11 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) setShowUserImpactWarning(true); }; + const onSolutionViewChange = (updatedSpace: Partial) => { + setSolution(updatedSpace.solution); + onChangeFeatures(updatedSpace); + }; + const onClickSubmit = () => { if (showUserImpactWarning) { setShowAlteringActiveSpaceDialog(true); @@ -194,6 +195,8 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) ); }; + const validator = new SpaceValidator(); + return ( <> {doShowAlteringActiveSpaceDialog()} @@ -207,9 +210,9 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) /> - + - {shouldShowFeaturesVisibility && ( + {(solution == null || solution === 'classic') && ( <> Date: Mon, 19 Aug 2024 15:33:52 -0700 Subject: [PATCH 090/129] Correction for solution visibility / feature visibility --- .../management/edit_space/manage_space_page.tsx | 13 +++++++------ .../management/spaces_management_app.test.tsx | 2 +- .../public/management/spaces_management_app.tsx | 2 ++ .../public/management/view_space/footer.tsx | 4 +++- .../management/view_space/hooks/use_tabs.ts | 3 ++- .../public/management/view_space/view_space.tsx | 17 ++++------------- .../view_space/view_space_general_tab.tsx | 14 ++++++++++---- .../management/view_space/view_space_tabs.tsx | 8 ++++++-- .../functional/apps/spaces/create_edit_space.ts | 14 ++++++++------ .../test/functional/apps/spaces/spaces_grid.ts | 2 +- 10 files changed, 44 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 7fe813eef4f2b..be8ebb1caebb0 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -66,9 +66,9 @@ interface State { features: KibanaFeature[]; originalSpace?: Partial; showAlteringActiveSpaceDialog: boolean; + showVisibleFeaturesPicker: boolean; haveDisabledFeaturesChanged: boolean; hasSolutionViewChanged: boolean; - allowFeatureVisibility: boolean; isLoading: boolean; saveInProgress: boolean; formError?: { @@ -86,12 +86,12 @@ export class ManageSpacePage extends Component { this.state = { isLoading: true, showAlteringActiveSpaceDialog: false, + showVisibleFeaturesPicker: !!props.allowFeatureVisibility, saveInProgress: false, space: { color: getSpaceColor({}), }, features: [], - allowFeatureVisibility: true, haveDisabledFeaturesChanged: false, hasSolutionViewChanged: false, }; @@ -187,6 +187,7 @@ export class ManageSpacePage extends Component { public getForm = () => { const { showAlteringActiveSpaceDialog } = this.state; + return (
{ )} - {this.state.allowFeatureVisibility && ( + {this.state.showVisibleFeaturesPicker && ( <> { }; private onSolutionViewChange = (space: Partial) => { - let allowFeatureVisibility = false; + let showVisibleFeaturesPicker = false; if (space.solution === 'classic' || space.solution == null) { - allowFeatureVisibility = true; + showVisibleFeaturesPicker = true; } - this.setState((state) => ({ ...state, allowFeatureVisibility })); + this.setState((state) => ({ ...state, showVisibleFeaturesPicker })); this.onSpaceChange(space); }; diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index 6785427b167db..00c640333d1db 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -173,7 +173,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"spaceId":"some-space","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"notifications":{"toasts":{}},"overlays":{"banners":{}}} + Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}}}
`); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index a217542ee34a8..c262b6182f314 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -149,6 +149,8 @@ export const spacesManagementApp = Object.freeze({ history={history} selectedTabId={selectedTabId} getRolesAPIClient={getRolesAPIClient} + allowFeatureVisibility={config.allowFeatureVisibility} + allowSolutionVisibility={config.allowSolutionVisibility} /> ); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 35e2d3f26f6e2..fbfa71a86483a 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -50,7 +50,9 @@ export const ViewSpaceTabFooter: React.FC = ({ {isDirty && ( <> - Cancel + + Cancel + & { features: KibanaFeature[] | null; currentSelectedTabId: string; history: ScopedHistory; - isSolutionNavEnabled: boolean; + allowFeatureVisibility: boolean; + allowSolutionVisibility: boolean; }; export const useTabs = ({ diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 67a169c4e8046..e519061dad690 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -54,9 +54,10 @@ interface PageProps extends ViewSpaceServices { history: ScopedHistory; selectedTabId?: string; capabilities: Capabilities; - solutionNavExperiment?: Promise; getFeatures: FeaturesPluginStart['getFeatures']; onLoadSpace: (space: Space) => void; + allowFeatureVisibility: boolean; + allowSolutionVisibility: boolean; } const handleApiError = (error: Error) => { @@ -73,7 +74,6 @@ export const ViewSpacePage: FC = (props) => { spacesManager, history, onLoadSpace, - solutionNavExperiment, selectedTabId: _selectedTabId, capabilities, getUrlForApp, @@ -88,16 +88,13 @@ export const ViewSpacePage: FC = (props) => { const [isLoadingSpace, setIsLoadingSpace] = useState(true); const [isLoadingFeatures, setIsLoadingFeatures] = useState(true); const [isLoadingRoles, setIsLoadingRoles] = useState(true); - const [isSolutionNavEnabled, setIsSolutionNavEnabled] = useState(false); const selectedTabId = getSelectedTabId(Boolean(capabilities?.roles?.view), _selectedTabId); const [tabs, selectedTabContent] = useTabs({ space, features, roles, - capabilities, - history, currentSelectedTabId: selectedTabId, - isSolutionNavEnabled, + ...props, }); useEffect(() => { @@ -168,12 +165,6 @@ export const ViewSpacePage: FC = (props) => { } }, [onLoadSpace, space]); - useEffect(() => { - solutionNavExperiment?.then((isEnabled) => { - setIsSolutionNavEnabled(isEnabled); - }); - }, [solutionNavExperiment]); - if (!space) { return null; } @@ -198,7 +189,7 @@ export const ViewSpacePage: FC = (props) => { const { id, solution: spaceSolution } = space; const solution = spaceSolution ?? 'classic'; - const shouldShowSolutionBadge = isSolutionNavEnabled || solution !== 'classic'; + const shouldShowSolutionBadge = props.allowSolutionVisibility || solution !== 'classic'; return (
diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 2091e73bfcfe4..6618af3c9d1d1 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -28,9 +28,11 @@ interface Props { space: Space; history: ScopedHistory; features: KibanaFeature[]; + allowFeatureVisibility: boolean; + allowSolutionVisibility: boolean; } -export const ViewSpaceSettings: React.FC = ({ space, features, history }) => { +export const ViewSpaceSettings: React.FC = ({ space, features, history, ...props }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button @@ -209,10 +211,14 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history }) validator={validator} /> - - + {props.allowSolutionVisibility && ( + <> + + + + )} - {(solution == null || solution === 'classic') && ( + {props.allowFeatureVisibility && (solution == null || solution === 'classic') && ( <> { const canUserViewRoles = Boolean(capabilities?.roles?.view); const canUserModifyRoles = Boolean(capabilities?.roles?.save); @@ -76,7 +78,9 @@ export const getTabs = ({ name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { defaultMessage: 'General settings', }), - content: , + content: ( + + ), }, ]; diff --git a/x-pack/test/functional/apps/spaces/create_edit_space.ts b/x-pack/test/functional/apps/spaces/create_edit_space.ts index 6ef6588629cbd..6b0166b4c77fa 100644 --- a/x-pack/test/functional/apps/spaces/create_edit_space.ts +++ b/x-pack/test/functional/apps/spaces/create_edit_space.ts @@ -16,7 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesServices = getService('spaces'); const log = getService('log'); - describe('Create and edit Space', () => { + describe('Spaces Management: Create and Edit', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); }); @@ -64,8 +64,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('spacesManagement'); await testSubjects.existOrFail('spaces-grid-page'); - await testSubjects.click(`${spaceId}-hyperlink`); - await testSubjects.existOrFail('spaces-view-page > generalPanel'); }); after(async () => { @@ -74,8 +72,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allows changing space initials', async () => { const spaceInitials = faker.string.alpha(2); + + await testSubjects.click(`${spaceId}-hyperlink`); + await testSubjects.existOrFail('spaces-view-page > generalPanel'); + await testSubjects.setValue('spaceLetterInitial', spaceInitials); await testSubjects.click('save-space-button'); + await testSubjects.existOrFail('spaces-grid-page'); // wait for grid page to reload await testSubjects.existOrFail(`space-avatar-${spaceId}`); expect(await testSubjects.getVisibleText(`space-avatar-${spaceId}`)).to.be(spaceInitials); @@ -83,15 +86,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('solution view', () => { - // FIXME: no longer a valid test? - it.skip('does not show solution view panel', async () => { + it('does not show solution view panel', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/spaces/edit/default', { shouldUseHashForSubUrl: false, }); await testSubjects.existOrFail('spaces-view-page'); await testSubjects.existOrFail('spaces-view-page > generalPanel'); - await testSubjects.missingOrFail('spaces-view-page > navigationPanel'); + await testSubjects.missingOrFail('spaces-view-page > navigationPanel'); // xpack.spaces.allowSolutionVisibility is not enabled, so the solution view picker should not appear }); }); }); diff --git a/x-pack/test/functional/apps/spaces/spaces_grid.ts b/x-pack/test/functional/apps/spaces/spaces_grid.ts index c5e8ef6e54087..e2c8695649717 100644 --- a/x-pack/test/functional/apps/spaces/spaces_grid.ts +++ b/x-pack/test/functional/apps/spaces/spaces_grid.ts @@ -25,7 +25,7 @@ export default function spaceDetailsViewFunctionalTests({ ...Array.from(new Array(5)).map((_) => `space-${crypto.randomUUID()}`), ]; - describe('Spaces grid', function () { + describe('Spaces Management: List of Spaces', function () { before(async () => { for (const testSpaceId of testSpacesIds) { await spacesService.create({ id: testSpaceId, name: `${testSpaceId}-name` }); From ec6b2f9ba9aa0fdc025b55ffe01b3066ce619e25 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 19 Aug 2024 16:06:10 -0700 Subject: [PATCH 091/129] fix tests --- .../management/spaces_management_app.test.tsx | 2 +- .../public/management/view_space/footer.tsx | 33 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index 00c640333d1db..a285abf0119d6 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -173,7 +173,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}}} + Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true}
`); diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index fbfa71a86483a..bdfa6cf0b623f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -47,24 +47,23 @@ export const ViewSpaceTabFooter: React.FC = ({
+ + + Cancel + + + {isDirty && ( - <> - - - Cancel - - - - - Update space - - - + + + Update space + + )}
)} From 5f824652b16d6292c033d3438f29c305fa011a42 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 21 Aug 2024 16:56:05 -0700 Subject: [PATCH 092/129] Memoize functions that are passed as props to child components --- .../view_space/view_space_general_tab.tsx | 168 ++++++++++-------- 1 file changed, 90 insertions(+), 78 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 6618af3c9d1d1..7ec6d8cfdb206 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -6,7 +6,7 @@ */ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/common'; @@ -51,99 +51,111 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . history, }); - const onChangeSpaceSettings = (formValues: FormValues & Partial) => { - const { - customIdentifier, - avatarType, - customAvatarInitials, - customAvatarColor, - ...updatedSpace - } = formValues; - setSpaceSettings({ ...spaceSettings, ...updatedSpace }); - setIsDirty(true); - }; - - const onChangeFeatures = (updatedSpace: Partial) => { - setSpaceSettings({ ...spaceSettings, ...updatedSpace }); - setIsDirty(true); - setShowUserImpactWarning(true); - }; + const onChangeSpaceSettings = useCallback( + (formValues: FormValues & Partial) => { + const { + customIdentifier, + avatarType, + customAvatarInitials, + customAvatarColor, + ...updatedSpace + } = formValues; + setSpaceSettings({ ...spaceSettings, ...updatedSpace }); + setIsDirty(true); + }, + [spaceSettings] + ); - const onSolutionViewChange = (updatedSpace: Partial) => { - setSolution(updatedSpace.solution); - onChangeFeatures(updatedSpace); - }; + const onChangeFeatures = useCallback( + (updatedSpace: Partial) => { + setSpaceSettings({ ...spaceSettings, ...updatedSpace }); + setIsDirty(true); + setShowUserImpactWarning(true); + }, + [spaceSettings] + ); - const onClickSubmit = () => { - if (showUserImpactWarning) { - setShowAlteringActiveSpaceDialog(true); - } else { - performSave({ requiresReload: false }); - } - }; + const onSolutionViewChange = useCallback( + (updatedSpace: Partial) => { + setSolution(updatedSpace.solution); + onChangeFeatures(updatedSpace); + }, + [onChangeFeatures] + ); - const backToSpacesList = () => { + const backToSpacesList = useCallback(() => { history.push('/'); - }; + }, [history]); - const onClickCancel = () => { + const onClickCancel = useCallback(() => { setShowAlteringActiveSpaceDialog(false); setShowUserImpactWarning(false); backToSpacesList(); - }; + }, [backToSpacesList]); - const onClickDeleteSpace = () => { + const onClickDeleteSpace = useCallback(() => { setShowConfirmDeleteModal(true); - }; + }, []); // TODO cancel previous request, if there is one pending // TODO flush analytics - const performSave = async ({ requiresReload = false }) => { - const { id, name, disabledFeatures } = spaceSettings; - if (!id) { - throw new Error(`Can not update space without id field!`); - } - if (!name) { - throw new Error(`Can not update space without name field!`); - } + const performSave = useCallback( + async ({ requiresReload = false }) => { + const { id, name, disabledFeatures } = spaceSettings; + if (!id) { + throw new Error(`Can not update space without id field!`); + } + if (!name) { + throw new Error(`Can not update space without name field!`); + } + + setIsLoading(true); + + try { + await spacesManager.updateSpace({ + id, + name, + disabledFeatures: disabledFeatures ?? [], + ...spaceSettings, + }); + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.spaces.management.spaceDetails.spaceSuccessfullySavedNotificationMessage', + { + defaultMessage: 'Space "{name}" was saved.', + values: { name }, + } + ) + ); - setIsLoading(true); - - try { - await spacesManager.updateSpace({ - id, - name, - disabledFeatures: disabledFeatures ?? [], - ...spaceSettings, - }); - - notifications.toasts.addSuccess( - i18n.translate( - 'xpack.spaces.management.spaceDetails.spaceSuccessfullySavedNotificationMessage', - { - defaultMessage: 'Space "{name}" was saved.', - values: { name }, - } - ) - ); - - setIsDirty(false); - backToSpacesList(); - if (requiresReload) { - window.location.reload(); + setIsDirty(false); + backToSpacesList(); + if (requiresReload) { + window.location.reload(); + } + } catch (error) { + const message = error?.body?.message ?? error.toString(); + notifications.toasts.addDanger( + i18n.translate('xpack.spaces.management.spaceDetails.errorSavingSpaceTitle', { + defaultMessage: 'Error saving space: {message}', + values: { message }, + }) + ); + } finally { + setIsLoading(false); } - } catch (error) { - const message = error?.body?.message ?? error.toString(); - notifications.toasts.addDanger( - i18n.translate('xpack.spaces.management.spaceDetails.errorSavingSpaceTitle', { - defaultMessage: 'Error saving space: {message}', - values: { message }, - }) - ); - } finally { - setIsLoading(false); + }, + [backToSpacesList, notifications.toasts, spaceSettings, spacesManager] + ); + + const onClickSubmit = useCallback(() => { + if (showUserImpactWarning) { + setShowAlteringActiveSpaceDialog(true); + } else { + performSave({ requiresReload: false }); } - }; + }, [performSave, showUserImpactWarning]); const doShowAlteringActiveSpaceDialog = () => { return ( From 73bf1b6fce93b485c272e02de56c5a7716456493 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 21 Aug 2024 17:34:11 -0700 Subject: [PATCH 093/129] todos --- .../public/management/view_space/footer.tsx | 1 + .../hooks/view_space_context_provider.tsx | 3 ++ .../management/view_space/view_space.tsx | 1 + .../view_space/view_space_features_tab.tsx | 1 + .../view_space_general_tab.test.tsx | 44 +++++++++++++++++++ .../view_space/view_space_general_tab.tsx | 1 + .../view_space/view_space_roles.tsx | 1 + .../management/view_space/view_space_tabs.tsx | 1 + 8 files changed, 53 insertions(+) create mode 100644 x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index bdfa6cf0b623f..9cd9faf6d0d71 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -22,6 +22,7 @@ interface Props { onClickDeleteSpace: () => void; } +// FIXME: rename to EditSpaceTabFooter export const ViewSpaceTabFooter: React.FC = ({ isDirty, isLoading, diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index 3e64cc7fc0934..ee0bfbf1014aa 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -16,6 +16,7 @@ import type { RolesAPIClient } from '@kbn/security-plugin-types-public'; import type { SpacesManager } from '../../../spaces_manager'; +// FIXME: rename to EditSpaceServices export interface ViewSpaceServices { capabilities: ApplicationStart['capabilities']; getUrlForApp: ApplicationStart['getUrlForApp']; @@ -30,6 +31,7 @@ export interface ViewSpaceServices { const ViewSpaceContext = createContext(null); +// FIXME: rename to EditSpaceContextProvider export const ViewSpaceContextProvider: FC> = ({ children, ...services @@ -37,6 +39,7 @@ export const ViewSpaceContextProvider: FC> return {children}; }; +// FIXME: rename to useEditSpaceServices export const useViewSpaceServices = (): ViewSpaceServices => { const context = useContext(ViewSpaceContext); if (!context) { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index e519061dad690..dee51f28c798c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -66,6 +66,7 @@ const handleApiError = (error: Error) => { throw error; }; +// FIXME: rename to EditSpacePage // FIXME: add eventTracker export const ViewSpacePage: FC = (props) => { const { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index 5f510461be94c..d0c21fda3db7e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -23,6 +23,7 @@ interface Props { onChange: (updatedSpace: Partial) => void; } +// FIXME: rename to EditSpaceEnabledFeatures export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { const { capabilities, getUrlForApp } = useViewSpaceServices(); const canManageRoles = capabilities.management?.security?.roles === true; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx new file mode 100644 index 0000000000000..d0d88c4669a5d --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx @@ -0,0 +1,44 @@ +/* + * 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. + */ + +describe('ViewSpaceSettings', () => { + it('shows solution view select when visible', async () => { + // TODO + }); + + it('hides solution view select when not visible', async () => { + // TODO + }); + + it('shows feature visibility controls when allowed', async () => { + // TODO + }); + + it('hides feature visibility controls when not allowed', async () => { + // TODO + }); + + it('allows a space to be updated', async () => { + // TODO + }); + + it('sets calculated fields for existing spaces', async () => { + // TODO + }); + + it('notifies when there is an error retrieving features', async () => { + // TODO + }); + + it('warns when updating features in the active space', async () => { + // TODO + }); + + it('does not warn when features are left alone in the active space', async () => { + // TODO + }); +}); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 7ec6d8cfdb206..e916c51825ce6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -32,6 +32,7 @@ interface Props { allowSolutionVisibility: boolean; } +// FIXME: rename to EditSpaceSettings export const ViewSpaceSettings: React.FC = ({ space, features, history, ...props }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index ecc55ef4ebd7c..51a8fa17ac003 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -65,6 +65,7 @@ const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { ); }; +// FIXME: rename to EditSpaceAssignedRoles export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 0723b01aca8e0..d6fd9a92d73ec 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -17,6 +17,7 @@ import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import type { Space } from '../../../common'; +// FIXME: rename to EditSpaceTab export interface ViewSpaceTab { id: string; name: string; From c53c6d5e9f8b23b9deb8482f01f9db894d826329 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 22 Aug 2024 15:55:38 -0700 Subject: [PATCH 094/129] Unit test for General Settings tab --- .../view_space/view_space_features_tab.tsx | 2 +- .../view_space_general_tab.test.tsx | 145 ++++++++++++++++-- 2 files changed, 137 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index d0c21fda3db7e..4d4a1a1668b0f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -33,7 +33,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange } return ( - + diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx index d0d88c4669a5d..7d7c4a39d51b9 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx @@ -5,40 +5,167 @@ * 2.0. */ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { + httpServiceMock, + notificationServiceMock, + overlayServiceMock, + scopedHistoryMock, +} from '@kbn/core/public/mocks'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { KibanaFeature } from '@kbn/features-plugin/common'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + +import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; +import { ViewSpaceSettings } from './view_space_general_tab'; +import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; +import { getRolesAPIClientMock } from '../roles_api_client.mock'; + +const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true }; +const history = scopedHistoryMock.create(); +const getUrlForApp = (appId: string) => appId; +const navigateToUrl = jest.fn(); +const spacesManager = spacesManagerMock.create(); +const getRolesAPIClient = getRolesAPIClientMock(); + describe('ViewSpaceSettings', () => { + const TestComponent: React.FC = ({ children }) => { + return ( + + + {children} + + + ); + }; + + it('should render matching snapshot', () => { + render( + + + + ); + + expect(screen.getByTestId('addSpaceName')).toBeInTheDocument(); + expect(screen.getByTestId('descriptionSpaceText')).toBeInTheDocument(); + expect(screen.getByTestId('spaceLetterInitial')).toBeInTheDocument(); + expect(screen.getByTestId('euiColorPickerAnchor')).toBeInTheDocument(); + }); + it('shows solution view select when visible', async () => { - // TODO + render( + + + + ); + + expect(screen.getByTestId('solutionViewSelect')).toBeInTheDocument(); }); it('hides solution view select when not visible', async () => { - // TODO + render( + + + + ); + + expect(screen.queryByTestId('solutionViewSelect')).not.toBeInTheDocument(); }); it('shows feature visibility controls when allowed', async () => { - // TODO + const features = [ + new KibanaFeature({ + id: 'feature-1', + name: 'feature 1', + app: [], + category: DEFAULT_APP_CATEGORIES.kibana, + privileges: null, + }), + ]; + + render( + + + + ); + + expect(screen.getByTestId('enabled-features-panel')).toBeInTheDocument(); }); it('hides feature visibility controls when not allowed', async () => { - // TODO + render( + + + + ); + + expect(screen.queryByTestId('enabled-features-panel')).not.toBeInTheDocument(); }); - it('allows a space to be updated', async () => { + it.skip('allows a space to be updated', async () => { // TODO }); - it('sets calculated fields for existing spaces', async () => { + it.skip('sets calculated fields for existing spaces', async () => { // TODO }); - it('notifies when there is an error retrieving features', async () => { + it.skip('notifies when there is an error retrieving features', async () => { // TODO }); - it('warns when updating features in the active space', async () => { + it.skip('warns when updating features in the active space', async () => { // TODO }); - it('does not warn when features are left alone in the active space', async () => { + it.skip('does not warn when features are left alone in the active space', async () => { // TODO }); }); From 24034b3fcb3e0c98e6b45b2fdd4074de3347b4e0 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 23 Aug 2024 11:36:04 -0700 Subject: [PATCH 095/129] Update comments --- .../public/management/view_space/view_space_general_tab.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index e916c51825ce6..2f0d6b61d3687 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -98,8 +98,6 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . setShowConfirmDeleteModal(true); }, []); - // TODO cancel previous request, if there is one pending - // TODO flush analytics const performSave = useCallback( async ({ requiresReload = false }) => { const { id, name, disabledFeatures } = spaceSettings; From 986e8f59219e110a0ecd2b4c072ab2c5cd91e2c1 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Fri, 23 Aug 2024 12:12:41 -0700 Subject: [PATCH 096/129] More unit test for General Settings tab --- .../public/management/view_space/footer.tsx | 6 +- .../view_space_general_tab.test.tsx | 290 +++++++++++++++--- .../view_space/view_space_general_tab.tsx | 8 +- .../management/view_space/view_space_tabs.tsx | 11 +- 4 files changed, 274 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/view_space/footer.tsx index 9cd9faf6d0d71..14f036a0ee13c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/footer.tsx @@ -42,7 +42,11 @@ export const ViewSpaceTabFooter: React.FC = ({ {!isLoading && ( - + Delete space diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx index 7d7c4a39d51b9..bad47aa9d2ca2 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { @@ -20,6 +20,7 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; import { ViewSpaceSettings } from './view_space_general_tab'; +import type { SolutionView } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; @@ -29,8 +30,27 @@ const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); const spacesManager = spacesManagerMock.create(); const getRolesAPIClient = getRolesAPIClientMock(); +const reloadWindow = jest.fn(); + +const http = httpServiceMock.createStartContract(); +const notifications = notificationServiceMock.createStartContract(); +const overlays = overlayServiceMock.createStartContract(); + +const navigateSpy = jest.spyOn(history, 'push').mockImplementation(() => {}); +const updateSpaceSpy = jest + .spyOn(spacesManager, 'updateSpace') + .mockImplementation(() => Promise.resolve()); +const deleteSpaceSpy = jest + .spyOn(spacesManager, 'deleteSpace') + .mockImplementation(() => Promise.resolve()); describe('ViewSpaceSettings', () => { + beforeEach(() => { + navigateSpy.mockReset(); + updateSpaceSpy.mockReset(); + deleteSpaceSpy.mockReset(); + }); + const TestComponent: React.FC = ({ children }) => { return ( @@ -46,9 +66,9 @@ describe('ViewSpaceSettings', () => { serverBasePath="" spacesManager={spacesManager} getRolesAPIClient={getRolesAPIClient} - http={httpServiceMock.createStartContract()} - notifications={notificationServiceMock.createStartContract()} - overlays={overlayServiceMock.createStartContract()} + http={http} + notifications={notifications} + overlays={overlays} > {children} @@ -56,7 +76,7 @@ describe('ViewSpaceSettings', () => { ); }; - it('should render matching snapshot', () => { + it('should render controls for initial state of editing a space', () => { render( { features={[]} allowFeatureVisibility={false} allowSolutionVisibility={false} + reloadWindow={reloadWindow} /> ); @@ -73,6 +94,9 @@ describe('ViewSpaceSettings', () => { expect(screen.getByTestId('descriptionSpaceText')).toBeInTheDocument(); expect(screen.getByTestId('spaceLetterInitial')).toBeInTheDocument(); expect(screen.getByTestId('euiColorPickerAnchor')).toBeInTheDocument(); + + expect(screen.queryByTestId('solutionViewSelect')).not.toBeInTheDocument(); // hides solution view when not not set to visible + expect(screen.queryByTestId('enabled-features-panel')).not.toBeInTheDocument(); // hides navigation features table when not set to visible }); it('shows solution view select when visible', async () => { @@ -84,27 +108,13 @@ describe('ViewSpaceSettings', () => { features={[]} allowFeatureVisibility={false} allowSolutionVisibility={true} + reloadWindow={reloadWindow} /> ); expect(screen.getByTestId('solutionViewSelect')).toBeInTheDocument(); - }); - - it('hides solution view select when not visible', async () => { - render( - - - - ); - - expect(screen.queryByTestId('solutionViewSelect')).not.toBeInTheDocument(); + expect(screen.queryByTestId('enabled-features-panel')).not.toBeInTheDocument(); // hides navigation features table when not set to visible }); it('shows feature visibility controls when allowed', async () => { @@ -126,46 +136,256 @@ describe('ViewSpaceSettings', () => { features={features} allowFeatureVisibility={true} allowSolutionVisibility={false} + reloadWindow={reloadWindow} /> ); expect(screen.getByTestId('enabled-features-panel')).toBeInTheDocument(); + expect(screen.queryByTestId('solutionViewSelect')).not.toBeInTheDocument(); // hides solution view when not not set to visible }); - it('hides feature visibility controls when not allowed', async () => { + 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' as SolutionView, + }; + render( ); - expect(screen.queryByTestId('enabled-features-panel')).not.toBeInTheDocument(); - }); + await act(async () => { + // update the space name + const nameInput = screen.getByTestId('addSpaceName'); + fireEvent.change(nameInput, { target: { value: 'Updated Name Of Space' } }); + + expect(screen.queryByTestId('userImpactWarning')).not.toBeInTheDocument(); + expect(screen.queryByTestId('confirmModalTitleText')).not.toBeInTheDocument(); - it.skip('allows a space to be updated', async () => { - // TODO + const updateButton = await screen.findByTestId('save-space-button'); // appears via re-render + fireEvent.click(updateButton); + + expect(updateSpaceSpy).toHaveBeenCalledWith({ + ...spaceToUpdate, + name: 'Updated Name Of Space', + initials: 'UN', + color: '#D6BF57', + }); + }); + + expect(navigateSpy).toHaveBeenCalledTimes(1); }); - it.skip('sets calculated fields for existing spaces', async () => { - // TODO + it('allows space to be deleted', async () => { + const spaceToDelete = { + id: 'delete-me-space', + name: 'Delete Me Space', + description: 'This is a very nice space... for me to DELETE!', + color: '#aabbcc', + initials: 'XX', + disabledFeatures: [], + }; + + render( + + + + ); + + await act(async () => { + const deleteButton = screen.getByTestId('delete-space-button'); + fireEvent.click(deleteButton); + + const confirmButton = await screen.findByTestId('confirmModalConfirmButton'); // click delete confirm + fireEvent.click(confirmButton); + + expect(deleteSpaceSpy).toHaveBeenCalledWith(spaceToDelete); + }); }); - it.skip('notifies when there is an error retrieving features', async () => { - // TODO + 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: [], + }; + + render( + + + + ); + + await act(async () => { + // update the space name + const nameInput = screen.getByTestId('addSpaceName'); + fireEvent.change(nameInput, { target: { value: 'Updated Existing Space' } }); + + const updateButton = await screen.findByTestId('save-space-button'); // appears via re-render + fireEvent.click(updateButton); + + expect(updateSpaceSpy).toHaveBeenCalledWith({ + ...spaceToUpdate, + name: 'Updated Existing Space', + color: '#D6BF57', + initials: 'UE', + }); + }); }); - it.skip('warns when updating features in the active space', async () => { - // TODO + it('warns when updating solution view', async () => { + const spaceToUpdate = { + id: 'existing-space', + name: 'Existing Space', + description: 'hey an existing space', + color: '#aabbcc', + initials: 'AB', + disabledFeatures: [], + solution: undefined, + }; + + render( + + + + ); + + // update the space solution view + await act(async () => { + const solutionViewPicker = screen.getByTestId('solutionViewSelect'); + fireEvent.click(solutionViewPicker); + + const esSolutionOption = await screen.findByTestId('solutionViewEsOption'); // appears via re-render + fireEvent.click(esSolutionOption); + + expect(screen.getByTestId('userImpactWarning')).toBeInTheDocument(); + expect(screen.queryByTestId('confirmModalTitleText')).not.toBeInTheDocument(); + + const updateButton = screen.getByTestId('save-space-button'); + fireEvent.click(updateButton); + + expect(screen.getByTestId('confirmModalTitleText')).toBeInTheDocument(); + + const confirmButton = screen.getByTestId('confirmModalConfirmButton'); + fireEvent.click(confirmButton); + + await waitFor(() => { + expect(updateSpaceSpy).toHaveBeenCalledWith({ + ...spaceToUpdate, + solution: 'es', + }); + }); + }); + + expect(navigateSpy).toHaveBeenCalledTimes(1); }); - it.skip('does not warn when features are left alone in the active space', async () => { - // TODO + it('warns when updating features in the active space', async () => { + const features = [ + new KibanaFeature({ + id: 'feature-1', + name: 'feature 1', + app: [], + category: DEFAULT_APP_CATEGORIES.kibana, + privileges: null, + }), + ]; + + const spaceToUpdate = { + id: 'existing-space', + name: 'Existing Space', + description: 'hey an existing space', + color: '#aabbcc', + initials: 'AB', + disabledFeatures: [], + solution: 'classic' as SolutionView, + }; + + render( + + + + ); + + // update the space visible features + await act(async () => { + const feature1Checkbox = screen.getByTestId('featureCheckbox_feature-1'); + expect(feature1Checkbox).toBeChecked(); + + fireEvent.click(feature1Checkbox); + await waitFor(() => { + expect(feature1Checkbox).not.toBeChecked(); + }); + + expect(screen.getByTestId('userImpactWarning')).toBeInTheDocument(); + expect(screen.queryByTestId('confirmModalTitleText')).not.toBeInTheDocument(); + + const updateButton = screen.getByTestId('save-space-button'); + fireEvent.click(updateButton); + + expect(screen.getByTestId('confirmModalTitleText')).toBeInTheDocument(); + + const confirmButton = screen.getByTestId('confirmModalConfirmButton'); + fireEvent.click(confirmButton); + + await waitFor(() => { + expect(updateSpaceSpy).toHaveBeenCalledWith({ + ...spaceToUpdate, + disabledFeatures: ['feature-1'], + }); + }); + }); + + expect(navigateSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 2f0d6b61d3687..6b92662af420e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -30,6 +30,7 @@ interface Props { features: KibanaFeature[]; allowFeatureVisibility: boolean; allowSolutionVisibility: boolean; + reloadWindow: () => void; } // FIXME: rename to EditSpaceSettings @@ -131,7 +132,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . setIsDirty(false); backToSpacesList(); if (requiresReload) { - window.location.reload(); + props.reloadWindow(); } } catch (error) { const message = error?.body?.message ?? error.toString(); @@ -145,7 +146,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . setIsLoading(false); } }, - [backToSpacesList, notifications.toasts, spaceSettings, spacesManager] + [backToSpacesList, notifications.toasts, spaceSettings, spacesManager, props] ); const onClickSubmit = useCallback(() => { @@ -200,8 +201,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . title="Warning" data-test-subj="userImpactWarning" > - {' '} - The changes made will impact all users in the space.{' '} + The changes made will impact all users in the space.
) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index d6fd9a92d73ec..8a82cc1bcaa9e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -72,6 +72,9 @@ export const getTabs = ({ }: GetTabsProps): ViewSpaceTab[] => { const canUserViewRoles = Boolean(capabilities?.roles?.view); const canUserModifyRoles = Boolean(capabilities?.roles?.save); + const reloadWindow = () => { + window.location.reload(); + }; const tabsDefinition: ViewSpaceTab[] = [ { @@ -80,7 +83,13 @@ export const getTabs = ({ defaultMessage: 'General settings', }), content: ( - + ), }, ]; From 19a81bc3f8f8e79b794972ffea8ba3bf3ec0598c Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:14:54 +0000 Subject: [PATCH 097/129] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/spaces/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 67f6258b5c001..dbcb925f9cc5c 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/core-overlays-browser", "@kbn/core-notifications-browser", "@kbn/shared-ux-utility", + "@kbn/core-application-common", ], "exclude": [ "target/**/*", From ae18346f0e0a0cc1e5ef18aaa9c27d34010279bb Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Tue, 27 Aug 2024 09:53:07 -0700 Subject: [PATCH 098/129] [Spaces] Manage Space: features picker is shown only when solution is set to "classic" --- .../edit_space/manage_space_page.test.tsx | 45 +++++++++++++++++++ .../edit_space/manage_space_page.tsx | 10 +++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index 3b82be30c6026..f169395688de1 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -237,6 +237,51 @@ describe('ManageSpacePage', () => { expect(wrapper.find(EnabledFeatures)).toHaveLength(0); }); + it('hides feature visibility controls when solution view is not "classic"', async () => { + const spacesManager = spacesManagerMock.create(); + + const wrapper = mountWithIntl( + + ); + + await waitFor(async () => { + await Promise.resolve(); + + wrapper.update(); + + // default for create space: expect visible features table to exist + expect(wrapper.find(EnabledFeatures)).toHaveLength(1); + }); + + await waitFor(() => { + // switch to observability view + updateSpace(wrapper, false, 'oblt'); + // expect visible features table to not exist + expect(wrapper.find(EnabledFeatures)).toHaveLength(0); + }); + + await waitFor(() => { + // switch to classic + updateSpace(wrapper, false, 'classic'); + // expect visible features table to exist again + expect(wrapper.find(EnabledFeatures)).toHaveLength(1); + }); + }); + it('allows a space to be updated', async () => { const spaceToUpdate = { id: 'existing-space', diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index be8ebb1caebb0..cb81ae9304260 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -351,11 +351,13 @@ export class ManageSpacePage extends Component { }; private onSolutionViewChange = (space: Partial) => { - let showVisibleFeaturesPicker = false; - if (space.solution === 'classic' || space.solution == null) { - showVisibleFeaturesPicker = true; + 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, showVisibleFeaturesPicker })); this.onSpaceChange(space); }; From 2338e068b00f9a0d0c3199c6a4be63967ebcba1d Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 23 Jul 2024 16:13:21 +0200 Subject: [PATCH 099/129] create spaces assigned role table --- .../component/space_assigned_roles_table.tsx | 369 ++++++++++++++++++ .../view_space/view_space_roles.tsx | 127 ++---- .../management/view_space/view_space_tabs.tsx | 2 +- 3 files changed, 399 insertions(+), 99 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx new file mode 100644 index 0000000000000..fde66153a4982 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -0,0 +1,369 @@ +/* + * 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 { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiContextMenu, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiInMemoryTable, + EuiPopover, + EuiText, + EuiTextColor, +} from '@elastic/eui'; +import type { + CriteriaWithPagination, + EuiBasicTableColumn, + EuiInMemoryTableProps, + EuiSearchBarProps, + EuiTableFieldDataColumnType, + EuiTableSelectionType, +} from '@elastic/eui'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import type { Role } from '@kbn/security-plugin-types-common'; + +interface ISpaceAssignedRolesTableProps { + isReadOnly: boolean; + assignedRoles: Role[]; + onAssignNewRoleClick: () => Promise; +} + +export const isReservedRole = (role: Role) => { + return role.metadata?._reserved; +}; + +const getTableColumns = ({ isReadOnly }: Pick) => { + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.spaces.management.spaceDetails.rolesTable.column.name.title', { + defaultMessage: 'Role', + }), + }, + { + field: 'privileges', + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.privileges.title', + { + defaultMessage: 'Privileges', + } + ), + render: (_, record) => { + return record.kibana.map((kibanaPrivilege) => { + return kibanaPrivilege.base.join(', '); + }); + }, + }, + { + field: 'metadata', + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.title', + { + defaultMessage: 'Role type', + } + ), + render: (_value: Role['metadata']) => { + return React.createElement(EuiBadge, { + children: _value?._reserved + ? 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', + }); + }, + }, + ]; + + if (!isReadOnly) { + columns.push({ + name: 'Actions', + actions: [ + { + type: 'icon', + icon: 'lock', + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.actions.reservedIndictor.title', + { + defaultMessage: 'Reserved', + } + ), + isPrimary: true, + enabled: () => false, + available: (rowRecord) => isReservedRole(rowRecord), + onClick() { + return null; + }, + }, + { + type: 'icon', + icon: 'pencil', + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.actions.edit.title', + { + defaultMessage: 'Remove from space', + } + ), + isPrimary: true, + description: 'Click this action to edit the role privileges of this user for this space.', + showOnHover: true, + available: (rowRecord) => !isReservedRole(rowRecord), + onClick: () => { + window.alert('Not yet implemented.'); + }, + }, + { + isPrimary: true, + type: 'icon', + icon: 'trash', + color: 'danger', + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.actions.remove.title', + { + defaultMessage: 'Remove from space', + } + ), + description: 'Click this action to remove the user from this space.', + showOnHover: true, + available: (rowRecord) => !isReservedRole(rowRecord), + onClick: () => { + window.alert('Not yet implemented.'); + }, + }, + ], + }); + } + + return columns; +}; + +const getRowProps = (item: Role) => { + const { name } = item; + return { + 'data-test-subj': `space-role-row-${name}`, + onClick: () => {}, + }; +}; + +const getCellProps = (item: Role, column: EuiTableFieldDataColumnType) => { + const { name } = item; + const { field } = column; + return { + 'data-test-subj': `space-role-cell-${name}-${String(field)}`, + textOnly: true, + }; +}; + +export const SpaceAssignedRolesTable = ({ + isReadOnly, + assignedRoles, + onAssignNewRoleClick, +}: ISpaceAssignedRolesTableProps) => { + const tableColumns = useMemo(() => getTableColumns({ isReadOnly }), [isReadOnly]); + const [rolesInView, setRolesInView] = useState(assignedRoles); + const [selectedRoles, setSelectedRoles] = useState([]); + const [isBulkActionContextOpen, setBulkActionContextOpen] = useState(false); + const selectableRoles = useRef(rolesInView.filter((role) => !isReservedRole(role))); + const [pagination, setPagination] = useState['page']>({ + index: 0, + size: 10, + }); + + const onSearchQueryChange = useCallback>>( + ({ query }) => { + if (query?.text) { + setRolesInView( + assignedRoles.filter((role) => role.name.includes(query.text.toLowerCase())) + ); + } + }, + [assignedRoles] + ); + + const searchElementDefinition = useMemo(() => { + return { + box: { + incremental: true, + placeholder: i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.searchField.placeholder', + { defaultMessage: 'Search' } + ), + }, + onChange: onSearchQueryChange, + toolsRight: ( + <> + {!isReadOnly && ( + + + {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign', { + defaultMessage: 'Assign role', + })} + + + )} + + ), + }; + }, [isReadOnly, onAssignNewRoleClick, onSearchQueryChange]); + + const tableHeader = useMemo['childrenBetween']>(() => { + const pageSize = pagination.size; + + return ( + + + + + + + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.selectedStatusInfo', + { + defaultMessage: + 'Showing: {pageItemLength} of {rolesInViewCount} | Selected: {selectedCount} roles', + values: { + pageItemLength: + rolesInView.length < pageSize ? rolesInView.length : pageSize, + rolesInViewCount: rolesInView.length, + selectedCount: selectedRoles.length, + }, + } + )} + + + + + + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.bulkActions.contextMenuOpener', + { + defaultMessage: 'Bulk actions', + } + )} + + } + isOpen={isBulkActionContextOpen} + closePopover={setBulkActionContextOpen.bind(null, false)} + anchorPosition="downCenter" + > + , + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.bulkActions.editPrivilege', + { + defaultMessage: 'Edit privileges', + } + ), + onClick: () => {}, + }, + { + icon: , + name: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.bulkActions.remove', + { + defaultMessage: 'Remove from space', + } + ), + onClick: () => {}, + }, + ], + }, + ]} + /> + + + + + {i18n.translate('xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', { + defaultMessage: 'Select all {selectableRolesCount} roles', + values: { + selectableRolesCount: selectableRoles.current.length, + }, + })} + + + + + + + + + ); + }, [isBulkActionContextOpen, pagination, rolesInView, selectedRoles]); + + const onTableChange = ({ page }: CriteriaWithPagination) => { + setPagination(page); + }; + + const onSelectionChange = (selection: Role[]) => { + setSelectedRoles(selection); + }; + + const selection: EuiTableSelectionType = { + selected: selectedRoles, + selectable: (role: Role) => !isReservedRole(role), + selectableMessage: (selectable: boolean, role: Role) => + !selectable ? `${role.name} is reserved` : `Select ${role.name}`, + onSelectionChange, + }; + + return ( + + + + search={searchElementDefinition} + childrenBetween={tableHeader} + itemId="name" + columns={tableColumns} + items={rolesInView} + rowProps={getRowProps} + cellProps={getCellProps} + selection={selection} + pagination={{ + pageSize: pagination.size, + pageIndex: pagination.index, + pageSizeOptions: [50, 25, 10, 0], + }} + onChange={onTableChange} + /> + + + ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 51a8fa17ac003..ad271e754948c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -6,7 +6,6 @@ */ import { - EuiBasicTable, EuiButton, EuiButtonEmpty, EuiButtonGroup, @@ -19,15 +18,12 @@ import { EuiFlyoutHeader, EuiForm, EuiFormRow, + EuiLink, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; -import type { - EuiBasicTableColumn, - EuiComboBoxOptionOption, - EuiTableFieldDataColumnType, -} from '@elastic/eui'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import type { FC } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; @@ -36,6 +32,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { Role } from '@kbn/security-plugin-types-common'; +import { SpaceAssignedRolesTable } from './component/space_assigned_roles_table'; import { useViewSpaceServices, type ViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; import { FeatureTable } from '../edit_space/enabled_features/feature_table'; @@ -73,7 +70,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe const rolesAPIClient = useRef(); - const { getRolesAPIClient } = useViewSpaceServices(); + const { getRolesAPIClient, getUrlForApp } = useViewSpaceServices(); const resolveRolesAPIClient = useCallback(async () => { try { @@ -100,63 +97,6 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe } }, [roleAPIClientInitialized]); - const getRowProps = (item: Role) => { - const { name } = item; - return { - 'data-test-subj': `space-role-row-${name}`, - onClick: () => {}, - }; - }; - - const getCellProps = (item: Role, column: EuiTableFieldDataColumnType) => { - const { name } = item; - const { field } = column; - return { - 'data-test-subj': `space-role-cell-${name}-${String(field)}`, - textOnly: true, - }; - }; - - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.spaces.management.spaceDetails.roles.column.name.title', { - defaultMessage: 'Role', - }), - }, - { - field: 'privileges', - name: i18n.translate('xpack.spaces.management.spaceDetails.roles.column.privileges.title', { - defaultMessage: 'Privileges', - }), - render: (_value, record) => { - return record.kibana.map((kibanaPrivilege) => { - return kibanaPrivilege.base.join(', '); - }); - }, - }, - ]; - - if (!isReadOnly) { - columns.push({ - name: 'Actions', - actions: [ - { - name: i18n.translate( - 'xpack.spaces.management.spaceDetails.roles.column.actions.remove.title', - { - defaultMessage: 'Remove from space', - } - ), - description: 'Click this action to remove the role privileges from this space.', - onClick: () => { - window.alert('Not yet implemented.'); - }, - }, - ], - }); - } - const rolesInUse = filterRolesAssignedToSpace(roles, space); if (!rolesInUse) { @@ -182,42 +122,33 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe )} - - - -

- {i18n.translate('xpack.spaces.management.spaceDetails.roles.heading', { - defaultMessage: - 'Roles that can access this space. Privileges are managed at the role level.', - })} -

-
-
- {!isReadOnly && ( - - { - if (!roleAPIClientInitialized) { - await resolveRolesAPIClient(); - } - setShowRolesPrivilegeEditor(true); - }} - > - {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign', { - defaultMessage: 'Assign role', - })} - - - )} -
+ + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.rolesPageAnchorText', + { defaultMessage: 'Roles' } + )} + + ), + }} + /> +
- { + if (!roleAPIClientInitialized) { + await resolveRolesAPIClient(); + } + setShowRolesPrivilegeEditor(true); + }} />
diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 8a82cc1bcaa9e..bd17d2e68abad 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -98,7 +98,7 @@ export const getTabs = ({ tabsDefinition.push({ id: TAB_ID_ROLES, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.roles.heading', { - defaultMessage: 'Assigned roles', + defaultMessage: 'Roles', }), append: ( From 8872a5dd9dbefac779ba401393294be4222c8180 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Fri, 26 Jul 2024 15:47:35 +0200 Subject: [PATCH 100/129] extend definition of roles that can be edited --- .../component/space_assigned_roles_table.tsx | 63 ++++++++++++------- .../public/management/view_space/utils.ts | 14 +++++ .../view_space/view_space_roles.tsx | 23 ++----- .../management/view_space/view_space_tabs.tsx | 7 ++- 4 files changed, 62 insertions(+), 45 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/utils.ts diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index fde66153a4982..72711ee081fb8 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -38,8 +38,17 @@ interface ISpaceAssignedRolesTableProps { onAssignNewRoleClick: () => Promise; } -export const isReservedRole = (role: Role) => { - return role.metadata?._reserved; +/** + * @description checks if the passed role qualifies as one that can + * be edited by a user with sufficient permissions + */ +export const isEditableRole = (role: Role) => { + return !( + role.metadata?._reserved || + role.kibana.reduce((acc, cur) => { + return cur.spaces.includes('*') || acc; + }, false) + ); }; const getTableColumns = ({ isReadOnly }: Pick) => { @@ -100,6 +109,8 @@ const getTableColumns = ({ isReadOnly }: Pick false, - available: (rowRecord) => isReservedRole(rowRecord), - onClick() { - return null; - }, + available: (rowRecord) => !isEditableRole(rowRecord), }, { type: 'icon', @@ -125,7 +133,7 @@ const getTableColumns = ({ isReadOnly }: Pick !isReservedRole(rowRecord), + available: (rowRecord) => isEditableRole(rowRecord), onClick: () => { window.alert('Not yet implemented.'); }, @@ -143,8 +151,8 @@ const getTableColumns = ({ isReadOnly }: Pick !isReservedRole(rowRecord), - onClick: () => { + available: (rowRecord) => isEditableRole(rowRecord), + onClick: (rowRecord, event) => { window.alert('Not yet implemented.'); }, }, @@ -181,7 +189,7 @@ export const SpaceAssignedRolesTable = ({ const [rolesInView, setRolesInView] = useState(assignedRoles); const [selectedRoles, setSelectedRoles] = useState([]); const [isBulkActionContextOpen, setBulkActionContextOpen] = useState(false); - const selectableRoles = useRef(rolesInView.filter((role) => !isReservedRole(role))); + const selectableRoles = useRef(rolesInView.filter((role) => isEditableRole(role))); const [pagination, setPagination] = useState['page']>({ index: 0, size: 10, @@ -193,6 +201,8 @@ export const SpaceAssignedRolesTable = ({ setRolesInView( assignedRoles.filter((role) => role.name.includes(query.text.toLowerCase())) ); + } else { + setRolesInView(assignedRoles); } }, [assignedRoles] @@ -306,19 +316,24 @@ export const SpaceAssignedRolesTable = ({ /> - - - {i18n.translate('xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', { - defaultMessage: 'Select all {selectableRolesCount} roles', - values: { - selectableRolesCount: selectableRoles.current.length, - }, - })} - - + {Boolean(selectableRoles.current.length) && ( + + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', + { + defaultMessage: 'Select all {selectableRolesCount} roles', + values: { + selectableRolesCount: selectableRoles.current.length, + }, + } + )} + + + )} @@ -338,7 +353,7 @@ export const SpaceAssignedRolesTable = ({ const selection: EuiTableSelectionType = { selected: selectedRoles, - selectable: (role: Role) => !isReservedRole(role), + selectable: (role: Role) => isEditableRole(role), selectableMessage: (selectable: boolean, role: Role) => !selectable ? `${role.name} is reserved` : `Select ${role.name}`, onSelectionChange, diff --git a/x-pack/plugins/spaces/public/management/view_space/utils.ts b/x-pack/plugins/spaces/public/management/view_space/utils.ts new file mode 100644 index 0000000000000..2b10933688534 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/utils.ts @@ -0,0 +1,14 @@ +import type { Role } from '@kbn/security-plugin-types-common'; +import type { Space } from '../../../common'; + +export const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { + return roles.filter((role) => + role.kibana.reduce((acc, cur) => { + return ( + (cur.spaces.includes(space.name) || cur.spaces.includes('*')) && + Boolean(cur.base.length) && + acc + ); + }, true) + ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index ad271e754948c..586185fdcfbf3 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -45,23 +45,14 @@ type KibanaPrivilegeBase = keyof NonNullable; interface Props { space: Space; + /** + * List of roles assigned to this space + */ roles: Role[]; features: KibanaFeature[]; isReadOnly: boolean; } -const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { - return roles.filter((role) => - role.kibana.reduce((acc, cur) => { - return ( - (cur.spaces.includes(space.name) || cur.spaces.includes('*')) && - Boolean(cur.base.length) && - acc - ); - }, true) - ); -}; - // FIXME: rename to EditSpaceAssignedRoles export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); @@ -97,12 +88,6 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe } }, [roleAPIClientInitialized]); - const rolesInUse = filterRolesAssignedToSpace(roles, space); - - if (!rolesInUse) { - return null; - } - return ( <> {showRolesPrivilegeEditor && ( @@ -142,7 +127,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe { if (!roleAPIClientInitialized) { await resolveRolesAPIClient(); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index bd17d2e68abad..1ba963a17bb73 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -15,6 +15,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; +import { filterRolesAssignedToSpace } from './utils'; import type { Space } from '../../../common'; // FIXME: rename to EditSpaceTab @@ -95,6 +96,8 @@ export const getTabs = ({ ]; if (canUserViewRoles) { + const rolesAssignedToSpace = filterRolesAssignedToSpace(roles, space); + tabsDefinition.push({ id: TAB_ID_ROLES, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.roles.heading', { @@ -102,13 +105,13 @@ export const getTabs = ({ }), append: ( - {roles.length} + {rolesAssignedToSpace.length} ), content: ( From bb69ad5d018ceef5fe703ca44b9bb0a3510c73aa Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Fri, 26 Jul 2024 16:03:10 +0200 Subject: [PATCH 101/129] fix pluralization in select all button --- .../view_space/component/space_assigned_roles_table.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index 72711ee081fb8..4e1353f5bbade 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -325,9 +325,10 @@ export const SpaceAssignedRolesTable = ({ {i18n.translate( 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', { - defaultMessage: 'Select all {selectableRolesCount} roles', + defaultMessage: + 'Select {count, plural, one {role} other {all {count} roles}}', values: { - selectableRolesCount: selectableRoles.current.length, + count: selectableRoles.current.length, }, } )} From 73a0dd133d447640922f2a898a5e2debdb24a1ab Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Fri, 26 Jul 2024 18:08:54 +0200 Subject: [PATCH 102/129] slight adjustments rendered items count --- .../view_space/component/space_assigned_roles_table.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index 4e1353f5bbade..3c139baa85ee8 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -236,6 +236,7 @@ export const SpaceAssignedRolesTable = ({ const tableHeader = useMemo['childrenBetween']>(() => { const pageSize = pagination.size; + const pageIndex = pagination.index; return ( @@ -251,8 +252,11 @@ export const SpaceAssignedRolesTable = ({ defaultMessage: 'Showing: {pageItemLength} of {rolesInViewCount} | Selected: {selectedCount} roles', values: { - pageItemLength: - rolesInView.length < pageSize ? rolesInView.length : pageSize, + pageItemLength: Math.floor( + rolesInView.length / (pageSize * (pageIndex + 1)) + ) + ? pageSize * (pageIndex + 1) + : rolesInView.length % pageSize, rolesInViewCount: rolesInView.length, selectedCount: selectedRoles.length, }, From 88a2c2df731a0cf62182ea9736b2e080a3aae012 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Wed, 31 Jul 2024 16:58:41 +0200 Subject: [PATCH 103/129] fix text, and hide assign to space button when there are roles to assign --- .../component/space_assigned_roles_table.tsx | 21 +++++++++++++-- .../hooks/view_space_context_provider.tsx | 2 +- .../view_space/view_space_roles.tsx | 27 +++++++++++++------ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index 3c139baa85ee8..ee24792b1d43b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -117,6 +117,12 @@ const getTableColumns = ({ isReadOnly }: Pick false, available: (rowRecord) => !isEditableRole(rowRecord), @@ -131,7 +137,13 @@ const getTableColumns = ({ isReadOnly }: Pick isEditableRole(rowRecord), onClick: () => { @@ -149,7 +161,12 @@ const getTableColumns = ({ isReadOnly }: Pick isEditableRole(rowRecord), onClick: (rowRecord, event) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index ee0bfbf1014aa..3e05d122d7de8 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -44,7 +44,7 @@ export const useViewSpaceServices = (): ViewSpaceServices => { const context = useContext(ViewSpaceContext); if (!context) { throw new Error( - 'ViewSpace Context is mising. Ensure the component or React root is wrapped with ViewSpaceContext' + 'ViewSpace Context is missing. Ensure the component or React root is wrapped with ViewSpaceContext' ); } diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 586185fdcfbf3..5d00b54bd521c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -57,7 +57,7 @@ interface Props { export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); - const [systemRoles, setSystemRoles] = useState([]); + const [spaceUnallocatedRole, setSpaceUnallocatedRole] = useState([]); const rolesAPIClient = useRef(); @@ -80,13 +80,24 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe useEffect(() => { async function fetchAllSystemRoles() { - setSystemRoles((await rolesAPIClient.current?.getRoles()) ?? []); + const systemRoles = (await rolesAPIClient.current?.getRoles()) ?? []; + + // exclude roles that are already assigned to this space + const spaceUnallocatedRoles = systemRoles.filter( + (role) => + !role.metadata?._reserved && + role.kibana.some((privileges) => { + return !privileges.spaces.includes(space.id) || !privileges.spaces.includes('*'); + }) + ); + + setSpaceUnallocatedRole(spaceUnallocatedRoles); } if (roleAPIClientInitialized) { fetchAllSystemRoles?.(); } - }, [roleAPIClientInitialized]); + }, [roleAPIClientInitialized, space.id]); return ( <> @@ -100,7 +111,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe onSaveClick={() => { setShowRolesPrivilegeEditor(false); }} - systemRoles={systemRoles} + spaceUnallocatedRole={spaceUnallocatedRole} // rolesAPIClient would have been initialized before the privilege editor is displayed roleAPIClient={rolesAPIClient.current!} /> @@ -126,7 +137,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe { if (!roleAPIClientInitialized) { @@ -144,7 +155,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe interface PrivilegesRolesFormProps extends Omit { closeFlyout: () => void; onSaveClick: () => void; - systemRoles: Role[]; + spaceUnallocatedRole: Role[]; roleAPIClient: RolesAPIClient; } @@ -155,7 +166,7 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { - const { onSaveClick, closeFlyout, features, roleAPIClient, systemRoles } = props; + const { onSaveClick, closeFlyout, features, roleAPIClient, spaceUnallocatedRole } = props; const [space, setSpaceState] = useState>(props.space); const [spacePrivilege, setSpacePrivilege] = useState('all'); @@ -192,7 +203,7 @@ export const PrivilegesRolesForm: FC = (props) => { values: { spaceName: space.name }, })} placeholder="Select roles" - options={createRolesComboBoxOptions(systemRoles)} + options={createRolesComboBoxOptions(spaceUnallocatedRole)} selectedOptions={selectedRoles} onChange={(value) => { setSelectedRoles((prevRoles) => { From 4b76e01daddddc55091e1dba3a5c0a29f947e758 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 1 Aug 2024 13:25:40 +0200 Subject: [PATCH 104/129] integrate security packages --- .../plugin_types_public/src/privileges/privileges_api_client.ts | 2 +- .../src/kibana_privilege_table/feature_table.test.tsx | 2 +- .../kibana_privilege_table/feature_table_expanded_row.test.tsx | 2 +- .../src/kibana_privilege_table/sub_feature_form.tsx | 1 - .../privilege_form_calculator/privilege_form_calculator.test.ts | 2 +- .../plugins/spaces/public/management/spaces_management_app.tsx | 2 ++ 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/x-pack/packages/security/plugin_types_public/src/privileges/privileges_api_client.ts b/x-pack/packages/security/plugin_types_public/src/privileges/privileges_api_client.ts index e3a97398db7a3..25d768cb7b1ac 100644 --- a/x-pack/packages/security/plugin_types_public/src/privileges/privileges_api_client.ts +++ b/x-pack/packages/security/plugin_types_public/src/privileges/privileges_api_client.ts @@ -15,7 +15,7 @@ export interface PrivilegesAPIClientGetAllArgs { */ respectLicenseLevel: boolean; } -// TODO: Eyo include the proper return types for contract + export abstract class PrivilegesAPIClientPublicContract { abstract getAll(args: PrivilegesAPIClientGetAllArgs): Promise; } diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx index 83a0da2e26815..2380088dd713f 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx @@ -15,10 +15,10 @@ import { kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; +import type { Role } from '@kbn/security-plugin-types-common'; import { getDisplayedFeaturePrivileges } from './__fixtures__'; import { FeatureTable } from './feature_table'; -import type { Role } from '@kbn/security-plugin-types-common'; import { PrivilegeFormCalculator } from '../privilege_form_calculator'; const createRole = (kibana: Role['kibana'] = []): Role => { diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx index 3b787f01cdf92..5e4f4ce021d44 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx @@ -12,10 +12,10 @@ import { createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; +import type { Role } from '@kbn/security-plugin-types-common'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; import { FeatureTableExpandedRow } from './feature_table_expanded_row'; -import type { Role } from '@kbn/security-plugin-types-common'; import { PrivilegeFormCalculator } from '../privilege_form_calculator'; const createRole = (kibana: Role['kibana'] = []): Role => { diff --git a/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx b/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx index 9155d8ae52835..2797e4d64a35e 100644 --- a/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx +++ b/x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx @@ -21,7 +21,6 @@ import type { SubFeaturePrivilege, SubFeaturePrivilegeGroup, } from '@kbn/security-role-management-model'; - import { NO_PRIVILEGE_VALUE } from '../constants'; import type { PrivilegeFormCalculator } from '../privilege_form_calculator'; diff --git a/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts b/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts index 0281605f00f34..e61134b816ffa 100644 --- a/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts +++ b/x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts @@ -9,9 +9,9 @@ import { createKibanaPrivileges, kibanaFeatures, } from '@kbn/security-role-management-model/src/__fixtures__'; +import type { Role } from '@kbn/security-plugin-types-common'; import { PrivilegeFormCalculator } from './privilege_form_calculator'; -import type { Role } from '@kbn/security-plugin-types-common'; const createRole = (kibana: Role['kibana'] = []): Role => { return { diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 037453e1a215f..4644cf1f07212 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -44,6 +44,7 @@ export const spacesManagementApp = Object.freeze({ config, eventTracker, getRolesAPIClient, + getPrivilegesAPIClient, }: CreateParams) { const title = i18n.translate('xpack.spaces.displayName', { defaultMessage: 'Spaces', @@ -155,6 +156,7 @@ export const spacesManagementApp = Object.freeze({ getRolesAPIClient={getRolesAPIClient} allowFeatureVisibility={config.allowFeatureVisibility} allowSolutionVisibility={config.allowSolutionVisibility} + getPrivilegesAPIClient={getPrivilegesAPIClient} /> ); }; From 9cbf1ece26608b75732a092a8185656ed41690c1 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Fri, 2 Aug 2024 18:26:12 +0200 Subject: [PATCH 105/129] add tests for provisioning privilege API client --- .../management/privilege_api_client.mock.ts | 18 ++++++++++++++++++ .../public/management/roles_api_client.mock.ts | 1 + 2 files changed, 19 insertions(+) create mode 100644 x-pack/plugins/spaces/public/management/privilege_api_client.mock.ts diff --git a/x-pack/plugins/spaces/public/management/privilege_api_client.mock.ts b/x-pack/plugins/spaces/public/management/privilege_api_client.mock.ts new file mode 100644 index 0000000000000..a8351e2d88ad5 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/privilege_api_client.mock.ts @@ -0,0 +1,18 @@ +/* + * 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 type { PrivilegesAPIClientPublicContract } from '@kbn/security-plugin-types-public'; + +export const createPrivilegeAPIClientMock = (): PrivilegesAPIClientPublicContract => { + return { + getAll: jest.fn(), + }; +}; + +export const getPrivilegeAPIClientMock = jest + .fn() + .mockResolvedValue(createPrivilegeAPIClientMock()); diff --git a/x-pack/plugins/spaces/public/management/roles_api_client.mock.ts b/x-pack/plugins/spaces/public/management/roles_api_client.mock.ts index dd996814f9e51..66a356b3fdb75 100644 --- a/x-pack/plugins/spaces/public/management/roles_api_client.mock.ts +++ b/x-pack/plugins/spaces/public/management/roles_api_client.mock.ts @@ -13,6 +13,7 @@ export const createRolesAPIClientMock = (): RolesAPIClient => { getRole: jest.fn(), saveRole: jest.fn(), deleteRole: jest.fn(), + bulkUpdateRoles: jest.fn(), }; }; From 8f2f38d91630ce63b496c0d18ef05ae71ec69e6f Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 12 Aug 2024 13:14:27 +0200 Subject: [PATCH 106/129] even more UI improvements --- .../component/space_assigned_roles_table.tsx | 89 +++++++++++++------ .../public/management/view_space/utils.ts | 8 ++ .../view_space/view_space_roles.tsx | 40 ++++++++- .../management/view_space/view_space_tabs.tsx | 8 +- 4 files changed, 113 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index ee24792b1d43b..4bd9692c96feb 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -36,6 +36,8 @@ interface ISpaceAssignedRolesTableProps { isReadOnly: boolean; assignedRoles: Role[]; onAssignNewRoleClick: () => Promise; + onClickBulkEdit: (selectedRoles: Role[]) => Promise; + onClickBulkRemove: (selectedRoles: Role[]) => Promise; } /** @@ -69,6 +71,14 @@ const getTableColumns = ({ isReadOnly }: Pick { return record.kibana.map((kibanaPrivilege) => { + if (!kibanaPrivilege.base.length) { + return i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.privileges.customPrivilege', + { + defaultMessage: 'custom', + } + ); + } return kibanaPrivilege.base.join(', '); }); }, @@ -201,6 +211,8 @@ export const SpaceAssignedRolesTable = ({ isReadOnly, assignedRoles, onAssignNewRoleClick, + onClickBulkEdit, + onClickBulkRemove, }: ISpaceAssignedRolesTableProps) => { const tableColumns = useMemo(() => getTableColumns({ isReadOnly }), [isReadOnly]); const [rolesInView, setRolesInView] = useState(assignedRoles); @@ -319,17 +331,23 @@ export const SpaceAssignedRolesTable = ({ defaultMessage: 'Edit privileges', } ), - onClick: () => {}, + onClick: async () => { + await onClickBulkEdit(selectedRoles); + }, }, { icon: , - name: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.bulkActions.remove', - { - defaultMessage: 'Remove from space', - } + name: ( + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.bulkActions.remove', + { defaultMessage: 'Remove from space' } + )} + ), - onClick: () => {}, + onClick: async () => { + await onClickBulkRemove(selectedRoles); + }, }, ], }, @@ -337,25 +355,36 @@ export const SpaceAssignedRolesTable = ({ /> - {Boolean(selectableRoles.current.length) && ( - - - {i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', - { - defaultMessage: - 'Select {count, plural, one {role} other {all {count} roles}}', - values: { - count: selectableRoles.current.length, - }, + + {React.createElement(EuiButtonEmpty, { + size: 's', + ...(Boolean(selectedRoles.length) + ? { + iconType: 'crossInCircle', + onClick: setSelectedRoles.bind(null, []), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', + { + defaultMessage: 'Clear selection', + } + ), } - )} - - - )} + : { + iconType: 'pagesSelect', + onClick: setSelectedRoles.bind(null, selectableRoles.current), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', + { + defaultMessage: + 'Select {count, plural, one {role} other {all {count} roles}}', + values: { + count: selectableRoles.current.length, + }, + } + ), + }), + })} + @@ -363,7 +392,15 @@ export const SpaceAssignedRolesTable = ({ ); - }, [isBulkActionContextOpen, pagination, rolesInView, selectedRoles]); + }, [ + isBulkActionContextOpen, + onClickBulkEdit, + onClickBulkRemove, + pagination.index, + pagination.size, + rolesInView.length, + selectedRoles, + ]); const onTableChange = ({ page }: CriteriaWithPagination) => { setPagination(page); diff --git a/x-pack/plugins/spaces/public/management/view_space/utils.ts b/x-pack/plugins/spaces/public/management/view_space/utils.ts index 2b10933688534..2492c8b081df9 100644 --- a/x-pack/plugins/spaces/public/management/view_space/utils.ts +++ b/x-pack/plugins/spaces/public/management/view_space/utils.ts @@ -1,4 +1,12 @@ +/* + * 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 type { Role } from '@kbn/security-plugin-types-common'; + import type { Space } from '../../../common'; export const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 5d00b54bd521c..f44ffdd339b3c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -9,6 +9,7 @@ import { EuiButton, EuiButtonEmpty, EuiButtonGroup, + EuiCallOut, EuiComboBox, EuiFlexGroup, EuiFlexItem, @@ -25,7 +26,7 @@ import { } from '@elastic/eui'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import type { FC } from 'react'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { KibanaFeature, KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; @@ -173,6 +174,12 @@ export const PrivilegesRolesForm: FC = (props) => { const [selectedRoles, setSelectedRoles] = useState>( [] ); + const selectedRolesHasPrivilegeConflict = useMemo(() => { + return selectedRoles.reduce((result, selectedRole) => { + // TODO: determine heuristics for role privilege conflicts + return result; + }, false); + }, [selectedRoles]); const [assigningToRole, setAssigningToRole] = useState(false); @@ -231,6 +238,30 @@ export const PrivilegesRolesForm: FC = (props) => { fullWidth /> + <> + {!selectedRolesHasPrivilegeConflict && ( + + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.assign.privilegeConflictMsg.description', + { + defaultMessage: + 'Updating the settings here in a bulk will override current individual settings.', + } + )} + + + )} + = (props) => { -

Assign role to {space.name}

+

+ {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign.privileges.custom', { + defaultMessage: 'Assign role to {spaceName}', + values: { spaceName: space.name }, + })} +

diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 1ba963a17bb73..8210ab2d7a1cc 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -15,7 +15,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; -import { filterRolesAssignedToSpace } from './utils'; +// import { filterRolesAssignedToSpace } from './utils'; import type { Space } from '../../../common'; // FIXME: rename to EditSpaceTab @@ -96,7 +96,7 @@ export const getTabs = ({ ]; if (canUserViewRoles) { - const rolesAssignedToSpace = filterRolesAssignedToSpace(roles, space); + // const rolesAssignedToSpace = filterRolesAssignedToSpace(roles, space); tabsDefinition.push({ id: TAB_ID_ROLES, @@ -105,13 +105,13 @@ export const getTabs = ({ }), append: ( - {rolesAssignedToSpace.length} + {roles.length} ), content: ( From 1bc65288d1ddcfdebd102526355fef0e88c8fdb3 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 13 Aug 2024 12:17:23 +0200 Subject: [PATCH 107/129] refactor trigger for flyout and integrate it with bulk actions --- .../management/spaces_management_app.tsx | 4 +- .../space_assign_role_privilege_form.tsx | 342 ++++++++++++++++ .../component/space_assigned_roles_table.tsx | 19 +- .../hooks/view_space_context_provider.tsx | 16 +- .../view_space/view_space_roles.tsx | 366 ++++-------------- 5 files changed, 431 insertions(+), 316 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 4644cf1f07212..49663dcf9191b 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -72,7 +72,7 @@ export const spacesManagementApp = Object.freeze({ text: title, href: `/`, }; - const { notifications, application, chrome, http, overlays } = coreStart; + const { notifications, application, chrome, http, overlays, theme } = coreStart; chrome.docTitle.change(title); @@ -148,6 +148,8 @@ export const spacesManagementApp = Object.freeze({ http={http} overlays={overlays} notifications={notifications} + theme={theme} + i18n={coreStart.i18n} spacesManager={spacesManager} spaceId={spaceId} onLoadSpace={onLoadSpace} diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx new file mode 100644 index 0000000000000..f7728d4d9b8e8 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx @@ -0,0 +1,342 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiButtonGroup, + EuiCallOut, + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { FC } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import type { KibanaFeature, KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { Role } from '@kbn/security-plugin-types-common'; +import { KibanaPrivileges, type RawKibanaPrivileges } from '@kbn/security-role-management-model'; +import { KibanaPrivilegeTable, PrivilegeFormCalculator } from '@kbn/security-ui-components'; + +import type { Space } from '../../../../common'; +import type { ViewSpaceServices } from '../hooks/view_space_context_provider'; + +export type RolesAPIClient = ReturnType extends Promise< + infer R +> + ? R + : never; + +export type PrivilegesAPIClient = ReturnType< + ViewSpaceServices['getPrivilegesAPIClient'] +> extends Promise + ? R + : never; + +type KibanaRolePrivilege = keyof NonNullable | 'custom'; + +interface PrivilegesRolesFormProps { + space: Space; + features: KibanaFeature[]; + closeFlyout: () => void; + onSaveClick: () => void; + roleAPIClient: RolesAPIClient; + privilegesAPIClient: PrivilegesAPIClient; + spaceUnallocatedRole: Role[]; + defaultSelected?: Role[]; +} + +const createRolesComboBoxOptions = (roles: Role[]): Array> => + roles.map((role) => ({ + label: role.name, + value: role, + })); + +export const PrivilegesRolesForm: FC = (props) => { + const { + onSaveClick, + closeFlyout, + features, + roleAPIClient, + defaultSelected = [], + privilegesAPIClient, + spaceUnallocatedRole, + } = props; + + const [space, setSpaceState] = useState>(props.space); + const [roleSpacePrivilege, setRoleSpacePrivilege] = useState('all'); + const [selectedRoles, setSelectedRoles] = useState>( + createRolesComboBoxOptions(defaultSelected) + ); + const [privileges, setPrivileges] = useState<[RawKibanaPrivileges] | null>(null); + + const selectedRolesHasPrivilegeConflict = useMemo(() => { + let privilegeAnchor: string; + + return selectedRoles.reduce((result, selectedRole) => { + let rolePrivilege: string; + + selectedRole.value?.kibana.forEach(({ spaces, base }) => { + // TODO: consider wildcard situations + if (spaces.includes(space.id!) && base.length) { + rolePrivilege = base[0]; + } + + if (!privilegeAnchor) { + setRoleSpacePrivilege((privilegeAnchor = rolePrivilege)); + } + }); + + return result || privilegeAnchor !== rolePrivilege; + }, false); + }, [selectedRoles, space.id]); + + useEffect(() => { + Promise.all([ + privilegesAPIClient.getAll({ includeActions: true, respectLicenseLevel: false }), + privilegesAPIClient.getBuiltIn(), + ]).then( + ([kibanaPrivileges, builtInESPrivileges]) => + setPrivileges([kibanaPrivileges, builtInESPrivileges]) + // (err) => fatalErrors.add(err) + ); + }, [privilegesAPIClient]); + + const [assigningToRole, setAssigningToRole] = useState(false); + + const assignRolesToSpace = useCallback(async () => { + try { + setAssigningToRole(true); + + await Promise.all( + selectedRoles.map((selectedRole) => { + roleAPIClient.saveRole({ role: selectedRole.value! }); + }) + ).then(setAssigningToRole.bind(null, false)); + + onSaveClick(); + } catch { + // Handle resulting error + } + }, [onSaveClick, roleAPIClient, selectedRoles]); + + const getForm = () => { + return ( + + + { + setSelectedRoles((prevRoles) => { + if (prevRoles.length < value.length) { + const newlyAdded = value[value.length - 1]; + + const { id: spaceId } = space; + if (!spaceId) { + throw new Error('space state requires space to have an ID'); + } + + // Add kibana space privilege definition to role + newlyAdded.value!.kibana.push({ + base: roleSpacePrivilege === 'custom' ? [] : [roleSpacePrivilege], + feature: {}, + spaces: [spaceId], + }); + + return prevRoles.concat(newlyAdded); + } else { + return value; + } + }); + }} + fullWidth + /> + + <> + {selectedRolesHasPrivilegeConflict && ( + + + {i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.assign.privilegeConflictMsg.description', + { + defaultMessage: + 'Updating the settings here in a bulk will override current individual settings.', + } + )} + + + )} + + + ({ + ...privilege, + 'data-test-subj': `${privilege.id}-privilege-button`, + }))} + color="primary" + idSelected={roleSpacePrivilege} + onChange={(id) => setRoleSpacePrivilege(id as KibanaRolePrivilege)} + buttonSize="compressed" + isFullWidth + /> + + {roleSpacePrivilege === 'custom' && ( + + <> + +

+ +

+
+ + {/** TODO: rework privilege table to accommodate operating on multiple roles */} + + +
+ )} +
+ ); + }; + + const getSaveButton = () => { + return ( + assignRolesToSpace()} + data-test-subj={'createRolesPrivilegeButton'} + > + {i18n.translate('xpack.spaces.management.spaceDetails.roles.assignRoleButton', { + defaultMessage: 'Assign roles', + })} + + ); + }; + + return ( + + + +

+ {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign.privileges.custom', { + defaultMessage: 'Assign role to {spaceName}', + values: { spaceName: space.name }, + })} +

+
+ + +

+ +

+
+
+ {getForm()} + + + + + {i18n.translate('xpack.spaces.management.spaceDetails.roles.cancelRoleButton', { + defaultMessage: 'Cancel', + })} + + + {getSaveButton()} + + +
+ ); +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index 4bd9692c96feb..f1bf4da4c6b17 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -36,8 +36,8 @@ interface ISpaceAssignedRolesTableProps { isReadOnly: boolean; assignedRoles: Role[]; onAssignNewRoleClick: () => Promise; - onClickBulkEdit: (selectedRoles: Role[]) => Promise; - onClickBulkRemove: (selectedRoles: Role[]) => Promise; + onClickBulkEdit: (selectedRoles: Role[]) => void; + onClickBulkRemove: (selectedRoles: Role[]) => void; } /** @@ -268,7 +268,7 @@ export const SpaceAssignedRolesTable = ({ const pageIndex = pagination.index; return ( - + @@ -297,12 +297,16 @@ export const SpaceAssignedRolesTable = ({ {i18n.translate( 'xpack.spaces.management.spaceDetails.rolesTable.bulkActions.contextMenuOpener', @@ -312,9 +316,6 @@ export const SpaceAssignedRolesTable = ({ )} } - isOpen={isBulkActionContextOpen} - closePopover={setBulkActionContextOpen.bind(null, false)} - anchorPosition="downCenter" > { await onClickBulkEdit(selectedRoles); + setBulkActionContextOpen(false); }, }, { @@ -347,6 +349,7 @@ export const SpaceAssignedRolesTable = ({ ), onClick: async () => { await onClickBulkRemove(selectedRoles); + setBulkActionContextOpen(false); }, }, ], diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx index 3e05d122d7de8..d5476966cf6dd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx @@ -9,24 +9,24 @@ import type { FC, PropsWithChildren } from 'react'; import React, { createContext, useContext } from 'react'; import type { ApplicationStart } from '@kbn/core-application-browser'; -import type { HttpStart } from '@kbn/core-http-browser'; -import type { NotificationsStart } from '@kbn/core-notifications-browser'; -import type { OverlayStart } from '@kbn/core-overlays-browser'; -import type { RolesAPIClient } from '@kbn/security-plugin-types-public'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { + PrivilegesAPIClientPublicContract, + RolesAPIClient, +} from '@kbn/security-plugin-types-public'; import type { SpacesManager } from '../../../spaces_manager'; // FIXME: rename to EditSpaceServices -export interface ViewSpaceServices { +export interface ViewSpaceServices + extends Pick { capabilities: ApplicationStart['capabilities']; getUrlForApp: ApplicationStart['getUrlForApp']; navigateToUrl: ApplicationStart['navigateToUrl']; serverBasePath: string; spacesManager: SpacesManager; getRolesAPIClient: () => Promise; - http: HttpStart; - overlays: OverlayStart; - notifications: NotificationsStart; + getPrivilegesAPIClient: () => Promise; } const ViewSpaceContext = createContext(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index f44ffdd339b3c..9a11be03fa96e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -5,44 +5,24 @@ * 2.0. */ -import { - EuiButton, - EuiButtonEmpty, - EuiButtonGroup, - EuiCallOut, - EuiComboBox, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiForm, - EuiFormRow, - EuiLink, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import type { FC } from 'react'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; -import type { KibanaFeature, KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import type { Role } from '@kbn/security-plugin-types-common'; +import { + type PrivilegesAPIClient, + PrivilegesRolesForm, + type RolesAPIClient, +} from './component/space_assign_role_privilege_form'; import { SpaceAssignedRolesTable } from './component/space_assigned_roles_table'; -import { useViewSpaceServices, type ViewSpaceServices } from './hooks/view_space_context_provider'; +import { useViewSpaceServices } from './hooks/view_space_context_provider'; import type { Space } from '../../../common'; -import { FeatureTable } from '../edit_space/enabled_features/feature_table'; - -type RolesAPIClient = ReturnType extends Promise - ? R - : never; - -type KibanaPrivilegeBase = keyof NonNullable; interface Props { space: Space; @@ -56,28 +36,39 @@ interface Props { // FIXME: rename to EditSpaceAssignedRoles export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { - const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); + // const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); const [spaceUnallocatedRole, setSpaceUnallocatedRole] = useState([]); const rolesAPIClient = useRef(); - - const { getRolesAPIClient, getUrlForApp } = useViewSpaceServices(); - - const resolveRolesAPIClient = useCallback(async () => { + const privilegesAPIClient = useRef(); + + const { + getRolesAPIClient, + getUrlForApp, + getPrivilegesAPIClient, + overlays, + theme, + i18n: i18nStart, + } = useViewSpaceServices(); + + const resolveAPIClients = useCallback(async () => { try { - rolesAPIClient.current = await getRolesAPIClient(); + [rolesAPIClient.current, privilegesAPIClient.current] = await Promise.all([ + getRolesAPIClient(), + getPrivilegesAPIClient(), + ]); setRoleAPIClientInitialized(true); } catch { // } - }, [getRolesAPIClient]); + }, [getPrivilegesAPIClient, getRolesAPIClient]); useEffect(() => { if (!isReadOnly) { - resolveRolesAPIClient(); + resolveAPIClients(); } - }, [isReadOnly, resolveRolesAPIClient]); + }, [isReadOnly, resolveAPIClients]); useEffect(() => { async function fetchAllSystemRoles() { @@ -88,7 +79,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe (role) => !role.metadata?._reserved && role.kibana.some((privileges) => { - return !privileges.spaces.includes(space.id) || !privileges.spaces.includes('*'); + return !privileges.spaces.includes(space.id) && !privileges.spaces.includes('*'); }) ); @@ -100,23 +91,35 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe } }, [roleAPIClientInitialized, space.id]); + const showRolesPrivilegeEditor = useCallback( + (defaultSelected?: Role[]) => { + const overlayRef = overlays.openFlyout( + toMountPoint( + overlayRef.close(), + closeFlyout: () => overlayRef.close(), + defaultSelected, + spaceUnallocatedRole, + // APIClient would have been initialized before the privilege editor is displayed + roleAPIClient: rolesAPIClient.current!, + privilegesAPIClient: privilegesAPIClient.current!, + }} + />, + { theme, i18n: i18nStart } + ), + { + size: 's', + } + ); + }, + [features, i18nStart, overlays, space, spaceUnallocatedRole, theme] + ); + return ( - <> - {showRolesPrivilegeEditor && ( - { - setShowRolesPrivilegeEditor(false); - }} - onSaveClick={() => { - setShowRolesPrivilegeEditor(false); - }} - spaceUnallocatedRole={spaceUnallocatedRole} - // rolesAPIClient would have been initialized before the privilege editor is displayed - roleAPIClient={rolesAPIClient.current!} - /> - )} + @@ -138,256 +141,21 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe { + // TODO: add logic to remove selected roles from space + }} onAssignNewRoleClick={async () => { if (!roleAPIClientInitialized) { - await resolveRolesAPIClient(); + await resolveAPIClients(); } - setShowRolesPrivilegeEditor(true); + showRolesPrivilegeEditor(); }} /> - - ); -}; - -interface PrivilegesRolesFormProps extends Omit { - closeFlyout: () => void; - onSaveClick: () => void; - spaceUnallocatedRole: Role[]; - roleAPIClient: RolesAPIClient; -} - -const createRolesComboBoxOptions = (roles: Role[]): Array> => - roles.map((role) => ({ - label: role.name, - value: role, - })); - -export const PrivilegesRolesForm: FC = (props) => { - const { onSaveClick, closeFlyout, features, roleAPIClient, spaceUnallocatedRole } = props; - - const [space, setSpaceState] = useState>(props.space); - const [spacePrivilege, setSpacePrivilege] = useState('all'); - const [selectedRoles, setSelectedRoles] = useState>( - [] - ); - const selectedRolesHasPrivilegeConflict = useMemo(() => { - return selectedRoles.reduce((result, selectedRole) => { - // TODO: determine heuristics for role privilege conflicts - return result; - }, false); - }, [selectedRoles]); - - const [assigningToRole, setAssigningToRole] = useState(false); - - const assignRolesToSpace = useCallback(async () => { - try { - setAssigningToRole(true); - - await Promise.all( - selectedRoles.map((selectedRole) => { - roleAPIClient.saveRole({ role: selectedRole.value! }); - }) - ).then(setAssigningToRole.bind(null, false)); - - onSaveClick(); - } catch { - // Handle resulting error - } - }, [onSaveClick, roleAPIClient, selectedRoles]); - - const getForm = () => { - return ( - - - { - setSelectedRoles((prevRoles) => { - if (prevRoles.length < value.length) { - const newlyAdded = value[value.length - 1]; - - const { name: spaceName } = space; - if (!spaceName) { - throw new Error('space state requires name!'); - } - - // Add kibana space privilege definition to role - newlyAdded.value!.kibana.push({ - spaces: [spaceName], - base: spacePrivilege === 'custom' ? [] : [spacePrivilege], - feature: {}, - }); - - return prevRoles.concat(newlyAdded); - } else { - return value; - } - }); - }} - fullWidth - /> - - <> - {!selectedRolesHasPrivilegeConflict && ( - - - {i18n.translate( - 'xpack.spaces.management.spaceDetails.roles.assign.privilegeConflictMsg.description', - { - defaultMessage: - 'Updating the settings here in a bulk will override current individual settings.', - } - )} - - - )} - - - ({ - ...privilege, - 'data-test-subj': `${privilege.id}-privilege-button`, - }))} - color="primary" - idSelected={spacePrivilege} - onChange={(id) => setSpacePrivilege(id as KibanaPrivilegeBase | 'custom')} - buttonSize="compressed" - isFullWidth - /> - - {spacePrivilege === 'custom' && ( - - <> - -

- -

-
- - - -
- )} -
- ); - }; - - const getSaveButton = () => { - return ( - assignRolesToSpace()} - data-test-subj={'createRolesPrivilegeButton'} - > - {i18n.translate('xpack.spaces.management.spaceDetails.roles.assignRoleButton', { - defaultMessage: 'Assign roles', - })} - - ); - }; - - return ( - - - -

- {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign.privileges.custom', { - defaultMessage: 'Assign role to {spaceName}', - values: { spaceName: space.name }, - })} -

-
- - -

- -

-
-
- {getForm()} - - - - - {i18n.translate('xpack.spaces.management.spaceDetails.roles.cancelRoleButton', { - defaultMessage: 'Cancel', - })} - - - {getSaveButton()} - - -
+
); }; From 63035a70e835be6487d4e130ec81b59d6767f261 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 13 Aug 2024 13:05:22 +0200 Subject: [PATCH 108/129] make accomodation for edit existing record --- .../component/space_assigned_roles_table.tsx | 34 ++++++++++++------- .../view_space/view_space_roles.tsx | 6 +++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index f1bf4da4c6b17..0804f2273186b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -35,9 +35,11 @@ import type { Role } from '@kbn/security-plugin-types-common'; interface ISpaceAssignedRolesTableProps { isReadOnly: boolean; assignedRoles: Role[]; - onAssignNewRoleClick: () => Promise; + onClickAssignNewRole: () => Promise; onClickBulkEdit: (selectedRoles: Role[]) => void; onClickBulkRemove: (selectedRoles: Role[]) => void; + onClickRowEditAction: (role: Role) => void; + onClickRowRemoveAction: (role: Role) => void; } /** @@ -53,7 +55,14 @@ export const isEditableRole = (role: Role) => { ); }; -const getTableColumns = ({ isReadOnly }: Pick) => { +const getTableColumns = ({ + isReadOnly, + onClickRowEditAction, + onClickRowRemoveAction, +}: Pick< + ISpaceAssignedRolesTableProps, + 'isReadOnly' | 'onClickRowEditAction' | 'onClickRowRemoveAction' +>) => { const columns: Array> = [ { field: 'name', @@ -156,9 +165,7 @@ const getTableColumns = ({ isReadOnly }: Pick isEditableRole(rowRecord), - onClick: () => { - window.alert('Not yet implemented.'); - }, + onClick: onClickRowEditAction, }, { isPrimary: true, @@ -179,9 +186,7 @@ const getTableColumns = ({ isReadOnly }: Pick isEditableRole(rowRecord), - onClick: (rowRecord, event) => { - window.alert('Not yet implemented.'); - }, + onClick: onClickRowRemoveAction, }, ], }); @@ -210,11 +215,16 @@ const getCellProps = (item: Role, column: EuiTableFieldDataColumnType) => export const SpaceAssignedRolesTable = ({ isReadOnly, assignedRoles, - onAssignNewRoleClick, + onClickAssignNewRole, onClickBulkEdit, onClickBulkRemove, + onClickRowEditAction, + onClickRowRemoveAction, }: ISpaceAssignedRolesTableProps) => { - const tableColumns = useMemo(() => getTableColumns({ isReadOnly }), [isReadOnly]); + const tableColumns = useMemo( + () => getTableColumns({ isReadOnly, onClickRowEditAction, onClickRowRemoveAction }), + [isReadOnly, onClickRowEditAction, onClickRowRemoveAction] + ); const [rolesInView, setRolesInView] = useState(assignedRoles); const [selectedRoles, setSelectedRoles] = useState([]); const [isBulkActionContextOpen, setBulkActionContextOpen] = useState(false); @@ -251,7 +261,7 @@ export const SpaceAssignedRolesTable = ({ <> {!isReadOnly && ( - + {i18n.translate('xpack.spaces.management.spaceDetails.roles.assign', { defaultMessage: 'Assign role', })} @@ -261,7 +271,7 @@ export const SpaceAssignedRolesTable = ({ ), }; - }, [isReadOnly, onAssignNewRoleClick, onSearchQueryChange]); + }, [isReadOnly, onClickAssignNewRole, onSearchQueryChange]); const tableHeader = useMemo['childrenBetween']>(() => { const pageSize = pagination.size; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 9a11be03fa96e..3d1257302cebc 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -144,10 +144,14 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe isReadOnly={isReadOnly} assignedRoles={roles} onClickBulkEdit={showRolesPrivilegeEditor} + onClickRowEditAction={(rowRecord) => showRolesPrivilegeEditor([rowRecord])} onClickBulkRemove={(selectedRoles) => { // TODO: add logic to remove selected roles from space }} - onAssignNewRoleClick={async () => { + onClickRowRemoveAction={(rowRecord) => { + // TODO: add logic to remove single role from space + }} + onClickAssignNewRole={async () => { if (!roleAPIClientInitialized) { await resolveAPIClients(); } From 9541fc431a45233a0ae5ff183763ec8eb581bf81 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 13 Aug 2024 23:59:57 +0200 Subject: [PATCH 109/129] integrate API to update roles, leverage this to update existing space privilege --- .../src/roles/roles_api_client.ts | 1 + .../public/authentication/index.mock.ts | 2 + .../authorization/authorization_service.ts | 1 + .../management/roles/roles_api_client.mock.ts | 1 + .../management/roles/roles_api_client.ts | 11 +++++ .../plugins/security/public/plugin.test.tsx | 1 + .../space_assign_role_privilege_form.tsx | 46 ++++++++++++------- .../component/space_assigned_roles_table.tsx | 31 ++++++++----- .../view_space/view_space_roles.tsx | 19 ++++++-- 9 files changed, 83 insertions(+), 30 deletions(-) diff --git a/x-pack/packages/security/plugin_types_public/src/roles/roles_api_client.ts b/x-pack/packages/security/plugin_types_public/src/roles/roles_api_client.ts index b5c45c5160fde..a936741ad806e 100644 --- a/x-pack/packages/security/plugin_types_public/src/roles/roles_api_client.ts +++ b/x-pack/packages/security/plugin_types_public/src/roles/roles_api_client.ts @@ -16,4 +16,5 @@ export interface RolesAPIClient { getRole: (roleName: string) => Promise; deleteRole: (roleName: string) => Promise; saveRole: (payload: RolePutPayload) => Promise; + bulkUpdateRoles: (payload: { rolesUpdate: Role[] }) => Promise; } diff --git a/x-pack/plugins/security/public/authentication/index.mock.ts b/x-pack/plugins/security/public/authentication/index.mock.ts index 166583b1274cb..f30d47af3f701 100644 --- a/x-pack/plugins/security/public/authentication/index.mock.ts +++ b/x-pack/plugins/security/public/authentication/index.mock.ts @@ -31,6 +31,7 @@ export const authorizationMock = { getRole: jest.fn(), deleteRole: jest.fn(), saveRole: jest.fn(), + bulkUpdateRoles: jest.fn(), }, privileges: { getAll: jest.fn(), @@ -43,6 +44,7 @@ export const authorizationMock = { getRole: jest.fn(), deleteRole: jest.fn(), saveRole: jest.fn(), + bulkUpdateRoles: jest.fn(), }, privileges: { getAll: jest.fn(), diff --git a/x-pack/plugins/security/public/authorization/authorization_service.ts b/x-pack/plugins/security/public/authorization/authorization_service.ts index c650d381be1af..4fbae4fb54e6a 100644 --- a/x-pack/plugins/security/public/authorization/authorization_service.ts +++ b/x-pack/plugins/security/public/authorization/authorization_service.ts @@ -29,6 +29,7 @@ export class AuthorizationService { getRole: rolesAPIClient.getRole, deleteRole: rolesAPIClient.deleteRole, saveRole: rolesAPIClient.saveRole, + bulkUpdateRoles: rolesAPIClient.bulkUpdateRoles, }, privileges: { getAll: privilegesAPIClient.getAll.bind(privilegesAPIClient), diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts index 0e756e87c081c..5f868fda093a4 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts @@ -11,5 +11,6 @@ export const rolesAPIClientMock = { getRole: jest.fn(), deleteRole: jest.fn(), saveRole: jest.fn(), + bulkUpdateRoles: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts index c870f99e24dd3..5bf58244a969a 100644 --- a/x-pack/plugins/security/public/management/roles/roles_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -32,6 +32,17 @@ export class RolesAPIClient { }); }; + public bulkUpdateRoles = async ({ rolesUpdate }: { rolesUpdate: Role[] }) => { + await this.http.post('/api/security/roles', { + body: JSON.stringify({ + roles: rolesUpdate.reduce((transformed, value) => { + transformed[value.name] = this.transformRoleForSave(copyRole(value)); + return transformed; + }, {} as Record>), + }), + }); + }; + private transformRoleForSave = (role: Role) => { // Remove any placeholder index privileges const isPlaceholderPrivilege = ( diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 336a42a1fd324..e58539bf2bc8f 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -137,6 +137,7 @@ describe('Security Plugin', () => { "getAll": [Function], }, "roles": Object { + "bulkUpdateRoles": [Function], "deleteRole": [Function], "getRole": [Function], "getRoles": [Function], diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx index f7728d4d9b8e8..bc6d7615510fd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx @@ -54,7 +54,7 @@ interface PrivilegesRolesFormProps { space: Space; features: KibanaFeature[]; closeFlyout: () => void; - onSaveClick: () => void; + onSaveCompleted: () => void; roleAPIClient: RolesAPIClient; privilegesAPIClient: PrivilegesAPIClient; spaceUnallocatedRole: Role[]; @@ -69,7 +69,7 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { const { - onSaveClick, + onSaveCompleted, closeFlyout, features, roleAPIClient, @@ -83,6 +83,7 @@ export const PrivilegesRolesForm: FC = (props) => { const [selectedRoles, setSelectedRoles] = useState>( createRolesComboBoxOptions(defaultSelected) ); + const [assigningToRole, setAssigningToRole] = useState(false); const [privileges, setPrivileges] = useState<[RawKibanaPrivileges] | null>(null); const selectedRolesHasPrivilegeConflict = useMemo(() => { @@ -91,14 +92,16 @@ export const PrivilegesRolesForm: FC = (props) => { return selectedRoles.reduce((result, selectedRole) => { let rolePrivilege: string; - selectedRole.value?.kibana.forEach(({ spaces, base }) => { + selectedRole.value!.kibana.forEach(({ spaces, base }) => { // TODO: consider wildcard situations if (spaces.includes(space.id!) && base.length) { rolePrivilege = base[0]; } if (!privilegeAnchor) { - setRoleSpacePrivilege((privilegeAnchor = rolePrivilege)); + setRoleSpacePrivilege( + (privilegeAnchor = rolePrivilege === '*' ? 'custom' : rolePrivilege) + ); } }); @@ -117,23 +120,34 @@ export const PrivilegesRolesForm: FC = (props) => { ); }, [privilegesAPIClient]); - const [assigningToRole, setAssigningToRole] = useState(false); - const assignRolesToSpace = useCallback(async () => { try { setAssigningToRole(true); - await Promise.all( - selectedRoles.map((selectedRole) => { - roleAPIClient.saveRole({ role: selectedRole.value! }); - }) - ).then(setAssigningToRole.bind(null, false)); + await roleAPIClient + .bulkUpdateRoles({ rolesUpdate: selectedRoles.map((role) => role.value!) }) + .then(setAssigningToRole.bind(null, false)); - onSaveClick(); - } catch { + onSaveCompleted(); + } catch (err) { // Handle resulting error } - }, [onSaveClick, roleAPIClient, selectedRoles]); + }, [onSaveCompleted, roleAPIClient, selectedRoles]); + + useEffect(() => { + setSelectedRoles((prevSelectedRoles) => { + return structuredClone(prevSelectedRoles).map((selectedRole) => { + for (let i = 0; i < selectedRole.value!.kibana.length; i++) { + if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { + selectedRole.value!.kibana[i].base = [roleSpacePrivilege]; + break; + } + } + + return selectedRole; + }); + }); + }, [roleSpacePrivilege, space.id]); const getForm = () => { return ( @@ -152,13 +166,13 @@ export const PrivilegesRolesForm: FC = (props) => { setSelectedRoles((prevRoles) => { if (prevRoles.length < value.length) { const newlyAdded = value[value.length - 1]; - const { id: spaceId } = space; + if (!spaceId) { throw new Error('space state requires space to have an ID'); } - // Add kibana space privilege definition to role + // Add new kibana privilege definition particular for the current space to role newlyAdded.value!.kibana.push({ base: roleSpacePrivilege === 'custom' ? [] : [roleSpacePrivilege], feature: {}, diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx index 0804f2273186b..461532a502e3c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx @@ -79,17 +79,26 @@ const getTableColumns = ({ } ), render: (_, record) => { - return record.kibana.map((kibanaPrivilege) => { - if (!kibanaPrivilege.base.length) { - return i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.column.privileges.customPrivilege', - { - defaultMessage: 'custom', - } - ); - } - return kibanaPrivilege.base.join(', '); - }); + const uniquePrivilege = new Set( + record.kibana.reduce((privilegeBaseTuple, kibanaPrivilege) => { + if (!kibanaPrivilege.base.length) { + privilegeBaseTuple.push( + i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.privileges.customPrivilege', + { + defaultMessage: 'custom', + } + ) + ); + + return privilegeBaseTuple; + } + + return privilegeBaseTuple.concat(kibanaPrivilege.base); + }, [] as string[]) + ); + + return Array.from(uniquePrivilege).join(','); }, }, { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 3d1257302cebc..b4a3a2a56284e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -36,7 +36,6 @@ interface Props { // FIXME: rename to EditSpaceAssignedRoles export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { - // const [showRolesPrivilegeEditor, setShowRolesPrivilegeEditor] = useState(false); const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); const [spaceUnallocatedRole, setSpaceUnallocatedRole] = useState([]); @@ -50,6 +49,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe overlays, theme, i18n: i18nStart, + notifications, } = useViewSpaceServices(); const resolveAPIClients = useCallback(async () => { @@ -99,7 +99,20 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe {...{ space, features, - onSaveClick: () => overlayRef.close(), + onSaveCompleted: () => { + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.assignmentSuccessMsg', + { + defaultMessage: `Selected roles have been assigned to the {spaceName} space`, + values: { + spaceName: space.name, + }, + } + ) + ); + overlayRef.close(); + }, closeFlyout: () => overlayRef.close(), defaultSelected, spaceUnallocatedRole, @@ -115,7 +128,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe } ); }, - [features, i18nStart, overlays, space, spaceUnallocatedRole, theme] + [features, i18nStart, notifications.toasts, overlays, space, spaceUnallocatedRole, theme] ); return ( From ee5e218b687a5ec566ff9a104dc76c33c7f7b110 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Wed, 14 Aug 2024 11:56:03 +0200 Subject: [PATCH 110/129] add logic to handle removing roles from space --- .../space_assign_role_privilege_form.tsx | 65 ++++++++++--------- .../view_space/view_space_roles.tsx | 51 +++++++++++++-- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx index bc6d7615510fd..8b748fa7fa367 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx @@ -86,27 +86,22 @@ export const PrivilegesRolesForm: FC = (props) => { const [assigningToRole, setAssigningToRole] = useState(false); const [privileges, setPrivileges] = useState<[RawKibanaPrivileges] | null>(null); - const selectedRolesHasPrivilegeConflict = useMemo(() => { - let privilegeAnchor: string; - - return selectedRoles.reduce((result, selectedRole) => { - let rolePrivilege: string; - - selectedRole.value!.kibana.forEach(({ spaces, base }) => { - // TODO: consider wildcard situations - if (spaces.includes(space.id!) && base.length) { - rolePrivilege = base[0]; + const selectedRolesHasSpacePrivilegeConflict = useMemo(() => { + const combinedPrivilege = new Set( + selectedRoles.reduce((result, selectedRole) => { + let match: string[] = []; + for (let i = 0; i < selectedRole.value!.kibana.length; i++) { + if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { + match = selectedRole.value!.kibana[i].base; + break; + } } - if (!privilegeAnchor) { - setRoleSpacePrivilege( - (privilegeAnchor = rolePrivilege === '*' ? 'custom' : rolePrivilege) - ); - } - }); + return result.concat(match); + }, [] as string[]) + ); - return result || privilegeAnchor !== rolePrivilege; - }, false); + return combinedPrivilege.size > 1; }, [selectedRoles, space.id]); useEffect(() => { @@ -134,20 +129,28 @@ export const PrivilegesRolesForm: FC = (props) => { } }, [onSaveCompleted, roleAPIClient, selectedRoles]); - useEffect(() => { - setSelectedRoles((prevSelectedRoles) => { - return structuredClone(prevSelectedRoles).map((selectedRole) => { - for (let i = 0; i < selectedRole.value!.kibana.length; i++) { - if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { - selectedRole.value!.kibana[i].base = [roleSpacePrivilege]; - break; + const updateRoleSpacePrivilege = useCallback( + (spacePrivilege: KibanaRolePrivilege) => { + // persist select privilege for UI + setRoleSpacePrivilege(spacePrivilege); + + // update preselected roles with new privilege + setSelectedRoles((prevSelectedRoles) => { + return structuredClone(prevSelectedRoles).map((selectedRole) => { + for (let i = 0; i < selectedRole.value!.kibana.length; i++) { + if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { + selectedRole.value!.kibana[i].base = + spacePrivilege === 'custom' ? [] : [spacePrivilege]; + break; + } } - } - return selectedRole; + return selectedRole; + }); }); - }); - }, [roleSpacePrivilege, space.id]); + }, + [space.id] + ); const getForm = () => { return ( @@ -189,7 +192,7 @@ export const PrivilegesRolesForm: FC = (props) => { />
<> - {selectedRolesHasPrivilegeConflict && ( + {selectedRolesHasSpacePrivilegeConflict && ( = (props) => { }))} color="primary" idSelected={roleSpacePrivilege} - onChange={(id) => setRoleSpacePrivilege(id as KibanaRolePrivilege)} + onChange={(id) => updateRoleSpacePrivilege(id as KibanaRolePrivilege)} buttonSize="compressed" isFullWidth /> diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index b4a3a2a56284e..4745bec219a0c 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -78,9 +78,10 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe const spaceUnallocatedRoles = systemRoles.filter( (role) => !role.metadata?._reserved && - role.kibana.some((privileges) => { - return !privileges.spaces.includes(space.id) && !privileges.spaces.includes('*'); - }) + (!role.kibana.length || + role.kibana.some((privileges) => { + return !privileges.spaces.includes(space.id) && !privileges.spaces.includes('*'); + })) ); setSpaceUnallocatedRole(spaceUnallocatedRoles); @@ -131,6 +132,42 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe [features, i18nStart, notifications.toasts, overlays, space, spaceUnallocatedRole, theme] ); + const removeRole = useCallback( + async (payload: Role[]) => { + const updateDoc = structuredClone(payload).map((roleDef) => { + roleDef.kibana = roleDef.kibana.filter(({ spaces }) => { + let spaceIdIndex: number; + + if (spaces.length && (spaceIdIndex = spaces.indexOf(space.id)) > -1) { + if (spaces.length > 1) { + spaces.splice(spaceIdIndex, 1); + return true; + } else { + return false; + } + } + return true; + }); + + return roleDef; + }); + + await rolesAPIClient.current?.bulkUpdateRoles({ rolesUpdate: updateDoc }).then(() => + notifications.toasts.addSuccess( + i18n.translate('xpack.spaces.management.spaceDetails.roles.removalSuccessMsg', { + defaultMessage: + 'Removed {count, plural, one {role} other {{count} roles}} from {spaceName} space', + values: { + spaceName: space.name, + count: updateDoc.length, + }, + }) + ) + ); + }, + [notifications.toasts, space.id, space.name] + ); + return ( @@ -158,11 +195,11 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe assignedRoles={roles} onClickBulkEdit={showRolesPrivilegeEditor} onClickRowEditAction={(rowRecord) => showRolesPrivilegeEditor([rowRecord])} - onClickBulkRemove={(selectedRoles) => { - // TODO: add logic to remove selected roles from space + onClickBulkRemove={async (selectedRoles) => { + await removeRole(selectedRoles); }} - onClickRowRemoveAction={(rowRecord) => { - // TODO: add logic to remove single role from space + onClickRowRemoveAction={async (rowRecord) => { + await removeRole([rowRecord]); }} onClickAssignNewRole={async () => { if (!roleAPIClientInitialized) { From c13613e0e54420c65e49611efa8592a6b8799b09 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 15 Aug 2024 07:21:45 +0200 Subject: [PATCH 111/129] refactor implementation to provide visual feedback on UI actions --- .../management/view_space/hooks/use_tabs.ts | 2 +- .../hooks/view_space_context_provider.tsx | 52 ---- .../public/management/view_space/index.tsx | 40 +++ .../view_space/{ => provider}/index.ts | 7 +- .../view_space/provider/reducers/index.ts | 53 ++++ .../provider/view_space_provider.tsx | 164 ++++++++++++ .../space_assign_role_privilege_form.tsx | 144 ++++++---- .../component/space_assigned_roles_table.tsx | 123 +++++---- .../management/view_space/view_space.tsx | 253 +++++++++--------- .../view_space/view_space_content_tab.tsx | 2 +- .../view_space/view_space_features_tab.tsx | 2 +- .../view_space/view_space_general_tab.tsx | 2 +- .../view_space/view_space_roles.tsx | 112 ++------ .../management/view_space/view_space_tabs.tsx | 8 +- 14 files changed, 584 insertions(+), 380 deletions(-) delete mode 100644 x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx create mode 100644 x-pack/plugins/spaces/public/management/view_space/index.tsx rename x-pack/plugins/spaces/public/management/view_space/{ => provider}/index.ts (54%) create mode 100644 x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts create mode 100644 x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx rename x-pack/plugins/spaces/public/management/view_space/{ => roles}/component/space_assign_role_privilege_form.tsx (76%) rename x-pack/plugins/spaces/public/management/view_space/{ => roles}/component/space_assigned_roles_table.tsx (82%) diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts index 38bf7fd02f94f..1623f19920bcd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts @@ -13,7 +13,7 @@ import type { KibanaFeature } from '@kbn/features-plugin/public'; import type { Space } from '../../../../common'; import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs'; -type UseTabsProps = Pick & { +type UseTabsProps = Pick & { space: Space | null; features: KibanaFeature[] | null; currentSelectedTabId: string; diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx deleted file mode 100644 index d5476966cf6dd..0000000000000 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx +++ /dev/null @@ -1,52 +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 type { FC, PropsWithChildren } from 'react'; -import React, { createContext, useContext } from 'react'; - -import type { ApplicationStart } from '@kbn/core-application-browser'; -import type { CoreStart } from '@kbn/core-lifecycle-browser'; -import type { - PrivilegesAPIClientPublicContract, - RolesAPIClient, -} from '@kbn/security-plugin-types-public'; - -import type { SpacesManager } from '../../../spaces_manager'; - -// FIXME: rename to EditSpaceServices -export interface ViewSpaceServices - extends Pick { - capabilities: ApplicationStart['capabilities']; - getUrlForApp: ApplicationStart['getUrlForApp']; - navigateToUrl: ApplicationStart['navigateToUrl']; - serverBasePath: string; - spacesManager: SpacesManager; - getRolesAPIClient: () => Promise; - getPrivilegesAPIClient: () => Promise; -} - -const ViewSpaceContext = createContext(null); - -// FIXME: rename to EditSpaceContextProvider -export const ViewSpaceContextProvider: FC> = ({ - children, - ...services -}) => { - return {children}; -}; - -// FIXME: rename to useEditSpaceServices -export const useViewSpaceServices = (): ViewSpaceServices => { - const context = useContext(ViewSpaceContext); - if (!context) { - throw new Error( - 'ViewSpace Context is missing. Ensure the component or React root is wrapped with ViewSpaceContext' - ); - } - - return context; -}; diff --git a/x-pack/plugins/spaces/public/management/view_space/index.tsx b/x-pack/plugins/spaces/public/management/view_space/index.tsx new file mode 100644 index 0000000000000..8a796fdd33f41 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/index.tsx @@ -0,0 +1,40 @@ +/* + * 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 type { ComponentProps, PropsWithChildren } from 'react'; + +import { ViewSpaceProvider, type ViewSpaceProviderProps } from './provider'; +import { ViewSpace } from './view_space'; + +type ViewSpacePageProps = ComponentProps & ViewSpaceProviderProps; + +export function ViewSpacePage({ + spaceId, + getFeatures, + history, + onLoadSpace, + selectedTabId, + allowFeatureVisibility, + allowSolutionVisibility, + children, + ...viewSpaceServicesProps +}: PropsWithChildren) { + return ( + + + + ); +} diff --git a/x-pack/plugins/spaces/public/management/view_space/index.ts b/x-pack/plugins/spaces/public/management/view_space/provider/index.ts similarity index 54% rename from x-pack/plugins/spaces/public/management/view_space/index.ts rename to x-pack/plugins/spaces/public/management/view_space/provider/index.ts index ff9ddac4a28e5..74c713ee2e56a 100644 --- a/x-pack/plugins/spaces/public/management/view_space/index.ts +++ b/x-pack/plugins/spaces/public/management/view_space/provider/index.ts @@ -5,4 +5,9 @@ * 2.0. */ -export { ViewSpacePage } from './view_space'; +export { ViewSpaceProvider, useViewSpaceServices, useViewSpaceStore } from './view_space_provider'; +export type { + ViewSpaceProviderProps, + ViewSpaceServices, + ViewSpaceStore, +} from './view_space_provider'; diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts b/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts new file mode 100644 index 0000000000000..6040b69d3ba9d --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts @@ -0,0 +1,53 @@ +/* + * 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 { type Reducer } from 'react'; + +import type { Role } from '@kbn/security-plugin-types-common'; + +export type IDispatchAction = + | { + /** @description updates a single role record */ + type: 'update_roles' | 'remove_roles'; + payload: Role[]; + } + | { + type: 'string'; + payload: any; + }; + +export interface IViewSpaceStoreState { + /** roles assigned to current space */ + roles: Map; +} + +export const createSpaceRolesReducer: Reducer = ( + state, + action +) => { + const _state = structuredClone(state); + + switch (action.type) { + case 'update_roles': { + action.payload.forEach((role) => { + _state.roles.set(role.name, role); + }); + + return _state; + } + case 'remove_roles': { + action.payload.forEach((role) => { + _state.roles.delete(role.name); + }); + + return _state; + } + default: { + return _state; + } + } +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx new file mode 100644 index 0000000000000..e2f31b15d7df1 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx @@ -0,0 +1,164 @@ +/* + * 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 { once } from 'lodash'; +import React, { + createContext, + type Dispatch, + type FC, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useReducer, + useRef, +} from 'react'; + +import type { ApplicationStart } from '@kbn/core-application-browser'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { + PrivilegesAPIClientPublicContract, + RolesAPIClient, +} from '@kbn/security-plugin-types-public'; + +import { + createSpaceRolesReducer, + type IDispatchAction, + type IViewSpaceStoreState, +} from './reducers'; +import type { SpacesManager } from '../../../spaces_manager'; + +// FIXME: rename to EditSpaceServices +export interface ViewSpaceProviderProps + extends Pick { + capabilities: ApplicationStart['capabilities']; + getUrlForApp: ApplicationStart['getUrlForApp']; + navigateToUrl: ApplicationStart['navigateToUrl']; + serverBasePath: string; + spacesManager: SpacesManager; + getRolesAPIClient: () => Promise; + getPrivilegesAPIClient: () => Promise; +} + +export interface ViewSpaceServices + extends Omit { + invokeClient Promise>( + arg: ARG + ): ReturnType; +} + +interface ViewSpaceClients { + spacesManager: ViewSpaceProviderProps['spacesManager']; + rolesClient: RolesAPIClient; + privilegesClient: PrivilegesAPIClientPublicContract; +} + +export interface ViewSpaceStore { + state: IViewSpaceStoreState; + dispatch: Dispatch; +} + +const createSpaceRolesContext = once(() => + createContext({ + state: { + roles: [], + }, + dispatch: () => { }, + }) +); + +const createViewSpaceServicesContext = once(() => createContext(null)); + +// FIXME: rename to EditSpaceProvider +export const ViewSpaceProvider: FC> = ({ + children, + getRolesAPIClient, + getPrivilegesAPIClient, + ...services +}) => { + const ViewSpaceStoreContext = createSpaceRolesContext(); + const ViewSpaceServicesContext = createViewSpaceServicesContext(); + + const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()])); + const rolesAPIClientRef = useRef(); + const privilegesClientRef = useRef(); + + const initialStoreState = useRef({ + roles: new Map(), + }); + + const resolveAPIClients = useCallback(async () => { + try { + [rolesAPIClientRef.current, privilegesClientRef.current] = await clients.current; + } catch { + // handle errors + } + }, []); + + useEffect(() => { + resolveAPIClients(); + }, [resolveAPIClients]); + + const createInitialState = useCallback((state: IViewSpaceStoreState) => { + return state; + }, []); + + const [state, dispatch] = useReducer( + createSpaceRolesReducer, + initialStoreState.current, + createInitialState + ); + + const invokeClient = useCallback( + async (...args: Parameters) => { + await resolveAPIClients(); + + return args[0]({ + spacesManager: services.spacesManager, + rolesClient: rolesAPIClientRef.current!, + privilegesClient: privilegesClientRef.current!, + }); + }, + [resolveAPIClients, services.spacesManager] + ); + + return ( + + + {children} + + + ); +}; + +// FIXME: rename to useEditSpaceServices +export const useViewSpaceServices = (): ViewSpaceServices => { + const context = useContext(createViewSpaceServicesContext()); + if (!context) { + throw new Error( + 'ViewSpaceService Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider' + ); + } + + return context; +}; + +export const useViewSpaceStore = () => { + const context = useContext(createSpaceRolesContext()); + if (!context) { + throw new Error( + 'ViewSpaceStore Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider' + ); + } + + return context; +}; + +export const useViewSpaceStoreDispatch = () => { + const { dispatch } = useViewSpaceStore(); + return dispatch; +}; diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx similarity index 76% rename from x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx rename to x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx index 8b748fa7fa367..ece27928bbb66 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx @@ -33,20 +33,8 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { KibanaPrivileges, type RawKibanaPrivileges } from '@kbn/security-role-management-model'; import { KibanaPrivilegeTable, PrivilegeFormCalculator } from '@kbn/security-ui-components'; -import type { Space } from '../../../../common'; -import type { ViewSpaceServices } from '../hooks/view_space_context_provider'; - -export type RolesAPIClient = ReturnType extends Promise< - infer R -> - ? R - : never; - -export type PrivilegesAPIClient = ReturnType< - ViewSpaceServices['getPrivilegesAPIClient'] -> extends Promise - ? R - : never; +import type { Space } from '../../../../../common'; +import type { ViewSpaceServices, ViewSpaceStore } from '../../provider'; type KibanaRolePrivilege = keyof NonNullable | 'custom'; @@ -55,10 +43,9 @@ interface PrivilegesRolesFormProps { features: KibanaFeature[]; closeFlyout: () => void; onSaveCompleted: () => void; - roleAPIClient: RolesAPIClient; - privilegesAPIClient: PrivilegesAPIClient; - spaceUnallocatedRole: Role[]; defaultSelected?: Role[]; + storeDispatch: ViewSpaceStore['dispatch']; + spacesClientsInvocator: ViewSpaceServices['invokeClient']; } const createRolesComboBoxOptions = (roles: Role[]): Array> => @@ -72,64 +59,79 @@ export const PrivilegesRolesForm: FC = (props) => { onSaveCompleted, closeFlyout, features, - roleAPIClient, defaultSelected = [], - privilegesAPIClient, - spaceUnallocatedRole, + spacesClientsInvocator, + storeDispatch, } = props; - const [space, setSpaceState] = useState>(props.space); - const [roleSpacePrivilege, setRoleSpacePrivilege] = useState('all'); + const [assigningToRole, setAssigningToRole] = useState(false); + const [fetchingSystemRoles, setFetchingSystemRoles] = useState(false); + const [privileges, setPrivileges] = useState<[RawKibanaPrivileges] | null>(null); + const [spaceUnallocatedRoles, setSpaceUnallocatedRole] = useState([]); const [selectedRoles, setSelectedRoles] = useState>( createRolesComboBoxOptions(defaultSelected) ); - const [assigningToRole, setAssigningToRole] = useState(false); - const [privileges, setPrivileges] = useState<[RawKibanaPrivileges] | null>(null); + const selectedRolesCombinedPrivileges = useMemo(() => { - const selectedRolesHasSpacePrivilegeConflict = useMemo(() => { const combinedPrivilege = new Set( selectedRoles.reduce((result, selectedRole) => { - let match: string[] = []; + let match: Array> = []; for (let i = 0; i < selectedRole.value!.kibana.length; i++) { if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { + // @ts-ignore - TODO resolve this match = selectedRole.value!.kibana[i].base; break; } } return result.concat(match); - }, [] as string[]) + }, [] as Array>) ); - return combinedPrivilege.size > 1; + return Array.from(combinedPrivilege); }, [selectedRoles, space.id]); + const [roleSpacePrivilege, setRoleSpacePrivilege] = useState( + selectedRolesCombinedPrivileges.length === 1 ? selectedRolesCombinedPrivileges[0] : 'all' + ); + + useEffect(() => { + async function fetchAllSystemRoles() { + setFetchingSystemRoles(true); + const systemRoles = await spacesClientsInvocator((clients) => clients.rolesClient.getRoles()); + + // exclude roles that are already assigned to this space + setSpaceUnallocatedRole( + systemRoles.filter( + (role) => + !role.metadata?._reserved && + (!role.kibana.length || + role.kibana.some((rolePrivileges) => { + return ( + !rolePrivileges.spaces.includes(space.id!) && !rolePrivileges.spaces.includes('*') + ); + })) + ) + ); + } + + fetchAllSystemRoles().finally(() => setFetchingSystemRoles(false)); + }, [space.id, spacesClientsInvocator]); + useEffect(() => { Promise.all([ - privilegesAPIClient.getAll({ includeActions: true, respectLicenseLevel: false }), - privilegesAPIClient.getBuiltIn(), + spacesClientsInvocator((clients) => + clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }) + ), + spacesClientsInvocator((clients) => clients.privilegesClient.getBuiltIn()), ]).then( ([kibanaPrivileges, builtInESPrivileges]) => setPrivileges([kibanaPrivileges, builtInESPrivileges]) // (err) => fatalErrors.add(err) ); - }, [privilegesAPIClient]); - - const assignRolesToSpace = useCallback(async () => { - try { - setAssigningToRole(true); + }, [spacesClientsInvocator]); - await roleAPIClient - .bulkUpdateRoles({ rolesUpdate: selectedRoles.map((role) => role.value!) }) - .then(setAssigningToRole.bind(null, false)); - - onSaveCompleted(); - } catch (err) { - // Handle resulting error - } - }, [onSaveCompleted, roleAPIClient, selectedRoles]); - - const updateRoleSpacePrivilege = useCallback( + const onRoleSpacePrivilegeChange = useCallback( (spacePrivilege: KibanaRolePrivilege) => { // persist select privilege for UI setRoleSpacePrivilege(spacePrivilege); @@ -152,6 +154,29 @@ export const PrivilegesRolesForm: FC = (props) => { [space.id] ); + const assignRolesToSpace = useCallback(async () => { + try { + setAssigningToRole(true); + + const updatedRoles = selectedRoles.map((role) => role.value!); + + await spacesClientsInvocator((clients) => + clients.rolesClient + .bulkUpdateRoles({ rolesUpdate: updatedRoles }) + .then(setAssigningToRole.bind(null, false)) + ); + + storeDispatch({ + type: 'update_roles', + payload: updatedRoles, + }); + + onSaveCompleted(); + } catch (err) { + // Handle resulting error + } + }, [onSaveCompleted, selectedRoles, spacesClientsInvocator, storeDispatch]); + const getForm = () => { return ( @@ -162,8 +187,14 @@ export const PrivilegesRolesForm: FC = (props) => { defaultMessage: 'Select role to assign to the {spaceName} space', values: { spaceName: space.name }, })} - placeholder="Select roles" - options={createRolesComboBoxOptions(spaceUnallocatedRole)} + isLoading={fetchingSystemRoles} + placeholder={i18n.translate( + 'xpack.spaces.management.spaceDetails.roles.selectRolesPlaceholder', + { + defaultMessage: 'Select roles', + } + )} + options={createRolesComboBoxOptions(spaceUnallocatedRoles)} selectedOptions={selectedRoles} onChange={(value) => { setSelectedRoles((prevRoles) => { @@ -192,7 +223,7 @@ export const PrivilegesRolesForm: FC = (props) => { /> <> - {selectedRolesHasSpacePrivilegeConflict && ( + {selectedRolesCombinedPrivileges.length > 1 && ( = (props) => { }))} color="primary" idSelected={roleSpacePrivilege} - onChange={(id) => updateRoleSpacePrivilege(id as KibanaRolePrivilege)} + onChange={(id) => onRoleSpacePrivilegeChange(id as KibanaRolePrivilege)} buttonSize="compressed" isFullWidth /> @@ -284,7 +315,16 @@ export const PrivilegesRolesForm: FC = (props) => { { + console.log('value returned from change!', args); + // setSpaceState() + }} + onChangeAll={(privilege) => { + // setSelectedRoles((prevRoleDefinition) => { + // prevRoleDefinition.slice(0)[0].value?.kibana[0].base.concat(privilege); + // return prevRoleDefinition; + // }); + }} kibanaPrivileges={new KibanaPrivileges(privileges?.[0]!, features)} privilegeCalculator={ new PrivilegeFormCalculator( @@ -292,6 +332,8 @@ export const PrivilegesRolesForm: FC = (props) => { selectedRoles[0].value! ) } + allSpacesSelected={false} + canCustomizeSubFeaturePrivileges={false} /> diff --git a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx similarity index 82% rename from x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx rename to x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx index 461532a502e3c..ebbe1235e9f2e 100644 --- a/x-pack/plugins/spaces/public/management/view_space/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx @@ -27,14 +27,17 @@ import type { EuiTableFieldDataColumnType, EuiTableSelectionType, } from '@elastic/eui'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; +import type { Space } from '../../../../../common'; + interface ISpaceAssignedRolesTableProps { isReadOnly: boolean; - assignedRoles: Role[]; + currentSpace: Space; + assignedRoles: Map; onClickAssignNewRole: () => Promise; onClickBulkEdit: (selectedRoles: Role[]) => void; onClickBulkRemove: (selectedRoles: Role[]) => void; @@ -57,11 +60,12 @@ export const isEditableRole = (role: Role) => { const getTableColumns = ({ isReadOnly, + currentSpace, onClickRowEditAction, onClickRowRemoveAction, }: Pick< ISpaceAssignedRolesTableProps, - 'isReadOnly' | 'onClickRowEditAction' | 'onClickRowRemoveAction' + 'isReadOnly' | 'onClickRowEditAction' | 'onClickRowRemoveAction' | 'currentSpace' >) => { const columns: Array> = [ { @@ -81,20 +85,25 @@ const getTableColumns = ({ render: (_, record) => { const uniquePrivilege = new Set( record.kibana.reduce((privilegeBaseTuple, kibanaPrivilege) => { - if (!kibanaPrivilege.base.length) { - privilegeBaseTuple.push( - i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.column.privileges.customPrivilege', - { - defaultMessage: 'custom', - } - ) - ); - - return privilegeBaseTuple; + if ( + kibanaPrivilege.spaces.includes(currentSpace.id) || + kibanaPrivilege.spaces.includes('*') + ) { + if (!kibanaPrivilege.base.length) { + privilegeBaseTuple.push( + i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.column.privileges.customPrivilege', + { + defaultMessage: 'custom', + } + ) + ); + } else { + return privilegeBaseTuple.concat(kibanaPrivilege.base); + } } - return privilegeBaseTuple.concat(kibanaPrivilege.base); + return privilegeBaseTuple; }, [] as string[]) ); @@ -113,17 +122,17 @@ const getTableColumns = ({ return React.createElement(EuiBadge, { children: _value?._reserved ? i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.reserved', - { - defaultMessage: 'Reserved', - } - ) + 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.reserved', + { + defaultMessage: 'Reserved', + } + ) : i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.custom', - { - defaultMessage: 'Custom', - } - ), + 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.custom', + { + defaultMessage: 'Custom', + } + ), color: _value?._reserved ? undefined : 'success', }); }, @@ -208,7 +217,7 @@ const getRowProps = (item: Role) => { const { name } = item; return { 'data-test-subj': `space-role-row-${name}`, - onClick: () => {}, + onClick: () => { }, }; }; @@ -224,6 +233,7 @@ const getCellProps = (item: Role, column: EuiTableFieldDataColumnType) => export const SpaceAssignedRolesTable = ({ isReadOnly, assignedRoles, + currentSpace, onClickAssignNewRole, onClickBulkEdit, onClickBulkRemove, @@ -231,10 +241,11 @@ export const SpaceAssignedRolesTable = ({ onClickRowRemoveAction, }: ISpaceAssignedRolesTableProps) => { const tableColumns = useMemo( - () => getTableColumns({ isReadOnly, onClickRowEditAction, onClickRowRemoveAction }), - [isReadOnly, onClickRowEditAction, onClickRowRemoveAction] + () => + getTableColumns({ isReadOnly, onClickRowEditAction, onClickRowRemoveAction, currentSpace }), + [currentSpace, isReadOnly, onClickRowEditAction, onClickRowRemoveAction] ); - const [rolesInView, setRolesInView] = useState(assignedRoles); + const [rolesInView, setRolesInView] = useState([]); const [selectedRoles, setSelectedRoles] = useState([]); const [isBulkActionContextOpen, setBulkActionContextOpen] = useState(false); const selectableRoles = useRef(rolesInView.filter((role) => isEditableRole(role))); @@ -243,14 +254,20 @@ export const SpaceAssignedRolesTable = ({ size: 10, }); + useEffect(() => { + setRolesInView(Array.from(assignedRoles.values())); + }, [assignedRoles]); + const onSearchQueryChange = useCallback>>( ({ query }) => { + const _assignedRolesTransformed = Array.from(assignedRoles.values()); + if (query?.text) { setRolesInView( - assignedRoles.filter((role) => role.name.includes(query.text.toLowerCase())) + _assignedRolesTransformed.filter((role) => role.name.includes(query.text.toLowerCase())) ); } else { - setRolesInView(assignedRoles); + setRolesInView(_assignedRolesTransformed); } }, [assignedRoles] @@ -382,29 +399,29 @@ export const SpaceAssignedRolesTable = ({ size: 's', ...(Boolean(selectedRoles.length) ? { - iconType: 'crossInCircle', - onClick: setSelectedRoles.bind(null, []), - children: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', - { - defaultMessage: 'Clear selection', - } - ), - } + iconType: 'crossInCircle', + onClick: setSelectedRoles.bind(null, []), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', + { + defaultMessage: 'Clear selection', + } + ), + } : { - iconType: 'pagesSelect', - onClick: setSelectedRoles.bind(null, selectableRoles.current), - children: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', - { - defaultMessage: - 'Select {count, plural, one {role} other {all {count} roles}}', - values: { - count: selectableRoles.current.length, - }, - } - ), - }), + iconType: 'pagesSelect', + onClick: setSelectedRoles.bind(null, selectableRoles.current), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', + { + defaultMessage: + 'Select {count, plural, one {role} other {all {count} roles}}', + values: { + count: selectableRoles.current.length, + }, + } + ), + }), })} diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index dee51f28c798c..037004fdd7b96 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -20,7 +20,7 @@ import { import React, { lazy, Suspense, useEffect, useState } from 'react'; import type { FC } from 'react'; -import type { Capabilities, ScopedHistory } from '@kbn/core/public'; +import type { ScopedHistory } from '@kbn/core/public'; import type { FeaturesPluginStart, KibanaFeature } from '@kbn/features-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; @@ -28,10 +28,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; -import { - ViewSpaceContextProvider, - type ViewSpaceServices, -} from './hooks/view_space_context_provider'; +import { useViewSpaceServices, useViewSpaceStore } from './provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import { SpaceSolutionBadge } from '../../space_solution_badge'; @@ -49,11 +46,10 @@ const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) => : TAB_ID_GENERAL; }; -interface PageProps extends ViewSpaceServices { +interface PageProps { spaceId?: string; history: ScopedHistory; selectedTabId?: string; - capabilities: Capabilities; getFeatures: FeaturesPluginStart['getFeatures']; onLoadSpace: (space: Space) => void; allowFeatureVisibility: boolean; @@ -68,24 +64,20 @@ const handleApiError = (error: Error) => { // FIXME: rename to EditSpacePage // FIXME: add eventTracker -export const ViewSpacePage: FC = (props) => { - const { - spaceId, - getFeatures, - spacesManager, - history, - onLoadSpace, - selectedTabId: _selectedTabId, - capabilities, - getUrlForApp, - navigateToUrl, - ...viewSpaceServices - } = props; - +export const ViewSpace: FC = ({ + spaceId, + getFeatures, + history, + onLoadSpace, + selectedTabId: _selectedTabId, + ...props +}) => { + const { state, dispatch } = useViewSpaceStore(); + const { invokeClient } = useViewSpaceServices(); + const { spacesManager, capabilities, serverBasePath } = useViewSpaceServices(); const [space, setSpace] = useState(null); const [userActiveSpace, setUserActiveSpace] = useState(null); const [features, setFeatures] = useState(null); - const [roles, setRoles] = useState([]); const [isLoadingSpace, setIsLoadingSpace] = useState(true); const [isLoadingFeatures, setIsLoadingFeatures] = useState(true); const [isLoadingRoles, setIsLoadingRoles] = useState(true); @@ -93,7 +85,9 @@ export const ViewSpacePage: FC = (props) => { const [tabs, selectedTabContent] = useTabs({ space, features, - roles, + rolesCount: state.roles.size, + capabilities, + history, currentSelectedTabId: selectedTabId, ...props, }); @@ -123,33 +117,38 @@ export const ViewSpacePage: FC = (props) => { } const getRoles = async () => { - let result: Role[] = []; - try { - result = await spacesManager.getRolesForSpace(spaceId); - } catch (error) { - const message = error?.body?.message ?? error.toString(); - const statusCode = error?.body?.statusCode ?? null; - if (statusCode === 403) { - // eslint-disable-next-line no-console - console.log('Insufficient permissions to get list of roles for the space'); - // eslint-disable-next-line no-console - console.log(message); - } else { - // eslint-disable-next-line no-console - console.error('Encountered error while getting list of roles for space!'); - // eslint-disable-next-line no-console - console.error(error); - throw error; + await invokeClient(async (clients) => { + let result: Role[] = []; + try { + result = await clients.spacesManager.getRolesForSpace(spaceId); + + dispatch({ type: 'update_roles', payload: result }); + } catch (error) { + const message = error?.body?.message ?? error.toString(); + const statusCode = error?.body?.statusCode ?? null; + if (statusCode === 403) { + // eslint-disable-next-line no-console + console.log('Insufficient permissions to get list of roles for the space'); + // eslint-disable-next-line no-console + console.log(message); + } else { + // eslint-disable-next-line no-console + console.error('Encountered error while getting list of roles for space!'); + // eslint-disable-next-line no-console + console.error(error); + throw error; + } } - } + }); - setRoles(result); setIsLoadingRoles(false); }; - // maybe we do not make this call if user can't view roles? 🤔 - getRoles().catch(handleApiError); - }, [spaceId, spacesManager]); + if (!state.roles.size) { + // maybe we do not make this call if user can't view roles? 🤔 + getRoles().catch(handleApiError); + } + }, [dispatch, invokeClient, spaceId, state.roles]); useEffect(() => { const _getFeatures = async () => { @@ -194,98 +193,90 @@ export const ViewSpacePage: FC = (props) => { return (
- - - - - - - - -

- {space.name} - {shouldShowSolutionBadge ? ( - <> - {' '} - + + + + + + +

+ {space.name} + {shouldShowSolutionBadge ? ( + <> + {' '} + + + ) : null} + {userActiveSpace?.id === id ? ( + <> + {' '} + + - - ) : null} - {userActiveSpace?.id === id ? ( - <> - {' '} - - - - - ) : null} -

-
+ + + ) : null} +

+
- -

- {space.description ?? ( - - )} -

-
-
- {userActiveSpace?.id !== id ? ( - - + +

+ {space.description ?? ( - - - ) : null} - + )} +

+
+
+ {userActiveSpace?.id !== id ? ( + + + + + + ) : null} +
- + - - - - {tabs.map((tab, index) => ( - - {tab.name} - - ))} - - - {selectedTabContent ?? null} - -
-
+ + + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + {selectedTabContent ?? null} + +
); }; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx index 6e256a14330d0..61d6ff516e027 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx @@ -18,7 +18,7 @@ import { capitalize } from 'lodash'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; -import { useViewSpaceServices } from './hooks/view_space_context_provider'; +import { useViewSpaceServices } from './provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import type { SpaceContentTypeSummaryItem } from '../../types'; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index 4d4a1a1668b0f..5f7fc4df3f3bc 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -12,7 +12,7 @@ import React from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useViewSpaceServices } from './hooks/view_space_context_provider'; +import { useViewSpaceServices } from './provider'; import type { Space } from '../../../common'; import { FeatureTable } from '../edit_space/enabled_features/feature_table'; import { SectionPanel } from '../edit_space/section_panel'; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 6b92662af420e..7e4b9ee160931 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt'; import { ViewSpaceTabFooter } from './footer'; -import { useViewSpaceServices } from './hooks/view_space_context_provider'; +import { useViewSpaceServices } from './provider'; import { ViewSpaceEnabledFeatures } from './view_space_features_tab'; import type { Space } from '../../../common'; import { ConfirmDeleteModal } from '../components'; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx index 4745bec219a0c..a4d3e11366537 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import type { FC } from 'react'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback } from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; @@ -15,83 +15,29 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { Role } from '@kbn/security-plugin-types-common'; -import { - type PrivilegesAPIClient, - PrivilegesRolesForm, - type RolesAPIClient, -} from './component/space_assign_role_privilege_form'; -import { SpaceAssignedRolesTable } from './component/space_assigned_roles_table'; -import { useViewSpaceServices } from './hooks/view_space_context_provider'; +import { useViewSpaceServices, useViewSpaceStore } from './provider'; +import { PrivilegesRolesForm } from './roles/component/space_assign_role_privilege_form'; +import { SpaceAssignedRolesTable } from './roles/component/space_assigned_roles_table'; import type { Space } from '../../../common'; interface Props { space: Space; - /** - * List of roles assigned to this space - */ - roles: Role[]; features: KibanaFeature[]; isReadOnly: boolean; } // FIXME: rename to EditSpaceAssignedRoles -export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isReadOnly }) => { - const [roleAPIClientInitialized, setRoleAPIClientInitialized] = useState(false); - const [spaceUnallocatedRole, setSpaceUnallocatedRole] = useState([]); - - const rolesAPIClient = useRef(); - const privilegesAPIClient = useRef(); - +export const ViewSpaceAssignedRoles: FC = ({ space, features, isReadOnly }) => { + const { dispatch, state } = useViewSpaceStore(); const { - getRolesAPIClient, getUrlForApp, - getPrivilegesAPIClient, overlays, theme, i18n: i18nStart, notifications, + invokeClient, } = useViewSpaceServices(); - const resolveAPIClients = useCallback(async () => { - try { - [rolesAPIClient.current, privilegesAPIClient.current] = await Promise.all([ - getRolesAPIClient(), - getPrivilegesAPIClient(), - ]); - setRoleAPIClientInitialized(true); - } catch { - // - } - }, [getPrivilegesAPIClient, getRolesAPIClient]); - - useEffect(() => { - if (!isReadOnly) { - resolveAPIClients(); - } - }, [isReadOnly, resolveAPIClients]); - - useEffect(() => { - async function fetchAllSystemRoles() { - const systemRoles = (await rolesAPIClient.current?.getRoles()) ?? []; - - // exclude roles that are already assigned to this space - const spaceUnallocatedRoles = systemRoles.filter( - (role) => - !role.metadata?._reserved && - (!role.kibana.length || - role.kibana.some((privileges) => { - return !privileges.spaces.includes(space.id) && !privileges.spaces.includes('*'); - })) - ); - - setSpaceUnallocatedRole(spaceUnallocatedRoles); - } - - if (roleAPIClientInitialized) { - fetchAllSystemRoles?.(); - } - }, [roleAPIClientInitialized, space.id]); - const showRolesPrivilegeEditor = useCallback( (defaultSelected?: Role[]) => { const overlayRef = overlays.openFlyout( @@ -116,10 +62,8 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe }, closeFlyout: () => overlayRef.close(), defaultSelected, - spaceUnallocatedRole, - // APIClient would have been initialized before the privilege editor is displayed - roleAPIClient: rolesAPIClient.current!, - privilegesAPIClient: privilegesAPIClient.current!, + storeDispatch: dispatch, + spacesClientsInvocator: invokeClient, }} />, { theme, i18n: i18nStart } @@ -129,7 +73,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe } ); }, - [features, i18nStart, notifications.toasts, overlays, space, spaceUnallocatedRole, theme] + [dispatch, features, i18nStart, invokeClient, notifications.toasts, overlays, space, theme] ); const removeRole = useCallback( @@ -152,20 +96,24 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe return roleDef; }); - await rolesAPIClient.current?.bulkUpdateRoles({ rolesUpdate: updateDoc }).then(() => - notifications.toasts.addSuccess( - i18n.translate('xpack.spaces.management.spaceDetails.roles.removalSuccessMsg', { - defaultMessage: - 'Removed {count, plural, one {role} other {{count} roles}} from {spaceName} space', - values: { - spaceName: space.name, - count: updateDoc.length, - }, - }) - ) - ); + await invokeClient((clients) => { + return clients.rolesClient.bulkUpdateRoles({ rolesUpdate: updateDoc }).then(() => + notifications.toasts.addSuccess( + i18n.translate('xpack.spaces.management.spaceDetails.roles.removalSuccessMsg', { + defaultMessage: + 'Removed {count, plural, one {role} other {{count} roles}} from {spaceName} space', + values: { + spaceName: space.name, + count: updateDoc.length, + }, + }) + ) + ); + }); + + dispatch({ type: 'remove_roles', payload: updateDoc }); }, - [notifications.toasts, space.id, space.name] + [dispatch, invokeClient, notifications.toasts, space.id, space.name] ); return ( @@ -192,7 +140,8 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe showRolesPrivilegeEditor([rowRecord])} onClickBulkRemove={async (selectedRoles) => { @@ -202,9 +151,6 @@ export const ViewSpaceAssignedRoles: FC = ({ space, roles, features, isRe await removeRole([rowRecord]); }} onClickAssignNewRole={async () => { - if (!roleAPIClientInitialized) { - await resolveAPIClients(); - } showRolesPrivilegeEditor(); }} /> diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 8210ab2d7a1cc..138afbf01121f 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -11,7 +11,6 @@ import React from 'react'; import type { Capabilities, ScopedHistory } from '@kbn/core/public'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; -import type { Role } from '@kbn/security-plugin-types-common'; import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; @@ -29,7 +28,7 @@ export interface ViewSpaceTab { export interface GetTabsProps { space: Space; - roles: Role[]; + rolesCount: number; features: KibanaFeature[]; history: ScopedHistory; capabilities: Capabilities & { @@ -68,7 +67,7 @@ export const getTabs = ({ features, history, capabilities, - roles, + rolesCount, ...props }: GetTabsProps): ViewSpaceTab[] => { const canUserViewRoles = Boolean(capabilities?.roles?.view); @@ -105,13 +104,12 @@ export const getTabs = ({ }), append: ( - {roles.length} + {rolesCount} ), content: ( From a50eade2a6d8dbaa8cc620e2ed8941ae613cae06 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 15 Aug 2024 07:53:06 +0200 Subject: [PATCH 112/129] fix logic for excluding roles already ppart of space --- .../roles/component/space_assign_role_privilege_form.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx index ece27928bbb66..45ebb05c8259b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx @@ -106,9 +106,9 @@ export const PrivilegesRolesForm: FC = (props) => { (role) => !role.metadata?._reserved && (!role.kibana.length || - role.kibana.some((rolePrivileges) => { - return ( - !rolePrivileges.spaces.includes(space.id!) && !rolePrivileges.spaces.includes('*') + role.kibana.every((rolePrivileges) => { + return !( + rolePrivileges.spaces.includes(space.id!) || rolePrivileges.spaces.includes('*') ); })) ) From ad1e5263c3446897ecd87caa6da99d71e2d80227 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 15 Aug 2024 08:03:38 +0200 Subject: [PATCH 113/129] fix logic with selectable items --- .../component/space_assigned_roles_table.tsx | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx index ebbe1235e9f2e..daa9a863b7471 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx @@ -27,7 +27,7 @@ import type { EuiTableFieldDataColumnType, EuiTableSelectionType, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { Role } from '@kbn/security-plugin-types-common'; @@ -122,17 +122,17 @@ const getTableColumns = ({ return React.createElement(EuiBadge, { children: _value?._reserved ? i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.reserved', - { - defaultMessage: 'Reserved', - } - ) + 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.reserved', + { + defaultMessage: 'Reserved', + } + ) : i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.custom', - { - defaultMessage: 'Custom', - } - ), + 'xpack.spaces.management.spaceDetails.rolesTable.column.roleType.custom', + { + defaultMessage: 'Custom', + } + ), color: _value?._reserved ? undefined : 'success', }); }, @@ -217,7 +217,7 @@ const getRowProps = (item: Role) => { const { name } = item; return { 'data-test-subj': `space-role-row-${name}`, - onClick: () => { }, + onClick: () => {}, }; }; @@ -248,7 +248,6 @@ export const SpaceAssignedRolesTable = ({ const [rolesInView, setRolesInView] = useState([]); const [selectedRoles, setSelectedRoles] = useState([]); const [isBulkActionContextOpen, setBulkActionContextOpen] = useState(false); - const selectableRoles = useRef(rolesInView.filter((role) => isEditableRole(role))); const [pagination, setPagination] = useState['page']>({ index: 0, size: 10, @@ -303,6 +302,8 @@ export const SpaceAssignedRolesTable = ({ const pageSize = pagination.size; const pageIndex = pagination.index; + const selectableRoles = rolesInView.filter((role) => isEditableRole(role)); + return ( @@ -399,29 +400,29 @@ export const SpaceAssignedRolesTable = ({ size: 's', ...(Boolean(selectedRoles.length) ? { - iconType: 'crossInCircle', - onClick: setSelectedRoles.bind(null, []), - children: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', - { - defaultMessage: 'Clear selection', - } - ), - } + iconType: 'crossInCircle', + onClick: setSelectedRoles.bind(null, []), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', + { + defaultMessage: 'Clear selection', + } + ), + } : { - iconType: 'pagesSelect', - onClick: setSelectedRoles.bind(null, selectableRoles.current), - children: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', - { - defaultMessage: - 'Select {count, plural, one {role} other {all {count} roles}}', - values: { - count: selectableRoles.current.length, - }, - } - ), - }), + iconType: 'pagesSelect', + onClick: setSelectedRoles.bind(null, selectableRoles), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', + { + defaultMessage: + 'Select {count, plural, one {role} other {all {count} roles}}', + values: { + count: selectableRoles.length, + }, + } + ), + }), })} @@ -437,7 +438,7 @@ export const SpaceAssignedRolesTable = ({ onClickBulkRemove, pagination.index, pagination.size, - rolesInView.length, + rolesInView, selectedRoles, ]); From f51800b1025f7073a8c5c1d30f1fb62b2b642a3a Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Thu, 15 Aug 2024 11:47:00 +0200 Subject: [PATCH 114/129] add implementation for assigning custom roles privileges --- .../space_assign_role_privilege_form.tsx | 220 ++++++++++-------- 1 file changed, 127 insertions(+), 93 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx index 45ebb05c8259b..60c9e0da83c34 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx @@ -56,6 +56,7 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { const { + space, onSaveCompleted, closeFlyout, features, @@ -63,42 +64,54 @@ export const PrivilegesRolesForm: FC = (props) => { spacesClientsInvocator, storeDispatch, } = props; - const [space, setSpaceState] = useState>(props.space); const [assigningToRole, setAssigningToRole] = useState(false); - const [fetchingSystemRoles, setFetchingSystemRoles] = useState(false); - const [privileges, setPrivileges] = useState<[RawKibanaPrivileges] | null>(null); + const [fetchingDataDeps, setFetchingDataDeps] = useState(false); + const [kibanaPrivileges, setKibanaPrivileges] = useState(null); const [spaceUnallocatedRoles, setSpaceUnallocatedRole] = useState([]); const [selectedRoles, setSelectedRoles] = useState>( createRolesComboBoxOptions(defaultSelected) ); - const selectedRolesCombinedPrivileges = useMemo(() => { + const [roleCustomizationAnchor, setRoleCustomizationAnchor] = useState({ + value: selectedRoles?.[0]?.value, + privilegeIndex: 0, + }); + const selectedRolesCombinedPrivileges = useMemo(() => { const combinedPrivilege = new Set( selectedRoles.reduce((result, selectedRole) => { - let match: Array> = []; + let match: KibanaRolePrivilege[] = []; for (let i = 0; i < selectedRole.value!.kibana.length; i++) { - if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { + const { spaces, base } = selectedRole.value!.kibana[i]; + if (spaces.includes(space.id!)) { // @ts-ignore - TODO resolve this - match = selectedRole.value!.kibana[i].base; + match = base.length ? base : ['custom']; break; } } return result.concat(match); - }, [] as Array>) + }, [] as KibanaRolePrivilege[]) ); return Array.from(combinedPrivilege); }, [selectedRoles, space.id]); const [roleSpacePrivilege, setRoleSpacePrivilege] = useState( - selectedRolesCombinedPrivileges.length === 1 ? selectedRolesCombinedPrivileges[0] : 'all' + !selectedRoles.length || selectedRolesCombinedPrivileges.length > 1 + ? 'all' + : selectedRolesCombinedPrivileges[0] ); useEffect(() => { - async function fetchAllSystemRoles() { - setFetchingSystemRoles(true); - const systemRoles = await spacesClientsInvocator((clients) => clients.rolesClient.getRoles()); + async function fetchAllSystemRoles(spaceId: string) { + setFetchingDataDeps(true); + + const [systemRoles, _kibanaPrivileges] = await Promise.all([ + spacesClientsInvocator((clients) => clients.rolesClient.getRoles()), + spacesClientsInvocator((clients) => + clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }) + ), + ]); // exclude roles that are already assigned to this space setSpaceUnallocatedRole( @@ -108,57 +121,85 @@ export const PrivilegesRolesForm: FC = (props) => { (!role.kibana.length || role.kibana.every((rolePrivileges) => { return !( - rolePrivileges.spaces.includes(space.id!) || rolePrivileges.spaces.includes('*') + rolePrivileges.spaces.includes(spaceId) || rolePrivileges.spaces.includes('*') ); })) ) ); + + setKibanaPrivileges(_kibanaPrivileges); } - fetchAllSystemRoles().finally(() => setFetchingSystemRoles(false)); + fetchAllSystemRoles(space.id!).finally(() => setFetchingDataDeps(false)); }, [space.id, spacesClientsInvocator]); useEffect(() => { - Promise.all([ - spacesClientsInvocator((clients) => - clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }) - ), - spacesClientsInvocator((clients) => clients.privilegesClient.getBuiltIn()), - ]).then( - ([kibanaPrivileges, builtInESPrivileges]) => - setPrivileges([kibanaPrivileges, builtInESPrivileges]) - // (err) => fatalErrors.add(err) - ); - }, [spacesClientsInvocator]); - - const onRoleSpacePrivilegeChange = useCallback( - (spacePrivilege: KibanaRolePrivilege) => { - // persist select privilege for UI - setRoleSpacePrivilege(spacePrivilege); - - // update preselected roles with new privilege - setSelectedRoles((prevSelectedRoles) => { - return structuredClone(prevSelectedRoles).map((selectedRole) => { - for (let i = 0; i < selectedRole.value!.kibana.length; i++) { - if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { - selectedRole.value!.kibana[i].base = - spacePrivilege === 'custom' ? [] : [spacePrivilege]; + if (roleSpacePrivilege === 'custom') { + let anchor: typeof roleCustomizationAnchor | null = null; + + /** + * when custom privilege is selected we selected the first role that already has a custom privilege + * and use that as the starting point for all customizations that will happen to all the other selected roles + */ + for (let i = 0; i < selectedRoles.length; i++) { + for (let j = 0; i < selectedRoles[i].value?.kibana!.length!; j++) { + let iterationIndexPrivilegeValue; + + // check that the current iteration has a value, since roles can have uneven privilege defs + if ((iterationIndexPrivilegeValue = selectedRoles[i].value?.kibana[j])) { + const { spaces, base } = iterationIndexPrivilegeValue; + if (spaces.includes(space.id) && !base.length) { + anchor = { + value: structuredClone(selectedRoles[i].value), + privilegeIndex: j, + }; break; } } + } - return selectedRole; - }); - }); - }, - [space.id] - ); + if (anchor) break; + } + + if (anchor) setRoleCustomizationAnchor(anchor); + } + }, [selectedRoles, roleSpacePrivilege, space.id]); + + const onRoleSpacePrivilegeChange = useCallback((spacePrivilege: KibanaRolePrivilege) => { + // persist selected privilege for UI + setRoleSpacePrivilege(spacePrivilege); + }, []); const assignRolesToSpace = useCallback(async () => { try { setAssigningToRole(true); - const updatedRoles = selectedRoles.map((role) => role.value!); + const newPrivileges = { + base: roleSpacePrivilege === 'custom' ? [] : [roleSpacePrivilege], + feature: + roleSpacePrivilege === 'custom' + ? roleCustomizationAnchor.value?.kibana[roleCustomizationAnchor.privilegeIndex].feature! + : {}, + }; + + const updatedRoles = structuredClone(selectedRoles).map((selectedRole) => { + let found = false; + + // TODO: account for case where previous assignment included multiple spaces assigned to a particular base + for (let i = 0; i < selectedRole.value!.kibana.length; i++) { + if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { + Object.assign(selectedRole.value!.kibana[i], newPrivileges); + found = true; + break; + } + } + + if (!found) { + selectedRole.value?.kibana.push(Object.assign({ spaces: [space.id] }, newPrivileges)); + } + + return selectedRole.value!; + }); await spacesClientsInvocator((clients) => clients.rolesClient @@ -175,7 +216,15 @@ export const PrivilegesRolesForm: FC = (props) => { } catch (err) { // Handle resulting error } - }, [onSaveCompleted, selectedRoles, spacesClientsInvocator, storeDispatch]); + }, [ + selectedRoles, + spacesClientsInvocator, + storeDispatch, + onSaveCompleted, + space.id, + roleSpacePrivilege, + roleCustomizationAnchor, + ]); const getForm = () => { return ( @@ -187,7 +236,7 @@ export const PrivilegesRolesForm: FC = (props) => { defaultMessage: 'Select role to assign to the {spaceName} space', values: { spaceName: space.name }, })} - isLoading={fetchingSystemRoles} + isLoading={fetchingDataDeps} placeholder={i18n.translate( 'xpack.spaces.management.spaceDetails.roles.selectRolesPlaceholder', { @@ -196,29 +245,7 @@ export const PrivilegesRolesForm: FC = (props) => { )} options={createRolesComboBoxOptions(spaceUnallocatedRoles)} selectedOptions={selectedRoles} - onChange={(value) => { - setSelectedRoles((prevRoles) => { - if (prevRoles.length < value.length) { - const newlyAdded = value[value.length - 1]; - const { id: spaceId } = space; - - if (!spaceId) { - throw new Error('space state requires space to have an ID'); - } - - // Add new kibana privilege definition particular for the current space to role - newlyAdded.value!.kibana.push({ - base: roleSpacePrivilege === 'custom' ? [] : [roleSpacePrivilege], - feature: {}, - spaces: [spaceId], - }); - - return prevRoles.concat(newlyAdded); - } else { - return value; - } - }); - }} + onChange={(value) => setSelectedRoles(value)} fullWidth /> @@ -312,29 +339,36 @@ export const PrivilegesRolesForm: FC = (props) => { {/** TODO: rework privilege table to accommodate operating on multiple roles */} - { - console.log('value returned from change!', args); - // setSpaceState() - }} - onChangeAll={(privilege) => { - // setSelectedRoles((prevRoleDefinition) => { - // prevRoleDefinition.slice(0)[0].value?.kibana[0].base.concat(privilege); - // return prevRoleDefinition; - // }); - }} - kibanaPrivileges={new KibanaPrivileges(privileges?.[0]!, features)} - privilegeCalculator={ - new PrivilegeFormCalculator( - new KibanaPrivileges(privileges?.[0]!, features), - selectedRoles[0].value! - ) - } - allSpacesSelected={false} - canCustomizeSubFeaturePrivileges={false} - /> + + {!kibanaPrivileges ? ( +

loading...

+ ) : ( + { + // apply selected changes only to customization anchor, this delay we delay reconciling the intending privileges + // of the selected roles till we decide to commit the changes chosen + setRoleCustomizationAnchor(({ value, privilegeIndex }) => { + value!.kibana[privilegeIndex].feature[featureId] = selectedPrivileges; + return { value, privilegeIndex }; + }); + }} + onChangeAll={(privilege) => { + // dummy function we wouldn't be using this + }} + kibanaPrivileges={new KibanaPrivileges(kibanaPrivileges, features)} + privilegeCalculator={ + new PrivilegeFormCalculator( + new KibanaPrivileges(kibanaPrivileges, features), + selectedRoles[0].value! + ) + } + allSpacesSelected={false} + canCustomizeSubFeaturePrivileges={false} + /> + )} +
)} From 8be80768b3eda6c19c478ff74c671702fe9692af Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 19 Aug 2024 11:35:57 +0200 Subject: [PATCH 115/129] refactor logic for selecting role cutomization anchor --- .../space_assign_role_privilege_form.tsx | 146 ++++++++++++------ 1 file changed, 101 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx index 60c9e0da83c34..cb3bfd1b0f367 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx @@ -18,6 +18,7 @@ import { EuiFlyoutHeader, EuiForm, EuiFormRow, + EuiLoadingSpinner, EuiSpacer, EuiText, EuiTitle, @@ -71,9 +72,17 @@ export const PrivilegesRolesForm: FC = (props) => { const [selectedRoles, setSelectedRoles] = useState>( createRolesComboBoxOptions(defaultSelected) ); - const [roleCustomizationAnchor, setRoleCustomizationAnchor] = useState({ - value: selectedRoles?.[0]?.value, - privilegeIndex: 0, + const [roleCustomizationAnchor, setRoleCustomizationAnchor] = useState(() => { + // support instance where the form is opened with roles already preselected + const defaultAnchor = selectedRoles?.[0]?.value; + const privilegeIndex = defaultAnchor?.kibana.findIndex(({ spaces }) => + spaces.includes(space.id!) + ); + + return { + value: defaultAnchor, + privilegeIndex: (privilegeIndex || -1) >= 0 ? privilegeIndex : 0, + }; }); const selectedRolesCombinedPrivileges = useMemo(() => { @@ -83,8 +92,7 @@ export const PrivilegesRolesForm: FC = (props) => { for (let i = 0; i < selectedRole.value!.kibana.length; i++) { const { spaces, base } = selectedRole.value!.kibana[i]; if (spaces.includes(space.id!)) { - // @ts-ignore - TODO resolve this - match = base.length ? base : ['custom']; + match = (base.length ? base : ['custom']) as [KibanaRolePrivilege]; break; } } @@ -98,12 +106,12 @@ export const PrivilegesRolesForm: FC = (props) => { const [roleSpacePrivilege, setRoleSpacePrivilege] = useState( !selectedRoles.length || selectedRolesCombinedPrivileges.length > 1 - ? 'all' + ? 'read' : selectedRolesCombinedPrivileges[0] ); useEffect(() => { - async function fetchAllSystemRoles(spaceId: string) { + async function fetchRequiredData(spaceId: string) { setFetchingDataDeps(true); const [systemRoles, _kibanaPrivileges] = await Promise.all([ @@ -130,45 +138,79 @@ export const PrivilegesRolesForm: FC = (props) => { setKibanaPrivileges(_kibanaPrivileges); } - fetchAllSystemRoles(space.id!).finally(() => setFetchingDataDeps(false)); + fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); }, [space.id, spacesClientsInvocator]); - useEffect(() => { - if (roleSpacePrivilege === 'custom') { + const computeRoleCustomizationAnchor = useCallback( + (spaceId: string, _selectedRoles: ReturnType) => { let anchor: typeof roleCustomizationAnchor | null = null; - /** - * when custom privilege is selected we selected the first role that already has a custom privilege - * and use that as the starting point for all customizations that will happen to all the other selected roles - */ - for (let i = 0; i < selectedRoles.length; i++) { - for (let j = 0; i < selectedRoles[i].value?.kibana!.length!; j++) { - let iterationIndexPrivilegeValue; - - // check that the current iteration has a value, since roles can have uneven privilege defs - if ((iterationIndexPrivilegeValue = selectedRoles[i].value?.kibana[j])) { - const { spaces, base } = iterationIndexPrivilegeValue; - if (spaces.includes(space.id) && !base.length) { - anchor = { - value: structuredClone(selectedRoles[i].value), - privilegeIndex: j, - }; - break; + for (let i = 0; i < _selectedRoles.length; i++) { + let role; + + if ((role = _selectedRoles[i].value)) { + for (let j = 0; j < _selectedRoles[i].value!.kibana.length; j++) { + let privilegeIterationIndexValue; + + if ((privilegeIterationIndexValue = role.kibana[j])) { + const { spaces, base } = privilegeIterationIndexValue; + /* + * check to see if current role already has a custom privilege, if it does we use that as the starting point for all customizations + * that will happen to all the other selected roles and exit + */ + if (spaces.includes(spaceId) && !base.length) { + anchor = { + value: structuredClone(role), + privilegeIndex: j, + }; + + break; + } } } } if (anchor) break; + + // provide a fallback anchor if no suitable anchor was discovered, and we have reached the end of selected roles iteration + if (!anchor && role && i === _selectedRoles.length - 1) { + const fallbackRole = structuredClone(role); + + const spacePrivilegeIndex = fallbackRole.kibana.findIndex(({ spaces }) => + spaces.includes(spaceId) + ); + + anchor = { + value: fallbackRole, + privilegeIndex: + (spacePrivilegeIndex || -1) >= 0 + ? spacePrivilegeIndex + : (fallbackRole?.kibana?.push?.({ + spaces: [spaceId], + base: [], + feature: {}, + }) || 0) - 1, + }; + } } - if (anchor) setRoleCustomizationAnchor(anchor); - } - }, [selectedRoles, roleSpacePrivilege, space.id]); + return anchor; + }, + [] + ); - const onRoleSpacePrivilegeChange = useCallback((spacePrivilege: KibanaRolePrivilege) => { - // persist selected privilege for UI - setRoleSpacePrivilege(spacePrivilege); - }, []); + const onRoleSpacePrivilegeChange = useCallback( + (spacePrivilege: KibanaRolePrivilege) => { + if (spacePrivilege === 'custom') { + const _roleCustomizationAnchor = computeRoleCustomizationAnchor(space.id, selectedRoles); + if (_roleCustomizationAnchor) setRoleCustomizationAnchor(_roleCustomizationAnchor); + } + + // persist selected privilege for UI + setRoleSpacePrivilege(spacePrivilege); + }, + [computeRoleCustomizationAnchor, selectedRoles, space.id] + ); const assignRolesToSpace = useCallback(async () => { try { @@ -178,18 +220,28 @@ export const PrivilegesRolesForm: FC = (props) => { base: roleSpacePrivilege === 'custom' ? [] : [roleSpacePrivilege], feature: roleSpacePrivilege === 'custom' - ? roleCustomizationAnchor.value?.kibana[roleCustomizationAnchor.privilegeIndex].feature! + ? roleCustomizationAnchor.value?.kibana[roleCustomizationAnchor.privilegeIndex!] + .feature! : {}, }; const updatedRoles = structuredClone(selectedRoles).map((selectedRole) => { let found = false; - // TODO: account for case where previous assignment included multiple spaces assigned to a particular base for (let i = 0; i < selectedRole.value!.kibana.length; i++) { - if (selectedRole.value!.kibana[i].spaces.includes(space.id!)) { - Object.assign(selectedRole.value!.kibana[i], newPrivileges); - found = true; + const { spaces } = selectedRole.value!.kibana[i]; + + if (spaces.includes(space.id!)) { + if (spaces.length > 1) { + // space belongs to a collection of other spaces that share the same privileges, + // so we have to assign the new privilege to apply only to the specific space + // hence we remove the space from the shared privilege + spaces.splice(i, 1); + } else { + Object.assign(selectedRole.value!.kibana[i], newPrivileges); + found = true; + } + break; } } @@ -338,19 +390,23 @@ export const PrivilegesRolesForm: FC = (props) => {

- {/** TODO: rework privilege table to accommodate operating on multiple roles */} {!kibanaPrivileges ? ( -

loading...

+ ) : ( { - // apply selected changes only to customization anchor, this delay we delay reconciling the intending privileges - // of the selected roles till we decide to commit the changes chosen + // apply selected changes only to customization anchor, this way we delay reconciling the intending privileges + // of the selected roles till we decide to commit the changes chosen setRoleCustomizationAnchor(({ value, privilegeIndex }) => { - value!.kibana[privilegeIndex].feature[featureId] = selectedPrivileges; + let privilege; + + if ((privilege = value!.kibana?.[privilegeIndex!])) { + privilege.feature[featureId] = selectedPrivileges; + } + return { value, privilegeIndex }; }); }} @@ -361,7 +417,7 @@ export const PrivilegesRolesForm: FC = (props) => { privilegeCalculator={ new PrivilegeFormCalculator( new KibanaPrivileges(kibanaPrivileges, features), - selectedRoles[0].value! + roleCustomizationAnchor.value! ) } allSpacesSelected={false} From e91dff48e6ad643b66c87e8367aea10c60571aae Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Wed, 21 Aug 2024 18:11:49 +0200 Subject: [PATCH 116/129] UI cleanup --- .../component/space_assigned_roles_table.tsx | 4 +++- .../public/management/view_space/utils.ts | 22 ------------------- .../management/view_space/view_space_tabs.tsx | 5 +---- 3 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 x-pack/plugins/spaces/public/management/view_space/utils.ts diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx index daa9a863b7471..8b12c5f69c9a6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx @@ -360,6 +360,8 @@ export const SpaceAssignedRolesTable = ({ panels={[ { id: 0, + size: 's', + width: 180, items: [ { icon: , @@ -473,7 +475,7 @@ export const SpaceAssignedRolesTable = ({ pagination={{ pageSize: pagination.size, pageIndex: pagination.index, - pageSizeOptions: [50, 25, 10, 0], + pageSizeOptions: [50, 25, 10], }} onChange={onTableChange} /> diff --git a/x-pack/plugins/spaces/public/management/view_space/utils.ts b/x-pack/plugins/spaces/public/management/view_space/utils.ts deleted file mode 100644 index 2492c8b081df9..0000000000000 --- a/x-pack/plugins/spaces/public/management/view_space/utils.ts +++ /dev/null @@ -1,22 +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 type { Role } from '@kbn/security-plugin-types-common'; - -import type { Space } from '../../../common'; - -export const filterRolesAssignedToSpace = (roles: Role[], space: Space) => { - return roles.filter((role) => - role.kibana.reduce((acc, cur) => { - return ( - (cur.spaces.includes(space.name) || cur.spaces.includes('*')) && - Boolean(cur.base.length) && - acc - ); - }, true) - ); -}; diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx index 138afbf01121f..15a8b831bd46d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx @@ -14,7 +14,6 @@ import { i18n } from '@kbn/i18n'; import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; -// import { filterRolesAssignedToSpace } from './utils'; import type { Space } from '../../../common'; // FIXME: rename to EditSpaceTab @@ -95,12 +94,10 @@ export const getTabs = ({ ]; if (canUserViewRoles) { - // const rolesAssignedToSpace = filterRolesAssignedToSpace(roles, space); - tabsDefinition.push({ id: TAB_ID_ROLES, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.roles.heading', { - defaultMessage: 'Roles', + defaultMessage: 'Assigned roles', }), append: ( From 1b7d51689a1b3c020d5d1b736a638f5ffff0d12f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:17:01 +0000 Subject: [PATCH 117/129] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/packages/security/plugin_types_public/tsconfig.json | 3 ++- x-pack/plugins/spaces/tsconfig.json | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/packages/security/plugin_types_public/tsconfig.json b/x-pack/packages/security/plugin_types_public/tsconfig.json index 5c97e25656ecf..4a5db65acaf42 100644 --- a/x-pack/packages/security/plugin_types_public/tsconfig.json +++ b/x-pack/packages/security/plugin_types_public/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/core-user-profile-common", "@kbn/security-plugin-types-common", "@kbn/core-security-common", - "@kbn/security-authorization-core" + "@kbn/security-authorization-core", + "@kbn/security-role-management-model" ] } diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index dbcb925f9cc5c..a59765a8d866b 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -42,9 +42,10 @@ "@kbn/security-plugin-types-common", "@kbn/core-application-browser", "@kbn/unsaved-changes-prompt", - "@kbn/core-http-browser", - "@kbn/core-overlays-browser", - "@kbn/core-notifications-browser", + "@kbn/core-lifecycle-browser", + "@kbn/security-role-management-model", + "@kbn/security-ui-components", + "@kbn/react-kibana-mount", "@kbn/shared-ux-utility", "@kbn/core-application-common", ], From 7f5cff9c318e489017bbf996f6a42706c6115f3c Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 26 Aug 2024 15:51:05 +0200 Subject: [PATCH 118/129] fix failing tests --- .../management/spaces_management_app.test.tsx | 2 +- .../view_space/view_space_general_tab.test.tsx | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index c5a2672f61513..c3a5b8560da36 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -175,7 +175,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true} + Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"theme":{"theme$":{}},"i18n":{},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true} `); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx index bad47aa9d2ca2..6240242710feb 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx @@ -10,18 +10,21 @@ import React from 'react'; import { httpServiceMock, + i18nServiceMock, notificationServiceMock, overlayServiceMock, scopedHistoryMock, + themeServiceMock, } from '@kbn/core/public/mocks'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { KibanaFeature } from '@kbn/features-plugin/common'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { ViewSpaceContextProvider } from './hooks/view_space_context_provider'; +import { ViewSpaceProvider } from './provider/view_space_provider'; import { ViewSpaceSettings } from './view_space_general_tab'; import type { SolutionView } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; +import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true }; @@ -30,11 +33,14 @@ const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); const spacesManager = spacesManagerMock.create(); const getRolesAPIClient = getRolesAPIClientMock(); +const getPrivilegeAPIClient = getPrivilegeAPIClientMock(); const reloadWindow = jest.fn(); const http = httpServiceMock.createStartContract(); const notifications = notificationServiceMock.createStartContract(); const overlays = overlayServiceMock.createStartContract(); +const theme = themeServiceMock.createStartContract(); +const i18n = i18nServiceMock.createStartContract(); const navigateSpy = jest.spyOn(history, 'push').mockImplementation(() => {}); const updateSpaceSpy = jest @@ -54,7 +60,7 @@ describe('ViewSpaceSettings', () => { const TestComponent: React.FC = ({ children }) => { return ( - { http={http} notifications={notifications} overlays={overlays} + getPrivilegesAPIClient={getPrivilegeAPIClient} + theme={theme} + i18n={i18n} > {children} - + ); }; From 27b1428acc9d3023cc5a05b9dd1938ccb902cc9a Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 26 Aug 2024 16:47:37 +0200 Subject: [PATCH 119/129] UI tweaks --- .../component/space_assigned_roles_table.tsx | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx index 8b12c5f69c9a6..8e61c080af7c6 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx @@ -398,34 +398,35 @@ export const SpaceAssignedRolesTable = ({
- {React.createElement(EuiButtonEmpty, { - size: 's', - ...(Boolean(selectedRoles.length) - ? { - iconType: 'crossInCircle', - onClick: setSelectedRoles.bind(null, []), - children: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', - { - defaultMessage: 'Clear selection', - } - ), - } - : { - iconType: 'pagesSelect', - onClick: setSelectedRoles.bind(null, selectableRoles), - children: i18n.translate( - 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', - { - defaultMessage: - 'Select {count, plural, one {role} other {all {count} roles}}', - values: { - count: selectableRoles.length, - }, - } - ), - }), - })} + {Boolean(selectableRoles.length) && + React.createElement(EuiButtonEmpty, { + size: 's', + ...(Boolean(selectedRoles.length) + ? { + iconType: 'crossInCircle', + onClick: setSelectedRoles.bind(null, []), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.clearRolesSelection', + { + defaultMessage: 'Clear selection', + } + ), + } + : { + iconType: 'pagesSelect', + onClick: setSelectedRoles.bind(null, selectableRoles), + children: i18n.translate( + 'xpack.spaces.management.spaceDetails.rolesTable.selectAllRoles', + { + defaultMessage: + 'Select {count, plural, one {role} other {all {count} roles}}', + values: { + count: selectableRoles.length, + }, + } + ), + }), + })} From 3fb9d8e097718382cccd1311ce67e63769ee033a Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 26 Aug 2024 18:49:56 +0200 Subject: [PATCH 120/129] add tests for view space provider --- .../provider/view_space_provider.test.tsx | 110 ++++++++++++++++++ .../provider/view_space_provider.tsx | 22 +--- 2 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx new file mode 100644 index 0000000000000..872454da0afc5 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx @@ -0,0 +1,110 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import type { PropsWithChildren } from 'react'; +import React from 'react'; + +import { + httpServiceMock, + i18nServiceMock, + notificationServiceMock, + overlayServiceMock, + themeServiceMock, +} from '@kbn/core/public/mocks'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; + +import { useViewSpaceServices, useViewSpaceStore, ViewSpaceProvider } from './view_space_provider'; +import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock'; +import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock'; +import { getRolesAPIClientMock } from '../../roles_api_client.mock'; + +const http = httpServiceMock.createStartContract(); +const notifications = notificationServiceMock.createStartContract(); +const overlays = overlayServiceMock.createStartContract(); +const theme = themeServiceMock.createStartContract(); +const i18n = i18nServiceMock.createStartContract(); + +const spacesManager = spacesManagerMock.create(); + +const SUTProvider = ({ + children, + capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + spaces: { manage: true }, + }, +}: PropsWithChildren>>) => { + return ( + + _, + getRolesAPIClient: getRolesAPIClientMock, + getPrivilegesAPIClient: getPrivilegeAPIClientMock, + navigateToUrl: jest.fn(), + capabilities, + }} + > + {children} + + + ); +}; + +describe('ViewSpaceProvider', () => { + describe('useViewSpaceServices', () => { + it('returns an object of predefined properties', () => { + const { result } = renderHook(useViewSpaceServices, { wrapper: SUTProvider }); + + expect(result.current).toEqual( + expect.objectContaining({ + invokeClient: expect.any(Function), + }) + ); + }); + + it('throws when the hook is used within a tree that does not have the provider', () => { + const { result } = renderHook(useViewSpaceServices); + expect(result.error).toBeDefined(); + expect(result.error?.message).toEqual( + expect.stringMatching('ViewSpaceService Context is missing.') + ); + }); + }); + + describe('useViewSpaceStore', () => { + it('returns an object of predefined properties', () => { + const { result } = renderHook(useViewSpaceStore, { wrapper: SUTProvider }); + + expect(result.current).toEqual( + expect.objectContaining({ + state: expect.objectContaining({ roles: expect.any(Map) }), + dispatch: expect.any(Function), + }) + ); + }); + + it('throws when the hook is used within a tree that does not have the provider', () => { + const { result } = renderHook(useViewSpaceStore); + + expect(result.error).toBeDefined(); + expect(result.error?.message).toEqual( + expect.stringMatching('ViewSpaceStore Context is missing.') + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx b/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx index e2f31b15d7df1..86732f63b5fdf 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx @@ -9,7 +9,6 @@ import { once } from 'lodash'; import React, { createContext, type Dispatch, - type FC, type PropsWithChildren, useCallback, useContext, @@ -46,9 +45,7 @@ export interface ViewSpaceProviderProps export interface ViewSpaceServices extends Omit { - invokeClient Promise>( - arg: ARG - ): ReturnType; + invokeClient(arg: (clients: ViewSpaceClients) => Promise): Promise; } interface ViewSpaceClients { @@ -62,24 +59,17 @@ export interface ViewSpaceStore { dispatch: Dispatch; } -const createSpaceRolesContext = once(() => - createContext({ - state: { - roles: [], - }, - dispatch: () => { }, - }) -); +const createSpaceRolesContext = once(() => createContext(null)); const createViewSpaceServicesContext = once(() => createContext(null)); // FIXME: rename to EditSpaceProvider -export const ViewSpaceProvider: FC> = ({ +export const ViewSpaceProvider = ({ children, getRolesAPIClient, getPrivilegesAPIClient, ...services -}) => { +}: PropsWithChildren) => { const ViewSpaceStoreContext = createSpaceRolesContext(); const ViewSpaceServicesContext = createViewSpaceServicesContext(); @@ -113,8 +103,8 @@ export const ViewSpaceProvider: FC> = createInitialState ); - const invokeClient = useCallback( - async (...args: Parameters) => { + const invokeClient: ViewSpaceServices['invokeClient'] = useCallback( + async (...args) => { await resolveAPIClients(); return args[0]({ From f18eba3af08b33122d8bc09f516db98e28d1835d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 27 Aug 2024 07:52:25 +0000 Subject: [PATCH 121/129] [CI] Auto-commit changed files from 'node scripts/notice' [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/spaces/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index a59765a8d866b..bde909653702d 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -48,6 +48,7 @@ "@kbn/react-kibana-mount", "@kbn/shared-ux-utility", "@kbn/core-application-common", + "@kbn/security-authorization-core", ], "exclude": [ "target/**/*", From d29d066bb1a9c79ac3979e16fd08941d7c8f44c3 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Mon, 26 Aug 2024 17:43:12 +0200 Subject: [PATCH 122/129] add tests for space assign role privilege form --- .../space_assign_role_privilege_form.test.tsx | 203 ++++++++++++++++++ .../space_assign_role_privilege_form.tsx | 20 +- 2 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx new file mode 100644 index 0000000000000..bf645d1d17178 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -0,0 +1,203 @@ +/* + * 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 { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import crypto from 'crypto'; +import React from 'react'; + +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import type { Role } from '@kbn/security-plugin-types-common'; +import { + createRawKibanaPrivileges, + kibanaFeatures, +} from '@kbn/security-role-management-model/src/__fixtures__'; + +import { PrivilegesRolesForm } from './space_assign_role_privilege_form'; +import type { Space } from '../../../../../common'; +import { createPrivilegeAPIClientMock } from '../../../privilege_api_client.mock'; +import { createRolesAPIClientMock } from '../../../roles_api_client.mock'; + +const rolesAPIClient = createRolesAPIClientMock(); +const privilegeAPIClient = createPrivilegeAPIClientMock(); + +const createRole = (roleName: string, kibana: Role['kibana'] = []): Role => { + return { + name: roleName, + elasticsearch: { cluster: [], run_as: [], indices: [] }, + kibana, + }; +}; + +const space: Space = { + id: crypto.randomUUID(), + name: 'Odyssey', + description: 'Journey vs. Destination', + disabledFeatures: [], +}; + +const spacesClientsInvocatorMock = jest.fn((fn) => + fn({ + rolesClient: rolesAPIClient, + privilegesClient: privilegeAPIClient, + }) +); +const dispatchMock = jest.fn(); +const onSaveCompleted = jest.fn(); +const closeFlyout = jest.fn(); + +const renderPrivilegeRolesForm = ({ + preSelectedRoles, +}: { + preSelectedRoles?: Role[]; +} = {}) => { + return render( + + + + ); +}; + +describe('PrivilegesRolesForm', () => { + let getRolesSpy: jest.SpiedFunction['getRoles']>; + let getAllKibanaPrivilegeSpy: jest.SpiedFunction< + ReturnType['getAll'] + >; + + beforeAll(() => { + getRolesSpy = jest.spyOn(rolesAPIClient, 'getRoles'); + getAllKibanaPrivilegeSpy = jest.spyOn(privilegeAPIClient, 'getAll'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the privilege permission selector disabled when no role is selected', async () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + renderPrivilegeRolesForm(); + + await waitFor(() => null); + + ['all', 'read', 'custom'].forEach((privilege) => { + expect(screen.getByTestId(`${privilege}-privilege-button`)).toBeDisabled(); + }); + }); + + it('preselects the privilege of the selected role when one is provided', async () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const privilege = 'all'; + + renderPrivilegeRolesForm({ + preSelectedRoles: [ + createRole('test_role_1', [{ base: [privilege], feature: {}, spaces: [space.id] }]), + ], + }); + + await waitFor(() => null); + + expect(screen.getByTestId(`${privilege}-privilege-button`)).toHaveAttribute( + 'aria-pressed', + String(true) + ); + }); + + it('displays a warning message when roles with different privilege levels are selected', async () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const roles: Role[] = [ + createRole('test_role_1', [{ base: ['all'], feature: {}, spaces: [space.id] }]), + createRole('test_role_2', [{ base: ['read'], feature: {}, spaces: [space.id] }]), + ]; + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await waitFor(() => null); + + expect(screen.getByTestId('privilege-conflict-callout')).toBeInTheDocument(); + }); + + describe('applying custom privileges', () => { + it('displays the privilege customization form, when custom privilege button is selected', async () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const roles: Role[] = [ + createRole('test_role_1', [{ base: ['all'], feature: {}, spaces: [space.id] }]), + ]; + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await waitFor(() => null); + + expect(screen.queryByTestId('rolePrivilegeCustomizationForm')).not.toBeInTheDocument(); + + userEvent.click(screen.getByTestId('custom-privilege-button')); + + expect(screen.getByTestId('rolePrivilegeCustomizationForm')).toBeInTheDocument(); + }); + + it('for a selection of roles pre-assigned to a space, the first encountered privilege with a custom privilege is used as the starting point', async () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featureIds: string[] = kibanaFeatures.map((kibanaFeature) => kibanaFeature.id); + + const roles: Role[] = [ + createRole('test_role_1', [{ base: ['all'], feature: {}, spaces: [space.id] }]), + createRole('test_role_2', [ + { base: [], feature: { [featureIds[0]]: ['all'] }, spaces: [space.id] }, + ]), + createRole('test_role_3', [{ base: ['read'], feature: {}, spaces: [space.id] }]), + createRole('test_role_4', [{ base: ['read'], feature: {}, spaces: [space.id] }]), + createRole('test_role_5', [ + { base: [], feature: { [featureIds[0]]: ['read'] }, spaces: [space.id] }, + ]), + ]; + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await waitFor(() => null); + + expect(screen.queryByTestId('rolePrivilegeCustomizationForm')).not.toBeInTheDocument(); + + userEvent.click(screen.getByTestId('custom-privilege-button')); + + expect(screen.getByTestId('rolePrivilegeCustomizationForm')).toBeInTheDocument(); + + expect(screen.queryByTestId(`${featureIds[0]}_read`)).not.toHaveAttribute( + 'aria-pressed', + String(true) + ); + + expect(screen.getByTestId(`${featureIds[0]}_all`)).toHaveAttribute( + 'aria-pressed', + String(true) + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx index cb3bfd1b0f367..e8b281bf3e2ab 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx @@ -30,8 +30,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { KibanaFeature, KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { type RawKibanaPrivileges } from '@kbn/security-authorization-core'; import type { Role } from '@kbn/security-plugin-types-common'; -import { KibanaPrivileges, type RawKibanaPrivileges } from '@kbn/security-role-management-model'; +import { KibanaPrivileges } from '@kbn/security-role-management-model'; import { KibanaPrivilegeTable, PrivilegeFormCalculator } from '@kbn/security-ui-components'; import type { Space } from '../../../../../common'; @@ -105,7 +106,7 @@ export const PrivilegesRolesForm: FC = (props) => { }, [selectedRoles, space.id]); const [roleSpacePrivilege, setRoleSpacePrivilege] = useState( - !selectedRoles.length || selectedRolesCombinedPrivileges.length > 1 + !selectedRoles.length || !selectedRolesCombinedPrivileges.length ? 'read' : selectedRolesCombinedPrivileges[0] ); @@ -114,12 +115,12 @@ export const PrivilegesRolesForm: FC = (props) => { async function fetchRequiredData(spaceId: string) { setFetchingDataDeps(true); - const [systemRoles, _kibanaPrivileges] = await Promise.all([ - spacesClientsInvocator((clients) => clients.rolesClient.getRoles()), - spacesClientsInvocator((clients) => - clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }) - ), - ]); + const [systemRoles, _kibanaPrivileges] = await spacesClientsInvocator((clients) => + Promise.all([ + clients.rolesClient.getRoles(), + clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }), + ]) + ); // exclude roles that are already assigned to this space setSpaceUnallocatedRole( @@ -307,6 +308,7 @@ export const PrivilegesRolesForm: FC = (props) => { = (props) => { )} > = (props) => { {roleSpacePrivilege === 'custom' && ( Date: Tue, 27 Aug 2024 18:47:03 +0000 Subject: [PATCH 123/129] [CI] Auto-commit changed files from 'node scripts/notice' --- x-pack/packages/security/plugin_types_public/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/packages/security/plugin_types_public/tsconfig.json b/x-pack/packages/security/plugin_types_public/tsconfig.json index 4a5db65acaf42..305d4411b42e5 100644 --- a/x-pack/packages/security/plugin_types_public/tsconfig.json +++ b/x-pack/packages/security/plugin_types_public/tsconfig.json @@ -15,6 +15,5 @@ "@kbn/security-plugin-types-common", "@kbn/core-security-common", "@kbn/security-authorization-core", - "@kbn/security-role-management-model" ] } From 18c0d8365c0963c595218f67921bb3e12663c382 Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Tue, 27 Aug 2024 23:45:09 +0200 Subject: [PATCH 124/129] pass appropriate types to component --- .../management/view_space/view_space_general_tab.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx index 6240242710feb..81f4de6680ac7 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx @@ -32,8 +32,8 @@ const history = scopedHistoryMock.create(); const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); const spacesManager = spacesManagerMock.create(); -const getRolesAPIClient = getRolesAPIClientMock(); -const getPrivilegeAPIClient = getPrivilegeAPIClientMock(); +const getRolesAPIClient = getRolesAPIClientMock; +const getPrivilegeAPIClient = getPrivilegeAPIClientMock; const reloadWindow = jest.fn(); const http = httpServiceMock.createStartContract(); From 699088d19675985a8f09e3c488c672bfb3b332de Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 28 Aug 2024 10:58:17 -0700 Subject: [PATCH 125/129] remove the EuiText that surrounds the view-space-page --- .../management/view_space/view_space.tsx | 154 +++++++++--------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx index 037004fdd7b96..e4c0c6db284c8 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space.tsx @@ -193,90 +193,88 @@ export const ViewSpace: FC = ({ return (
- - - - - - - -

- {space.name} - {shouldShowSolutionBadge ? ( - <> - {' '} - + + + + + +

+ {space.name} + {shouldShowSolutionBadge ? ( + <> + {' '} + + + ) : null} + {userActiveSpace?.id === id ? ( + <> + {' '} + + - - ) : null} - {userActiveSpace?.id === id ? ( - <> - {' '} - - - - - ) : null} -

-
+ + + ) : null} +

+
- -

- {space.description ?? ( - - )} -

-
-
- {userActiveSpace?.id !== id ? ( - - + +

+ {space.description ?? ( - - - ) : null} - + )} +

+
+
+ {userActiveSpace?.id !== id ? ( + + + + + + ) : null} +
- + - - - - {tabs.map((tab, index) => ( - - {tab.name} - - ))} - - - {selectedTabContent ?? null} - -
+ + + + {tabs.map((tab, index) => ( + + {tab.name} + + ))} + + + {selectedTabContent ?? null} +
); }; From 70fb39faec93e29b7131e430e33ec0bb80f8aa94 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 29 Aug 2024 11:26:11 -0700 Subject: [PATCH 126/129] --wip-- [skip ci] --- .../public/management/view_space/view_space_features_tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx index 5f7fc4df3f3bc..9a4b63a89b669 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx @@ -23,7 +23,7 @@ interface Props { onChange: (updatedSpace: Partial) => void; } -// FIXME: rename to EditSpaceEnabledFeatures +// FIXME: rename to EditSpaceEnabledFeaturesPanel export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { const { capabilities, getUrlForApp } = useViewSpaceServices(); const canManageRoles = capabilities.management?.security?.roles === true; From cfb79727022ecd0bdd6b6a1ae572b8913b06f162 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 29 Aug 2024 15:42:07 -0700 Subject: [PATCH 127/129] fix ts --- .../management/view_space/view_space_general_tab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx index 7e4b9ee160931..2c252f5f674fd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx +++ b/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx @@ -225,7 +225,12 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . {props.allowSolutionVisibility && ( <> - + )} From 467bea8151789cc2cac284e01777e2c875c88582 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 29 Aug 2024 16:15:38 -0700 Subject: [PATCH 128/129] file/folder reorg --- .../get_supported_response_actions.ts | 2 +- ...irm_alter_active_space_modal.test.tsx.snap | 0 .../confirm_alter_active_space_modal.test.tsx | 0 .../confirm_alter_active_space_modal.tsx | 0 .../confirm_alter_active_space_modal/index.ts | 0 .../customize_space.test.tsx.snap | 0 .../customize_space_avatar.test.tsx.snap | 0 .../customize_space/customize_space.test.tsx | 0 .../customize_space/customize_space.tsx | 2 +- .../customize_space_avatar.test.tsx | 0 .../customize_space_avatar.tsx | 2 +- .../customize_space/index.ts | 0 .../enabled_features.test.tsx.snap | 0 .../enabled_features.test.tsx | 0 .../enabled_features/enabled_features.tsx | 0 .../enabled_features/feature_table.tsx | 0 .../enabled_features/index.ts | 0 .../enabled_features/toggle_all_features.scss | 0 .../enabled_features/toggle_all_features.tsx | 0 .../__snapshots__/section_panel.test.tsx.snap | 0 .../section_panel/index.ts | 0 .../section_panel/section_panel.scss | 0 .../section_panel/section_panel.test.tsx | 0 .../section_panel/section_panel.tsx | 0 .../solution_view/index.ts | 0 .../solution_view/solution_view.tsx | 0 .../create_space_page.test.tsx} | 30 ++++----- .../create_space_page.tsx} | 12 ++-- .../public/management/create_space/index.ts | 9 +++ .../{view_space => edit_space}/constants.ts | 0 .../edit_space.tsx} | 12 ++-- .../edit_space_content_tab.tsx} | 6 +- .../edit_space_features_tab.tsx} | 19 +++--- .../edit_space_general_tab.test.tsx} | 26 ++++---- .../edit_space_general_tab.tsx} | 23 ++++--- .../edit_space_page.tsx} | 18 +++--- .../edit_space_roles_tab.tsx} | 9 ++- .../edit_space_tabs.tsx} | 31 +++++----- .../{view_space => edit_space}/footer.tsx | 3 +- .../hooks/use_tabs.ts | 4 +- .../public/management/edit_space/index.ts | 2 +- .../provider/index.ts | 8 +-- .../provider/reducers/index.ts | 4 +- .../provider/view_space_provider.test.tsx | 24 ++++---- .../provider/view_space_provider.tsx | 61 +++++++++---------- .../space_assign_role_privilege_form.test.tsx | 1 + .../space_assign_role_privilege_form.tsx | 6 +- .../component/space_assigned_roles_table.tsx | 0 .../public/management/lib/validate_space.ts | 2 +- .../management/spaces_management_app.test.tsx | 16 ++--- .../management/spaces_management_app.tsx | 6 +- 51 files changed, 169 insertions(+), 169 deletions(-) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/__snapshots__/customize_space.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space.tsx (99%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space_avatar.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space_avatar.tsx (99%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/__snapshots__/enabled_features.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/enabled_features.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/enabled_features.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/feature_table.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/toggle_all_features.scss (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/toggle_all_features.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/__snapshots__/section_panel.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/section_panel.scss (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/section_panel.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/section_panel.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/solution_view/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/solution_view/solution_view.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space/manage_space_page.test.tsx => create_space/create_space_page.test.tsx} (97%) rename x-pack/plugins/spaces/public/management/{edit_space/manage_space_page.tsx => create_space/create_space_page.tsx} (97%) create mode 100644 x-pack/plugins/spaces/public/management/create_space/index.ts rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/constants.ts (100%) rename x-pack/plugins/spaces/public/management/{view_space/view_space.tsx => edit_space/edit_space.tsx} (96%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_content_tab.tsx => edit_space/edit_space_content_tab.tsx} (94%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_features_tab.tsx => edit_space/edit_space_features_tab.tsx} (79%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_general_tab.test.tsx => edit_space/edit_space_general_tab.test.tsx} (96%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_general_tab.tsx => edit_space/edit_space_general_tab.tsx} (91%) rename x-pack/plugins/spaces/public/management/{view_space/index.tsx => edit_space/edit_space_page.tsx} (67%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_roles.tsx => edit_space/edit_space_roles_tab.tsx} (94%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_tabs.tsx => edit_space/edit_space_tabs.tsx} (77%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/footer.tsx (95%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/hooks/use_tabs.ts (89%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/index.ts (72%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/reducers/index.ts (91%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/view_space_provider.test.tsx (81%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/view_space_provider.tsx (61%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/roles/component/space_assign_role_privilege_form.test.tsx (99%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/roles/component/space_assign_role_privilege_form.tsx (99%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/roles/component/space_assigned_roles_table.tsx (100%) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts index 95fc300a3fe57..fdce9f4d1e682 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { EnabledFeatures } from '@kbn/spaces-plugin/public/management/edit_space/enabled_features'; +import type { EnabledFeatures } from '@kbn/spaces-plugin/public/management/components/enabled_features'; import { ResponseActionTypes, ResponseActionTypesEnum, diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.test.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.test.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx index 33113f3338960..1d0e04694604b 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx +++ b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx @@ -22,9 +22,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; import { getSpaceAvatarComponent, getSpaceColor, getSpaceInitials } from '../../../space_avatar'; +import type { FormValues } from '../../create_space'; import type { SpaceValidator } from '../../lib'; import { toSpaceIdentifier } from '../../lib'; -import type { FormValues } from '../manage_space_page'; 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. diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx index 827ef592459f7..185a90532c126 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { MAX_SPACE_INITIALS } from '../../../../common'; import { encode, imageTypes } from '../../../../common/lib/dataurl'; +import type { FormValues } from '../../create_space'; import type { SpaceValidator } from '../../lib'; -import type { FormValues } from '../manage_space_page'; interface Props { space: FormValues; diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/index.ts b/x-pack/plugins/spaces/public/management/components/customize_space/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/index.ts rename to x-pack/plugins/spaces/public/management/components/customize_space/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/feature_table.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/feature_table.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/index.ts b/x-pack/plugins/spaces/public/management/components/enabled_features/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/index.ts rename to x-pack/plugins/spaces/public/management/components/enabled_features/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.scss b/x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.scss similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.scss rename to x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.scss diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/section_panel/__snapshots__/section_panel.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/section_panel/__snapshots__/section_panel.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/index.ts b/x-pack/plugins/spaces/public/management/components/section_panel/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/index.ts rename to x-pack/plugins/spaces/public/management/components/section_panel/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.scss b/x-pack/plugins/spaces/public/management/components/section_panel/section_panel.scss similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.scss rename to x-pack/plugins/spaces/public/management/components/section_panel/section_panel.scss diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx b/x-pack/plugins/spaces/public/management/components/section_panel/section_panel.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx rename to x-pack/plugins/spaces/public/management/components/section_panel/section_panel.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx b/x-pack/plugins/spaces/public/management/components/section_panel/section_panel.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx rename to x-pack/plugins/spaces/public/management/components/section_panel/section_panel.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/solution_view/index.ts b/x-pack/plugins/spaces/public/management/components/solution_view/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/solution_view/index.ts rename to x-pack/plugins/spaces/public/management/components/solution_view/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx b/x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx rename to x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx similarity index 97% rename from x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx rename to x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx index ac60902920fa9..801601d9f9f4e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx @@ -18,13 +18,13 @@ import { KibanaFeature } from '@kbn/features-plugin/public'; import { featuresPluginMock } from '@kbn/features-plugin/public/mocks'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; -import { EnabledFeatures } from './enabled_features'; -import { ManageSpacePage } from './manage_space_page'; +import { CreateSpacePage } from './create_space_page'; 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. // https://github.com/elastic/eui/issues/3712 @@ -70,7 +70,7 @@ describe('ManageSpacePage', () => { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { const spacesManager = spacesManagerMock.create(); const wrapper = mountWithIntl( - { const onLoadSpace = jest.fn(); const wrapper = mountWithIntl( - { const onLoadSpace = jest.fn(); const wrapper = mountWithIntl( - { const notifications = notificationServiceMock.createStartContract(); const wrapper = mountWithIntl( - Promise.reject(error)} notifications={notifications} @@ -542,7 +542,7 @@ describe('ManageSpacePage', () => { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { +export class CreateSpacePage extends Component { private readonly validator: SpaceValidator; constructor(props: Props) { diff --git a/x-pack/plugins/spaces/public/management/create_space/index.ts b/x-pack/plugins/spaces/public/management/create_space/index.ts new file mode 100644 index 0000000000000..16705209eb450 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/create_space/index.ts @@ -0,0 +1,9 @@ +/* + * 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 type { FormValues } from './create_space_page'; +export { CreateSpacePage } from './create_space_page'; diff --git a/x-pack/plugins/spaces/public/management/view_space/constants.ts b/x-pack/plugins/spaces/public/management/edit_space/constants.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/view_space/constants.ts rename to x-pack/plugins/spaces/public/management/edit_space/constants.ts diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx similarity index 96% rename from x-pack/plugins/spaces/public/management/view_space/view_space.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx index e4c0c6db284c8..a2b4cc9cd65d1 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx @@ -28,7 +28,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; -import { useViewSpaceServices, useViewSpaceStore } from './provider'; +import { useEditSpaceServices, useEditSpaceStore } from './provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import { SpaceSolutionBadge } from '../../space_solution_badge'; @@ -62,9 +62,7 @@ const handleApiError = (error: Error) => { throw error; }; -// FIXME: rename to EditSpacePage -// FIXME: add eventTracker -export const ViewSpace: FC = ({ +export const EditSpace: FC = ({ spaceId, getFeatures, history, @@ -72,9 +70,9 @@ export const ViewSpace: FC = ({ selectedTabId: _selectedTabId, ...props }) => { - const { state, dispatch } = useViewSpaceStore(); - const { invokeClient } = useViewSpaceServices(); - const { spacesManager, capabilities, serverBasePath } = useViewSpaceServices(); + const { state, dispatch } = useEditSpaceStore(); + const { invokeClient } = useEditSpaceServices(); + const { spacesManager, capabilities, serverBasePath } = useEditSpaceServices(); const [space, setSpace] = useState(null); const [userActiveSpace, setUserActiveSpace] = useState(null); const [features, setFeatures] = useState(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.tsx similarity index 94% rename from x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.tsx index 61d6ff516e027..dcb7b17d26e06 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.tsx @@ -18,7 +18,7 @@ import { capitalize } from 'lodash'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; -import { useViewSpaceServices } from './provider'; +import { useEditSpaceServices } from './provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import type { SpaceContentTypeSummaryItem } from '../../types'; @@ -28,9 +28,9 @@ const handleApiError = (error: Error) => { throw error; }; -export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { +export const EditSpaceContentTab: FC<{ space: Space }> = ({ space }) => { const { id: spaceId } = space; - const { spacesManager, serverBasePath } = useViewSpaceServices(); + const { spacesManager, serverBasePath } = useEditSpaceServices(); const [isLoading, setIsLoading] = useState(true); const [items, setItems] = useState(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx similarity index 79% rename from x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx index 9a4b63a89b669..eacb29c1a098d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx @@ -12,10 +12,10 @@ import React from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useViewSpaceServices } from './provider'; +import { useEditSpaceServices } from './provider'; import type { Space } from '../../../common'; -import { FeatureTable } from '../edit_space/enabled_features/feature_table'; -import { SectionPanel } from '../edit_space/section_panel'; +import { FeatureTable } from '../components/enabled_features/feature_table'; +import { SectionPanel } from '../components/section_panel'; interface Props { space: Partial; @@ -23,9 +23,8 @@ interface Props { onChange: (updatedSpace: Partial) => void; } -// FIXME: rename to EditSpaceEnabledFeaturesPanel -export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { - const { capabilities, getUrlForApp } = useViewSpaceServices(); +export const EditSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { + const { capabilities, getUrlForApp } = useEditSpaceServices(); const canManageRoles = capabilities.management?.security?.roles === true; if (!features) { @@ -39,7 +38,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange

@@ -48,19 +47,19 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange

) : ( ), diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx similarity index 96% rename from x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx index 81f4de6680ac7..b1518c197c247 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx @@ -20,8 +20,8 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { KibanaFeature } from '@kbn/features-plugin/common'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { ViewSpaceProvider } from './provider/view_space_provider'; -import { ViewSpaceSettings } from './view_space_general_tab'; +import { EditSpaceSettingsTab } from './edit_space_general_tab'; +import { EditSpaceProvider } from './provider/view_space_provider'; import type { SolutionView } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; @@ -50,7 +50,7 @@ const deleteSpaceSpy = jest .spyOn(spacesManager, 'deleteSpace') .mockImplementation(() => Promise.resolve()); -describe('ViewSpaceSettings', () => { +describe('EditSpaceSettings', () => { beforeEach(() => { navigateSpy.mockReset(); updateSpaceSpy.mockReset(); @@ -60,7 +60,7 @@ describe('ViewSpaceSettings', () => { const TestComponent: React.FC = ({ children }) => { return ( - { i18n={i18n} > {children} - + ); }; @@ -88,7 +88,7 @@ describe('ViewSpaceSettings', () => { it('should render controls for initial state of editing a space', () => { render( - { it('shows solution view select when visible', async () => { render( - { render( - { render( - { render( - { render( - { render( - { render( - void; } -// FIXME: rename to EditSpaceSettings -export const ViewSpaceSettings: React.FC = ({ space, features, history, ...props }) => { +export const EditSpaceSettingsTab: React.FC = ({ space, features, history, ...props }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button const [showUserImpactWarning, setShowUserImpactWarning] = useState(false); const [showAlteringActiveSpaceDialog, setShowAlteringActiveSpaceDialog] = useState(false); const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); - const { http, overlays, notifications, navigateToUrl, spacesManager } = useViewSpaceServices(); + const { http, overlays, notifications, navigateToUrl, spacesManager } = useEditSpaceServices(); const [solution, setSolution] = useState(space.solution); @@ -237,7 +236,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . {props.allowFeatureVisibility && (solution == null || solution === 'classic') && ( <> - = ({ space, features, history, . {doShowUserImpactWarning()} - & ViewSpaceProviderProps; +type EditSpacePageProps = ComponentProps & EditSpaceProviderProps; -export function ViewSpacePage({ +export function EditSpacePage({ spaceId, getFeatures, history, @@ -22,11 +22,11 @@ export function ViewSpacePage({ allowFeatureVisibility, allowSolutionVisibility, children, - ...viewSpaceServicesProps -}: PropsWithChildren) { + ...editSpaceServicesProps +}: PropsWithChildren) { return ( - - + - + ); } diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx similarity index 94% rename from x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx index a4d3e11366537..64381d5c1bd5b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { Role } from '@kbn/security-plugin-types-common'; -import { useViewSpaceServices, useViewSpaceStore } from './provider'; +import { useEditSpaceServices, useEditSpaceStore } from './provider'; import { PrivilegesRolesForm } from './roles/component/space_assign_role_privilege_form'; import { SpaceAssignedRolesTable } from './roles/component/space_assigned_roles_table'; import type { Space } from '../../../common'; @@ -26,9 +26,8 @@ interface Props { isReadOnly: boolean; } -// FIXME: rename to EditSpaceAssignedRoles -export const ViewSpaceAssignedRoles: FC = ({ space, features, isReadOnly }) => { - const { dispatch, state } = useViewSpaceStore(); +export const EditSpaceAssignedRolesTab: FC = ({ space, features, isReadOnly }) => { + const { dispatch, state } = useEditSpaceStore(); const { getUrlForApp, overlays, @@ -36,7 +35,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, features, isReadOnly i18n: i18nStart, notifications, invokeClient, - } = useViewSpaceServices(); + } = useEditSpaceServices(); const showRolesPrivilegeEditor = useCallback( (defaultSelected?: Role[]) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_tabs.tsx similarity index 77% rename from x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_tabs.tsx index 15a8b831bd46d..176df46f42deb 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_tabs.tsx @@ -16,8 +16,7 @@ import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import type { Space } from '../../../common'; -// FIXME: rename to EditSpaceTab -export interface ViewSpaceTab { +export interface EditSpaceTab { id: string; name: string; content: JSX.Element; @@ -37,26 +36,26 @@ export interface GetTabsProps { allowSolutionVisibility: boolean; } -const SuspenseViewSpaceSettings = withSuspense( +const SuspenseEditSpaceSettingsTab = withSuspense( React.lazy(() => - import('./view_space_general_tab').then(({ ViewSpaceSettings }) => ({ - default: ViewSpaceSettings, + import('./edit_space_general_tab').then(({ EditSpaceSettingsTab }) => ({ + default: EditSpaceSettingsTab, })) ) ); -const SuspenseViewSpaceAssignedRoles = withSuspense( +const SuspenseEditSpaceAssignedRolesTab = withSuspense( React.lazy(() => - import('./view_space_roles').then(({ ViewSpaceAssignedRoles }) => ({ - default: ViewSpaceAssignedRoles, + import('./edit_space_roles_tab').then(({ EditSpaceAssignedRolesTab }) => ({ + default: EditSpaceAssignedRolesTab, })) ) ); -const SuspenseViewSpaceContent = withSuspense( +const SuspenseEditSpaceContentTab = withSuspense( React.lazy(() => - import('./view_space_content_tab').then(({ ViewSpaceContent }) => ({ - default: ViewSpaceContent, + import('./edit_space_content_tab').then(({ EditSpaceContentTab }) => ({ + default: EditSpaceContentTab, })) ) ); @@ -68,21 +67,21 @@ export const getTabs = ({ capabilities, rolesCount, ...props -}: GetTabsProps): ViewSpaceTab[] => { +}: GetTabsProps): EditSpaceTab[] => { const canUserViewRoles = Boolean(capabilities?.roles?.view); const canUserModifyRoles = Boolean(capabilities?.roles?.save); const reloadWindow = () => { window.location.reload(); }; - const tabsDefinition: ViewSpaceTab[] = [ + const tabsDefinition: EditSpaceTab[] = [ { id: TAB_ID_GENERAL, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { defaultMessage: 'General settings', }), content: ( - ), content: ( - , + content: , }); return tabsDefinition; diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/edit_space/footer.tsx similarity index 95% rename from x-pack/plugins/spaces/public/management/view_space/footer.tsx rename to x-pack/plugins/spaces/public/management/edit_space/footer.tsx index 14f036a0ee13c..ab66706f83cee 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/footer.tsx @@ -22,8 +22,7 @@ interface Props { onClickDeleteSpace: () => void; } -// FIXME: rename to EditSpaceTabFooter -export const ViewSpaceTabFooter: React.FC = ({ +export const EditSpaceTabFooter: React.FC = ({ isDirty, isLoading, onClickCancel, diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/edit_space/hooks/use_tabs.ts similarity index 89% rename from x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts rename to x-pack/plugins/spaces/public/management/edit_space/hooks/use_tabs.ts index 1623f19920bcd..fc583e54b0693 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/hooks/use_tabs.ts @@ -11,7 +11,7 @@ import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/public'; import type { Space } from '../../../../common'; -import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs'; +import { type EditSpaceTab, getTabs, type GetTabsProps } from '../edit_space_tabs'; type UseTabsProps = Pick & { space: Space | null; @@ -27,7 +27,7 @@ export const useTabs = ({ features, currentSelectedTabId, ...getTabsArgs -}: UseTabsProps): [ViewSpaceTab[], JSX.Element | undefined] => { +}: UseTabsProps): [EditSpaceTab[], JSX.Element | undefined] => { const [tabs, selectedTabContent] = useMemo(() => { if (space === null || features === null) { return [[]]; diff --git a/x-pack/plugins/spaces/public/management/edit_space/index.ts b/x-pack/plugins/spaces/public/management/edit_space/index.ts index 78c3b0fc42e04..c85e8f1c2e499 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ManageSpacePage } from './manage_space_page'; +export { EditSpacePage } from './edit_space_page'; diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts similarity index 72% rename from x-pack/plugins/spaces/public/management/view_space/provider/index.ts rename to x-pack/plugins/spaces/public/management/edit_space/provider/index.ts index 74c713ee2e56a..8f70d38f27d44 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts @@ -5,9 +5,9 @@ * 2.0. */ -export { ViewSpaceProvider, useViewSpaceServices, useViewSpaceStore } from './view_space_provider'; +export { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './view_space_provider'; export type { - ViewSpaceProviderProps, - ViewSpaceServices, - ViewSpaceStore, + EditSpaceProviderProps, + EditSpaceServices, + EditSpaceStore, } from './view_space_provider'; diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/reducers/index.ts similarity index 91% rename from x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts rename to x-pack/plugins/spaces/public/management/edit_space/provider/reducers/index.ts index 6040b69d3ba9d..f640ef95c7147 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/reducers/index.ts @@ -20,12 +20,12 @@ export type IDispatchAction = payload: any; }; -export interface IViewSpaceStoreState { +export interface IEditSpaceStoreState { /** roles assigned to current space */ roles: Map; } -export const createSpaceRolesReducer: Reducer = ( +export const createSpaceRolesReducer: Reducer = ( state, action ) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.test.tsx similarity index 81% rename from x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx rename to x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.test.tsx index 872454da0afc5..2d06ddb23fadd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.test.tsx @@ -19,7 +19,7 @@ import { import type { ApplicationStart } from '@kbn/core-application-browser'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { useViewSpaceServices, useViewSpaceStore, ViewSpaceProvider } from './view_space_provider'; +import { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './view_space_provider'; import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../../roles_api_client.mock'; @@ -43,7 +43,7 @@ const SUTProvider = ({ }: PropsWithChildren>>) => { return ( - {children} - + ); }; -describe('ViewSpaceProvider', () => { - describe('useViewSpaceServices', () => { +describe('EditSpaceProvider', () => { + describe('useEditSpaceServices', () => { it('returns an object of predefined properties', () => { - const { result } = renderHook(useViewSpaceServices, { wrapper: SUTProvider }); + const { result } = renderHook(useEditSpaceServices, { wrapper: SUTProvider }); expect(result.current).toEqual( expect.objectContaining({ @@ -78,17 +78,17 @@ describe('ViewSpaceProvider', () => { }); it('throws when the hook is used within a tree that does not have the provider', () => { - const { result } = renderHook(useViewSpaceServices); + const { result } = renderHook(useEditSpaceServices); expect(result.error).toBeDefined(); expect(result.error?.message).toEqual( - expect.stringMatching('ViewSpaceService Context is missing.') + expect.stringMatching('EditSpaceService Context is missing.') ); }); }); - describe('useViewSpaceStore', () => { + describe('useEditSpaceStore', () => { it('returns an object of predefined properties', () => { - const { result } = renderHook(useViewSpaceStore, { wrapper: SUTProvider }); + const { result } = renderHook(useEditSpaceStore, { wrapper: SUTProvider }); expect(result.current).toEqual( expect.objectContaining({ @@ -99,11 +99,11 @@ describe('ViewSpaceProvider', () => { }); it('throws when the hook is used within a tree that does not have the provider', () => { - const { result } = renderHook(useViewSpaceStore); + const { result } = renderHook(useEditSpaceStore); expect(result.error).toBeDefined(); expect(result.error?.message).toEqual( - expect.stringMatching('ViewSpaceStore Context is missing.') + expect.stringMatching('EditSpaceStore Context is missing.') ); }); }); diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.tsx similarity index 61% rename from x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx rename to x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.tsx index 86732f63b5fdf..8de7b96dcc6d2 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.tsx @@ -27,12 +27,11 @@ import type { import { createSpaceRolesReducer, type IDispatchAction, - type IViewSpaceStoreState, + type IEditSpaceStoreState, } from './reducers'; import type { SpacesManager } from '../../../spaces_manager'; -// FIXME: rename to EditSpaceServices -export interface ViewSpaceProviderProps +export interface EditSpaceProviderProps extends Pick { capabilities: ApplicationStart['capabilities']; getUrlForApp: ApplicationStart['getUrlForApp']; @@ -43,41 +42,40 @@ export interface ViewSpaceProviderProps getPrivilegesAPIClient: () => Promise; } -export interface ViewSpaceServices - extends Omit { - invokeClient(arg: (clients: ViewSpaceClients) => Promise): Promise; +export interface EditSpaceServices + extends Omit { + invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; } -interface ViewSpaceClients { - spacesManager: ViewSpaceProviderProps['spacesManager']; +interface EditSpaceClients { + spacesManager: EditSpaceProviderProps['spacesManager']; rolesClient: RolesAPIClient; privilegesClient: PrivilegesAPIClientPublicContract; } -export interface ViewSpaceStore { - state: IViewSpaceStoreState; +export interface EditSpaceStore { + state: IEditSpaceStoreState; dispatch: Dispatch; } -const createSpaceRolesContext = once(() => createContext(null)); +const createSpaceRolesContext = once(() => createContext(null)); -const createViewSpaceServicesContext = once(() => createContext(null)); +const createEditSpaceServicesContext = once(() => createContext(null)); -// FIXME: rename to EditSpaceProvider -export const ViewSpaceProvider = ({ +export const EditSpaceProvider = ({ children, getRolesAPIClient, getPrivilegesAPIClient, ...services -}: PropsWithChildren) => { - const ViewSpaceStoreContext = createSpaceRolesContext(); - const ViewSpaceServicesContext = createViewSpaceServicesContext(); +}: PropsWithChildren) => { + const EditSpaceStoreContext = createSpaceRolesContext(); + const EditSpaceServicesContext = createEditSpaceServicesContext(); const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()])); const rolesAPIClientRef = useRef(); const privilegesClientRef = useRef(); - const initialStoreState = useRef({ + const initialStoreState = useRef({ roles: new Map(), }); @@ -93,7 +91,7 @@ export const ViewSpaceProvider = ({ resolveAPIClients(); }, [resolveAPIClients]); - const createInitialState = useCallback((state: IViewSpaceStoreState) => { + const createInitialState = useCallback((state: IEditSpaceStoreState) => { return state; }, []); @@ -103,7 +101,7 @@ export const ViewSpaceProvider = ({ createInitialState ); - const invokeClient: ViewSpaceServices['invokeClient'] = useCallback( + const invokeClient: EditSpaceServices['invokeClient'] = useCallback( async (...args) => { await resolveAPIClients(); @@ -117,38 +115,37 @@ export const ViewSpaceProvider = ({ ); return ( - - + + {children} - - + + ); }; -// FIXME: rename to useEditSpaceServices -export const useViewSpaceServices = (): ViewSpaceServices => { - const context = useContext(createViewSpaceServicesContext()); +export const useEditSpaceServices = (): EditSpaceServices => { + const context = useContext(createEditSpaceServicesContext()); if (!context) { throw new Error( - 'ViewSpaceService Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider' + 'EditSpaceService Context is missing. Ensure the component or React root is wrapped with EditSpaceProvider' ); } return context; }; -export const useViewSpaceStore = () => { +export const useEditSpaceStore = () => { const context = useContext(createSpaceRolesContext()); if (!context) { throw new Error( - 'ViewSpaceStore Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider' + 'EditSpaceStore Context is missing. Ensure the component or React root is wrapped with EditSpaceProvider' ); } return context; }; -export const useViewSpaceStoreDispatch = () => { - const { dispatch } = useViewSpaceStore(); +export const useEditSpaceStoreDispatch = () => { + const { dispatch } = useEditSpaceStore(); return dispatch; }; diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx rename to x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index bf645d1d17178..f3d6d06c4d643 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx rename to x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index e8b281bf3e2ab..a8081d29350f7 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -36,7 +36,7 @@ import { KibanaPrivileges } from '@kbn/security-role-management-model'; import { KibanaPrivilegeTable, PrivilegeFormCalculator } from '@kbn/security-ui-components'; import type { Space } from '../../../../../common'; -import type { ViewSpaceServices, ViewSpaceStore } from '../../provider'; +import type { EditSpaceServices, EditSpaceStore } from '../../provider'; type KibanaRolePrivilege = keyof NonNullable | 'custom'; @@ -46,8 +46,8 @@ interface PrivilegesRolesFormProps { closeFlyout: () => void; onSaveCompleted: () => void; defaultSelected?: Role[]; - storeDispatch: ViewSpaceStore['dispatch']; - spacesClientsInvocator: ViewSpaceServices['invokeClient']; + storeDispatch: EditSpaceStore['dispatch']; + spacesClientsInvocator: EditSpaceServices['invokeClient']; } const createRolesComboBoxOptions = (roles: Role[]): Array> => diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx rename to x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx diff --git a/x-pack/plugins/spaces/public/management/lib/validate_space.ts b/x-pack/plugins/spaces/public/management/lib/validate_space.ts index 9a9ae0cbe98fd..002db631c155c 100644 --- a/x-pack/plugins/spaces/public/management/lib/validate_space.ts +++ b/x-pack/plugins/spaces/public/management/lib/validate_space.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { isValidSpaceIdentifier } from './space_identifier_utils'; import { isReservedSpace } from '../../../common/is_reserved_space'; -import type { FormValues } from '../edit_space/manage_space_page'; +import type { FormValues } from '../create_space'; interface SpaceValidatorOptions { shouldValidate?: boolean; diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index c3a5b8560da36..85a79d761dc3f 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -9,21 +9,21 @@ jest.mock('./spaces_grid', () => ({ SpacesGridPage: (props: any) => `Spaces Page: ${JSON.stringify(props)}`, })); -jest.mock('./edit_space', () => ({ - ManageSpacePage: (props: any) => { +jest.mock('./create_space', () => ({ + CreateSpacePage: (props: any) => { if (props.spacesManager && props.onLoadSpace) { props.spacesManager.getSpace().then((space: any) => props.onLoadSpace(space)); } - return `Spaces Edit Page: ${JSON.stringify(props)}`; + return `Spaces Create Page: ${JSON.stringify(props)}`; }, })); -jest.mock('./view_space', () => ({ - ViewSpacePage: (props: any) => { +jest.mock('./edit_space', () => ({ + EditSpacePage: (props: any) => { if (props.spacesManager && props.onLoadSpace) { props.spacesManager.getSpace().then((space: any) => props.onLoadSpace(space)); } - return `Spaces View Page: ${JSON.stringify(props)}`; + return `Spaces Edit Page: ${JSON.stringify(props)}`; }, })); @@ -142,7 +142,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}} + Spaces Create Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}} `); @@ -175,7 +175,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"theme":{"theme$":{}},"i18n":{},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true} + Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"theme":{"theme$":{}},"i18n":{},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true} `); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 49663dcf9191b..1a442e5b36262 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -59,13 +59,13 @@ export const spacesManagementApp = Object.freeze({ const [ [coreStart, { features }], { SpacesGridPage }, - { ManageSpacePage: CreateSpacePage }, - { ViewSpacePage: EditSpacePage }, + { CreateSpacePage }, + { EditSpacePage }, ] = await Promise.all([ getStartServices(), import('./spaces_grid'), + import('./create_space'), import('./edit_space'), - import('./view_space'), ]); const spacesFirstBreadcrumb = { From acae0c5172c6b2674ecfa0066fdefe1021596312 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Thu, 29 Aug 2024 16:15:38 -0700 Subject: [PATCH 129/129] file/folder reorg --- .../get_supported_response_actions.ts | 2 +- ...irm_alter_active_space_modal.test.tsx.snap | 0 .../confirm_alter_active_space_modal.test.tsx | 0 .../confirm_alter_active_space_modal.tsx | 0 .../confirm_alter_active_space_modal/index.ts | 0 .../customize_space.test.tsx.snap | 0 .../customize_space_avatar.test.tsx.snap | 0 .../customize_space/customize_space.test.tsx | 0 .../customize_space/customize_space.tsx | 2 +- .../customize_space_avatar.test.tsx | 0 .../customize_space_avatar.tsx | 2 +- .../customize_space/index.ts | 0 .../delete_spaces_button.test.tsx | 0 .../delete_spaces_button.tsx | 2 +- .../enabled_features.test.tsx.snap | 0 .../enabled_features.test.tsx | 0 .../enabled_features/enabled_features.tsx | 0 .../enabled_features/feature_table.tsx | 0 .../enabled_features/index.ts | 0 .../enabled_features/toggle_all_features.scss | 0 .../enabled_features/toggle_all_features.tsx | 0 .../__snapshots__/section_panel.test.tsx.snap | 0 .../section_panel/index.ts | 0 .../section_panel/section_panel.scss | 0 .../section_panel/section_panel.test.tsx | 0 .../section_panel/section_panel.tsx | 0 .../solution_view/index.ts | 0 .../solution_view/solution_view.tsx | 0 .../create_space_page.test.tsx} | 30 ++++----- .../create_space_page.tsx} | 12 ++-- .../public/management/create_space/index.ts | 9 +++ .../{view_space => edit_space}/constants.ts | 0 .../edit_space.tsx} | 12 ++-- .../edit_space_content_tab.tsx} | 6 +- .../edit_space_features_tab.tsx} | 19 +++--- .../edit_space_general_tab.test.tsx} | 26 ++++---- .../edit_space_general_tab.tsx} | 23 ++++--- .../edit_space_page.tsx} | 18 +++--- .../edit_space_roles_tab.tsx} | 9 ++- .../edit_space_tabs.tsx} | 31 +++++----- .../{view_space => edit_space}/footer.tsx | 3 +- .../hooks/use_tabs.ts | 4 +- .../public/management/edit_space/index.ts | 2 +- .../provider/index.ts | 8 +-- .../provider/reducers/index.ts | 4 +- .../provider/view_space_provider.test.tsx | 24 ++++---- .../provider/view_space_provider.tsx | 61 +++++++++---------- .../space_assign_role_privilege_form.test.tsx | 1 + .../space_assign_role_privilege_form.tsx | 6 +- .../component/space_assigned_roles_table.tsx | 4 +- .../public/management/lib/validate_space.ts | 2 +- .../management/spaces_management_app.test.tsx | 16 ++--- .../management/spaces_management_app.tsx | 6 +- 53 files changed, 172 insertions(+), 172 deletions(-) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/confirm_alter_active_space_modal/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/__snapshots__/customize_space.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space.tsx (99%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space_avatar.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/customize_space_avatar.tsx (99%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/customize_space/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/delete_spaces_button.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/delete_spaces_button.tsx (97%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/__snapshots__/enabled_features.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/enabled_features.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/enabled_features.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/feature_table.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/toggle_all_features.scss (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/enabled_features/toggle_all_features.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/__snapshots__/section_panel.test.tsx.snap (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/section_panel.scss (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/section_panel.test.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/section_panel/section_panel.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/solution_view/index.ts (100%) rename x-pack/plugins/spaces/public/management/{edit_space => components}/solution_view/solution_view.tsx (100%) rename x-pack/plugins/spaces/public/management/{edit_space/manage_space_page.test.tsx => create_space/create_space_page.test.tsx} (97%) rename x-pack/plugins/spaces/public/management/{edit_space/manage_space_page.tsx => create_space/create_space_page.tsx} (97%) create mode 100644 x-pack/plugins/spaces/public/management/create_space/index.ts rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/constants.ts (100%) rename x-pack/plugins/spaces/public/management/{view_space/view_space.tsx => edit_space/edit_space.tsx} (96%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_content_tab.tsx => edit_space/edit_space_content_tab.tsx} (94%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_features_tab.tsx => edit_space/edit_space_features_tab.tsx} (79%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_general_tab.test.tsx => edit_space/edit_space_general_tab.test.tsx} (96%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_general_tab.tsx => edit_space/edit_space_general_tab.tsx} (91%) rename x-pack/plugins/spaces/public/management/{view_space/index.tsx => edit_space/edit_space_page.tsx} (67%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_roles.tsx => edit_space/edit_space_roles_tab.tsx} (94%) rename x-pack/plugins/spaces/public/management/{view_space/view_space_tabs.tsx => edit_space/edit_space_tabs.tsx} (77%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/footer.tsx (95%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/hooks/use_tabs.ts (89%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/index.ts (72%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/reducers/index.ts (91%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/view_space_provider.test.tsx (81%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/provider/view_space_provider.tsx (61%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/roles/component/space_assign_role_privilege_form.test.tsx (99%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/roles/component/space_assign_role_privilege_form.tsx (99%) rename x-pack/plugins/spaces/public/management/{view_space => edit_space}/roles/component/space_assigned_roles_table.tsx (99%) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts index 95fc300a3fe57..fdce9f4d1e682 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { EnabledFeatures } from '@kbn/spaces-plugin/public/management/edit_space/enabled_features'; +import type { EnabledFeatures } from '@kbn/spaces-plugin/public/management/components/enabled_features'; import { ResponseActionTypes, ResponseActionTypesEnum, diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts b/x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/index.ts rename to x-pack/plugins/spaces/public/management/components/confirm_alter_active_space_modal/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.test.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.test.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx index 33113f3338960..1d0e04694604b 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space.tsx +++ b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space.tsx @@ -22,9 +22,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { CustomizeSpaceAvatar } from './customize_space_avatar'; import { getSpaceAvatarComponent, getSpaceColor, getSpaceInitials } from '../../../space_avatar'; +import type { FormValues } from '../../create_space'; import type { SpaceValidator } from '../../lib'; import { toSpaceIdentifier } from '../../lib'; -import type { FormValues } from '../manage_space_page'; 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. diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.test.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx rename to x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx index 827ef592459f7..185a90532c126 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/plugins/spaces/public/management/components/customize_space/customize_space_avatar.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { MAX_SPACE_INITIALS } from '../../../../common'; import { encode, imageTypes } from '../../../../common/lib/dataurl'; +import type { FormValues } from '../../create_space'; import type { SpaceValidator } from '../../lib'; -import type { FormValues } from '../manage_space_page'; interface Props { space: FormValues; diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/index.ts b/x-pack/plugins/spaces/public/management/components/customize_space/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/customize_space/index.ts rename to x-pack/plugins/spaces/public/management/components/customize_space/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx b/x-pack/plugins/spaces/public/management/components/delete_spaces_button.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.test.tsx rename to x-pack/plugins/spaces/public/management/components/delete_spaces_button.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx b/x-pack/plugins/spaces/public/management/components/delete_spaces_button.tsx similarity index 97% rename from x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx rename to x-pack/plugins/spaces/public/management/components/delete_spaces_button.tsx index 1395867d724d0..99c65eb5aafa8 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/management/components/delete_spaces_button.tsx @@ -13,9 +13,9 @@ import type { NotificationsStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { ConfirmDeleteModal } from './confirm_delete_modal'; import type { Space } from '../../../common'; import type { SpacesManager } from '../../spaces_manager'; -import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; interface Props { style?: 'button' | 'icon'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/feature_table.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/feature_table.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/index.ts b/x-pack/plugins/spaces/public/management/components/enabled_features/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/index.ts rename to x-pack/plugins/spaces/public/management/components/enabled_features/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.scss b/x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.scss similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.scss rename to x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.scss diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/enabled_features/toggle_all_features.tsx rename to x-pack/plugins/spaces/public/management/components/enabled_features/toggle_all_features.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/section_panel/__snapshots__/section_panel.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap rename to x-pack/plugins/spaces/public/management/components/section_panel/__snapshots__/section_panel.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/index.ts b/x-pack/plugins/spaces/public/management/components/section_panel/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/index.ts rename to x-pack/plugins/spaces/public/management/components/section_panel/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.scss b/x-pack/plugins/spaces/public/management/components/section_panel/section_panel.scss similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.scss rename to x-pack/plugins/spaces/public/management/components/section_panel/section_panel.scss diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx b/x-pack/plugins/spaces/public/management/components/section_panel/section_panel.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.test.tsx rename to x-pack/plugins/spaces/public/management/components/section_panel/section_panel.test.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx b/x-pack/plugins/spaces/public/management/components/section_panel/section_panel.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/section_panel/section_panel.tsx rename to x-pack/plugins/spaces/public/management/components/section_panel/section_panel.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/solution_view/index.ts b/x-pack/plugins/spaces/public/management/components/solution_view/index.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/solution_view/index.ts rename to x-pack/plugins/spaces/public/management/components/solution_view/index.ts diff --git a/x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx b/x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/edit_space/solution_view/solution_view.tsx rename to x-pack/plugins/spaces/public/management/components/solution_view/solution_view.tsx diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx similarity index 97% rename from x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx rename to x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx index ac60902920fa9..801601d9f9f4e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/create_space/create_space_page.test.tsx @@ -18,13 +18,13 @@ import { KibanaFeature } from '@kbn/features-plugin/public'; import { featuresPluginMock } from '@kbn/features-plugin/public/mocks'; import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers'; -import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; -import { EnabledFeatures } from './enabled_features'; -import { ManageSpacePage } from './manage_space_page'; +import { CreateSpacePage } from './create_space_page'; 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. // https://github.com/elastic/eui/issues/3712 @@ -70,7 +70,7 @@ describe('ManageSpacePage', () => { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { const spacesManager = spacesManagerMock.create(); const wrapper = mountWithIntl( - { const onLoadSpace = jest.fn(); const wrapper = mountWithIntl( - { const onLoadSpace = jest.fn(); const wrapper = mountWithIntl( - { const notifications = notificationServiceMock.createStartContract(); const wrapper = mountWithIntl( - Promise.reject(error)} notifications={notifications} @@ -542,7 +542,7 @@ describe('ManageSpacePage', () => { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { spacesManager.getActiveSpace = jest.fn().mockResolvedValue(space); const wrapper = mountWithIntl( - { +export class CreateSpacePage extends Component { private readonly validator: SpaceValidator; constructor(props: Props) { diff --git a/x-pack/plugins/spaces/public/management/create_space/index.ts b/x-pack/plugins/spaces/public/management/create_space/index.ts new file mode 100644 index 0000000000000..16705209eb450 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/create_space/index.ts @@ -0,0 +1,9 @@ +/* + * 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 type { FormValues } from './create_space_page'; +export { CreateSpacePage } from './create_space_page'; diff --git a/x-pack/plugins/spaces/public/management/view_space/constants.ts b/x-pack/plugins/spaces/public/management/edit_space/constants.ts similarity index 100% rename from x-pack/plugins/spaces/public/management/view_space/constants.ts rename to x-pack/plugins/spaces/public/management/edit_space/constants.ts diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx similarity index 96% rename from x-pack/plugins/spaces/public/management/view_space/view_space.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx index e4c0c6db284c8..a2b4cc9cd65d1 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space.tsx @@ -28,7 +28,7 @@ import type { Role } from '@kbn/security-plugin-types-common'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import { useTabs } from './hooks/use_tabs'; -import { useViewSpaceServices, useViewSpaceStore } from './provider'; +import { useEditSpaceServices, useEditSpaceStore } from './provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import { SpaceSolutionBadge } from '../../space_solution_badge'; @@ -62,9 +62,7 @@ const handleApiError = (error: Error) => { throw error; }; -// FIXME: rename to EditSpacePage -// FIXME: add eventTracker -export const ViewSpace: FC = ({ +export const EditSpace: FC = ({ spaceId, getFeatures, history, @@ -72,9 +70,9 @@ export const ViewSpace: FC = ({ selectedTabId: _selectedTabId, ...props }) => { - const { state, dispatch } = useViewSpaceStore(); - const { invokeClient } = useViewSpaceServices(); - const { spacesManager, capabilities, serverBasePath } = useViewSpaceServices(); + const { state, dispatch } = useEditSpaceStore(); + const { invokeClient } = useEditSpaceServices(); + const { spacesManager, capabilities, serverBasePath } = useEditSpaceServices(); const [space, setSpace] = useState(null); const [userActiveSpace, setUserActiveSpace] = useState(null); const [features, setFeatures] = useState(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.tsx similarity index 94% rename from x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.tsx index 61d6ff516e027..dcb7b17d26e06 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.tsx @@ -18,7 +18,7 @@ import { capitalize } from 'lodash'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; -import { useViewSpaceServices } from './provider'; +import { useEditSpaceServices } from './provider'; import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common'; import type { SpaceContentTypeSummaryItem } from '../../types'; @@ -28,9 +28,9 @@ const handleApiError = (error: Error) => { throw error; }; -export const ViewSpaceContent: FC<{ space: Space }> = ({ space }) => { +export const EditSpaceContentTab: FC<{ space: Space }> = ({ space }) => { const { id: spaceId } = space; - const { spacesManager, serverBasePath } = useViewSpaceServices(); + const { spacesManager, serverBasePath } = useEditSpaceServices(); const [isLoading, setIsLoading] = useState(true); const [items, setItems] = useState(null); diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx similarity index 79% rename from x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx index 9a4b63a89b669..eacb29c1a098d 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_features_tab.tsx @@ -12,10 +12,10 @@ import React from 'react'; import type { KibanaFeature } from '@kbn/features-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useViewSpaceServices } from './provider'; +import { useEditSpaceServices } from './provider'; import type { Space } from '../../../common'; -import { FeatureTable } from '../edit_space/enabled_features/feature_table'; -import { SectionPanel } from '../edit_space/section_panel'; +import { FeatureTable } from '../components/enabled_features/feature_table'; +import { SectionPanel } from '../components/section_panel'; interface Props { space: Partial; @@ -23,9 +23,8 @@ interface Props { onChange: (updatedSpace: Partial) => void; } -// FIXME: rename to EditSpaceEnabledFeaturesPanel -export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { - const { capabilities, getUrlForApp } = useViewSpaceServices(); +export const EditSpaceEnabledFeatures: FC = ({ features, space, onChange }) => { + const { capabilities, getUrlForApp } = useEditSpaceServices(); const canManageRoles = capabilities.management?.security?.roles === true; if (!features) { @@ -39,7 +38,7 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange

@@ -48,19 +47,19 @@ export const ViewSpaceEnabledFeatures: FC = ({ features, space, onChange

) : ( ), diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx similarity index 96% rename from x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx index 81f4de6680ac7..b1518c197c247 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx @@ -20,8 +20,8 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { KibanaFeature } from '@kbn/features-plugin/common'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { ViewSpaceProvider } from './provider/view_space_provider'; -import { ViewSpaceSettings } from './view_space_general_tab'; +import { EditSpaceSettingsTab } from './edit_space_general_tab'; +import { EditSpaceProvider } from './provider/view_space_provider'; import type { SolutionView } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; @@ -50,7 +50,7 @@ const deleteSpaceSpy = jest .spyOn(spacesManager, 'deleteSpace') .mockImplementation(() => Promise.resolve()); -describe('ViewSpaceSettings', () => { +describe('EditSpaceSettings', () => { beforeEach(() => { navigateSpy.mockReset(); updateSpaceSpy.mockReset(); @@ -60,7 +60,7 @@ describe('ViewSpaceSettings', () => { const TestComponent: React.FC = ({ children }) => { return ( - { i18n={i18n} > {children} - + ); }; @@ -88,7 +88,7 @@ describe('ViewSpaceSettings', () => { it('should render controls for initial state of editing a space', () => { render( - { it('shows solution view select when visible', async () => { render( - { render( - { render( - { render( - { render( - { render( - { render( - void; } -// FIXME: rename to EditSpaceSettings -export const ViewSpaceSettings: React.FC = ({ space, features, history, ...props }) => { +export const EditSpaceSettingsTab: React.FC = ({ space, features, history, ...props }) => { const [spaceSettings, setSpaceSettings] = useState>(space); const [isDirty, setIsDirty] = useState(false); // track if unsaved changes have been made const [isLoading, setIsLoading] = useState(false); // track if user has just clicked the Update button const [showUserImpactWarning, setShowUserImpactWarning] = useState(false); const [showAlteringActiveSpaceDialog, setShowAlteringActiveSpaceDialog] = useState(false); const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); - const { http, overlays, notifications, navigateToUrl, spacesManager } = useViewSpaceServices(); + const { http, overlays, notifications, navigateToUrl, spacesManager } = useEditSpaceServices(); const [solution, setSolution] = useState(space.solution); @@ -237,7 +236,7 @@ export const ViewSpaceSettings: React.FC = ({ space, features, history, . {props.allowFeatureVisibility && (solution == null || solution === 'classic') && ( <> - = ({ space, features, history, . {doShowUserImpactWarning()} - & ViewSpaceProviderProps; +type EditSpacePageProps = ComponentProps & EditSpaceProviderProps; -export function ViewSpacePage({ +export function EditSpacePage({ spaceId, getFeatures, history, @@ -22,11 +22,11 @@ export function ViewSpacePage({ allowFeatureVisibility, allowSolutionVisibility, children, - ...viewSpaceServicesProps -}: PropsWithChildren) { + ...editSpaceServicesProps +}: PropsWithChildren) { return ( - - + - + ); } diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx similarity index 94% rename from x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx index a4d3e11366537..64381d5c1bd5b 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { Role } from '@kbn/security-plugin-types-common'; -import { useViewSpaceServices, useViewSpaceStore } from './provider'; +import { useEditSpaceServices, useEditSpaceStore } from './provider'; import { PrivilegesRolesForm } from './roles/component/space_assign_role_privilege_form'; import { SpaceAssignedRolesTable } from './roles/component/space_assigned_roles_table'; import type { Space } from '../../../common'; @@ -26,9 +26,8 @@ interface Props { isReadOnly: boolean; } -// FIXME: rename to EditSpaceAssignedRoles -export const ViewSpaceAssignedRoles: FC = ({ space, features, isReadOnly }) => { - const { dispatch, state } = useViewSpaceStore(); +export const EditSpaceAssignedRolesTab: FC = ({ space, features, isReadOnly }) => { + const { dispatch, state } = useEditSpaceStore(); const { getUrlForApp, overlays, @@ -36,7 +35,7 @@ export const ViewSpaceAssignedRoles: FC = ({ space, features, isReadOnly i18n: i18nStart, notifications, invokeClient, - } = useViewSpaceServices(); + } = useEditSpaceServices(); const showRolesPrivilegeEditor = useCallback( (defaultSelected?: Role[]) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_tabs.tsx similarity index 77% rename from x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx rename to x-pack/plugins/spaces/public/management/edit_space/edit_space_tabs.tsx index 15a8b831bd46d..176df46f42deb 100644 --- a/x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_tabs.tsx @@ -16,8 +16,7 @@ import { withSuspense } from '@kbn/shared-ux-utility'; import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants'; import type { Space } from '../../../common'; -// FIXME: rename to EditSpaceTab -export interface ViewSpaceTab { +export interface EditSpaceTab { id: string; name: string; content: JSX.Element; @@ -37,26 +36,26 @@ export interface GetTabsProps { allowSolutionVisibility: boolean; } -const SuspenseViewSpaceSettings = withSuspense( +const SuspenseEditSpaceSettingsTab = withSuspense( React.lazy(() => - import('./view_space_general_tab').then(({ ViewSpaceSettings }) => ({ - default: ViewSpaceSettings, + import('./edit_space_general_tab').then(({ EditSpaceSettingsTab }) => ({ + default: EditSpaceSettingsTab, })) ) ); -const SuspenseViewSpaceAssignedRoles = withSuspense( +const SuspenseEditSpaceAssignedRolesTab = withSuspense( React.lazy(() => - import('./view_space_roles').then(({ ViewSpaceAssignedRoles }) => ({ - default: ViewSpaceAssignedRoles, + import('./edit_space_roles_tab').then(({ EditSpaceAssignedRolesTab }) => ({ + default: EditSpaceAssignedRolesTab, })) ) ); -const SuspenseViewSpaceContent = withSuspense( +const SuspenseEditSpaceContentTab = withSuspense( React.lazy(() => - import('./view_space_content_tab').then(({ ViewSpaceContent }) => ({ - default: ViewSpaceContent, + import('./edit_space_content_tab').then(({ EditSpaceContentTab }) => ({ + default: EditSpaceContentTab, })) ) ); @@ -68,21 +67,21 @@ export const getTabs = ({ capabilities, rolesCount, ...props -}: GetTabsProps): ViewSpaceTab[] => { +}: GetTabsProps): EditSpaceTab[] => { const canUserViewRoles = Boolean(capabilities?.roles?.view); const canUserModifyRoles = Boolean(capabilities?.roles?.save); const reloadWindow = () => { window.location.reload(); }; - const tabsDefinition: ViewSpaceTab[] = [ + const tabsDefinition: EditSpaceTab[] = [ { id: TAB_ID_GENERAL, name: i18n.translate('xpack.spaces.management.spaceDetails.contentTabs.general.heading', { defaultMessage: 'General settings', }), content: ( - ), content: ( - , + content: , }); return tabsDefinition; diff --git a/x-pack/plugins/spaces/public/management/view_space/footer.tsx b/x-pack/plugins/spaces/public/management/edit_space/footer.tsx similarity index 95% rename from x-pack/plugins/spaces/public/management/view_space/footer.tsx rename to x-pack/plugins/spaces/public/management/edit_space/footer.tsx index 14f036a0ee13c..ab66706f83cee 100644 --- a/x-pack/plugins/spaces/public/management/view_space/footer.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/footer.tsx @@ -22,8 +22,7 @@ interface Props { onClickDeleteSpace: () => void; } -// FIXME: rename to EditSpaceTabFooter -export const ViewSpaceTabFooter: React.FC = ({ +export const EditSpaceTabFooter: React.FC = ({ isDirty, isLoading, onClickCancel, diff --git a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts b/x-pack/plugins/spaces/public/management/edit_space/hooks/use_tabs.ts similarity index 89% rename from x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts rename to x-pack/plugins/spaces/public/management/edit_space/hooks/use_tabs.ts index 1623f19920bcd..fc583e54b0693 100644 --- a/x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/hooks/use_tabs.ts @@ -11,7 +11,7 @@ import type { ScopedHistory } from '@kbn/core-application-browser'; import type { KibanaFeature } from '@kbn/features-plugin/public'; import type { Space } from '../../../../common'; -import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs'; +import { type EditSpaceTab, getTabs, type GetTabsProps } from '../edit_space_tabs'; type UseTabsProps = Pick & { space: Space | null; @@ -27,7 +27,7 @@ export const useTabs = ({ features, currentSelectedTabId, ...getTabsArgs -}: UseTabsProps): [ViewSpaceTab[], JSX.Element | undefined] => { +}: UseTabsProps): [EditSpaceTab[], JSX.Element | undefined] => { const [tabs, selectedTabContent] = useMemo(() => { if (space === null || features === null) { return [[]]; diff --git a/x-pack/plugins/spaces/public/management/edit_space/index.ts b/x-pack/plugins/spaces/public/management/edit_space/index.ts index 78c3b0fc42e04..c85e8f1c2e499 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ManageSpacePage } from './manage_space_page'; +export { EditSpacePage } from './edit_space_page'; diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts similarity index 72% rename from x-pack/plugins/spaces/public/management/view_space/provider/index.ts rename to x-pack/plugins/spaces/public/management/edit_space/provider/index.ts index 74c713ee2e56a..8f70d38f27d44 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts @@ -5,9 +5,9 @@ * 2.0. */ -export { ViewSpaceProvider, useViewSpaceServices, useViewSpaceStore } from './view_space_provider'; +export { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './view_space_provider'; export type { - ViewSpaceProviderProps, - ViewSpaceServices, - ViewSpaceStore, + EditSpaceProviderProps, + EditSpaceServices, + EditSpaceStore, } from './view_space_provider'; diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/reducers/index.ts similarity index 91% rename from x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts rename to x-pack/plugins/spaces/public/management/edit_space/provider/reducers/index.ts index 6040b69d3ba9d..f640ef95c7147 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/reducers/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/reducers/index.ts @@ -20,12 +20,12 @@ export type IDispatchAction = payload: any; }; -export interface IViewSpaceStoreState { +export interface IEditSpaceStoreState { /** roles assigned to current space */ roles: Map; } -export const createSpaceRolesReducer: Reducer = ( +export const createSpaceRolesReducer: Reducer = ( state, action ) => { diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.test.tsx similarity index 81% rename from x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx rename to x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.test.tsx index 872454da0afc5..2d06ddb23fadd 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.test.tsx @@ -19,7 +19,7 @@ import { import type { ApplicationStart } from '@kbn/core-application-browser'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { useViewSpaceServices, useViewSpaceStore, ViewSpaceProvider } from './view_space_provider'; +import { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './view_space_provider'; import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../../roles_api_client.mock'; @@ -43,7 +43,7 @@ const SUTProvider = ({ }: PropsWithChildren>>) => { return ( - {children} - + ); }; -describe('ViewSpaceProvider', () => { - describe('useViewSpaceServices', () => { +describe('EditSpaceProvider', () => { + describe('useEditSpaceServices', () => { it('returns an object of predefined properties', () => { - const { result } = renderHook(useViewSpaceServices, { wrapper: SUTProvider }); + const { result } = renderHook(useEditSpaceServices, { wrapper: SUTProvider }); expect(result.current).toEqual( expect.objectContaining({ @@ -78,17 +78,17 @@ describe('ViewSpaceProvider', () => { }); it('throws when the hook is used within a tree that does not have the provider', () => { - const { result } = renderHook(useViewSpaceServices); + const { result } = renderHook(useEditSpaceServices); expect(result.error).toBeDefined(); expect(result.error?.message).toEqual( - expect.stringMatching('ViewSpaceService Context is missing.') + expect.stringMatching('EditSpaceService Context is missing.') ); }); }); - describe('useViewSpaceStore', () => { + describe('useEditSpaceStore', () => { it('returns an object of predefined properties', () => { - const { result } = renderHook(useViewSpaceStore, { wrapper: SUTProvider }); + const { result } = renderHook(useEditSpaceStore, { wrapper: SUTProvider }); expect(result.current).toEqual( expect.objectContaining({ @@ -99,11 +99,11 @@ describe('ViewSpaceProvider', () => { }); it('throws when the hook is used within a tree that does not have the provider', () => { - const { result } = renderHook(useViewSpaceStore); + const { result } = renderHook(useEditSpaceStore); expect(result.error).toBeDefined(); expect(result.error?.message).toEqual( - expect.stringMatching('ViewSpaceStore Context is missing.') + expect.stringMatching('EditSpaceStore Context is missing.') ); }); }); diff --git a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.tsx similarity index 61% rename from x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx rename to x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.tsx index 86732f63b5fdf..8de7b96dcc6d2 100644 --- a/x-pack/plugins/spaces/public/management/view_space/provider/view_space_provider.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/view_space_provider.tsx @@ -27,12 +27,11 @@ import type { import { createSpaceRolesReducer, type IDispatchAction, - type IViewSpaceStoreState, + type IEditSpaceStoreState, } from './reducers'; import type { SpacesManager } from '../../../spaces_manager'; -// FIXME: rename to EditSpaceServices -export interface ViewSpaceProviderProps +export interface EditSpaceProviderProps extends Pick { capabilities: ApplicationStart['capabilities']; getUrlForApp: ApplicationStart['getUrlForApp']; @@ -43,41 +42,40 @@ export interface ViewSpaceProviderProps getPrivilegesAPIClient: () => Promise; } -export interface ViewSpaceServices - extends Omit { - invokeClient(arg: (clients: ViewSpaceClients) => Promise): Promise; +export interface EditSpaceServices + extends Omit { + invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; } -interface ViewSpaceClients { - spacesManager: ViewSpaceProviderProps['spacesManager']; +interface EditSpaceClients { + spacesManager: EditSpaceProviderProps['spacesManager']; rolesClient: RolesAPIClient; privilegesClient: PrivilegesAPIClientPublicContract; } -export interface ViewSpaceStore { - state: IViewSpaceStoreState; +export interface EditSpaceStore { + state: IEditSpaceStoreState; dispatch: Dispatch; } -const createSpaceRolesContext = once(() => createContext(null)); +const createSpaceRolesContext = once(() => createContext(null)); -const createViewSpaceServicesContext = once(() => createContext(null)); +const createEditSpaceServicesContext = once(() => createContext(null)); -// FIXME: rename to EditSpaceProvider -export const ViewSpaceProvider = ({ +export const EditSpaceProvider = ({ children, getRolesAPIClient, getPrivilegesAPIClient, ...services -}: PropsWithChildren) => { - const ViewSpaceStoreContext = createSpaceRolesContext(); - const ViewSpaceServicesContext = createViewSpaceServicesContext(); +}: PropsWithChildren) => { + const EditSpaceStoreContext = createSpaceRolesContext(); + const EditSpaceServicesContext = createEditSpaceServicesContext(); const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()])); const rolesAPIClientRef = useRef(); const privilegesClientRef = useRef(); - const initialStoreState = useRef({ + const initialStoreState = useRef({ roles: new Map(), }); @@ -93,7 +91,7 @@ export const ViewSpaceProvider = ({ resolveAPIClients(); }, [resolveAPIClients]); - const createInitialState = useCallback((state: IViewSpaceStoreState) => { + const createInitialState = useCallback((state: IEditSpaceStoreState) => { return state; }, []); @@ -103,7 +101,7 @@ export const ViewSpaceProvider = ({ createInitialState ); - const invokeClient: ViewSpaceServices['invokeClient'] = useCallback( + const invokeClient: EditSpaceServices['invokeClient'] = useCallback( async (...args) => { await resolveAPIClients(); @@ -117,38 +115,37 @@ export const ViewSpaceProvider = ({ ); return ( - - + + {children} - - + + ); }; -// FIXME: rename to useEditSpaceServices -export const useViewSpaceServices = (): ViewSpaceServices => { - const context = useContext(createViewSpaceServicesContext()); +export const useEditSpaceServices = (): EditSpaceServices => { + const context = useContext(createEditSpaceServicesContext()); if (!context) { throw new Error( - 'ViewSpaceService Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider' + 'EditSpaceService Context is missing. Ensure the component or React root is wrapped with EditSpaceProvider' ); } return context; }; -export const useViewSpaceStore = () => { +export const useEditSpaceStore = () => { const context = useContext(createSpaceRolesContext()); if (!context) { throw new Error( - 'ViewSpaceStore Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider' + 'EditSpaceStore Context is missing. Ensure the component or React root is wrapped with EditSpaceProvider' ); } return context; }; -export const useViewSpaceStoreDispatch = () => { - const { dispatch } = useViewSpaceStore(); +export const useEditSpaceStoreDispatch = () => { + const { dispatch } = useEditSpaceStore(); return dispatch; }; diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx rename to x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index bf645d1d17178..f3d6d06c4d643 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx rename to x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index e8b281bf3e2ab..a8081d29350f7 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -36,7 +36,7 @@ import { KibanaPrivileges } from '@kbn/security-role-management-model'; import { KibanaPrivilegeTable, PrivilegeFormCalculator } from '@kbn/security-ui-components'; import type { Space } from '../../../../../common'; -import type { ViewSpaceServices, ViewSpaceStore } from '../../provider'; +import type { EditSpaceServices, EditSpaceStore } from '../../provider'; type KibanaRolePrivilege = keyof NonNullable | 'custom'; @@ -46,8 +46,8 @@ interface PrivilegesRolesFormProps { closeFlyout: () => void; onSaveCompleted: () => void; defaultSelected?: Role[]; - storeDispatch: ViewSpaceStore['dispatch']; - spacesClientsInvocator: ViewSpaceServices['invokeClient']; + storeDispatch: EditSpaceStore['dispatch']; + spacesClientsInvocator: EditSpaceServices['invokeClient']; } const createRolesComboBoxOptions = (roles: Role[]): Array> => diff --git a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx similarity index 99% rename from x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx rename to x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx index 8e61c080af7c6..df6cdcd7d3e91 100644 --- a/x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assigned_roles_table.tsx @@ -372,7 +372,7 @@ export const SpaceAssignedRolesTable = ({ } ), onClick: async () => { - await onClickBulkEdit(selectedRoles); + onClickBulkEdit(selectedRoles); setBulkActionContextOpen(false); }, }, @@ -387,7 +387,7 @@ export const SpaceAssignedRolesTable = ({ ), onClick: async () => { - await onClickBulkRemove(selectedRoles); + onClickBulkRemove(selectedRoles); setBulkActionContextOpen(false); }, }, diff --git a/x-pack/plugins/spaces/public/management/lib/validate_space.ts b/x-pack/plugins/spaces/public/management/lib/validate_space.ts index 9a9ae0cbe98fd..002db631c155c 100644 --- a/x-pack/plugins/spaces/public/management/lib/validate_space.ts +++ b/x-pack/plugins/spaces/public/management/lib/validate_space.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { isValidSpaceIdentifier } from './space_identifier_utils'; import { isReservedSpace } from '../../../common/is_reserved_space'; -import type { FormValues } from '../edit_space/manage_space_page'; +import type { FormValues } from '../create_space'; interface SpaceValidatorOptions { shouldValidate?: boolean; diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index c3a5b8560da36..85a79d761dc3f 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -9,21 +9,21 @@ jest.mock('./spaces_grid', () => ({ SpacesGridPage: (props: any) => `Spaces Page: ${JSON.stringify(props)}`, })); -jest.mock('./edit_space', () => ({ - ManageSpacePage: (props: any) => { +jest.mock('./create_space', () => ({ + CreateSpacePage: (props: any) => { if (props.spacesManager && props.onLoadSpace) { props.spacesManager.getSpace().then((space: any) => props.onLoadSpace(space)); } - return `Spaces Edit Page: ${JSON.stringify(props)}`; + return `Spaces Create Page: ${JSON.stringify(props)}`; }, })); -jest.mock('./view_space', () => ({ - ViewSpacePage: (props: any) => { +jest.mock('./edit_space', () => ({ + EditSpacePage: (props: any) => { if (props.spacesManager && props.onLoadSpace) { props.spacesManager.getSpace().then((space: any) => props.onLoadSpace(space)); } - return `Spaces View Page: ${JSON.stringify(props)}`; + return `Spaces Edit Page: ${JSON.stringify(props)}`; }, })); @@ -142,7 +142,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}} + Spaces Create Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true,"eventTracker":{"analytics":{}}} `); @@ -175,7 +175,7 @@ describe('spacesManagementApp', () => { css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)." data-test-subj="kbnRedirectAppLink" > - Spaces View Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"theme":{"theme$":{}},"i18n":{},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true} + Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"serverBasePath":"","http":{"basePath":{"basePath":"","serverBasePath":"","assetsHrefBase":""},"anonymousPaths":{},"externalUrl":{},"staticAssets":{}},"overlays":{"banners":{}},"notifications":{"toasts":{}},"theme":{"theme$":{}},"i18n":{},"spacesManager":{"onActiveSpaceChange$":{}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"allowFeatureVisibility":true,"allowSolutionVisibility":true} `); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 49663dcf9191b..1a442e5b36262 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -59,13 +59,13 @@ export const spacesManagementApp = Object.freeze({ const [ [coreStart, { features }], { SpacesGridPage }, - { ManageSpacePage: CreateSpacePage }, - { ViewSpacePage: EditSpacePage }, + { CreateSpacePage }, + { EditSpacePage }, ] = await Promise.all([ getStartServices(), import('./spaces_grid'), + import('./create_space'), import('./edit_space'), - import('./view_space'), ]); const spacesFirstBreadcrumb = {