Grouped features for role management (#78152)
* Grouped features for role management * address PR feedback Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
ea6bec6c9b
commit
b9a79836f8
|
@ -2,11 +2,11 @@
|
|||
[[xpack-security-authorization]]
|
||||
|
||||
=== Granting access to {kib}
|
||||
The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges.
|
||||
The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all {kib} features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired {kib} privileges.
|
||||
|
||||
When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_admin` has access to all the features in all spaces.
|
||||
When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants {kib} privileges is ineffective because `kibana_admin` has access to all the features in all spaces.
|
||||
|
||||
NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces.
|
||||
NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to {kib} segments is to grant users access to specific spaces.
|
||||
|
||||
[role="xpack"]
|
||||
[[xpack-kibana-role-management]]
|
||||
|
@ -17,26 +17,26 @@ To create a role that grants {kib} privileges, open the menu, go to *Stack Manag
|
|||
[[adding_kibana_privileges]]
|
||||
==== Adding {kib} privileges
|
||||
|
||||
To assign {kib} privileges to the role, click **Add space privilege** in the Kibana section.
|
||||
To assign {kib} privileges to the role, click **Add {kib} privilege** in the {kib} section.
|
||||
|
||||
[role="screenshot"]
|
||||
image::user/security/images/add-space-privileges.png[Add space privileges]
|
||||
image::user/security/images/add-space-privileges.png[Add {kib} privileges]
|
||||
|
||||
Open the **Spaces** selection control to specify whether to grant the role access to all spaces *** Global (all spaces)** or one or more individual spaces. If you select *** Global (all spaces)**, you can’t select individual spaces until you clear your selection.
|
||||
|
||||
Use the **Privilege** menu to grant access to features. The default is **Custom**, which you can use to grant access to individual features. Otherwise, you can grant read and write access to all current and future features by selecting **All**, or grant read access to all current and future features by selecting **Read**.
|
||||
|
||||
When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to Kibana, roles that use the custom option do not automatically get access to the new features. You must manually update the roles.
|
||||
When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to {kib}, roles that use the custom option do not automatically get access to the new features. You must manually update the roles.
|
||||
|
||||
NOTE: *{stack-monitor-app}* relies on built-in roles to grant access. When a
|
||||
user is assigned the appropriate roles, the *{stack-monitor-app}* application is
|
||||
available; otherwise, it is not visible.
|
||||
|
||||
To apply your changes, click **Create space privilege**. The space privilege shows up under the Kibana privileges section of the role.
|
||||
To apply your changes, click **Add {kib} privilege**. The privilege shows up under the {kib} privileges section of the role.
|
||||
|
||||
|
||||
[role="screenshot"]
|
||||
image::user/security/images/create-space-privilege.png[Create space privilege]
|
||||
image::user/security/images/create-space-privilege.png[Add {kib} privilege]
|
||||
|
||||
==== Feature availability
|
||||
|
||||
|
@ -64,9 +64,9 @@ Features are available to users when their roles grant access to the features, *
|
|||
|
||||
==== Assigning different privileges to different spaces
|
||||
|
||||
Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added space privileges, click **Add space privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control.
|
||||
Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added privileges, click **Add {kib} privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control.
|
||||
|
||||
Additionally, if you’ve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, space privileges are also a union. If you’ve already granted the user the **All** privilege at *** Global (all spaces)**, you’re not able to restrict the role to only the **Read** privilege at an individual space.
|
||||
Additionally, if you’ve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, {kib} privileges are also a union. If you’ve already granted the user the **All** privilege at *** Global (all spaces)**, you’re not able to restrict the role to only the **Read** privilege at an individual space.
|
||||
|
||||
|
||||
==== Privilege summary
|
||||
|
@ -78,39 +78,37 @@ image::user/security/images/view-privilege-summary.png[View privilege summary]
|
|||
|
||||
==== Example 1: Grant all access to Dashboard at an individual space
|
||||
|
||||
. Click **Add space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
. For **Spaces**, select an individual space.
|
||||
. For **Privilege**, leave the default selection of **Custom**.
|
||||
. For the Dashboard feature, select **All**
|
||||
. Click **Create space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
|
||||
[role="screenshot"]
|
||||
image::user/security/images/privilege-example-1.png[Privilege example 1]
|
||||
|
||||
==== Example 2: Grant all access to one space and read access to another
|
||||
|
||||
. Click **Add space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
. For **Spaces**, select the first space.
|
||||
. For **Privilege**, select **All**.
|
||||
. Click **Create space privilege**.
|
||||
. Click **Add space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
. For **Spaces**, select the second space.
|
||||
. For **Privilege**, select **Read**.
|
||||
. Click **Create space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
|
||||
[role="screenshot"]
|
||||
image::user/security/images/privilege-example-2.png[Privilege example 2]
|
||||
|
||||
==== Example 3: Grant read access to all spaces and write access to an individual space
|
||||
|
||||
. Click **Add space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
. For **Spaces**, select *** Global (all spaces)**.
|
||||
. For **Privilege**, select **Read**.
|
||||
. Click **Create space privilege**.
|
||||
. Click **Add space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
. For **Spaces**, select the individual space.
|
||||
. For **Privilege**, select **All**.
|
||||
. Click **Create space privilege**.
|
||||
. Click **Add {kib} privilege**.
|
||||
|
||||
[role="screenshot"]
|
||||
image::user/security/images/privilege-example-3.png[Privilege example 3]
|
||||
|
|
|
@ -14,14 +14,15 @@ export const createFeature = (
|
|||
excludeFromBaseAll?: boolean;
|
||||
excludeFromBaseRead?: boolean;
|
||||
privileges?: KibanaFeatureConfig['privileges'];
|
||||
category?: KibanaFeatureConfig['category'];
|
||||
}
|
||||
) => {
|
||||
const { excludeFromBaseAll, excludeFromBaseRead, privileges, ...rest } = config;
|
||||
const { excludeFromBaseAll, excludeFromBaseRead, privileges, category, ...rest } = config;
|
||||
return new KibanaFeature({
|
||||
icon: 'discoverApp',
|
||||
navLinkId: 'discover',
|
||||
app: [],
|
||||
category: { id: 'foo', label: 'foo' },
|
||||
category: category ?? { id: 'foo', label: 'foo' },
|
||||
catalogue: [],
|
||||
privileges:
|
||||
privileges === null
|
||||
|
|
|
@ -6,30 +6,37 @@
|
|||
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
import {
|
||||
EuiTableRow,
|
||||
EuiCheckbox,
|
||||
EuiCheckboxProps,
|
||||
EuiButtonGroup,
|
||||
EuiButtonGroupProps,
|
||||
} from '@elastic/eui';
|
||||
import { EuiCheckbox, EuiCheckboxProps, EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui';
|
||||
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { EuiAccordion } from '@elastic/eui';
|
||||
import { SubFeatureForm } from '../sub_feature_form';
|
||||
|
||||
export function getDisplayedFeaturePrivileges(wrapper: ReactWrapper<any>) {
|
||||
const allExpanderButtons = findTestSubject(wrapper, 'expandFeaturePrivilegeRow');
|
||||
const categoryExpander = findTestSubject(wrapper, 'featureCategoryButton_foo');
|
||||
categoryExpander.simulate('click');
|
||||
|
||||
const allExpanderButtons = findTestSubject(wrapper, 'featureTableCell');
|
||||
allExpanderButtons.forEach((button) => button.simulate('click'));
|
||||
|
||||
// each expanded row renders its own `EuiTableRow`, so there are 2 rows
|
||||
// for each feature: one for the primary feature privilege, and one for the sub privilege form
|
||||
const rows = wrapper.find(EuiTableRow);
|
||||
const featurePrivilegeControls = wrapper
|
||||
.find(EuiAccordion)
|
||||
.filter('[data-test-subj="featurePrivilegeControls"]');
|
||||
|
||||
return rows.reduce((acc, row) => {
|
||||
return featurePrivilegeControls.reduce((acc, featureControls) => {
|
||||
const buttonGroup = featureControls
|
||||
.find(EuiButtonGroup)
|
||||
.filter('[data-test-subj="primaryFeaturePrivilegeControl"]');
|
||||
const { name, idSelected } = buttonGroup.props();
|
||||
expect(name).toBeDefined();
|
||||
expect(idSelected).toBeDefined();
|
||||
|
||||
const featureId = name!.substr(`featurePrivilege_`.length);
|
||||
const primaryFeaturePrivilege = idSelected!.substr(`${featureId}_`.length);
|
||||
const subFeaturePrivileges = [];
|
||||
const subFeatureForm = row.find(SubFeatureForm);
|
||||
|
||||
const subFeatureForm = featureControls.find(SubFeatureForm);
|
||||
if (subFeatureForm.length > 0) {
|
||||
const { featureId } = subFeatureForm.props();
|
||||
const independentPrivileges = (subFeatureForm.find(EuiCheckbox) as ReactWrapper<
|
||||
EuiCheckboxProps
|
||||
>).reduce((acc2, checkbox) => {
|
||||
|
@ -47,30 +54,15 @@ export function getDisplayedFeaturePrivileges(wrapper: ReactWrapper<any>) {
|
|||
}, [] as string[]);
|
||||
|
||||
subFeaturePrivileges.push(...independentPrivileges, ...mutuallyExclusivePrivileges);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: {
|
||||
...acc[featureId],
|
||||
subFeaturePrivileges,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const buttonGroup = row.find(EuiButtonGroup);
|
||||
const { name, idSelected } = buttonGroup.props();
|
||||
expect(name).toBeDefined();
|
||||
expect(idSelected).toBeDefined();
|
||||
|
||||
const featureId = name!.substr(`featurePrivilege_`.length);
|
||||
const primaryFeaturePrivilege = idSelected!.substr(`${featureId}_`.length);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: {
|
||||
...acc[featureId],
|
||||
primaryFeaturePrivilege,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: {
|
||||
...acc[featureId],
|
||||
primaryFeaturePrivilege,
|
||||
subFeaturePrivileges,
|
||||
},
|
||||
};
|
||||
}, {} as Record<string, { primaryFeaturePrivilege: string; subFeaturePrivileges: string[] }>);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,14 @@
|
|||
|
||||
import './change_all_privileges.scss';
|
||||
|
||||
import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui';
|
||||
import {
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiLink,
|
||||
EuiPopover,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
|
@ -34,10 +41,13 @@ export class ChangeAllPrivilegesControl extends Component<Props, State> {
|
|||
className={'secPrivilegeFeatureChangeAllLink'}
|
||||
data-test-subj="changeAllPrivilegesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.changeAllPrivilegesLink"
|
||||
defaultMessage="(change all)"
|
||||
/>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.changeAllPrivilegesLink"
|
||||
defaultMessage="Bulk actions"
|
||||
/>{' '}
|
||||
<EuiIcon size="s" type="arrowDown" />
|
||||
</EuiText>
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.subFeaturePrivilegeExpandedRegion {
|
||||
background-color: $euiColorLightestShade;
|
||||
padding-left: $euiSizeXXL;
|
||||
padding-top: $euiSizeS;
|
||||
}
|
|
@ -13,7 +13,7 @@ import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileg
|
|||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { getDisplayedFeaturePrivileges } from './__fixtures__';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
|
||||
import { EuiAccordion } from '@elastic/eui';
|
||||
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
|
@ -86,18 +86,19 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -125,14 +126,15 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
|
@ -144,7 +146,7 @@ describe('FeatureTable', () => {
|
|||
'cool_all',
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
: { subFeaturePrivileges: [] }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -175,14 +177,15 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
...(canCustomizeSubFeaturePrivileges ? { subFeaturePrivileges: [] } : {}),
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'all',
|
||||
|
@ -194,7 +197,7 @@ describe('FeatureTable', () => {
|
|||
'cool_all',
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
: { subFeaturePrivileges: [] }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -279,6 +282,7 @@ describe('FeatureTable', () => {
|
|||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
|
@ -313,43 +317,18 @@ describe('FeatureTable', () => {
|
|||
});
|
||||
|
||||
kibanaFeatures.forEach((feature) => {
|
||||
const rowExpander = findTestSubject(wrapper, `expandFeaturePrivilegeRow-${feature.id}`);
|
||||
const { arrowDisplay } = wrapper
|
||||
.find(EuiAccordion)
|
||||
.filter(`#featurePrivilegeControls_${feature.id}`)
|
||||
.props();
|
||||
if (!feature.subFeatures || feature.subFeatures.length === 0) {
|
||||
expect(rowExpander).toHaveLength(0);
|
||||
expect(arrowDisplay).toEqual('none');
|
||||
} else {
|
||||
expect(rowExpander).toHaveLength(1);
|
||||
expect(arrowDisplay).toEqual('left');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the <FeatureTableExpandedRow> when the row is expanded', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
},
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {},
|
||||
},
|
||||
]);
|
||||
const { wrapper } = setup({
|
||||
role,
|
||||
features: kibanaFeatures,
|
||||
privilegeIndex: 1,
|
||||
calculateDisplayedPrivileges: false,
|
||||
canCustomizeSubFeaturePrivileges: true,
|
||||
});
|
||||
|
||||
expect(wrapper.find(FeatureTableExpandedRow)).toHaveLength(0);
|
||||
|
||||
findTestSubject(wrapper, 'expandFeaturePrivilegeRow').first().simulate('click');
|
||||
|
||||
expect(wrapper.find(FeatureTableExpandedRow)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders with sub-feature privileges granted when primary feature privilege is "all"', () => {
|
||||
const role = createRole([
|
||||
{
|
||||
|
@ -679,6 +658,7 @@ describe('FeatureTable', () => {
|
|||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
|
@ -716,15 +696,19 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -750,15 +734,19 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -843,6 +831,7 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
reserved_feature: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -873,16 +862,79 @@ describe('FeatureTable', () => {
|
|||
expect(displayedPrivileges).toEqual({
|
||||
excluded_from_base: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
no_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_excluded_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
with_sub_features: {
|
||||
primaryFeaturePrivilege: 'none',
|
||||
subFeaturePrivileges: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders features by category, indicating how many features are granted within', async () => {
|
||||
const role = createRole([
|
||||
{
|
||||
spaces: ['foo'],
|
||||
base: [],
|
||||
feature: {
|
||||
feature_1: ['all'],
|
||||
feature_3: ['all'],
|
||||
feature_4: ['all'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const features = [
|
||||
createFeature({
|
||||
id: 'feature_1',
|
||||
name: 'Feature1',
|
||||
category: { id: 'foo', label: 'foo' },
|
||||
}),
|
||||
createFeature({
|
||||
id: 'feature_2',
|
||||
name: 'Feature2',
|
||||
category: { id: 'foo', label: 'foo' },
|
||||
}),
|
||||
createFeature({
|
||||
id: 'feature_3',
|
||||
name: 'Feature3',
|
||||
category: { id: 'bar', label: 'bar' },
|
||||
}),
|
||||
createFeature({
|
||||
id: 'feature_4',
|
||||
name: 'Feature4',
|
||||
category: { id: 'bar', label: 'bar' },
|
||||
}),
|
||||
];
|
||||
|
||||
const { wrapper } = setup({
|
||||
role,
|
||||
features,
|
||||
privilegeIndex: 0,
|
||||
calculateDisplayedPrivileges: false,
|
||||
canCustomizeSubFeaturePrivileges: false,
|
||||
});
|
||||
|
||||
const fooCategory = findTestSubject(wrapper, 'featureCategory_foo');
|
||||
const barCategory = findTestSubject(wrapper, 'featureCategory_bar');
|
||||
|
||||
expect(fooCategory).toHaveLength(1);
|
||||
expect(barCategory).toHaveLength(1);
|
||||
|
||||
expect(findTestSubject(fooCategory, 'categoryLabel').text()).toMatchInlineSnapshot(
|
||||
`"1 / 2 features granted"`
|
||||
);
|
||||
|
||||
expect(findTestSubject(barCategory, 'categoryLabel').text()).toMatchInlineSnapshot(
|
||||
`"2 / 2 features granted"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,24 +5,31 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiAccordionProps,
|
||||
EuiButtonGroup,
|
||||
EuiIconTip,
|
||||
EuiInMemoryTable,
|
||||
EuiText,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
EuiHorizontalRule,
|
||||
EuiAccordion,
|
||||
EuiIcon,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
import { AppCategory } from 'kibana/public';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { ChangeAllPrivilegesControl } from './change_all_privileges';
|
||||
import { FeatureTableExpandedRow } from './feature_table_expanded_row';
|
||||
import { NO_PRIVILEGE_VALUE } from '../constants';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
import { FeatureTableCell } from '../feature_table_cell';
|
||||
import { KibanaPrivileges, SecuredFeature, KibanaPrivilege } from '../../../../model';
|
||||
import { KibanaPrivileges, SecuredFeature } from '../../../../model';
|
||||
import './feature_table.scss';
|
||||
|
||||
interface Props {
|
||||
role: Role;
|
||||
|
@ -35,88 +42,291 @@ interface Props {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
expandedFeatures: string[];
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
featureId: string;
|
||||
feature: SecuredFeature;
|
||||
inherited: KibanaPrivilege[];
|
||||
effective: KibanaPrivilege[];
|
||||
role: Role;
|
||||
}
|
||||
|
||||
export class FeatureTable extends Component<Props, State> {
|
||||
export class FeatureTable extends Component<Props, {}> {
|
||||
public static defaultProps = {
|
||||
privilegeIndex: -1,
|
||||
showLocks: true,
|
||||
};
|
||||
|
||||
private featureCategories: Map<string, SecuredFeature[]> = new Map();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expandedFeatures: [],
|
||||
};
|
||||
|
||||
// features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner
|
||||
props.kibanaPrivileges
|
||||
.getSecuredFeatures()
|
||||
.filter((feature) => feature.privileges != null || feature.reserved != null)
|
||||
.forEach((feature) => {
|
||||
if (!this.featureCategories.has(feature.category.id)) {
|
||||
this.featureCategories.set(feature.category.id, []);
|
||||
}
|
||||
this.featureCategories.get(feature.category.id)!.push(feature);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { role, kibanaPrivileges } = this.props;
|
||||
const basePrivileges = this.props.kibanaPrivileges.getBasePrivileges(
|
||||
this.props.role.kibana[this.props.privilegeIndex]
|
||||
);
|
||||
|
||||
const featurePrivileges = kibanaPrivileges
|
||||
.getSecuredFeatures()
|
||||
.filter((feature) => feature.privileges != null || feature.reserved != null);
|
||||
const accordions: Array<{ order: number; element: ReactElement }> = [];
|
||||
this.featureCategories.forEach((featuresInCategory) => {
|
||||
const { category } = featuresInCategory[0];
|
||||
|
||||
const items: TableRow[] = featurePrivileges
|
||||
.sort((feature1, feature2) => {
|
||||
if (feature1.reserved && !feature2.reserved) {
|
||||
return 1;
|
||||
const featureCount = featuresInCategory.length;
|
||||
const grantedCount = featuresInCategory.filter(
|
||||
(feature) =>
|
||||
this.props.privilegeCalculator.getEffectivePrimaryFeaturePrivilege(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
) != null
|
||||
).length;
|
||||
|
||||
const canExpandCategory = true; // featuresInCategory.length > 1;
|
||||
|
||||
const buttonContent = (
|
||||
<EuiFlexGroup
|
||||
data-test-subj={`featureCategoryButton_${category.id}`}
|
||||
alignItems={'center'}
|
||||
responsive={false}
|
||||
gutterSize="m"
|
||||
>
|
||||
{category.euiIconType ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type={category.euiIconType} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiTitle size="xs">
|
||||
<h4 className="eui-displayInlineBlock">{category.label}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const label: string = i18n.translate(
|
||||
'xpack.security.management.editRole.featureTable.featureAccordionSwitchLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{grantedCount} / {featureCount} {featureCount, plural, one {feature} other {features}} granted',
|
||||
values: {
|
||||
grantedCount,
|
||||
featureCount,
|
||||
},
|
||||
}
|
||||
);
|
||||
const extraAction = (
|
||||
<EuiText size="s" aria-hidden="true" color={'subdued'} data-test-subj="categoryLabel">
|
||||
{label}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
if (feature2.reserved && !feature1.reserved) {
|
||||
return -1;
|
||||
}
|
||||
const helpText = this.getCategoryHelpText(category);
|
||||
|
||||
return 0;
|
||||
})
|
||||
.map((feature) => {
|
||||
return {
|
||||
featureId: feature.id,
|
||||
feature,
|
||||
inherited: [],
|
||||
effective: [],
|
||||
role,
|
||||
};
|
||||
const accordion = (
|
||||
<EuiAccordion
|
||||
id={`featureCategory_${category.id}`}
|
||||
data-test-subj={`featureCategory_${category.id}`}
|
||||
key={category.id}
|
||||
arrowDisplay={canExpandCategory ? 'left' : 'none'}
|
||||
forceState={canExpandCategory ? undefined : 'closed'}
|
||||
buttonContent={buttonContent}
|
||||
extraAction={canExpandCategory ? extraAction : undefined}
|
||||
>
|
||||
<div>
|
||||
<EuiSpacer size="s" />
|
||||
{helpText && (
|
||||
<>
|
||||
<EuiCallOut iconType="iInCircle" size="s">
|
||||
{helpText}
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{featuresInCategory.map((feature) => (
|
||||
<EuiFlexItem key={feature.id}>
|
||||
{this.renderPrivilegeControlsForFeature(feature)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiAccordion>
|
||||
);
|
||||
|
||||
accordions.push({
|
||||
order: category.order ?? Number.MAX_SAFE_INTEGER,
|
||||
element: accordion,
|
||||
});
|
||||
});
|
||||
|
||||
accordions.sort((a1, a2) => a1.order - a2.order);
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
responsive={false}
|
||||
columns={this.getColumns()}
|
||||
itemId={'featureId'}
|
||||
itemIdToExpandedRowMap={this.state.expandedFeatures.reduce((acc, featureId) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: (
|
||||
<FeatureTableExpandedRow
|
||||
feature={featurePrivileges.find((f) => f.id === featureId)!}
|
||||
privilegeIndex={this.props.privilegeIndex}
|
||||
onChange={this.props.onChange}
|
||||
privilegeCalculator={this.props.privilegeCalculator}
|
||||
selectedFeaturePrivileges={
|
||||
this.props.role.kibana[this.props.privilegeIndex].feature[featureId] ?? []
|
||||
}
|
||||
disabled={this.props.disabled}
|
||||
<div>
|
||||
<EuiFlexGroup alignItems={'flexEnd'}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">
|
||||
<b>
|
||||
{i18n.translate(
|
||||
'xpack.security.management.editRole.featureTable.featureVisibilityTitle',
|
||||
{
|
||||
defaultMessage: 'Customize feature privileges',
|
||||
}
|
||||
)}
|
||||
</b>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{!this.props.disabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ChangeAllPrivilegesControl
|
||||
privileges={basePrivileges}
|
||||
onChange={this.onChangeAllFeaturePrivileges}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, {})}
|
||||
items={items}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin={'m'} />
|
||||
{accordions.flatMap((a, idx) => [
|
||||
a.element,
|
||||
<EuiHorizontalRule key={`accordion-hr-${idx}`} margin={'m'} />,
|
||||
])}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public onChange = (featureId: string) => (featurePrivilegeId: string) => {
|
||||
private renderPrivilegeControlsForFeature = (feature: SecuredFeature) => {
|
||||
const renderFeatureMarkup = (
|
||||
buttonContent: EuiAccordionProps['buttonContent'],
|
||||
extraAction: EuiAccordionProps['extraAction'],
|
||||
warningIcon: JSX.Element
|
||||
) => {
|
||||
const { canCustomizeSubFeaturePrivileges } = this.props;
|
||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>{warningIcon}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiAccordion
|
||||
id={`featurePrivilegeControls_${feature.id}`}
|
||||
data-test-subj="featurePrivilegeControls"
|
||||
buttonContent={buttonContent}
|
||||
extraAction={extraAction}
|
||||
forceState={
|
||||
canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges ? undefined : 'closed'
|
||||
}
|
||||
arrowDisplay={
|
||||
canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges ? 'left' : 'none'
|
||||
}
|
||||
>
|
||||
<div className="subFeaturePrivilegeExpandedRegion">
|
||||
<FeatureTableExpandedRow
|
||||
feature={feature}
|
||||
privilegeIndex={this.props.privilegeIndex}
|
||||
onChange={this.props.onChange}
|
||||
privilegeCalculator={this.props.privilegeCalculator}
|
||||
selectedFeaturePrivileges={
|
||||
this.props.role.kibana[this.props.privilegeIndex].feature[feature.id] ?? []
|
||||
}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</div>
|
||||
</EuiAccordion>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const primaryFeaturePrivileges = feature.getPrimaryFeaturePrivileges();
|
||||
|
||||
if (feature.reserved && primaryFeaturePrivileges.length === 0) {
|
||||
const buttonContent = (
|
||||
<>
|
||||
{<EuiIcon type="empty" size="l" />} <FeatureTableCell feature={feature} />
|
||||
</>
|
||||
);
|
||||
|
||||
const extraAction = (
|
||||
<EuiText size={'s'} data-test-subj="reservedFeatureDescription">
|
||||
{feature.reserved.description}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
return renderFeatureMarkup(buttonContent, extraAction, <EuiIcon type="empty" />);
|
||||
}
|
||||
|
||||
if (primaryFeaturePrivileges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPrivilegeId = this.props.privilegeCalculator.getDisplayedPrimaryFeaturePrivilegeId(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
);
|
||||
|
||||
const options = primaryFeaturePrivileges.map((privilege) => {
|
||||
return {
|
||||
id: `${feature.id}_${privilege.id}`,
|
||||
label: privilege.name,
|
||||
isDisabled: this.props.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
options.push({
|
||||
id: `${feature.id}_${NO_PRIVILEGE_VALUE}`,
|
||||
label: 'None',
|
||||
isDisabled: this.props.disabled,
|
||||
});
|
||||
|
||||
let warningIcon = <EuiIconTip type="empty" content={null} />;
|
||||
if (
|
||||
this.props.privilegeCalculator.hasCustomizedSubFeaturePrivileges(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
)
|
||||
) {
|
||||
warningIcon = (
|
||||
<EuiIconTip
|
||||
type="alert"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip"
|
||||
defaultMessage="Feature has customized sub-feature privileges. Expand this row for more information."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { canCustomizeSubFeaturePrivileges } = this.props;
|
||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||
|
||||
const showAccordionArrow = canCustomizeSubFeaturePrivileges && hasSubFeaturePrivileges;
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
{!showAccordionArrow && <EuiIcon type="empty" size="l" />}{' '}
|
||||
<FeatureTableCell feature={feature} />
|
||||
</>
|
||||
);
|
||||
|
||||
const extraAction = (
|
||||
<EuiButtonGroup
|
||||
name={`featurePrivilege_${feature.id}`}
|
||||
data-test-subj={`primaryFeaturePrivilegeControl`}
|
||||
isFullWidth={true}
|
||||
options={options}
|
||||
idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`}
|
||||
onChange={this.onChange(feature.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
return renderFeatureMarkup(buttonContent, extraAction, warningIcon);
|
||||
};
|
||||
|
||||
private onChange = (featureId: string) => (featurePrivilegeId: string) => {
|
||||
const privilege = featurePrivilegeId.substr(`${featureId}_`.length);
|
||||
if (privilege === NO_PRIVILEGE_VALUE) {
|
||||
this.props.onChange(featureId, []);
|
||||
|
@ -125,163 +335,6 @@ export class FeatureTable extends Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
private getColumns = () => {
|
||||
const basePrivileges = this.props.kibanaPrivileges.getBasePrivileges(
|
||||
this.props.role.kibana[this.props.privilegeIndex]
|
||||
);
|
||||
|
||||
const columns = [];
|
||||
|
||||
if (this.props.canCustomizeSubFeaturePrivileges) {
|
||||
columns.push({
|
||||
width: '30px',
|
||||
isExpander: true,
|
||||
field: 'featureId',
|
||||
name: '',
|
||||
render: (featureId: string, record: TableRow) => {
|
||||
const { feature } = record;
|
||||
const hasSubFeaturePrivileges = feature.getSubFeaturePrivileges().length > 0;
|
||||
if (!hasSubFeaturePrivileges) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
onClick={() => this.toggleExpandedFeature(featureId)}
|
||||
data-test-subj={`expandFeaturePrivilegeRow expandFeaturePrivilegeRow-${featureId}`}
|
||||
aria-label={this.state.expandedFeatures.includes(featureId) ? 'Collapse' : 'Expand'}
|
||||
iconType={this.state.expandedFeatures.includes(featureId) ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
field: 'feature',
|
||||
width: '200px',
|
||||
name: i18n.translate(
|
||||
'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Feature',
|
||||
}
|
||||
),
|
||||
render: (feature: SecuredFeature) => {
|
||||
return <FeatureTableCell feature={feature} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'privilege',
|
||||
width: '200px',
|
||||
name: (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle"
|
||||
defaultMessage="Privilege"
|
||||
/>
|
||||
{!this.props.disabled && (
|
||||
<ChangeAllPrivilegesControl
|
||||
privileges={basePrivileges}
|
||||
onChange={this.onChangeAllFeaturePrivileges}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
mobileOptions: {
|
||||
// Table isn't responsive, so skip rendering this for mobile. <ChangeAllPrivilegesControl /> isn't free...
|
||||
header: false,
|
||||
},
|
||||
render: (roleEntry: Role, record: TableRow) => {
|
||||
const { feature } = record;
|
||||
|
||||
const primaryFeaturePrivileges = feature.getPrimaryFeaturePrivileges();
|
||||
|
||||
if (feature.reserved && primaryFeaturePrivileges.length === 0) {
|
||||
return (
|
||||
<EuiText size={'s'} data-test-subj="reservedFeatureDescription">
|
||||
{feature.reserved.description}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
if (primaryFeaturePrivileges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPrivilegeId = this.props.privilegeCalculator.getDisplayedPrimaryFeaturePrivilegeId(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
);
|
||||
|
||||
const options = primaryFeaturePrivileges.map((privilege) => {
|
||||
return {
|
||||
id: `${feature.id}_${privilege.id}`,
|
||||
label: privilege.name,
|
||||
isDisabled: this.props.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
options.push({
|
||||
id: `${feature.id}_${NO_PRIVILEGE_VALUE}`,
|
||||
label: 'None',
|
||||
isDisabled: this.props.disabled,
|
||||
});
|
||||
|
||||
let warningIcon = <EuiIconTip type="empty" content={null} />;
|
||||
if (
|
||||
this.props.privilegeCalculator.hasCustomizedSubFeaturePrivileges(
|
||||
feature.id,
|
||||
this.props.privilegeIndex
|
||||
)
|
||||
) {
|
||||
warningIcon = (
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip"
|
||||
defaultMessage="Feature has customized sub-feature privileges. Expand this row for more information."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>{warningIcon}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonGroup
|
||||
name={`featurePrivilege_${feature.id}`}
|
||||
data-test-subj={`primaryFeaturePrivilegeControl`}
|
||||
buttonSize="compressed"
|
||||
color={'primary'}
|
||||
isFullWidth={true}
|
||||
options={options}
|
||||
idSelected={`${feature.id}_${selectedPrivilegeId ?? NO_PRIVILEGE_VALUE}`}
|
||||
onChange={this.onChange(feature.id)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
return columns;
|
||||
};
|
||||
|
||||
private toggleExpandedFeature = (featureId: string) => {
|
||||
if (this.state.expandedFeatures.includes(featureId)) {
|
||||
this.setState({
|
||||
expandedFeatures: this.state.expandedFeatures.filter((ef) => ef !== featureId),
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
expandedFeatures: [...this.state.expandedFeatures, featureId],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onChangeAllFeaturePrivileges = (privilege: string) => {
|
||||
if (privilege === NO_PRIVILEGE_VALUE) {
|
||||
this.props.onChangeAll([]);
|
||||
|
@ -289,4 +342,16 @@ export class FeatureTable extends Component<Props, State> {
|
|||
this.props.onChangeAll([privilege]);
|
||||
}
|
||||
};
|
||||
|
||||
private getCategoryHelpText = (category: AppCategory) => {
|
||||
if (category.id === 'management') {
|
||||
return i18n.translate(
|
||||
'xpack.security.management.editRole.featureTable.managementCategoryHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Access to Stack Management is determined by both Elasticsearch and Kibana privileges, and cannot be explicitly disabled.',
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
.secPrivilegeFeatureIcon {
|
||||
flex-shrink: 0;
|
||||
margin-right: $euiSizeS;
|
||||
}
|
|
@ -9,10 +9,10 @@ import { createFeature } from '../../../../__fixtures__/kibana_features';
|
|||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { FeatureTableCell } from '.';
|
||||
import { SecuredFeature } from '../../../../model';
|
||||
import { EuiIcon, EuiIconTip } from '@elastic/eui';
|
||||
import { EuiIconTip } from '@elastic/eui';
|
||||
|
||||
describe('FeatureTableCell', () => {
|
||||
it('renders an icon and feature name', () => {
|
||||
it('renders the feature name', () => {
|
||||
const feature = createFeature({
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
|
@ -23,13 +23,10 @@ describe('FeatureTableCell', () => {
|
|||
);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`);
|
||||
expect(wrapper.find(EuiIcon).props()).toMatchObject({
|
||||
type: feature.icon,
|
||||
});
|
||||
expect(wrapper.find(EuiIconTip)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders an icon and feature name with tooltip when configured', () => {
|
||||
it('renders a feature name with tooltip when configured', () => {
|
||||
const feature = createFeature({
|
||||
id: 'test-feature',
|
||||
name: 'Test Feature',
|
||||
|
@ -41,9 +38,7 @@ describe('FeatureTableCell', () => {
|
|||
);
|
||||
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"Test Feature "`);
|
||||
expect(wrapper.find(EuiIcon).first().props()).toMatchObject({
|
||||
type: feature.icon,
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiIconTip).props().content).toMatchInlineSnapshot(`
|
||||
<EuiText>
|
||||
<p>
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './feature_table_cell.scss';
|
||||
|
||||
import React from 'react';
|
||||
import { EuiText, EuiIconTip, EuiIcon, IconType } from '@elastic/eui';
|
||||
import { EuiText, EuiIconTip } from '@elastic/eui';
|
||||
import { SecuredFeature } from '../../../../model';
|
||||
|
||||
interface Props {
|
||||
|
@ -35,8 +33,7 @@ export const FeatureTableCell = ({ feature }: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<EuiIcon size="m" type={feature.icon as IconType} className="secPrivilegeFeatureIcon" />
|
||||
<span data-test-subj={`featureTableCell`}>
|
||||
{feature.name} {tooltipElement}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -6,16 +6,9 @@
|
|||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiButtonEmpty,
|
||||
EuiOverlayMask,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiOverlayMask, EuiButton } from '@elastic/eui';
|
||||
import { EuiFlyout } from '@elastic/eui';
|
||||
import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
|
||||
import { Space } from '../../../../../../../../spaces/common/model/space';
|
||||
import { Role } from '../../../../../../../common/model';
|
||||
import { PrivilegeSummaryTable } from './privilege_summary_table';
|
||||
|
@ -30,6 +23,9 @@ interface Props {
|
|||
export const PrivilegeSummary = (props: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const numberOfPrivilegeDefinitions = props.role.kibana.length;
|
||||
const flyoutSize = numberOfPrivilegeDefinitions > 5 ? 'l' : 'm';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonEmpty onClick={() => setIsOpen(true)} data-test-subj="viewPrivilegeSummaryButton">
|
||||
|
@ -39,33 +35,35 @@ export const PrivilegeSummary = (props: Props) => {
|
|||
/>
|
||||
</EuiButtonEmpty>
|
||||
{isOpen && (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={() => setIsOpen(false)} maxWidth={false}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle"
|
||||
defaultMessage="Privilege summary"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiOverlayMask headerZindexLocation="below">
|
||||
<EuiFlyout onClose={() => setIsOpen(false)} size={flyoutSize}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle"
|
||||
defaultMessage="Privilege summary"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<PrivilegeSummaryTable
|
||||
role={props.role}
|
||||
spaces={props.spaces}
|
||||
kibanaPrivileges={props.kibanaPrivileges}
|
||||
canCustomizeSubFeaturePrivileges={props.canCustomizeSubFeaturePrivileges}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiButton onClick={() => setIsOpen(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.privilegeSummary.closeSummaryButtonText"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</Fragment>
|
||||
|
|
|
@ -4,14 +4,19 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState, Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiSpacer,
|
||||
EuiAccordion,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { Space } from '../../../../../../../../spaces/common/model/space';
|
||||
import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
|
||||
|
@ -39,6 +44,22 @@ function getColumnKey(entry: RoleKibanaPrivilege) {
|
|||
export const PrivilegeSummaryTable = (props: Props) => {
|
||||
const [expandedFeatures, setExpandedFeatures] = useState<string[]>([]);
|
||||
|
||||
const featureCategories = useMemo(() => {
|
||||
const featureCategoryMap = new Map<string, SecuredFeature[]>();
|
||||
|
||||
props.kibanaPrivileges
|
||||
.getSecuredFeatures()
|
||||
.filter((feature) => feature.privileges != null || feature.reserved != null)
|
||||
.forEach((feature) => {
|
||||
if (!featureCategoryMap.has(feature.category.id)) {
|
||||
featureCategoryMap.set(feature.category.id, []);
|
||||
}
|
||||
featureCategoryMap.get(feature.category.id)!.push(feature);
|
||||
});
|
||||
|
||||
return featureCategoryMap;
|
||||
}, [props.kibanaPrivileges]);
|
||||
|
||||
const calculator = new PrivilegeSummaryCalculator(props.kibanaPrivileges, props.role);
|
||||
|
||||
const toggleExpandedFeature = (featureId: string) => {
|
||||
|
@ -140,35 +161,80 @@ export const PrivilegeSummaryTable = (props: Props) => {
|
|||
};
|
||||
}, {} as Record<string, EffectiveFeaturePrivileges>);
|
||||
|
||||
const items = props.kibanaPrivileges.getSecuredFeatures().map((feature) => {
|
||||
return {
|
||||
feature,
|
||||
featureId: feature.id,
|
||||
...privileges,
|
||||
};
|
||||
const accordions: any[] = [];
|
||||
|
||||
featureCategories.forEach((featuresInCategory) => {
|
||||
const { category } = featuresInCategory[0];
|
||||
|
||||
const buttonContent = (
|
||||
<EuiFlexGroup
|
||||
data-test-subj={`featureCategoryButton_${category.id}`}
|
||||
alignItems={'center'}
|
||||
responsive={false}
|
||||
gutterSize="m"
|
||||
>
|
||||
{category.euiIconType ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type={category.euiIconType} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiTitle size="xs">
|
||||
<h4 className="eui-displayInlineBlock">{category.label}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const categoryItems = featuresInCategory.map((feature) => {
|
||||
return {
|
||||
feature,
|
||||
featureId: feature.id,
|
||||
...privileges,
|
||||
};
|
||||
});
|
||||
|
||||
accordions.push(
|
||||
<EuiAccordion
|
||||
id={`privilegeSummaryFeatureCategory_${category.id}`}
|
||||
data-test-subj={`privilegeSummaryFeatureCategory_${category.id}`}
|
||||
key={category.id}
|
||||
buttonContent={buttonContent}
|
||||
initialIsOpen={true}
|
||||
>
|
||||
<EuiInMemoryTable
|
||||
columns={columns}
|
||||
items={categoryItems}
|
||||
itemId="featureId"
|
||||
rowProps={(record) => {
|
||||
return {
|
||||
'data-test-subj': `summaryTableRow-${record.featureId}`,
|
||||
};
|
||||
}}
|
||||
itemIdToExpandedRowMap={expandedFeatures.reduce((acc, featureId) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: (
|
||||
<PrivilegeSummaryExpandedRow
|
||||
feature={props.kibanaPrivileges.getSecuredFeature(featureId)}
|
||||
effectiveFeaturePrivileges={Object.values(privileges).map((p) => p[featureId])}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, {})}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
itemId="featureId"
|
||||
rowProps={(record) => {
|
||||
return {
|
||||
'data-test-subj': `summaryTableRow-${record.featureId}`,
|
||||
};
|
||||
}}
|
||||
itemIdToExpandedRowMap={expandedFeatures.reduce((acc, featureId) => {
|
||||
return {
|
||||
...acc,
|
||||
[featureId]: (
|
||||
<PrivilegeSummaryExpandedRow
|
||||
feature={props.kibanaPrivileges.getSecuredFeature(featureId)}
|
||||
effectiveFeaturePrivileges={Object.values(privileges).map((p) => p[featureId])}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}, {})}
|
||||
/>
|
||||
<>
|
||||
{accordions.map((a, idx) => (
|
||||
<Fragment key={idx}>
|
||||
{a}
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -43,7 +43,7 @@ const spaces = [
|
|||
];
|
||||
|
||||
describe('SpaceColumnHeader', () => {
|
||||
it('renders the Global privilege definition with a special label and popover control', () => {
|
||||
it('renders the Global privilege definition with a special label', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SpaceColumnHeader
|
||||
spaces={spaces}
|
||||
|
@ -55,10 +55,9 @@ describe('SpaceColumnHeader', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(SpacesPopoverList)).toHaveLength(1);
|
||||
// Snapshot includes space avatar (The first "G"), followed by the "Global" label,
|
||||
// followed by the (all spaces) text as part of the SpacesPopoverList
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"G Global(all spaces)"`);
|
||||
expect(wrapper.text()).toMatchInlineSnapshot(`"G All Spaces"`);
|
||||
});
|
||||
|
||||
it('renders a placeholder space when the requested space no longer exists', () => {
|
||||
|
|
|
@ -39,17 +39,7 @@ export const SpaceColumnHeader = (props: Props) => {
|
|||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName"
|
||||
defaultMessage="Global"
|
||||
/>
|
||||
<br />
|
||||
<SpacesPopoverList
|
||||
spaces={props.spaces.filter((s) => s.id !== '*')}
|
||||
buttonText={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink',
|
||||
{
|
||||
defaultMessage: '(all spaces)',
|
||||
}
|
||||
)}
|
||||
defaultMessage="All Spaces"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
|
|
@ -11,11 +11,11 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
|||
import { PrivilegeSpaceForm } from './privilege_space_form';
|
||||
import React from 'react';
|
||||
import { Space } from '../../../../../../../../spaces/public';
|
||||
import { EuiSuperSelect } from '@elastic/eui';
|
||||
import { FeatureTable } from '../feature_table';
|
||||
import { getDisplayedFeaturePrivileges } from '../feature_table/__fixtures__';
|
||||
import { findTestSubject } from 'test_utils/find_test_subject';
|
||||
import { SpaceSelector } from './space_selector';
|
||||
import { EuiButtonGroup } from '@elastic/eui';
|
||||
|
||||
const createRole = (kibana: Role['kibana'] = []): Role => {
|
||||
return {
|
||||
|
@ -59,7 +59,9 @@ describe('PrivilegeSpaceForm', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_custom`);
|
||||
expect(
|
||||
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
|
||||
).toEqual(`basePrivilege_custom`);
|
||||
expect(wrapper.find(FeatureTable).props().disabled).toEqual(true);
|
||||
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -69,6 +71,7 @@ describe('PrivilegeSpaceForm', () => {
|
|||
},
|
||||
"no_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "none",
|
||||
"subFeaturePrivileges": Array [],
|
||||
},
|
||||
"with_excluded_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "none",
|
||||
|
@ -106,7 +109,9 @@ describe('PrivilegeSpaceForm', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_all`);
|
||||
expect(
|
||||
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
|
||||
).toEqual(`basePrivilege_all`);
|
||||
expect(wrapper.find(FeatureTable).props().disabled).toEqual(true);
|
||||
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -116,6 +121,7 @@ describe('PrivilegeSpaceForm', () => {
|
|||
},
|
||||
"no_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "all",
|
||||
"subFeaturePrivileges": Array [],
|
||||
},
|
||||
"with_excluded_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "all",
|
||||
|
@ -159,7 +165,9 @@ describe('PrivilegeSpaceForm', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_custom`);
|
||||
expect(
|
||||
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
|
||||
).toEqual(`basePrivilege_custom`);
|
||||
expect(wrapper.find(FeatureTable).props().disabled).toEqual(false);
|
||||
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -169,6 +177,7 @@ describe('PrivilegeSpaceForm', () => {
|
|||
},
|
||||
"no_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "none",
|
||||
"subFeaturePrivileges": Array [],
|
||||
},
|
||||
"with_excluded_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "none",
|
||||
|
@ -256,7 +265,10 @@ describe('PrivilegeSpaceForm', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiSuperSelect).props().valueOfSelected).toEqual(`basePrivilege_custom`);
|
||||
expect(
|
||||
wrapper.find(EuiButtonGroup).filter('[name="basePrivilegeButtonGroup"]').props().idSelected
|
||||
).toEqual(`basePrivilege_custom`);
|
||||
|
||||
expect(wrapper.find(FeatureTable).props().disabled).toEqual(false);
|
||||
expect(getDisplayedFeaturePrivileges(wrapper)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -266,6 +278,7 @@ describe('PrivilegeSpaceForm', () => {
|
|||
},
|
||||
"no_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "none",
|
||||
"subFeaturePrivileges": Array [],
|
||||
},
|
||||
"with_excluded_sub_features": Object {
|
||||
"primaryFeaturePrivilege": "none",
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
EuiFormRow,
|
||||
EuiOverlayMask,
|
||||
EuiSpacer,
|
||||
EuiSuperSelect,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiErrorBoundary,
|
||||
|
@ -26,6 +25,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { EuiButtonGroup } from '@elastic/eui';
|
||||
import { Space } from '../../../../../../../../spaces/public';
|
||||
import { Role, copyRole } from '../../../../../../../common/model';
|
||||
import { SpaceSelector } from './space_selector';
|
||||
|
@ -95,7 +95,7 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
|
|||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.modalTitle"
|
||||
defaultMessage="Space privileges"
|
||||
defaultMessage="Kibana privileges"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
@ -164,6 +164,13 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
|
|||
defaultMessage: 'Spaces',
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Select one or more Kibana spaces to which you wish to assign privileges.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<SpaceSelector
|
||||
selectedSpaceIds={this.state.selectedSpaceIds}
|
||||
|
@ -179,104 +186,46 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
|
|||
label={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel',
|
||||
{
|
||||
defaultMessage: 'Privilege',
|
||||
defaultMessage: 'Privileges for all features',
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Assign the privilege level you wish to grant to all present and future features across this space.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj={'basePrivilegeComboBox'}
|
||||
fullWidth
|
||||
onChange={this.onSpaceBasePrivilegeChange}
|
||||
<EuiButtonGroup
|
||||
name={`basePrivilegeButtonGroup`}
|
||||
data-test-subj={`basePrivilegeButtonGroup`}
|
||||
isFullWidth={true}
|
||||
color={'primary'}
|
||||
options={[
|
||||
{
|
||||
value: 'basePrivilege_custom',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay"
|
||||
defaultMessage="Custom"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails"
|
||||
defaultMessage="Customize access by feature in selected spaces."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
id: 'basePrivilege_all',
|
||||
label: 'All',
|
||||
['data-test-subj']: 'basePrivilege_all',
|
||||
},
|
||||
{
|
||||
value: 'basePrivilege_read',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay"
|
||||
defaultMessage="Read"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails"
|
||||
defaultMessage="Grant read-only access to all features in selected spaces."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
id: 'basePrivilege_read',
|
||||
label: 'Read',
|
||||
['data-test-subj']: 'basePrivilege_read',
|
||||
},
|
||||
{
|
||||
value: 'basePrivilege_all',
|
||||
inputDisplay: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay"
|
||||
defaultMessage="All"
|
||||
/>
|
||||
</strong>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails"
|
||||
defaultMessage="Grant full access to all features in selected spaces."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
id: 'basePrivilege_custom',
|
||||
label: 'Customize',
|
||||
['data-test-subj']: 'basePrivilege_custom',
|
||||
},
|
||||
]}
|
||||
hasDividers
|
||||
valueOfSelected={this.getDisplayedBasePrivilege()}
|
||||
disabled={!hasSelectedSpaces}
|
||||
idSelected={this.getDisplayedBasePrivilege()}
|
||||
isDisabled={!hasSelectedSpaces}
|
||||
onChange={this.onSpaceBasePrivilegeChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTitle size="xxs">
|
||||
<h3>{this.getFeatureListLabel(this.state.selectedBasePrivilege.length > 0)}</h3>
|
||||
|
@ -338,7 +287,7 @@ export class PrivilegeSpaceForm extends Component<Props, State> {
|
|||
buttonText = (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton"
|
||||
defaultMessage="Create space privilege"
|
||||
defaultMessage="Add Kibana privilege"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import React, { Component } from 'react';
|
||||
import { Space, getSpaceColor } from '../../../../../../../../spaces/public';
|
||||
import { FeaturesPrivileges, Role, copyRole } from '../../../../../../../common/model';
|
||||
import { SpacesPopoverList } from '../../../spaces_popover_list';
|
||||
import { PrivilegeDisplay } from './privilege_display';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../privilege_utils';
|
||||
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
|
||||
|
@ -118,19 +117,7 @@ export class PrivilegeSpaceTable extends Component<Props, State> {
|
|||
const displayedSpaces = isExpanded ? spaces : spaces.slice(0, SPACES_DISPLAY_COUNT);
|
||||
|
||||
let button = null;
|
||||
if (record.isGlobal) {
|
||||
button = (
|
||||
<SpacesPopoverList
|
||||
spaces={this.props.displaySpaces.filter((s) => s.id !== '*')}
|
||||
buttonText={i18n.translate(
|
||||
'xpack.security.management.editRole.spacePrivilegeTable.showAllSpacesLink',
|
||||
{
|
||||
defaultMessage: 'show spaces',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (spaces.length > displayedSpaces.length) {
|
||||
if (spaces.length > displayedSpaces.length) {
|
||||
button = (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
|
|
|
@ -50,7 +50,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
|
|||
name: i18n.translate(
|
||||
'xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName',
|
||||
{
|
||||
defaultMessage: '* Global (all spaces)',
|
||||
defaultMessage: '* All Spaces',
|
||||
}
|
||||
),
|
||||
color: '#D3DAE6',
|
||||
|
@ -198,7 +198,7 @@ export class SpaceAwarePrivilegeSection extends Component<Props, State> {
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton"
|
||||
defaultMessage="Add space privilege"
|
||||
defaultMessage="Add Kibana privilege"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
|
|
@ -58,7 +58,7 @@ export class CustomizeSpace extends Component<Props, State> {
|
|||
};
|
||||
|
||||
return (
|
||||
<SectionPanel collapsible={false} title={panelTitle} description={panelTitle}>
|
||||
<SectionPanel title={panelTitle} description={panelTitle}>
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="xs">
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
|
||||
exports[`EnabledFeatures renders as expected 1`] = `
|
||||
<SectionPanel
|
||||
collapsible={false}
|
||||
data-test-subj="enabled-features-panel"
|
||||
description="Customize visible features"
|
||||
initiallyCollapsed={false}
|
||||
title={
|
||||
<span>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -34,8 +34,6 @@ export class EnabledFeatures extends Component<Props, {}> {
|
|||
|
||||
return (
|
||||
<SectionPanel
|
||||
collapsible={false}
|
||||
initiallyCollapsed={false}
|
||||
title={this.getPanelTitle()}
|
||||
description={description}
|
||||
data-test-subj="enabled-features-panel"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.spcFeatureTableAccordionContent {
|
||||
// Align accordion content with the feature category logo in the accordion's buttonContent
|
||||
padding-left: $euiSizeXL;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,17 +24,6 @@ exports[`it renders without blowing up 1`] = `
|
|||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLink
|
||||
aria-label="hide desc"
|
||||
data-test-subj="show-hide-section-link"
|
||||
onClick={[Function]}
|
||||
>
|
||||
hide
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<p>
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SectionPanel } from './section_panel';
|
||||
|
||||
test('it renders without blowing up', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<SectionPanel collapsible iconType="logoElasticsearch" title="Elasticsearch" description="desc">
|
||||
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch" description="desc">
|
||||
<p>child</p>
|
||||
</SectionPanel>
|
||||
);
|
||||
|
@ -19,9 +18,9 @@ test('it renders without blowing up', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders children by default', () => {
|
||||
test('it renders children', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SectionPanel collapsible iconType="logoElasticsearch" title="Elasticsearch" description="desc">
|
||||
<SectionPanel iconType="logoElasticsearch" title="Elasticsearch" description="desc">
|
||||
<p className="child">child 1</p>
|
||||
<p className="child">child 2</p>
|
||||
</SectionPanel>
|
||||
|
@ -30,19 +29,3 @@ test('it renders children by default', () => {
|
|||
expect(wrapper.find(SectionPanel)).toHaveLength(1);
|
||||
expect(wrapper.find('.child')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('it hides children when the "hide" link is clicked', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SectionPanel collapsible iconType="logoElasticsearch" title="Elasticsearch" description="desc">
|
||||
<p className="child">child 1</p>
|
||||
<p className="child">child 2</p>
|
||||
</SectionPanel>
|
||||
);
|
||||
|
||||
expect(wrapper.find(SectionPanel)).toHaveLength(1);
|
||||
expect(wrapper.find('.child')).toHaveLength(2);
|
||||
|
||||
wrapper.find(EuiLink).simulate('click');
|
||||
|
||||
expect(wrapper.find('.child')).toHaveLength(0);
|
||||
});
|
||||
|
|
|
@ -8,39 +8,20 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
IconType,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
iconType?: IconType;
|
||||
title: string | ReactNode;
|
||||
description: string;
|
||||
collapsible: boolean;
|
||||
initiallyCollapsed?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export class SectionPanel extends Component<Props, State> {
|
||||
public state = {
|
||||
collapsed: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: props.initiallyCollapsed || false,
|
||||
};
|
||||
}
|
||||
|
||||
export class SectionPanel extends Component<Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
|
@ -51,30 +32,6 @@ export class SectionPanel extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public getTitle = () => {
|
||||
const showLinkText = i18n.translate('xpack.spaces.management.collapsiblePanel.showLinkText', {
|
||||
defaultMessage: 'show',
|
||||
});
|
||||
|
||||
const hideLinkText = i18n.translate('xpack.spaces.management.collapsiblePanel.hideLinkText', {
|
||||
defaultMessage: 'hide',
|
||||
});
|
||||
|
||||
const showLinkDescription = i18n.translate(
|
||||
'xpack.spaces.management.collapsiblePanel.showLinkDescription',
|
||||
{
|
||||
defaultMessage: 'show {title}',
|
||||
values: { title: this.props.description },
|
||||
}
|
||||
);
|
||||
|
||||
const hideLinkDescription = i18n.translate(
|
||||
'xpack.spaces.management.collapsiblePanel.hideLinkDescription',
|
||||
{
|
||||
defaultMessage: 'hide {title}',
|
||||
values: { title: this.props.description },
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems={'baseline'} gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -93,26 +50,11 @@ export class SectionPanel extends Component<Props, State> {
|
|||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{this.props.collapsible && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="show-hide-section-link"
|
||||
onClick={this.toggleCollapsed}
|
||||
aria-label={this.state.collapsed ? showLinkDescription : hideLinkDescription}
|
||||
>
|
||||
{this.state.collapsed ? showLinkText : hideLinkText}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
public getForm = () => {
|
||||
if (this.state.collapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
|
@ -120,10 +62,4 @@ export class SectionPanel extends Component<Props, State> {
|
|||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
public toggleCollapsed = () => {
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14302,8 +14302,6 @@
|
|||
"xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "このロールがクラスターに対して実行できる操作を管理します。 ",
|
||||
"xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "権限として実行",
|
||||
"xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel": "サブ機能権限をカスタマイズする",
|
||||
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle": "権限",
|
||||
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle": "機能",
|
||||
"xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip": "機能でサブ機能の権限がカスタマイズされています。この行を展開すると詳細が表示されます。",
|
||||
"xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "インデックスの権限を削除",
|
||||
"xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "提供されたドキュメントのクエリ",
|
||||
|
@ -14345,35 +14343,24 @@
|
|||
"xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。",
|
||||
"xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "権限が不十分です",
|
||||
"xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "選択されたスペースの全機能への完全アクセスを許可します。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "すべて",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "すべて",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.cancelButton": "キャンセル",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivilegeDescription": "機能ごとに権限のレベルを上げます。機能によってはスペースごとに非表示になっているか、グローバルスペース権限による影響を受けているものもあります。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivileges": "機能ごとにカスタマイズ",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails": "選択されたスペースの機能ごとにアクセスをカスタマイズします",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay": "カスタム",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay": "カスタム",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.featurePrivilegeSummaryDescription": "機能によってはスペースごとに非表示になっているか、グローバルスペース権限による影響を受けているものもあります。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeNotice": "これらの権限はすべての現在および未来のスペースに適用されます。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeWarning": "グローバル権限の作成は他のスペース権限に影響を与える可能性があります。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "スペース権限",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "権限",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails": "選択されたスペースの全機能への読み込み専用アクセスを許可します。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay": "読み込み",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay": "読み込み",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormLabel": "スペース",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.summaryOfFeaturePrivileges": "機能権限のサマリー",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarning": "宣言された権限は、構成済みグローバル権限よりも許容度が低くなります。権限サマリーを表示すると有効な権限がわかります。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle": "グローバル権限に置き換え",
|
||||
"xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "グローバル",
|
||||
"xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink": "(すべてのスペース)",
|
||||
"xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink": "他 {count} 件",
|
||||
"xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "スペース権限を追加",
|
||||
"xpack.security.management.editRole.spacePrivilegeSection.noAccessToKibanaTitle": "このロールは Kibana へのアクセスを許可しません",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.deletePrivilegesLabel": "次のスペースの権限を削除: {spaceNames}",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.editPrivilegesLabel": "次のスペースの権限を編集: {spaceNames}",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.showAllSpacesLink": "スペースを表示",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.showLessSpacesLink": "縮小表示",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.showNMoreSpacesLink": "他 {count} 件",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.supersededPrivilegeWarning": "権限は、構成されたグローバル権限に置き換わります。権限サマリーを表示すると有効な権限がわかります。",
|
||||
|
@ -17360,10 +17347,6 @@
|
|||
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "このページの設定は、別途指定されていない限り {spaceName}’スペースに適用されます。’",
|
||||
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "設定",
|
||||
"xpack.spaces.management.breadcrumb": "スペース",
|
||||
"xpack.spaces.management.collapsiblePanel.hideLinkDescription": "{title} を非表示",
|
||||
"xpack.spaces.management.collapsiblePanel.hideLinkText": "非表示",
|
||||
"xpack.spaces.management.collapsiblePanel.showLinkDescription": "{title} を表示",
|
||||
"xpack.spaces.management.collapsiblePanel.showLinkText": "表示",
|
||||
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "キャンセル",
|
||||
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "このスペースで表示される機能を更新しました。保存後にページが更新されます。",
|
||||
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "スペースの更新の確認",
|
||||
|
|
|
@ -14311,8 +14311,6 @@
|
|||
"xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。 ",
|
||||
"xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "运行身份权限",
|
||||
"xpack.security.management.editRole.featureTable.customizeSubFeaturePrivilegesSwitchLabel": "定制子功能权限",
|
||||
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesEnabledColumnTitle": "权限",
|
||||
"xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle": "功能",
|
||||
"xpack.security.management.editRole.featureTable.privilegeCustomizationTooltip": "功能已定制子功能权限。展开此行以了解更多信息。",
|
||||
"xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限",
|
||||
"xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "已授权文档查询",
|
||||
|
@ -14354,35 +14352,24 @@
|
|||
"xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。",
|
||||
"xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足",
|
||||
"xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "授予对选定工作区所有功能的完全访问权限。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "全部",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "全部",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.cancelButton": "取消",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivilegeDescription": "按功能提高权限级别。某些功能可能被工作区隐藏或受全局工作区权限影响。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customizeFeaturePrivileges": "按功能定制",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDetails": "在选定工作区中按功能定制访问权限。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDisplay": "定制",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.customPrivilegeDropdownDisplay": "定制",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.featurePrivilegeSummaryDescription": "某些功能可能被工作区隐藏或受全局工作区权限影响。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeNotice": "这些权限将应用到所有当前和未来工作区。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.globalPrivilegeWarning": "创建全局权限可能会影响您的其他工作区权限。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.modalTitle": "工作区权限",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.privilegeSelectorFormLabel": "权限",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDetails": "授予对选定工作区所有功能的只读访问权限。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDisplay": "读取",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.readPrivilegeDropdownDisplay": "读取",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.spaceSelectorFormLabel": "工作区",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.summaryOfFeaturePrivileges": "功能权限的摘要",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarning": "声明的权限相对配置的全局权限有较小的宽容度。查看权限摘要以查看有效的权限。",
|
||||
"xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle": "已由全局权限取代",
|
||||
"xpack.security.management.editRole.spacePrivilegeMatrix.globalSpaceName": "全局",
|
||||
"xpack.security.management.editRole.spacePrivilegeMatrix.showAllSpacesLink": "(所有工作区)",
|
||||
"xpack.security.management.editRole.spacePrivilegeMatrix.showNMoreSpacesLink": "另外 {count} 个",
|
||||
"xpack.security.management.editRole.spacePrivilegeSection.addSpacePrivilegeButton": "添加工作区权限",
|
||||
"xpack.security.management.editRole.spacePrivilegeSection.noAccessToKibanaTitle": "此角色未授予对 Kibana 的访问权限",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.deletePrivilegesLabel": "删除以下工作区的权限:{spaceNames}。",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.editPrivilegesLabel": "编辑以下工作区的权限:{spaceNames}。",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.showAllSpacesLink": "显示工作区",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.showLessSpacesLink": "显示更少",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.showNMoreSpacesLink": "另外 {count} 个",
|
||||
"xpack.security.management.editRole.spacePrivilegeTable.supersededPrivilegeWarning": "权限已由配置的全局权限取代。查看权限摘要以查看有效的权限。",
|
||||
|
@ -17370,10 +17357,6 @@
|
|||
"xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "除非已指定,否则此页面上的设置适用于 {spaceName} 空间。",
|
||||
"xpack.spaces.management.advancedSettingsTitle.settingsTitle": "设置",
|
||||
"xpack.spaces.management.breadcrumb": "工作区",
|
||||
"xpack.spaces.management.collapsiblePanel.hideLinkDescription": "隐藏 {title}",
|
||||
"xpack.spaces.management.collapsiblePanel.hideLinkText": "隐藏",
|
||||
"xpack.spaces.management.collapsiblePanel.showLinkDescription": "显示 {title}",
|
||||
"xpack.spaces.management.collapsiblePanel.showLinkText": "显示",
|
||||
"xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "取消",
|
||||
"xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "您已更新此工作区中的可见功能。保存后,您的页面将重新加载。",
|
||||
"xpack.spaces.management.confirmAlterActiveSpaceModal.title": "确认更新工作区",
|
||||
|
|
|
@ -429,10 +429,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider
|
|||
const globalSpaceOption = await find.byCssSelector(`#spaceOption_\\*`);
|
||||
await globalSpaceOption.click();
|
||||
|
||||
await testSubjects.click('basePrivilegeComboBox');
|
||||
|
||||
const privilegeOption = await find.byCssSelector(`#basePrivilege_${privilegeName}`);
|
||||
await privilegeOption.click();
|
||||
await testSubjects.click(`basePrivilege_${privilegeName}`);
|
||||
|
||||
await testSubjects.click('createSpacePrivilegeButton');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue