[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:
Shivindera Singh 2021-10-21 10:50:22 +02:00 committed by GitHub
parent 647ca9a82b
commit 933ece47ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 210 additions and 146 deletions

View file

@ -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 && (

View file

@ -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",

View file

@ -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={

View file

@ -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,
};

View file

@ -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',

View file

@ -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();

View file

@ -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);

View file

@ -16,5 +16,6 @@ export interface IndexedFieldItem extends IndexedFieldItemBase {
excluded: boolean;
kbnType: string;
isMapped: boolean;
isUserEditable: boolean;
hasRuntime: boolean;
}

View file

@ -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++",

View file

@ -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>

View file

@ -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>
);
});

View file

@ -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={

View file

@ -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();
});
});

View file

@ -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',

View file

@ -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();

View file

@ -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);

View file

@ -11,4 +11,5 @@ export interface ScriptedFieldItem {
name: string;
lang: estypes.ScriptLanguage;
script: string;
isUserEditable?: boolean;
}

View file

@ -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,
]
);