Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ccd34f3

Browse files
authoredAug 28, 2024··
Merge pull request #14 from eokoneyo/assign-roles-to-space
Further improvements to spaces role assignment tab
2 parents ae18346 + 18c0d83 commit ccd34f3

34 files changed

+1863
-599
lines changed
 

Diff for: ‎x-pack/packages/security/plugin_types_public/src/privileges/privileges_api_client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface PrivilegesAPIClientGetAllArgs {
1515
*/
1616
respectLicenseLevel: boolean;
1717
}
18-
// TODO: Eyo include the proper return types for contract
18+
1919
export abstract class PrivilegesAPIClientPublicContract {
2020
abstract getAll(args: PrivilegesAPIClientGetAllArgs): Promise<RawKibanaPrivileges>;
2121
}

Diff for: ‎x-pack/packages/security/plugin_types_public/src/roles/roles_api_client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export interface RolesAPIClient {
1616
getRole: (roleName: string) => Promise<Role>;
1717
deleteRole: (roleName: string) => Promise<void>;
1818
saveRole: (payload: RolePutPayload) => Promise<void>;
19+
bulkUpdateRoles: (payload: { rolesUpdate: Role[] }) => Promise<void>;
1920
}

Diff for: ‎x-pack/packages/security/plugin_types_public/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
"@kbn/core-user-profile-common",
1515
"@kbn/security-plugin-types-common",
1616
"@kbn/core-security-common",
17-
"@kbn/security-authorization-core"
17+
"@kbn/security-authorization-core",
1818
]
1919
}

Diff for: ‎x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import {
1515
kibanaFeatures,
1616
} from '@kbn/security-role-management-model/src/__fixtures__';
1717
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
18+
import type { Role } from '@kbn/security-plugin-types-common';
1819

1920
import { getDisplayedFeaturePrivileges } from './__fixtures__';
2021
import { FeatureTable } from './feature_table';
21-
import type { Role } from '@kbn/security-plugin-types-common';
2222
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
2323

2424
const createRole = (kibana: Role['kibana'] = []): Role => {

Diff for: ‎x-pack/packages/security/ui_components/src/kibana_privilege_table/feature_table_expanded_row.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import {
1212
createKibanaPrivileges,
1313
kibanaFeatures,
1414
} from '@kbn/security-role-management-model/src/__fixtures__';
15+
import type { Role } from '@kbn/security-plugin-types-common';
1516
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
1617

1718
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
18-
import type { Role } from '@kbn/security-plugin-types-common';
1919
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
2020

2121
const createRole = (kibana: Role['kibana'] = []): Role => {

Diff for: ‎x-pack/packages/security/ui_components/src/kibana_privilege_table/sub_feature_form.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import type {
2121
SubFeaturePrivilege,
2222
SubFeaturePrivilegeGroup,
2323
} from '@kbn/security-role-management-model';
24-
2524
import { NO_PRIVILEGE_VALUE } from '../constants';
2625
import type { PrivilegeFormCalculator } from '../privilege_form_calculator';
2726

Diff for: ‎x-pack/packages/security/ui_components/src/privilege_form_calculator/privilege_form_calculator.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
createKibanaPrivileges,
1010
kibanaFeatures,
1111
} from '@kbn/security-role-management-model/src/__fixtures__';
12+
import type { Role } from '@kbn/security-plugin-types-common';
1213

1314
import { PrivilegeFormCalculator } from './privilege_form_calculator';
14-
import type { Role } from '@kbn/security-plugin-types-common';
1515

1616
const createRole = (kibana: Role['kibana'] = []): Role => {
1717
return {

Diff for: ‎x-pack/plugins/security/public/authentication/index.mock.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const authorizationMock = {
3131
getRole: jest.fn(),
3232
deleteRole: jest.fn(),
3333
saveRole: jest.fn(),
34+
bulkUpdateRoles: jest.fn(),
3435
},
3536
privileges: {
3637
getAll: jest.fn(),
@@ -43,6 +44,7 @@ export const authorizationMock = {
4344
getRole: jest.fn(),
4445
deleteRole: jest.fn(),
4546
saveRole: jest.fn(),
47+
bulkUpdateRoles: jest.fn(),
4648
},
4749
privileges: {
4850
getAll: jest.fn(),

Diff for: ‎x-pack/plugins/security/public/authorization/authorization_service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class AuthorizationService {
2929
getRole: rolesAPIClient.getRole,
3030
deleteRole: rolesAPIClient.deleteRole,
3131
saveRole: rolesAPIClient.saveRole,
32+
bulkUpdateRoles: rolesAPIClient.bulkUpdateRoles,
3233
},
3334
privileges: {
3435
getAll: privilegesAPIClient.getAll.bind(privilegesAPIClient),

Diff for: ‎x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ export const rolesAPIClientMock = {
1111
getRole: jest.fn(),
1212
deleteRole: jest.fn(),
1313
saveRole: jest.fn(),
14+
bulkUpdateRoles: jest.fn(),
1415
}),
1516
};

Diff for: ‎x-pack/plugins/security/public/management/roles/roles_api_client.ts

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ export class RolesAPIClient {
3232
});
3333
};
3434

35+
public bulkUpdateRoles = async ({ rolesUpdate }: { rolesUpdate: Role[] }) => {
36+
await this.http.post('/api/security/roles', {
37+
body: JSON.stringify({
38+
roles: rolesUpdate.reduce((transformed, value) => {
39+
transformed[value.name] = this.transformRoleForSave(copyRole(value));
40+
return transformed;
41+
}, {} as Record<string, ReturnType<typeof this.transformRoleForSave>>),
42+
}),
43+
});
44+
};
45+
3546
private transformRoleForSave = (role: Role) => {
3647
// Remove any placeholder index privileges
3748
const isPlaceholderPrivilege = (

Diff for: ‎x-pack/plugins/security/public/plugin.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ describe('Security Plugin', () => {
137137
"getAll": [Function],
138138
},
139139
"roles": Object {
140+
"bulkUpdateRoles": [Function],
140141
"deleteRole": [Function],
141142
"getRole": [Function],
142143
"getRoles": [Function],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { PrivilegesAPIClientPublicContract } from '@kbn/security-plugin-types-public';
9+
10+
export const createPrivilegeAPIClientMock = (): PrivilegesAPIClientPublicContract => {
11+
return {
12+
getAll: jest.fn(),
13+
};
14+
};
15+
16+
export const getPrivilegeAPIClientMock = jest
17+
.fn()
18+
.mockResolvedValue(createPrivilegeAPIClientMock());

Diff for: ‎x-pack/plugins/spaces/public/management/roles_api_client.mock.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const createRolesAPIClientMock = (): RolesAPIClient => {
1313
getRole: jest.fn(),
1414
saveRole: jest.fn(),
1515
deleteRole: jest.fn(),
16+
bulkUpdateRoles: jest.fn(),
1617
};
1718
};
1819

Diff for: ‎x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe('spacesManagementApp', () => {
175175
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)."
176176
data-test-subj="kbnRedirectAppLink"
177177
>
178-
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}
178+
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}
179179
</div>
180180
</div>
181181
`);

Diff for: ‎x-pack/plugins/spaces/public/management/spaces_management_app.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const spacesManagementApp = Object.freeze({
4444
config,
4545
eventTracker,
4646
getRolesAPIClient,
47+
getPrivilegesAPIClient,
4748
}: CreateParams) {
4849
const title = i18n.translate('xpack.spaces.displayName', {
4950
defaultMessage: 'Spaces',
@@ -71,7 +72,7 @@ export const spacesManagementApp = Object.freeze({
7172
text: title,
7273
href: `/`,
7374
};
74-
const { notifications, application, chrome, http, overlays } = coreStart;
75+
const { notifications, application, chrome, http, overlays, theme } = coreStart;
7576

7677
chrome.docTitle.change(title);
7778

@@ -147,6 +148,8 @@ export const spacesManagementApp = Object.freeze({
147148
http={http}
148149
overlays={overlays}
149150
notifications={notifications}
151+
theme={theme}
152+
i18n={coreStart.i18n}
150153
spacesManager={spacesManager}
151154
spaceId={spaceId}
152155
onLoadSpace={onLoadSpace}
@@ -155,6 +158,7 @@ export const spacesManagementApp = Object.freeze({
155158
getRolesAPIClient={getRolesAPIClient}
156159
allowFeatureVisibility={config.allowFeatureVisibility}
157160
allowSolutionVisibility={config.allowSolutionVisibility}
161+
getPrivilegesAPIClient={getPrivilegesAPIClient}
158162
/>
159163
);
160164
};

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/hooks/use_tabs.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { KibanaFeature } from '@kbn/features-plugin/public';
1313
import type { Space } from '../../../../common';
1414
import { getTabs, type GetTabsProps, type ViewSpaceTab } from '../view_space_tabs';
1515

16-
type UseTabsProps = Pick<GetTabsProps, 'roles' | 'capabilities'> & {
16+
type UseTabsProps = Pick<GetTabsProps, 'capabilities' | 'rolesCount'> & {
1717
space: Space | null;
1818
features: KibanaFeature[] | null;
1919
currentSelectedTabId: string;

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/hooks/view_space_context_provider.tsx

-52
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import type { ComponentProps, PropsWithChildren } from 'react';
10+
11+
import { ViewSpaceProvider, type ViewSpaceProviderProps } from './provider';
12+
import { ViewSpace } from './view_space';
13+
14+
type ViewSpacePageProps = ComponentProps<typeof ViewSpace> & ViewSpaceProviderProps;
15+
16+
export function ViewSpacePage({
17+
spaceId,
18+
getFeatures,
19+
history,
20+
onLoadSpace,
21+
selectedTabId,
22+
allowFeatureVisibility,
23+
allowSolutionVisibility,
24+
children,
25+
...viewSpaceServicesProps
26+
}: PropsWithChildren<ViewSpacePageProps>) {
27+
return (
28+
<ViewSpaceProvider {...viewSpaceServicesProps}>
29+
<ViewSpace
30+
spaceId={spaceId}
31+
getFeatures={getFeatures}
32+
history={history}
33+
onLoadSpace={onLoadSpace}
34+
selectedTabId={selectedTabId}
35+
allowFeatureVisibility={allowFeatureVisibility}
36+
allowSolutionVisibility={allowSolutionVisibility}
37+
/>
38+
</ViewSpaceProvider>
39+
);
40+
}

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/index.ts renamed to ‎x-pack/plugins/spaces/public/management/view_space/provider/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@
55
* 2.0.
66
*/
77

8-
export { ViewSpacePage } from './view_space';
8+
export { ViewSpaceProvider, useViewSpaceServices, useViewSpaceStore } from './view_space_provider';
9+
export type {
10+
ViewSpaceProviderProps,
11+
ViewSpaceServices,
12+
ViewSpaceStore,
13+
} from './view_space_provider';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { type Reducer } from 'react';
9+
10+
import type { Role } from '@kbn/security-plugin-types-common';
11+
12+
export type IDispatchAction =
13+
| {
14+
/** @description updates a single role record */
15+
type: 'update_roles' | 'remove_roles';
16+
payload: Role[];
17+
}
18+
| {
19+
type: 'string';
20+
payload: any;
21+
};
22+
23+
export interface IViewSpaceStoreState {
24+
/** roles assigned to current space */
25+
roles: Map<Role['name'], Role>;
26+
}
27+
28+
export const createSpaceRolesReducer: Reducer<IViewSpaceStoreState, IDispatchAction> = (
29+
state,
30+
action
31+
) => {
32+
const _state = structuredClone(state);
33+
34+
switch (action.type) {
35+
case 'update_roles': {
36+
action.payload.forEach((role) => {
37+
_state.roles.set(role.name, role);
38+
});
39+
40+
return _state;
41+
}
42+
case 'remove_roles': {
43+
action.payload.forEach((role) => {
44+
_state.roles.delete(role.name);
45+
});
46+
47+
return _state;
48+
}
49+
default: {
50+
return _state;
51+
}
52+
}
53+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { renderHook } from '@testing-library/react-hooks';
9+
import type { PropsWithChildren } from 'react';
10+
import React from 'react';
11+
12+
import {
13+
httpServiceMock,
14+
i18nServiceMock,
15+
notificationServiceMock,
16+
overlayServiceMock,
17+
themeServiceMock,
18+
} from '@kbn/core/public/mocks';
19+
import type { ApplicationStart } from '@kbn/core-application-browser';
20+
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
21+
22+
import { useViewSpaceServices, useViewSpaceStore, ViewSpaceProvider } from './view_space_provider';
23+
import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock';
24+
import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock';
25+
import { getRolesAPIClientMock } from '../../roles_api_client.mock';
26+
27+
const http = httpServiceMock.createStartContract();
28+
const notifications = notificationServiceMock.createStartContract();
29+
const overlays = overlayServiceMock.createStartContract();
30+
const theme = themeServiceMock.createStartContract();
31+
const i18n = i18nServiceMock.createStartContract();
32+
33+
const spacesManager = spacesManagerMock.create();
34+
35+
const SUTProvider = ({
36+
children,
37+
capabilities = {
38+
navLinks: {},
39+
management: {},
40+
catalogue: {},
41+
spaces: { manage: true },
42+
},
43+
}: PropsWithChildren<Partial<Pick<ApplicationStart, 'capabilities'>>>) => {
44+
return (
45+
<IntlProvider locale="en">
46+
<ViewSpaceProvider
47+
{...{
48+
i18n,
49+
http,
50+
theme,
51+
overlays,
52+
notifications,
53+
spacesManager,
54+
serverBasePath: '',
55+
getUrlForApp: (_) => _,
56+
getRolesAPIClient: getRolesAPIClientMock,
57+
getPrivilegesAPIClient: getPrivilegeAPIClientMock,
58+
navigateToUrl: jest.fn(),
59+
capabilities,
60+
}}
61+
>
62+
{children}
63+
</ViewSpaceProvider>
64+
</IntlProvider>
65+
);
66+
};
67+
68+
describe('ViewSpaceProvider', () => {
69+
describe('useViewSpaceServices', () => {
70+
it('returns an object of predefined properties', () => {
71+
const { result } = renderHook(useViewSpaceServices, { wrapper: SUTProvider });
72+
73+
expect(result.current).toEqual(
74+
expect.objectContaining({
75+
invokeClient: expect.any(Function),
76+
})
77+
);
78+
});
79+
80+
it('throws when the hook is used within a tree that does not have the provider', () => {
81+
const { result } = renderHook(useViewSpaceServices);
82+
expect(result.error).toBeDefined();
83+
expect(result.error?.message).toEqual(
84+
expect.stringMatching('ViewSpaceService Context is missing.')
85+
);
86+
});
87+
});
88+
89+
describe('useViewSpaceStore', () => {
90+
it('returns an object of predefined properties', () => {
91+
const { result } = renderHook(useViewSpaceStore, { wrapper: SUTProvider });
92+
93+
expect(result.current).toEqual(
94+
expect.objectContaining({
95+
state: expect.objectContaining({ roles: expect.any(Map) }),
96+
dispatch: expect.any(Function),
97+
})
98+
);
99+
});
100+
101+
it('throws when the hook is used within a tree that does not have the provider', () => {
102+
const { result } = renderHook(useViewSpaceStore);
103+
104+
expect(result.error).toBeDefined();
105+
expect(result.error?.message).toEqual(
106+
expect.stringMatching('ViewSpaceStore Context is missing.')
107+
);
108+
});
109+
});
110+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { once } from 'lodash';
9+
import React, {
10+
createContext,
11+
type Dispatch,
12+
type PropsWithChildren,
13+
useCallback,
14+
useContext,
15+
useEffect,
16+
useReducer,
17+
useRef,
18+
} from 'react';
19+
20+
import type { ApplicationStart } from '@kbn/core-application-browser';
21+
import type { CoreStart } from '@kbn/core-lifecycle-browser';
22+
import type {
23+
PrivilegesAPIClientPublicContract,
24+
RolesAPIClient,
25+
} from '@kbn/security-plugin-types-public';
26+
27+
import {
28+
createSpaceRolesReducer,
29+
type IDispatchAction,
30+
type IViewSpaceStoreState,
31+
} from './reducers';
32+
import type { SpacesManager } from '../../../spaces_manager';
33+
34+
// FIXME: rename to EditSpaceServices
35+
export interface ViewSpaceProviderProps
36+
extends Pick<CoreStart, 'theme' | 'i18n' | 'overlays' | 'http' | 'notifications'> {
37+
capabilities: ApplicationStart['capabilities'];
38+
getUrlForApp: ApplicationStart['getUrlForApp'];
39+
navigateToUrl: ApplicationStart['navigateToUrl'];
40+
serverBasePath: string;
41+
spacesManager: SpacesManager;
42+
getRolesAPIClient: () => Promise<RolesAPIClient>;
43+
getPrivilegesAPIClient: () => Promise<PrivilegesAPIClientPublicContract>;
44+
}
45+
46+
export interface ViewSpaceServices
47+
extends Omit<ViewSpaceProviderProps, 'getRolesAPIClient' | 'getPrivilegesAPIClient'> {
48+
invokeClient<R extends unknown>(arg: (clients: ViewSpaceClients) => Promise<R>): Promise<R>;
49+
}
50+
51+
interface ViewSpaceClients {
52+
spacesManager: ViewSpaceProviderProps['spacesManager'];
53+
rolesClient: RolesAPIClient;
54+
privilegesClient: PrivilegesAPIClientPublicContract;
55+
}
56+
57+
export interface ViewSpaceStore {
58+
state: IViewSpaceStoreState;
59+
dispatch: Dispatch<IDispatchAction>;
60+
}
61+
62+
const createSpaceRolesContext = once(() => createContext<ViewSpaceStore | null>(null));
63+
64+
const createViewSpaceServicesContext = once(() => createContext<ViewSpaceServices | null>(null));
65+
66+
// FIXME: rename to EditSpaceProvider
67+
export const ViewSpaceProvider = ({
68+
children,
69+
getRolesAPIClient,
70+
getPrivilegesAPIClient,
71+
...services
72+
}: PropsWithChildren<ViewSpaceProviderProps>) => {
73+
const ViewSpaceStoreContext = createSpaceRolesContext();
74+
const ViewSpaceServicesContext = createViewSpaceServicesContext();
75+
76+
const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()]));
77+
const rolesAPIClientRef = useRef<RolesAPIClient>();
78+
const privilegesClientRef = useRef<PrivilegesAPIClientPublicContract>();
79+
80+
const initialStoreState = useRef<IViewSpaceStoreState>({
81+
roles: new Map(),
82+
});
83+
84+
const resolveAPIClients = useCallback(async () => {
85+
try {
86+
[rolesAPIClientRef.current, privilegesClientRef.current] = await clients.current;
87+
} catch {
88+
// handle errors
89+
}
90+
}, []);
91+
92+
useEffect(() => {
93+
resolveAPIClients();
94+
}, [resolveAPIClients]);
95+
96+
const createInitialState = useCallback((state: IViewSpaceStoreState) => {
97+
return state;
98+
}, []);
99+
100+
const [state, dispatch] = useReducer(
101+
createSpaceRolesReducer,
102+
initialStoreState.current,
103+
createInitialState
104+
);
105+
106+
const invokeClient: ViewSpaceServices['invokeClient'] = useCallback(
107+
async (...args) => {
108+
await resolveAPIClients();
109+
110+
return args[0]({
111+
spacesManager: services.spacesManager,
112+
rolesClient: rolesAPIClientRef.current!,
113+
privilegesClient: privilegesClientRef.current!,
114+
});
115+
},
116+
[resolveAPIClients, services.spacesManager]
117+
);
118+
119+
return (
120+
<ViewSpaceServicesContext.Provider value={{ ...services, invokeClient }}>
121+
<ViewSpaceStoreContext.Provider value={{ state, dispatch }}>
122+
{children}
123+
</ViewSpaceStoreContext.Provider>
124+
</ViewSpaceServicesContext.Provider>
125+
);
126+
};
127+
128+
// FIXME: rename to useEditSpaceServices
129+
export const useViewSpaceServices = (): ViewSpaceServices => {
130+
const context = useContext(createViewSpaceServicesContext());
131+
if (!context) {
132+
throw new Error(
133+
'ViewSpaceService Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider'
134+
);
135+
}
136+
137+
return context;
138+
};
139+
140+
export const useViewSpaceStore = () => {
141+
const context = useContext(createSpaceRolesContext());
142+
if (!context) {
143+
throw new Error(
144+
'ViewSpaceStore Context is missing. Ensure the component or React root is wrapped with ViewSpaceProvider'
145+
);
146+
}
147+
148+
return context;
149+
};
150+
151+
export const useViewSpaceStoreDispatch = () => {
152+
const { dispatch } = useViewSpaceStore();
153+
return dispatch;
154+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import { render, screen, waitFor } from '@testing-library/react';
8+
import userEvent from '@testing-library/user-event';
9+
import crypto from 'crypto';
10+
import React from 'react';
11+
12+
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
13+
import type { Role } from '@kbn/security-plugin-types-common';
14+
import {
15+
createRawKibanaPrivileges,
16+
kibanaFeatures,
17+
} from '@kbn/security-role-management-model/src/__fixtures__';
18+
19+
import { PrivilegesRolesForm } from './space_assign_role_privilege_form';
20+
import type { Space } from '../../../../../common';
21+
import { createPrivilegeAPIClientMock } from '../../../privilege_api_client.mock';
22+
import { createRolesAPIClientMock } from '../../../roles_api_client.mock';
23+
24+
const rolesAPIClient = createRolesAPIClientMock();
25+
const privilegeAPIClient = createPrivilegeAPIClientMock();
26+
27+
const createRole = (roleName: string, kibana: Role['kibana'] = []): Role => {
28+
return {
29+
name: roleName,
30+
elasticsearch: { cluster: [], run_as: [], indices: [] },
31+
kibana,
32+
};
33+
};
34+
35+
const space: Space = {
36+
id: crypto.randomUUID(),
37+
name: 'Odyssey',
38+
description: 'Journey vs. Destination',
39+
disabledFeatures: [],
40+
};
41+
42+
const spacesClientsInvocatorMock = jest.fn((fn) =>
43+
fn({
44+
rolesClient: rolesAPIClient,
45+
privilegesClient: privilegeAPIClient,
46+
})
47+
);
48+
const dispatchMock = jest.fn();
49+
const onSaveCompleted = jest.fn();
50+
const closeFlyout = jest.fn();
51+
52+
const renderPrivilegeRolesForm = ({
53+
preSelectedRoles,
54+
}: {
55+
preSelectedRoles?: Role[];
56+
} = {}) => {
57+
return render(
58+
<IntlProvider locale="en">
59+
<PrivilegesRolesForm
60+
{...{
61+
space,
62+
features: kibanaFeatures,
63+
closeFlyout,
64+
defaultSelected: preSelectedRoles,
65+
onSaveCompleted,
66+
storeDispatch: dispatchMock,
67+
spacesClientsInvocator: spacesClientsInvocatorMock,
68+
}}
69+
/>
70+
</IntlProvider>
71+
);
72+
};
73+
74+
describe('PrivilegesRolesForm', () => {
75+
let getRolesSpy: jest.SpiedFunction<ReturnType<typeof createRolesAPIClientMock>['getRoles']>;
76+
let getAllKibanaPrivilegeSpy: jest.SpiedFunction<
77+
ReturnType<typeof createPrivilegeAPIClientMock>['getAll']
78+
>;
79+
80+
beforeAll(() => {
81+
getRolesSpy = jest.spyOn(rolesAPIClient, 'getRoles');
82+
getAllKibanaPrivilegeSpy = jest.spyOn(privilegeAPIClient, 'getAll');
83+
});
84+
85+
afterEach(() => {
86+
jest.clearAllMocks();
87+
});
88+
89+
it('renders the privilege permission selector disabled when no role is selected', async () => {
90+
getRolesSpy.mockResolvedValue([]);
91+
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
92+
93+
renderPrivilegeRolesForm();
94+
95+
await waitFor(() => null);
96+
97+
['all', 'read', 'custom'].forEach((privilege) => {
98+
expect(screen.getByTestId(`${privilege}-privilege-button`)).toBeDisabled();
99+
});
100+
});
101+
102+
it('preselects the privilege of the selected role when one is provided', async () => {
103+
getRolesSpy.mockResolvedValue([]);
104+
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
105+
106+
const privilege = 'all';
107+
108+
renderPrivilegeRolesForm({
109+
preSelectedRoles: [
110+
createRole('test_role_1', [{ base: [privilege], feature: {}, spaces: [space.id] }]),
111+
],
112+
});
113+
114+
await waitFor(() => null);
115+
116+
expect(screen.getByTestId(`${privilege}-privilege-button`)).toHaveAttribute(
117+
'aria-pressed',
118+
String(true)
119+
);
120+
});
121+
122+
it('displays a warning message when roles with different privilege levels are selected', async () => {
123+
getRolesSpy.mockResolvedValue([]);
124+
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
125+
126+
const roles: Role[] = [
127+
createRole('test_role_1', [{ base: ['all'], feature: {}, spaces: [space.id] }]),
128+
createRole('test_role_2', [{ base: ['read'], feature: {}, spaces: [space.id] }]),
129+
];
130+
131+
renderPrivilegeRolesForm({
132+
preSelectedRoles: roles,
133+
});
134+
135+
await waitFor(() => null);
136+
137+
expect(screen.getByTestId('privilege-conflict-callout')).toBeInTheDocument();
138+
});
139+
140+
describe('applying custom privileges', () => {
141+
it('displays the privilege customization form, when custom privilege button is selected', async () => {
142+
getRolesSpy.mockResolvedValue([]);
143+
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
144+
145+
const roles: Role[] = [
146+
createRole('test_role_1', [{ base: ['all'], feature: {}, spaces: [space.id] }]),
147+
];
148+
149+
renderPrivilegeRolesForm({
150+
preSelectedRoles: roles,
151+
});
152+
153+
await waitFor(() => null);
154+
155+
expect(screen.queryByTestId('rolePrivilegeCustomizationForm')).not.toBeInTheDocument();
156+
157+
userEvent.click(screen.getByTestId('custom-privilege-button'));
158+
159+
expect(screen.getByTestId('rolePrivilegeCustomizationForm')).toBeInTheDocument();
160+
});
161+
162+
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 () => {
163+
getRolesSpy.mockResolvedValue([]);
164+
getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures));
165+
166+
const featureIds: string[] = kibanaFeatures.map((kibanaFeature) => kibanaFeature.id);
167+
168+
const roles: Role[] = [
169+
createRole('test_role_1', [{ base: ['all'], feature: {}, spaces: [space.id] }]),
170+
createRole('test_role_2', [
171+
{ base: [], feature: { [featureIds[0]]: ['all'] }, spaces: [space.id] },
172+
]),
173+
createRole('test_role_3', [{ base: ['read'], feature: {}, spaces: [space.id] }]),
174+
createRole('test_role_4', [{ base: ['read'], feature: {}, spaces: [space.id] }]),
175+
createRole('test_role_5', [
176+
{ base: [], feature: { [featureIds[0]]: ['read'] }, spaces: [space.id] },
177+
]),
178+
];
179+
180+
renderPrivilegeRolesForm({
181+
preSelectedRoles: roles,
182+
});
183+
184+
await waitFor(() => null);
185+
186+
expect(screen.queryByTestId('rolePrivilegeCustomizationForm')).not.toBeInTheDocument();
187+
188+
userEvent.click(screen.getByTestId('custom-privilege-button'));
189+
190+
expect(screen.getByTestId('rolePrivilegeCustomizationForm')).toBeInTheDocument();
191+
192+
expect(screen.queryByTestId(`${featureIds[0]}_read`)).not.toHaveAttribute(
193+
'aria-pressed',
194+
String(true)
195+
);
196+
197+
expect(screen.getByTestId(`${featureIds[0]}_all`)).toHaveAttribute(
198+
'aria-pressed',
199+
String(true)
200+
);
201+
});
202+
});
203+
});

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/roles/component/space_assign_role_privilege_form.tsx

+495
Large diffs are not rendered by default.

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/roles/component/space_assigned_roles_table.tsx

+486
Large diffs are not rendered by default.

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space.tsx

+122-131
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,15 @@ import {
2020
import React, { lazy, Suspense, useEffect, useState } from 'react';
2121
import type { FC } from 'react';
2222

23-
import type { Capabilities, ScopedHistory } from '@kbn/core/public';
23+
import type { ScopedHistory } from '@kbn/core/public';
2424
import type { FeaturesPluginStart, KibanaFeature } from '@kbn/features-plugin/public';
2525
import { FormattedMessage } from '@kbn/i18n-react';
2626
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
2727
import type { Role } from '@kbn/security-plugin-types-common';
2828

2929
import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants';
3030
import { useTabs } from './hooks/use_tabs';
31-
import {
32-
ViewSpaceContextProvider,
33-
type ViewSpaceServices,
34-
} from './hooks/view_space_context_provider';
31+
import { useViewSpaceServices, useViewSpaceStore } from './provider';
3532
import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common';
3633
import { getSpaceAvatarComponent } from '../../space_avatar';
3734
import { SpaceSolutionBadge } from '../../space_solution_badge';
@@ -49,11 +46,10 @@ const getSelectedTabId = (canUserViewRoles: boolean, selectedTabId?: string) =>
4946
: TAB_ID_GENERAL;
5047
};
5148

52-
interface PageProps extends ViewSpaceServices {
49+
interface PageProps {
5350
spaceId?: string;
5451
history: ScopedHistory;
5552
selectedTabId?: string;
56-
capabilities: Capabilities;
5753
getFeatures: FeaturesPluginStart['getFeatures'];
5854
onLoadSpace: (space: Space) => void;
5955
allowFeatureVisibility: boolean;
@@ -68,32 +64,30 @@ const handleApiError = (error: Error) => {
6864

6965
// FIXME: rename to EditSpacePage
7066
// FIXME: add eventTracker
71-
export const ViewSpacePage: FC<PageProps> = (props) => {
72-
const {
73-
spaceId,
74-
getFeatures,
75-
spacesManager,
76-
history,
77-
onLoadSpace,
78-
selectedTabId: _selectedTabId,
79-
capabilities,
80-
getUrlForApp,
81-
navigateToUrl,
82-
...viewSpaceServices
83-
} = props;
84-
67+
export const ViewSpace: FC<PageProps> = ({
68+
spaceId,
69+
getFeatures,
70+
history,
71+
onLoadSpace,
72+
selectedTabId: _selectedTabId,
73+
...props
74+
}) => {
75+
const { state, dispatch } = useViewSpaceStore();
76+
const { invokeClient } = useViewSpaceServices();
77+
const { spacesManager, capabilities, serverBasePath } = useViewSpaceServices();
8578
const [space, setSpace] = useState<Space | null>(null);
8679
const [userActiveSpace, setUserActiveSpace] = useState<Space | null>(null);
8780
const [features, setFeatures] = useState<KibanaFeature[] | null>(null);
88-
const [roles, setRoles] = useState<Role[]>([]);
8981
const [isLoadingSpace, setIsLoadingSpace] = useState(true);
9082
const [isLoadingFeatures, setIsLoadingFeatures] = useState(true);
9183
const [isLoadingRoles, setIsLoadingRoles] = useState(true);
9284
const selectedTabId = getSelectedTabId(Boolean(capabilities?.roles?.view), _selectedTabId);
9385
const [tabs, selectedTabContent] = useTabs({
9486
space,
9587
features,
96-
roles,
88+
rolesCount: state.roles.size,
89+
capabilities,
90+
history,
9791
currentSelectedTabId: selectedTabId,
9892
...props,
9993
});
@@ -123,33 +117,38 @@ export const ViewSpacePage: FC<PageProps> = (props) => {
123117
}
124118

125119
const getRoles = async () => {
126-
let result: Role[] = [];
127-
try {
128-
result = await spacesManager.getRolesForSpace(spaceId);
129-
} catch (error) {
130-
const message = error?.body?.message ?? error.toString();
131-
const statusCode = error?.body?.statusCode ?? null;
132-
if (statusCode === 403) {
133-
// eslint-disable-next-line no-console
134-
console.log('Insufficient permissions to get list of roles for the space');
135-
// eslint-disable-next-line no-console
136-
console.log(message);
137-
} else {
138-
// eslint-disable-next-line no-console
139-
console.error('Encountered error while getting list of roles for space!');
140-
// eslint-disable-next-line no-console
141-
console.error(error);
142-
throw error;
120+
await invokeClient(async (clients) => {
121+
let result: Role[] = [];
122+
try {
123+
result = await clients.spacesManager.getRolesForSpace(spaceId);
124+
125+
dispatch({ type: 'update_roles', payload: result });
126+
} catch (error) {
127+
const message = error?.body?.message ?? error.toString();
128+
const statusCode = error?.body?.statusCode ?? null;
129+
if (statusCode === 403) {
130+
// eslint-disable-next-line no-console
131+
console.log('Insufficient permissions to get list of roles for the space');
132+
// eslint-disable-next-line no-console
133+
console.log(message);
134+
} else {
135+
// eslint-disable-next-line no-console
136+
console.error('Encountered error while getting list of roles for space!');
137+
// eslint-disable-next-line no-console
138+
console.error(error);
139+
throw error;
140+
}
143141
}
144-
}
142+
});
145143

146-
setRoles(result);
147144
setIsLoadingRoles(false);
148145
};
149146

150-
// maybe we do not make this call if user can't view roles? 🤔
151-
getRoles().catch(handleApiError);
152-
}, [spaceId, spacesManager]);
147+
if (!state.roles.size) {
148+
// maybe we do not make this call if user can't view roles? 🤔
149+
getRoles().catch(handleApiError);
150+
}
151+
}, [dispatch, invokeClient, spaceId, state.roles]);
153152

154153
useEffect(() => {
155154
const _getFeatures = async () => {
@@ -194,98 +193,90 @@ export const ViewSpacePage: FC<PageProps> = (props) => {
194193

195194
return (
196195
<div data-test-subj="spaces-view-page">
197-
<ViewSpaceContextProvider
198-
capabilities={capabilities}
199-
spacesManager={spacesManager}
200-
navigateToUrl={navigateToUrl}
201-
getUrlForApp={getUrlForApp}
202-
{...viewSpaceServices}
203-
>
204-
<EuiText>
205-
<EuiFlexGroup data-test-subj="spaceDetailsHeader" alignItems="flexStart">
206-
<EuiFlexItem grow={false}>
207-
<HeaderAvatar />
208-
</EuiFlexItem>
209-
<EuiFlexItem grow={true}>
210-
<EuiTitle size="l">
211-
<h1 data-test-subj="spaceTitle">
212-
{space.name}
213-
{shouldShowSolutionBadge ? (
214-
<>
215-
{' '}
216-
<SpaceSolutionBadge
217-
solution={solution}
218-
data-test-subj={`space-solution-badge-${solution}`}
196+
<EuiText>
197+
<EuiFlexGroup data-test-subj="spaceDetailsHeader" alignItems="flexStart">
198+
<EuiFlexItem grow={false}>
199+
<HeaderAvatar />
200+
</EuiFlexItem>
201+
<EuiFlexItem grow={true}>
202+
<EuiTitle size="l">
203+
<h1 data-test-subj="spaceTitle">
204+
{space.name}
205+
{shouldShowSolutionBadge ? (
206+
<>
207+
{' '}
208+
<SpaceSolutionBadge
209+
solution={solution}
210+
data-test-subj={`space-solution-badge-${solution}`}
211+
/>
212+
</>
213+
) : null}
214+
{userActiveSpace?.id === id ? (
215+
<>
216+
{' '}
217+
<EuiBadge color="primary">
218+
<FormattedMessage
219+
id="xpack.spaces.management.spaceDetails.space.badge.isCurrent"
220+
description="Text for a badge shown in the Space details page when the particular Space currently active."
221+
defaultMessage="Current"
219222
/>
220-
</>
221-
) : null}
222-
{userActiveSpace?.id === id ? (
223-
<>
224-
{' '}
225-
<EuiBadge color="primary">
226-
<FormattedMessage
227-
id="xpack.spaces.management.spaceDetails.space.badge.isCurrent"
228-
description="Text for a badge shown in the Space details page when the particular Space currently active."
229-
defaultMessage="Current"
230-
/>
231-
</EuiBadge>
232-
</>
233-
) : null}
234-
</h1>
235-
</EuiTitle>
223+
</EuiBadge>
224+
</>
225+
) : null}
226+
</h1>
227+
</EuiTitle>
236228

237-
<EuiText size="s">
238-
<p>
239-
{space.description ?? (
240-
<FormattedMessage
241-
id="xpack.spaces.management.spaceDetails.space.description"
242-
defaultMessage="Organize your saved objects and show related features for creating new content."
243-
/>
244-
)}
245-
</p>
246-
</EuiText>
247-
</EuiFlexItem>
248-
{userActiveSpace?.id !== id ? (
249-
<EuiFlexItem grow={false}>
250-
<EuiButton
251-
iconType="merge"
252-
href={addSpaceIdToPath(
253-
props.serverBasePath,
254-
id,
255-
`${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/edit/${id}`
256-
)}
257-
data-test-subj="spaceSwitcherButton"
258-
>
229+
<EuiText size="s">
230+
<p>
231+
{space.description ?? (
259232
<FormattedMessage
260-
id="xpack.spaces.management.spaceDetails.space.switchToSpaceButton.label"
261-
defaultMessage="Switch to this space"
233+
id="xpack.spaces.management.spaceDetails.space.description"
234+
defaultMessage="Organize your saved objects and show related features for creating new content."
262235
/>
263-
</EuiButton>
264-
</EuiFlexItem>
265-
) : null}
266-
</EuiFlexGroup>
236+
)}
237+
</p>
238+
</EuiText>
239+
</EuiFlexItem>
240+
{userActiveSpace?.id !== id ? (
241+
<EuiFlexItem grow={false}>
242+
<EuiButton
243+
iconType="merge"
244+
href={addSpaceIdToPath(
245+
serverBasePath,
246+
id,
247+
`${ENTER_SPACE_PATH}?next=/app/management/kibana/spaces/edit/${id}`
248+
)}
249+
data-test-subj="spaceSwitcherButton"
250+
>
251+
<FormattedMessage
252+
id="xpack.spaces.management.spaceDetails.space.switchToSpaceButton.label"
253+
defaultMessage="Switch to this space"
254+
/>
255+
</EuiButton>
256+
</EuiFlexItem>
257+
) : null}
258+
</EuiFlexGroup>
267259

268-
<EuiSpacer />
260+
<EuiSpacer />
269261

270-
<EuiFlexGroup direction="column">
271-
<EuiFlexItem>
272-
<EuiTabs>
273-
{tabs.map((tab, index) => (
274-
<EuiTab
275-
key={index}
276-
isSelected={tab.id === selectedTabId}
277-
append={tab.append}
278-
{...reactRouterNavigate(history, `/edit/${encodeURIComponent(id)}/${tab.id}`)}
279-
>
280-
{tab.name}
281-
</EuiTab>
282-
))}
283-
</EuiTabs>
284-
</EuiFlexItem>
285-
<EuiFlexItem>{selectedTabContent ?? null}</EuiFlexItem>
286-
</EuiFlexGroup>
287-
</EuiText>
288-
</ViewSpaceContextProvider>
262+
<EuiFlexGroup direction="column">
263+
<EuiFlexItem>
264+
<EuiTabs>
265+
{tabs.map((tab, index) => (
266+
<EuiTab
267+
key={index}
268+
isSelected={tab.id === selectedTabId}
269+
append={tab.append}
270+
{...reactRouterNavigate(history, `/edit/${encodeURIComponent(id)}/${tab.id}`)}
271+
>
272+
{tab.name}
273+
</EuiTab>
274+
))}
275+
</EuiTabs>
276+
</EuiFlexItem>
277+
<EuiFlexItem>{selectedTabContent ?? null}</EuiFlexItem>
278+
</EuiFlexGroup>
279+
</EuiText>
289280
</div>
290281
);
291282
};

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space_content_tab.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { capitalize } from 'lodash';
1818
import type { FC } from 'react';
1919
import React, { useEffect, useState } from 'react';
2020

21-
import { useViewSpaceServices } from './hooks/view_space_context_provider';
21+
import { useViewSpaceServices } from './provider';
2222
import { addSpaceIdToPath, ENTER_SPACE_PATH, type Space } from '../../../common';
2323
import type { SpaceContentTypeSummaryItem } from '../../types';
2424

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space_features_tab.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import React from 'react';
1212
import type { KibanaFeature } from '@kbn/features-plugin/common';
1313
import { FormattedMessage } from '@kbn/i18n-react';
1414

15-
import { useViewSpaceServices } from './hooks/view_space_context_provider';
15+
import { useViewSpaceServices } from './provider';
1616
import type { Space } from '../../../common';
1717
import { FeatureTable } from '../edit_space/enabled_features/feature_table';
1818
import { SectionPanel } from '../edit_space/section_panel';

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.test.tsx

+13-4
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,37 @@ import React from 'react';
1010

1111
import {
1212
httpServiceMock,
13+
i18nServiceMock,
1314
notificationServiceMock,
1415
overlayServiceMock,
1516
scopedHistoryMock,
17+
themeServiceMock,
1618
} from '@kbn/core/public/mocks';
1719
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
1820
import { KibanaFeature } from '@kbn/features-plugin/common';
1921
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
2022

21-
import { ViewSpaceContextProvider } from './hooks/view_space_context_provider';
23+
import { ViewSpaceProvider } from './provider/view_space_provider';
2224
import { ViewSpaceSettings } from './view_space_general_tab';
2325
import type { SolutionView } from '../../../common';
2426
import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock';
27+
import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock';
2528
import { getRolesAPIClientMock } from '../roles_api_client.mock';
2629

2730
const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true };
2831
const history = scopedHistoryMock.create();
2932
const getUrlForApp = (appId: string) => appId;
3033
const navigateToUrl = jest.fn();
3134
const spacesManager = spacesManagerMock.create();
32-
const getRolesAPIClient = getRolesAPIClientMock();
35+
const getRolesAPIClient = getRolesAPIClientMock;
36+
const getPrivilegeAPIClient = getPrivilegeAPIClientMock;
3337
const reloadWindow = jest.fn();
3438

3539
const http = httpServiceMock.createStartContract();
3640
const notifications = notificationServiceMock.createStartContract();
3741
const overlays = overlayServiceMock.createStartContract();
42+
const theme = themeServiceMock.createStartContract();
43+
const i18n = i18nServiceMock.createStartContract();
3844

3945
const navigateSpy = jest.spyOn(history, 'push').mockImplementation(() => {});
4046
const updateSpaceSpy = jest
@@ -54,7 +60,7 @@ describe('ViewSpaceSettings', () => {
5460
const TestComponent: React.FC = ({ children }) => {
5561
return (
5662
<IntlProvider locale="en">
57-
<ViewSpaceContextProvider
63+
<ViewSpaceProvider
5864
capabilities={{
5965
navLinks: {},
6066
management: {},
@@ -69,9 +75,12 @@ describe('ViewSpaceSettings', () => {
6975
http={http}
7076
notifications={notifications}
7177
overlays={overlays}
78+
getPrivilegesAPIClient={getPrivilegeAPIClient}
79+
theme={theme}
80+
i18n={i18n}
7281
>
7382
{children}
74-
</ViewSpaceContextProvider>
83+
</ViewSpaceProvider>
7584
</IntlProvider>
7685
);
7786
};

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space_general_tab.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
1414
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
1515

1616
import { ViewSpaceTabFooter } from './footer';
17-
import { useViewSpaceServices } from './hooks/view_space_context_provider';
17+
import { useViewSpaceServices } from './provider';
1818
import { ViewSpaceEnabledFeatures } from './view_space_features_tab';
1919
import type { Space } from '../../../common';
2020
import { ConfirmDeleteModal } from '../components';

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space_roles.tsx

+122-391
Large diffs are not rendered by default.

Diff for: ‎x-pack/plugins/spaces/public/management/view_space/view_space_tabs.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import React from 'react';
1111
import type { Capabilities, ScopedHistory } from '@kbn/core/public';
1212
import type { KibanaFeature } from '@kbn/features-plugin/common';
1313
import { i18n } from '@kbn/i18n';
14-
import type { Role } from '@kbn/security-plugin-types-common';
1514
import { withSuspense } from '@kbn/shared-ux-utility';
1615

1716
import { TAB_ID_CONTENT, TAB_ID_GENERAL, TAB_ID_ROLES } from './constants';
@@ -28,7 +27,7 @@ export interface ViewSpaceTab {
2827

2928
export interface GetTabsProps {
3029
space: Space;
31-
roles: Role[];
30+
rolesCount: number;
3231
features: KibanaFeature[];
3332
history: ScopedHistory;
3433
capabilities: Capabilities & {
@@ -67,7 +66,7 @@ export const getTabs = ({
6766
features,
6867
history,
6968
capabilities,
70-
roles,
69+
rolesCount,
7170
...props
7271
}: GetTabsProps): ViewSpaceTab[] => {
7372
const canUserViewRoles = Boolean(capabilities?.roles?.view);
@@ -102,13 +101,12 @@ export const getTabs = ({
102101
}),
103102
append: (
104103
<EuiNotificationBadge className="eui-alignCenter" color="subdued" size="m">
105-
{roles.length}
104+
{rolesCount}
106105
</EuiNotificationBadge>
107106
),
108107
content: (
109108
<SuspenseViewSpaceAssignedRoles
110109
space={space}
111-
roles={roles}
112110
features={features}
113111
isReadOnly={!canUserModifyRoles}
114112
/>

Diff for: ‎x-pack/plugins/spaces/tsconfig.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242
"@kbn/security-plugin-types-common",
4343
"@kbn/core-application-browser",
4444
"@kbn/unsaved-changes-prompt",
45-
"@kbn/core-http-browser",
46-
"@kbn/core-overlays-browser",
47-
"@kbn/core-notifications-browser",
45+
"@kbn/core-lifecycle-browser",
46+
"@kbn/security-role-management-model",
47+
"@kbn/security-ui-components",
48+
"@kbn/react-kibana-mount",
4849
"@kbn/shared-ux-utility",
4950
"@kbn/core-application-common",
51+
"@kbn/security-authorization-core",
5052
],
5153
"exclude": [
5254
"target/**/*",

0 commit comments

Comments
 (0)
Please sign in to comment.