From 2338e068b00f9a0d0c3199c6a4be63967ebcba1d Mon Sep 17 00:00:00 2001
From: Eyo Okon Eyo
Date: Tue, 23 Jul 2024 16:13:21 +0200
Subject: [PATCH 01/26] 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 02/26] 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 03/26] 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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] [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 20/26] 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 21/26] 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 22/26] 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 23/26] [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 24/26] 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 25/26] [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 26/26] 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();