mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Role Management: improve editing experience for DLS queries (#99977)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d2690d8ac8
commit
af38aca3fd
9 changed files with 190 additions and 52 deletions
|
@ -95,6 +95,11 @@ export interface Props {
|
|||
* Should the editor use a transparent background
|
||||
*/
|
||||
transparentBackground?: boolean;
|
||||
|
||||
/**
|
||||
* Should the editor be rendered using the fullWidth EUI attribute
|
||||
*/
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export class CodeEditor extends React.Component<Props, {}> {
|
||||
|
|
|
@ -51,7 +51,7 @@ export const CodeEditor: React.FunctionComponent<Props> = (props) => {
|
|||
* Renders a Monaco code editor in the same style as other EUI form fields.
|
||||
*/
|
||||
export const CodeEditorField: React.FunctionComponent<Props> = (props) => {
|
||||
const { width, height, options } = props;
|
||||
const { width, height, options, fullWidth } = props;
|
||||
const darkMode = useUiSetting<boolean>('theme:darkMode');
|
||||
const theme = darkMode ? darkTheme : lightTheme;
|
||||
const style = {
|
||||
|
@ -75,7 +75,12 @@ export const CodeEditorField: React.FunctionComponent<Props> = (props) => {
|
|||
</EuiFormControlLayout>
|
||||
}
|
||||
>
|
||||
<EuiFormControlLayout append={<div hidden />} style={style} readOnly={options?.readOnly}>
|
||||
<EuiFormControlLayout
|
||||
append={<div hidden />}
|
||||
style={style}
|
||||
readOnly={options?.readOnly}
|
||||
fullWidth={fullWidth}
|
||||
>
|
||||
<LazyBaseEditor {...props} useDarkTheme={darkMode} transparentBackground />
|
||||
</EuiFormControlLayout>
|
||||
</React.Suspense>
|
||||
|
|
|
@ -26,4 +26,18 @@ export class MonacoEditorService extends FtrService {
|
|||
|
||||
return values[nthIndex] as string;
|
||||
}
|
||||
|
||||
public async setCodeEditorValue(nthIndex: number, value: string) {
|
||||
await this.retry.try(async () => {
|
||||
await this.browser.execute(
|
||||
(editorIndex, codeEditorValue) => {
|
||||
const editor = (window as any).MonacoEnvironment.monaco.editor;
|
||||
const instance = editor.getModels()[editorIndex];
|
||||
instance.setValue(JSON.parse(codeEditorValue));
|
||||
},
|
||||
nthIndex,
|
||||
value
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,50 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`it renders without crashing 1`] = `<Fragment />`;
|
||||
exports[`it renders without crashing 1`] = `
|
||||
<IndexPrivileges
|
||||
availableIndexPrivileges={
|
||||
Array [
|
||||
"all",
|
||||
"read",
|
||||
"write",
|
||||
"index",
|
||||
]
|
||||
}
|
||||
editable={true}
|
||||
indexPatterns={Array []}
|
||||
indicesAPIClient={
|
||||
Object {
|
||||
"getFields": [MockFunction],
|
||||
}
|
||||
}
|
||||
license={
|
||||
Object {
|
||||
"features$": Observable {
|
||||
"_isScalar": false,
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
"getFeatures": [MockFunction],
|
||||
"getType": [MockFunction],
|
||||
"isEnabled": [MockFunction],
|
||||
"isLicenseAvailable": [MockFunction],
|
||||
}
|
||||
}
|
||||
onChange={[MockFunction]}
|
||||
role={
|
||||
Object {
|
||||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
"name": "",
|
||||
}
|
||||
}
|
||||
validator={
|
||||
RoleValidator {
|
||||
"shouldValidate": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -9,7 +9,12 @@ import { EuiButtonIcon, EuiComboBox, EuiTextArea } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
|
||||
import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
|
||||
import {
|
||||
CodeEditorField,
|
||||
KibanaContextProvider,
|
||||
} from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { indicesAPIClientMock } from '../../../index.mock';
|
||||
import { RoleValidator } from '../../validate_role';
|
||||
import { IndexPrivilegeForm } from './index_privilege_form';
|
||||
|
@ -180,9 +185,13 @@ describe(`document level security`, () => {
|
|||
...props,
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(<IndexPrivilegeForm {...testProps} />);
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<IndexPrivilegeForm {...testProps} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
expect(wrapper.find('EuiSwitch[data-test-subj="restrictDocumentsQuery0"]')).toHaveLength(1);
|
||||
expect(wrapper.find(EuiTextArea)).toHaveLength(1);
|
||||
expect(wrapper.find(CodeEditorField)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -15,16 +15,16 @@ import {
|
|||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTextArea,
|
||||
} from '@elastic/eui';
|
||||
import _ from 'lodash';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import type { monaco } from '@kbn/monaco';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
|
||||
import { CodeEditorField } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import type { RoleIndexPrivilege } from '../../../../../../common/model';
|
||||
import type { IndicesAPIClient } from '../../../indices_api_client';
|
||||
import type { RoleValidator } from '../../validate_role';
|
||||
|
@ -52,6 +52,7 @@ interface State {
|
|||
grantedFields: string[];
|
||||
exceptedFields: string[];
|
||||
documentQuery?: string;
|
||||
documentQueryEditorHeight: string;
|
||||
isFieldListLoading: boolean;
|
||||
flsOptions: string[];
|
||||
}
|
||||
|
@ -73,6 +74,7 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
grantedFields: grant,
|
||||
exceptedFields: except,
|
||||
documentQuery: props.indexPrivilege.query,
|
||||
documentQueryEditorHeight: '100px',
|
||||
isFieldListLoading: false,
|
||||
flsOptions: [],
|
||||
};
|
||||
|
@ -302,21 +304,19 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
<EuiFlexGroup direction="column">
|
||||
{!this.props.isRoleReadOnly && (
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<EuiSwitch
|
||||
data-test-subj={`restrictDocumentsQuery${this.props.formIndex}`}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.indexPrivilegeForm.grantReadPrivilegesLabel"
|
||||
defaultMessage="Grant read privileges to specific documents"
|
||||
/>
|
||||
}
|
||||
compressed={true}
|
||||
checked={this.state.queryExpanded}
|
||||
onChange={this.toggleDocumentQuery}
|
||||
disabled={isRoleReadOnly}
|
||||
/>
|
||||
}
|
||||
<EuiSwitch
|
||||
data-test-subj={`restrictDocumentsQuery${this.props.formIndex}`}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.indexPrivilegeForm.grantReadPrivilegesLabel"
|
||||
defaultMessage="Grant read privileges to specific documents"
|
||||
/>
|
||||
}
|
||||
compressed={true}
|
||||
checked={this.state.queryExpanded}
|
||||
onChange={this.toggleDocumentQuery}
|
||||
disabled={isRoleReadOnly}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{this.state.queryExpanded && (
|
||||
|
@ -329,14 +329,30 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
/>
|
||||
}
|
||||
fullWidth={true}
|
||||
data-test-subj={`queryInput${this.props.formIndex}`}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj={`queryInput${this.props.formIndex}`}
|
||||
style={{ resize: 'none' }}
|
||||
<CodeEditorField
|
||||
languageId="xjson"
|
||||
width="100%"
|
||||
fullWidth={true}
|
||||
value={indexPrivilege.query}
|
||||
height={this.state.documentQueryEditorHeight}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryEditorAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Granted documents query editor',
|
||||
}
|
||||
)}
|
||||
value={indexPrivilege.query ?? ''}
|
||||
onChange={this.onQueryChange}
|
||||
readOnly={this.props.isRoleReadOnly}
|
||||
options={{
|
||||
readOnly: this.props.isRoleReadOnly,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
// Prevent an empty form from showing an error
|
||||
renderValidationDecorations: indexPrivilege.query ? 'editable' : 'off',
|
||||
}}
|
||||
editorDidMount={this.editorDidMount}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
@ -345,6 +361,24 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
);
|
||||
};
|
||||
|
||||
private editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
/**
|
||||
* Resize the editor based on the contents of the editor itself.
|
||||
* Adapted from https://github.com/microsoft/monaco-editor/issues/794#issuecomment-688959283
|
||||
*/
|
||||
|
||||
const minHeight = 100;
|
||||
const maxHeight = 1000;
|
||||
|
||||
const updateHeight = () => {
|
||||
const contentHeight = Math.min(maxHeight, Math.max(minHeight, editor.getContentHeight()));
|
||||
this.setState({ documentQueryEditorHeight: `${contentHeight}px` });
|
||||
};
|
||||
|
||||
editor.onDidContentSizeChange(updateHeight);
|
||||
updateHeight();
|
||||
};
|
||||
|
||||
private toggleDocumentQuery = () => {
|
||||
const willToggleOff = this.state.queryExpanded;
|
||||
const willToggleOn = !willToggleOff;
|
||||
|
@ -457,10 +491,10 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
private onQueryChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
private onQueryChange = (query: string) => {
|
||||
this.props.onChange({
|
||||
...this.props.indexPrivilege,
|
||||
query: e.target.value,
|
||||
query,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
|
||||
import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { licenseMock } from '../../../../../../common/licensing/index.mock';
|
||||
import { indicesAPIClientMock } from '../../../index.mock';
|
||||
import { RoleValidator } from '../../validate_role';
|
||||
|
@ -44,9 +46,13 @@ test('it renders without crashing', async () => {
|
|||
indicesAPIClient: indicesAPIClientMock.create(),
|
||||
license,
|
||||
};
|
||||
const wrapper = shallowWithIntl(<IndexPrivileges {...props} />);
|
||||
const wrapper = shallowWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<IndexPrivileges {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
await flushPromises();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.children()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders a IndexPrivilegeForm for each privilege on the role', async () => {
|
||||
|
@ -86,7 +92,11 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async ()
|
|||
indicesAPIClient,
|
||||
license,
|
||||
};
|
||||
const wrapper = mountWithIntl(<IndexPrivileges {...props} />);
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<IndexPrivileges {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
await flushPromises();
|
||||
expect(wrapper.find(IndexPrivilegeForm)).toHaveLength(1);
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { FatalErrorsSetup, StartServicesAccessor } from 'src/core/public';
|
||||
import type { RegisterManagementAppArgs } from 'src/plugins/management/public';
|
||||
|
||||
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import type { SecurityLicense } from '../../../common/licensing';
|
||||
import type { PluginStartDependencies } from '../../plugin';
|
||||
import { tryDecodeURIComponent } from '../url_utils';
|
||||
|
@ -42,10 +43,7 @@ export const rolesManagementApp = Object.freeze({
|
|||
];
|
||||
|
||||
const [
|
||||
[
|
||||
{ application, docLinks, http, i18n: i18nStart, notifications, chrome },
|
||||
{ data, features, spaces },
|
||||
],
|
||||
[startServices, { data, features, spaces }],
|
||||
{ RolesGridPage },
|
||||
{ EditRolePage },
|
||||
{ RolesAPIClient },
|
||||
|
@ -62,6 +60,15 @@ export const rolesManagementApp = Object.freeze({
|
|||
import('../users'),
|
||||
]);
|
||||
|
||||
const {
|
||||
application,
|
||||
docLinks,
|
||||
http,
|
||||
i18n: i18nStart,
|
||||
notifications,
|
||||
chrome,
|
||||
} = startServices;
|
||||
|
||||
chrome.docTitle.change(title);
|
||||
|
||||
const rolesAPIClient = new RolesAPIClient(http);
|
||||
|
@ -119,21 +126,24 @@ export const rolesManagementApp = Object.freeze({
|
|||
};
|
||||
|
||||
render(
|
||||
<i18nStart.Context>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={['/', '']} exact={true}>
|
||||
<RolesGridPageWithBreadcrumbs />
|
||||
</Route>
|
||||
<Route path="/edit/:roleName?">
|
||||
<EditRolePageWithBreadcrumbs action="edit" />
|
||||
</Route>
|
||||
<Route path="/clone/:roleName">
|
||||
<EditRolePageWithBreadcrumbs action="clone" />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</i18nStart.Context>,
|
||||
<KibanaContextProvider services={startServices}>
|
||||
<i18nStart.Context>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={['/', '']} exact={true}>
|
||||
<RolesGridPageWithBreadcrumbs />
|
||||
</Route>
|
||||
<Route path="/edit/:roleName?">
|
||||
<EditRolePageWithBreadcrumbs action="edit" />
|
||||
</Route>
|
||||
<Route path="/clone/:roleName">
|
||||
<EditRolePageWithBreadcrumbs action="clone" />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</i18nStart.Context>
|
||||
</KibanaContextProvider>,
|
||||
|
||||
element
|
||||
);
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export class SecurityPageObject extends FtrService {
|
|||
private readonly deployment = this.ctx.getService('deployment');
|
||||
private readonly common = this.ctx.getPageObject('common');
|
||||
private readonly header = this.ctx.getPageObject('header');
|
||||
private readonly monacoEditor = this.ctx.getService('monacoEditor');
|
||||
|
||||
public loginPage = Object.freeze({
|
||||
login: async (username?: string, password?: string, options: LoginOptions = {}) => {
|
||||
|
@ -467,7 +468,10 @@ export class SecurityPageObject extends FtrService {
|
|||
|
||||
if (roleObj.elasticsearch.indices[0].query) {
|
||||
await this.testSubjects.click('restrictDocumentsQuery0');
|
||||
await this.testSubjects.setValue('queryInput0', roleObj.elasticsearch.indices[0].query);
|
||||
await this.monacoEditor.setCodeEditorValue(
|
||||
0,
|
||||
JSON.stringify(roleObj.elasticsearch.indices[0].query)
|
||||
);
|
||||
}
|
||||
|
||||
const globalPrivileges = (roleObj.kibana as any).global;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue