mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[AppServices] Space privilege "Index pattern management" read still shows delete button (#53682) (#115390)
* [AppServices] Space privilege Index pattern management read still shows delete button (#53682)
This commit is contained in:
parent
647ca9a82b
commit
933ece47ad
18 changed files with 210 additions and 146 deletions
|
@ -56,7 +56,7 @@ const confirmModalOptionsDelete = {
|
|||
|
||||
export const EditIndexPattern = withRouter(
|
||||
({ indexPattern, history, location }: EditIndexPatternProps) => {
|
||||
const { uiSettings, overlays, chrome, data } =
|
||||
const { application, uiSettings, overlays, chrome, data } =
|
||||
useKibana<IndexPatternManagmentContext>().services;
|
||||
const [fields, setFields] = useState<IndexPatternField[]>(indexPattern.getNonScriptedFields());
|
||||
const [conflictedFields, setConflictedFields] = useState<IndexPatternField[]>(
|
||||
|
@ -134,12 +134,14 @@ export const EditIndexPattern = withRouter(
|
|||
const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0));
|
||||
const kibana = useKibana();
|
||||
const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping;
|
||||
const userEditPermission = !!application?.capabilities?.indexPatterns?.save;
|
||||
|
||||
return (
|
||||
<div data-test-subj="editIndexPattern" role="region" aria-label={headingAriaLabel}>
|
||||
<IndexHeader
|
||||
indexPattern={indexPattern}
|
||||
setDefault={setDefaultPattern}
|
||||
deleteIndexPatternClick={removePattern}
|
||||
{...(userEditPermission ? { deleteIndexPatternClick: removePattern } : {})}
|
||||
defaultIndex={defaultIndex}
|
||||
>
|
||||
{showTagsSection && (
|
||||
|
|
|
@ -78,6 +78,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
|
|||
"terms",
|
||||
],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "Elastic",
|
||||
"searchable": true,
|
||||
|
@ -96,6 +97,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
|
|||
"date_histogram (interval: 30s, delay: 30s, UTC)",
|
||||
],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "timestamp",
|
||||
"type": "date",
|
||||
|
@ -111,6 +113,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "conflictingField",
|
||||
"type": "keyword, long",
|
||||
|
@ -133,6 +136,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
|
|||
"value_count",
|
||||
],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "amount",
|
||||
"type": "long",
|
||||
|
@ -166,6 +170,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "Elastic",
|
||||
"searchable": true,
|
||||
|
@ -200,6 +205,7 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "timestamp",
|
||||
"type": "date",
|
||||
|
@ -233,6 +239,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "Elastic",
|
||||
"searchable": true,
|
||||
|
@ -248,6 +255,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "timestamp",
|
||||
"type": "date",
|
||||
|
@ -263,6 +271,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "conflictingField",
|
||||
"type": "keyword, long",
|
||||
|
@ -277,6 +286,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": undefined,
|
||||
"name": "amount",
|
||||
"type": "long",
|
||||
|
|
|
@ -105,6 +105,7 @@ exports[`Table should render normally 1`] = `
|
|||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"data-test-subj": "editFieldFormat",
|
||||
"description": "Edit",
|
||||
"icon": "pencil",
|
||||
|
@ -141,6 +142,7 @@ exports[`Table should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": true,
|
||||
"isUserEditable": true,
|
||||
"kbnType": "string",
|
||||
"name": "Elastic",
|
||||
"searchable": true,
|
||||
|
@ -152,6 +154,7 @@ exports[`Table should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": true,
|
||||
"isUserEditable": true,
|
||||
"kbnType": "date",
|
||||
"name": "timestamp",
|
||||
"type": "date",
|
||||
|
@ -162,6 +165,7 @@ exports[`Table should render normally 1`] = `
|
|||
"hasRuntime": false,
|
||||
"info": Array [],
|
||||
"isMapped": true,
|
||||
"isUserEditable": true,
|
||||
"kbnType": "conflict",
|
||||
"name": "conflictingField",
|
||||
"type": "text, long",
|
||||
|
@ -172,10 +176,22 @@ exports[`Table should render normally 1`] = `
|
|||
"hasRuntime": true,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": true,
|
||||
"kbnType": "text",
|
||||
"name": "customer",
|
||||
"type": "keyword",
|
||||
},
|
||||
Object {
|
||||
"displayName": "noedit",
|
||||
"excluded": false,
|
||||
"hasRuntime": true,
|
||||
"info": Array [],
|
||||
"isMapped": false,
|
||||
"isUserEditable": false,
|
||||
"kbnType": "text",
|
||||
"name": "noedit",
|
||||
"type": "keyword",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={
|
||||
|
|
|
@ -26,6 +26,7 @@ const items: IndexedFieldItem[] = [
|
|||
kbnType: 'string',
|
||||
excluded: false,
|
||||
isMapped: true,
|
||||
isUserEditable: true,
|
||||
hasRuntime: false,
|
||||
},
|
||||
{
|
||||
|
@ -36,6 +37,7 @@ const items: IndexedFieldItem[] = [
|
|||
info: [],
|
||||
excluded: false,
|
||||
isMapped: true,
|
||||
isUserEditable: true,
|
||||
hasRuntime: false,
|
||||
},
|
||||
{
|
||||
|
@ -46,6 +48,7 @@ const items: IndexedFieldItem[] = [
|
|||
info: [],
|
||||
excluded: false,
|
||||
isMapped: true,
|
||||
isUserEditable: true,
|
||||
hasRuntime: false,
|
||||
},
|
||||
{
|
||||
|
@ -56,6 +59,18 @@ const items: IndexedFieldItem[] = [
|
|||
info: [],
|
||||
excluded: false,
|
||||
isMapped: false,
|
||||
isUserEditable: true,
|
||||
hasRuntime: true,
|
||||
},
|
||||
{
|
||||
name: 'noedit',
|
||||
displayName: 'noedit',
|
||||
type: 'keyword',
|
||||
kbnType: 'text',
|
||||
info: [],
|
||||
excluded: false,
|
||||
isMapped: false,
|
||||
isUserEditable: false,
|
||||
hasRuntime: true,
|
||||
},
|
||||
];
|
||||
|
@ -114,6 +129,13 @@ describe('Table', () => {
|
|||
expect(editField).toBeCalled();
|
||||
});
|
||||
|
||||
test('should not allow edit or deletion for user with only read access', () => {
|
||||
const editAvailable = renderTable().prop('columns')[6].actions[0].available(items[4]);
|
||||
const deleteAvailable = renderTable().prop('columns')[7].actions[0].available(items[4]);
|
||||
expect(editAvailable).toBeFalsy();
|
||||
expect(deleteAvailable).toBeFalsy();
|
||||
});
|
||||
|
||||
test('render name', () => {
|
||||
const mappedField = {
|
||||
name: 'customer',
|
||||
|
@ -122,6 +144,7 @@ describe('Table', () => {
|
|||
kbnType: 'string',
|
||||
type: 'keyword',
|
||||
isMapped: true,
|
||||
isUserEditable: true,
|
||||
hasRuntime: false,
|
||||
};
|
||||
|
||||
|
@ -134,6 +157,7 @@ describe('Table', () => {
|
|||
kbnType: 'string',
|
||||
type: 'keyword',
|
||||
isMapped: false,
|
||||
isUserEditable: true,
|
||||
hasRuntime: true,
|
||||
};
|
||||
|
||||
|
|
|
@ -319,6 +319,7 @@ export class Table extends PureComponent<IndexedFieldProps> {
|
|||
onClick: editField,
|
||||
type: 'icon',
|
||||
'data-test-subj': 'editFieldFormat',
|
||||
available: (field) => field.isUserEditable,
|
||||
},
|
||||
],
|
||||
width: '40px',
|
||||
|
@ -333,7 +334,7 @@ export class Table extends PureComponent<IndexedFieldProps> {
|
|||
onClick: (field) => deleteField(field.name),
|
||||
type: 'icon',
|
||||
'data-test-subj': 'deleteField',
|
||||
available: (field) => !field.isMapped,
|
||||
available: (field) => !field.isMapped && field.isUserEditable,
|
||||
},
|
||||
],
|
||||
width: '40px',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { IndexPatternField, IndexPattern, IndexPatternType } from 'src/plugins/data/public';
|
||||
import { IndexedFieldsTable } from './indexed_fields_table';
|
||||
import { getFieldInfo } from '../../utils';
|
||||
|
@ -78,15 +78,21 @@ const fields = [
|
|||
displayName: 'Elastic',
|
||||
searchable: true,
|
||||
esTypes: ['keyword'],
|
||||
isUserEditable: true,
|
||||
},
|
||||
{ name: 'timestamp', displayName: 'timestamp', esTypes: ['date'] },
|
||||
{ name: 'conflictingField', displayName: 'conflictingField', esTypes: ['keyword', 'long'] },
|
||||
{ name: 'amount', displayName: 'amount', esTypes: ['long'] },
|
||||
{ name: 'timestamp', displayName: 'timestamp', esTypes: ['date'], isUserEditable: true },
|
||||
{
|
||||
name: 'conflictingField',
|
||||
displayName: 'conflictingField',
|
||||
esTypes: ['keyword', 'long'],
|
||||
isUserEditable: true,
|
||||
},
|
||||
{ name: 'amount', displayName: 'amount', esTypes: ['long'], isUserEditable: true },
|
||||
].map(mockFieldToIndexPatternField);
|
||||
|
||||
describe('IndexedFieldsTable', () => {
|
||||
test('should render normally', async () => {
|
||||
const component = shallow(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow(
|
||||
<IndexedFieldsTable
|
||||
fields={fields}
|
||||
indexPattern={indexPattern}
|
||||
|
@ -97,7 +103,7 @@ describe('IndexedFieldsTable', () => {
|
|||
indexedFieldTypeFilter=""
|
||||
fieldFilter=""
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
component.update();
|
||||
|
@ -106,7 +112,7 @@ describe('IndexedFieldsTable', () => {
|
|||
});
|
||||
|
||||
test('should filter based on the query bar', async () => {
|
||||
const component = shallow(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow(
|
||||
<IndexedFieldsTable
|
||||
fields={fields}
|
||||
indexPattern={indexPattern}
|
||||
|
@ -117,7 +123,7 @@ describe('IndexedFieldsTable', () => {
|
|||
indexedFieldTypeFilter=""
|
||||
fieldFilter=""
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
component.setProps({ fieldFilter: 'Elast' });
|
||||
|
@ -127,7 +133,7 @@ describe('IndexedFieldsTable', () => {
|
|||
});
|
||||
|
||||
test('should filter based on the type filter', async () => {
|
||||
const component = shallow(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow(
|
||||
<IndexedFieldsTable
|
||||
fields={fields}
|
||||
indexPattern={indexPattern}
|
||||
|
@ -138,7 +144,7 @@ describe('IndexedFieldsTable', () => {
|
|||
indexedFieldTypeFilter=""
|
||||
fieldFilter=""
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
component.setProps({ indexedFieldTypeFilter: 'date' });
|
||||
|
@ -149,7 +155,7 @@ describe('IndexedFieldsTable', () => {
|
|||
|
||||
describe('IndexedFieldsTable with rollup index pattern', () => {
|
||||
test('should render normally', async () => {
|
||||
const component = shallow(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow(
|
||||
<IndexedFieldsTable
|
||||
fields={fields}
|
||||
indexPattern={rollupIndexPattern}
|
||||
|
@ -160,7 +166,7 @@ describe('IndexedFieldsTable', () => {
|
|||
indexedFieldTypeFilter=""
|
||||
fieldFilter=""
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
component.update();
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
import React, { Component } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public';
|
||||
import { useKibana } from '../../../../../../plugins/kibana_react/public';
|
||||
import { Table } from './components/table';
|
||||
import { IndexedFieldItem } from './types';
|
||||
import { IndexPatternManagmentContext } from '../../../types';
|
||||
|
||||
interface IndexedFieldsTableProps {
|
||||
fields: IndexPatternField[];
|
||||
|
@ -23,16 +25,23 @@ interface IndexedFieldsTableProps {
|
|||
getFieldInfo: (indexPattern: IndexPattern, field: IndexPatternField) => string[];
|
||||
};
|
||||
fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean;
|
||||
userEditPermission: boolean;
|
||||
}
|
||||
|
||||
interface IndexedFieldsTableState {
|
||||
fields: IndexedFieldItem[];
|
||||
}
|
||||
|
||||
export class IndexedFieldsTable extends Component<
|
||||
IndexedFieldsTableProps,
|
||||
IndexedFieldsTableState
|
||||
> {
|
||||
const withHooks = (Comp: typeof Component) => {
|
||||
return (props: any) => {
|
||||
const { application } = useKibana<IndexPatternManagmentContext>().services;
|
||||
const userEditPermission = !!application?.capabilities?.indexPatterns?.save;
|
||||
|
||||
return <Comp userEditPermission={userEditPermission} {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
class IndexedFields extends Component<IndexedFieldsTableProps, IndexedFieldsTableState> {
|
||||
constructor(props: IndexedFieldsTableProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -50,7 +59,7 @@ export class IndexedFieldsTable extends Component<
|
|||
}
|
||||
|
||||
mapFields(fields: IndexPatternField[]): IndexedFieldItem[] {
|
||||
const { indexPattern, fieldWildcardMatcher, helpers } = this.props;
|
||||
const { indexPattern, fieldWildcardMatcher, helpers, userEditPermission } = this.props;
|
||||
const sourceFilters =
|
||||
indexPattern.sourceFilters &&
|
||||
indexPattern.sourceFilters.map((f: Record<string, any>) => f.value);
|
||||
|
@ -68,6 +77,7 @@ export class IndexedFieldsTable extends Component<
|
|||
excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false,
|
||||
info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field),
|
||||
isMapped: !!field.isMapped,
|
||||
isUserEditable: userEditPermission,
|
||||
hasRuntime: !!field.runtimeField,
|
||||
};
|
||||
})) ||
|
||||
|
@ -114,3 +124,5 @@ export class IndexedFieldsTable extends Component<
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const IndexedFieldsTable = withHooks(IndexedFields);
|
||||
|
|
|
@ -16,5 +16,6 @@ export interface IndexedFieldItem extends IndexedFieldItemBase {
|
|||
excluded: boolean;
|
||||
kbnType: string;
|
||||
isMapped: boolean;
|
||||
isUserEditable: boolean;
|
||||
hasRuntime: boolean;
|
||||
}
|
||||
|
|
|
@ -27,11 +27,13 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
|
|||
items={
|
||||
Array [
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "ScriptedField",
|
||||
"script": "x++",
|
||||
},
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
|
@ -65,6 +67,7 @@ exports[`ScriptedFieldsTable should filter based on the query bar 1`] = `
|
|||
items={
|
||||
Array [
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
|
@ -123,11 +126,13 @@ exports[`ScriptedFieldsTable should render normally 1`] = `
|
|||
items={
|
||||
Array [
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "ScriptedField",
|
||||
"script": "x++",
|
||||
},
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
|
@ -161,11 +166,13 @@ exports[`ScriptedFieldsTable should show a delete modal 1`] = `
|
|||
items={
|
||||
Array [
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "ScriptedField",
|
||||
"script": "x++",
|
||||
},
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "JustATest",
|
||||
"script": "z++",
|
||||
|
|
|
@ -6,23 +6,7 @@ exports[`Header should render normally 1`] = `
|
|||
Object {
|
||||
"action": "PUSH",
|
||||
"block": [MockFunction],
|
||||
"createHref": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"pathname": "patterns/test/create-field/",
|
||||
"search": "",
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"createHref": [MockFunction],
|
||||
"createSubHistory": [MockFunction],
|
||||
"go": [MockFunction],
|
||||
"goBack": [MockFunction],
|
||||
|
@ -136,69 +120,6 @@ exports[`Header should render normally 1`] = `
|
|||
</EuiText>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<EuiButtonDisplay
|
||||
baseClassName="euiButton"
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
disabled={false}
|
||||
element="button"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
className="euiButton euiButton--primary"
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"minWidth": undefined,
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<EuiButtonContent
|
||||
className="euiButton__content"
|
||||
iconSide="left"
|
||||
textProps={
|
||||
Object {
|
||||
"className": "euiButton__text",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
className="euiButton__text"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add scripted field"
|
||||
id="indexPatternManagement.editIndexPattern.scripted.addFieldButton"
|
||||
values={Object {}}
|
||||
>
|
||||
<span>
|
||||
Add scripted field
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</span>
|
||||
</span>
|
||||
</EuiButtonContent>
|
||||
</button>
|
||||
</EuiButtonDisplay>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</Component>
|
||||
|
|
|
@ -22,7 +22,9 @@ interface HeaderProps extends RouteComponentProps {
|
|||
}
|
||||
|
||||
export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => {
|
||||
const docLinks = useKibana<IndexPatternManagmentContext>().services.docLinks?.links;
|
||||
const { application, docLinks } = useKibana<IndexPatternManagmentContext>().services;
|
||||
const links = docLinks?.links;
|
||||
const userEditPermission = !!application?.capabilities?.indexPatterns?.save;
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
|
@ -39,7 +41,7 @@ export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => {
|
|||
defaultMessage="Scripted fields are deprecated. Use {runtimeDocs} instead."
|
||||
values={{
|
||||
runtimeDocs: (
|
||||
<EuiLink target="_blank" href={docLinks.runtimeFields.overview}>
|
||||
<EuiLink target="_blank" href={links.runtimeFields.overview}>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.header.runtimeLink"
|
||||
defaultMessage="runtime fields"
|
||||
|
@ -52,17 +54,19 @@ export const Header = withRouter(({ indexPatternId, history }: HeaderProps) => {
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
{...reactRouterNavigate(history, `patterns/${indexPatternId}/create-field/`)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.editIndexPattern.scripted.addFieldButton"
|
||||
defaultMessage="Add scripted field"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{userEditPermission && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="addScriptedFieldLink"
|
||||
{...reactRouterNavigate(history, `patterns/${indexPatternId}/create-field/`)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="indexPatternManagement.editIndexPattern.scripted.addFieldButton"
|
||||
defaultMessage="Add scripted field"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ exports[`Table should render normally 1`] = `
|
|||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "Edit this field",
|
||||
"icon": "pencil",
|
||||
"name": "Edit",
|
||||
|
@ -44,6 +45,7 @@ exports[`Table should render normally 1`] = `
|
|||
"type": "icon",
|
||||
},
|
||||
Object {
|
||||
"available": [Function],
|
||||
"color": "danger",
|
||||
"description": "Delete this field",
|
||||
"icon": "trash",
|
||||
|
@ -60,10 +62,17 @@ exports[`Table should render normally 1`] = `
|
|||
items={
|
||||
Array [
|
||||
Object {
|
||||
"lang": "Elastic",
|
||||
"isUserEditable": true,
|
||||
"lang": "painless",
|
||||
"name": "1",
|
||||
"script": "",
|
||||
},
|
||||
Object {
|
||||
"isUserEditable": false,
|
||||
"lang": "painless",
|
||||
"name": "2",
|
||||
"script": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={
|
||||
|
|
|
@ -15,8 +15,10 @@ import { IIndexPattern } from 'src/plugins/data/public';
|
|||
|
||||
const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern);
|
||||
|
||||
// @ts-expect-error invalid lang type
|
||||
const items: ScriptedFieldItem[] = [{ name: '1', lang: 'Elastic', script: '' }];
|
||||
const items: ScriptedFieldItem[] = [
|
||||
{ name: '1', lang: 'painless', script: '', isUserEditable: true },
|
||||
{ name: '2', lang: 'painless', script: '', isUserEditable: false },
|
||||
];
|
||||
|
||||
describe('Table', () => {
|
||||
let indexPattern: IIndexPattern;
|
||||
|
@ -93,4 +95,19 @@ describe('Table', () => {
|
|||
component.prop('columns')[4].actions[1].onClick();
|
||||
expect(deleteField).toBeCalled();
|
||||
});
|
||||
|
||||
test('should not allow edit or deletion for user with only read access', () => {
|
||||
const component = shallow(
|
||||
<Table
|
||||
indexPattern={indexPattern}
|
||||
items={items}
|
||||
editField={() => {}}
|
||||
deleteField={() => {}}
|
||||
/>
|
||||
);
|
||||
const editAvailable = component.prop('columns')[4].actions[0].available(items[1]);
|
||||
const deleteAvailable = component.prop('columns')[4].actions[1].available(items[1]);
|
||||
expect(editAvailable).toBeFalsy();
|
||||
expect(deleteAvailable).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -106,6 +106,7 @@ export class Table extends PureComponent<TableProps> {
|
|||
),
|
||||
icon: 'pencil',
|
||||
onClick: editField,
|
||||
available: (field) => !!field.isUserEditable,
|
||||
},
|
||||
{
|
||||
type: 'icon',
|
||||
|
@ -122,6 +123,7 @@ export class Table extends PureComponent<TableProps> {
|
|||
icon: 'trash',
|
||||
color: 'danger',
|
||||
onClick: deleteField,
|
||||
available: (field) => !!field.isUserEditable,
|
||||
},
|
||||
],
|
||||
width: '40px',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { ScriptedFieldsTable } from '../scripted_fields_table';
|
||||
import { IIndexPattern, IndexPattern } from '../../../../../../plugins/data/common';
|
||||
|
@ -48,21 +48,28 @@ describe('ScriptedFieldsTable', () => {
|
|||
beforeEach(() => {
|
||||
indexPattern = getIndexPatternMock({
|
||||
getScriptedFields: () => [
|
||||
{ name: 'ScriptedField', lang: 'painless', script: 'x++' },
|
||||
{ name: 'JustATest', lang: 'painless', script: 'z++' },
|
||||
{ isUserEditable: true, name: 'ScriptedField', lang: 'painless', script: 'x++' },
|
||||
{
|
||||
isUserEditable: false,
|
||||
name: 'JustATest',
|
||||
lang: 'painless',
|
||||
script: 'z++',
|
||||
},
|
||||
],
|
||||
}) as IndexPattern;
|
||||
});
|
||||
|
||||
test('should render normally', async () => {
|
||||
const component = shallow<ScriptedFieldsTable>(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow<
|
||||
typeof ScriptedFieldsTable
|
||||
>(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={indexPattern}
|
||||
helpers={helpers}
|
||||
painlessDocLink={'painlessDoc'}
|
||||
saveIndexPattern={async () => {}}
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
// Allow the componentWillMount code to execute
|
||||
// https://github.com/airbnb/enzyme/issues/450
|
||||
|
@ -73,14 +80,14 @@ describe('ScriptedFieldsTable', () => {
|
|||
});
|
||||
|
||||
test('should filter based on the query bar', async () => {
|
||||
const component = shallow(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={indexPattern}
|
||||
helpers={helpers}
|
||||
painlessDocLink={'painlessDoc'}
|
||||
saveIndexPattern={async () => {}}
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
// Allow the componentWillMount code to execute
|
||||
// https://github.com/airbnb/enzyme/issues/450
|
||||
|
@ -94,14 +101,16 @@ describe('ScriptedFieldsTable', () => {
|
|||
});
|
||||
|
||||
test('should filter based on the lang filter', async () => {
|
||||
const component = shallow<ScriptedFieldsTable>(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow<
|
||||
typeof ScriptedFieldsTable
|
||||
>(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={
|
||||
getIndexPatternMock({
|
||||
getScriptedFields: () => [
|
||||
{ name: 'ScriptedField', lang: 'painless', script: 'x++' },
|
||||
{ name: 'JustATest', lang: 'painless', script: 'z++' },
|
||||
{ name: 'Bad', lang: 'somethingElse', script: 'z++' },
|
||||
{ isUserEditable: true, name: 'ScriptedField', lang: 'painless', script: 'x++' },
|
||||
{ isUserEditable: true, name: 'JustATest', lang: 'painless', script: 'z++' },
|
||||
{ isUserEditable: true, name: 'Bad', lang: 'somethingElse', script: 'z++' },
|
||||
],
|
||||
}) as IndexPattern
|
||||
}
|
||||
|
@ -109,7 +118,7 @@ describe('ScriptedFieldsTable', () => {
|
|||
helpers={helpers}
|
||||
saveIndexPattern={async () => {}}
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
// Allow the componentWillMount code to execute
|
||||
// https://github.com/airbnb/enzyme/issues/450
|
||||
|
@ -123,7 +132,7 @@ describe('ScriptedFieldsTable', () => {
|
|||
});
|
||||
|
||||
test('should hide the table if there are no scripted fields', async () => {
|
||||
const component = shallow(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={
|
||||
getIndexPatternMock({
|
||||
|
@ -134,7 +143,7 @@ describe('ScriptedFieldsTable', () => {
|
|||
helpers={helpers}
|
||||
saveIndexPattern={async () => {}}
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
// Allow the componentWillMount code to execute
|
||||
// https://github.com/airbnb/enzyme/issues/450
|
||||
|
@ -145,14 +154,16 @@ describe('ScriptedFieldsTable', () => {
|
|||
});
|
||||
|
||||
test('should show a delete modal', async () => {
|
||||
const component = shallow<ScriptedFieldsTable>(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow<
|
||||
typeof ScriptedFieldsTable
|
||||
>(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={indexPattern}
|
||||
helpers={helpers}
|
||||
painlessDocLink={'painlessDoc'}
|
||||
saveIndexPattern={async () => {}}
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
await component.update(); // Fire `componentWillMount()`
|
||||
// @ts-expect-error lang is not valid
|
||||
|
@ -165,7 +176,9 @@ describe('ScriptedFieldsTable', () => {
|
|||
|
||||
test('should delete a field', async () => {
|
||||
const removeScriptedField = jest.fn();
|
||||
const component = shallow<ScriptedFieldsTable>(
|
||||
const component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>> = shallow<
|
||||
typeof ScriptedFieldsTable
|
||||
>(
|
||||
<ScriptedFieldsTable
|
||||
indexPattern={
|
||||
{
|
||||
|
@ -177,13 +190,14 @@ describe('ScriptedFieldsTable', () => {
|
|||
painlessDocLink={'painlessDoc'}
|
||||
saveIndexPattern={async () => {}}
|
||||
/>
|
||||
);
|
||||
).dive();
|
||||
|
||||
await component.update(); // Fire `componentWillMount()`
|
||||
// @ts-expect-error lang is not valid
|
||||
// @ts-expect-error
|
||||
component.instance().startDeleteField({ name: 'ScriptedField', lang: '', script: '' });
|
||||
|
||||
await component.update();
|
||||
// @ts-expect-error
|
||||
await component.instance().deleteField();
|
||||
await component.update();
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@ import {
|
|||
|
||||
import { Table, Header, CallOuts, DeleteScritpedFieldConfirmationModal } from './components';
|
||||
import { ScriptedFieldItem } from './types';
|
||||
import { IndexPatternManagmentContext } from '../../../types';
|
||||
|
||||
import { IndexPattern, DataPublicPluginStart } from '../../../../../../plugins/data/public';
|
||||
import { useKibana } from '../../../../../../plugins/kibana_react/public';
|
||||
|
||||
interface ScriptedFieldsTableProps {
|
||||
indexPattern: IndexPattern;
|
||||
|
@ -29,6 +31,7 @@ interface ScriptedFieldsTableProps {
|
|||
onRemoveField?: () => void;
|
||||
painlessDocLink: string;
|
||||
saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject'];
|
||||
userEditPermission: boolean;
|
||||
}
|
||||
|
||||
interface ScriptedFieldsTableState {
|
||||
|
@ -38,10 +41,16 @@ interface ScriptedFieldsTableState {
|
|||
fields: ScriptedFieldItem[];
|
||||
}
|
||||
|
||||
export class ScriptedFieldsTable extends Component<
|
||||
ScriptedFieldsTableProps,
|
||||
ScriptedFieldsTableState
|
||||
> {
|
||||
const withHooks = (Comp: typeof Component) => {
|
||||
return (props: any) => {
|
||||
const { application } = useKibana<IndexPatternManagmentContext>().services;
|
||||
const userEditPermission = !!application?.capabilities?.indexPatterns?.save;
|
||||
|
||||
return <Comp userEditPermission={userEditPermission} {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
class ScriptedFields extends Component<ScriptedFieldsTableProps, ScriptedFieldsTableState> {
|
||||
constructor(props: ScriptedFieldsTableProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -79,7 +88,7 @@ export class ScriptedFieldsTable extends Component<
|
|||
|
||||
getFilteredItems = () => {
|
||||
const { fields } = this.state;
|
||||
const { fieldFilter, scriptedFieldLanguageFilter } = this.props;
|
||||
const { fieldFilter, scriptedFieldLanguageFilter, userEditPermission } = this.props;
|
||||
|
||||
let languageFilteredFields = fields;
|
||||
|
||||
|
@ -99,6 +108,8 @@ export class ScriptedFieldsTable extends Component<
|
|||
);
|
||||
}
|
||||
|
||||
filteredFields.forEach((field) => (field.isUserEditable = userEditPermission));
|
||||
|
||||
return filteredFields;
|
||||
};
|
||||
|
||||
|
@ -157,3 +168,5 @@ export class ScriptedFieldsTable extends Component<
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ScriptedFieldsTable = withHooks(ScriptedFields);
|
||||
|
|
|
@ -11,4 +11,5 @@ export interface ScriptedFieldItem {
|
|||
name: string;
|
||||
lang: estypes.ScriptLanguage;
|
||||
script: string;
|
||||
isUserEditable?: boolean;
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ export function Tabs({
|
|||
location,
|
||||
refreshFields,
|
||||
}: TabsProps) {
|
||||
const { uiSettings, docLinks, indexPatternFieldEditor } =
|
||||
const { application, uiSettings, docLinks, indexPatternFieldEditor } =
|
||||
useKibana<IndexPatternManagmentContext>().services;
|
||||
const [fieldFilter, setFieldFilter] = useState<string>('');
|
||||
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState<string>('');
|
||||
|
@ -149,6 +149,7 @@ export function Tabs({
|
|||
[uiSettings]
|
||||
);
|
||||
|
||||
const userEditPermission = !!application?.capabilities?.indexPatterns?.save;
|
||||
const getFilterSection = useCallback(
|
||||
(type: string) => {
|
||||
return (
|
||||
|
@ -174,11 +175,13 @@ export function Tabs({
|
|||
aria-label={filterAriaLabel}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={() => openFieldEditor()} data-test-subj="addField">
|
||||
{addFieldButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{userEditPermission && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={() => openFieldEditor()} data-test-subj="addField">
|
||||
{addFieldButtonLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && (
|
||||
|
@ -201,6 +204,7 @@ export function Tabs({
|
|||
scriptedFieldLanguageFilter,
|
||||
scriptedFieldLanguages,
|
||||
openFieldEditor,
|
||||
userEditPermission,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue