[Discover] Hide "Add a field", "Edit" and "Create a data view" buttons in viewer mode (#134582)

* [Discover] Hide "Add a field" button for read only access

* [Discover] Hide "Create a data view" button for read only access on desktop

* [Discover] Hide "Create a data view" and "Add a field" button for read only access on mobile

* [Discover] Make sure that error message is shown when access rights were reduced for a user in meantime

* [Discover] Make checks safe

* [Discover] Update tests

* [Discover] Streamline the logic

* [Discover] Update tests

* [Discover] Add tests

* [Discover] Add tests

* [Discover] Update code style

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2022-06-21 09:05:23 +02:00 committed by GitHub
parent 9ec4d311ed
commit 7410fbf4d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 272 additions and 136 deletions

View file

@ -245,7 +245,6 @@ export function DiscoverLayout({
resetSavedSearch={resetSavedSearch}
onChangeIndexPattern={onChangeIndexPattern}
onEditRuntimeField={onEditRuntimeField}
useNewFieldsApi={useNewFieldsApi}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
<SavedSearchURLConflictCallout

View file

@ -246,13 +246,13 @@ export interface DiscoverFieldProps {
multiFields?: Array<{ field: DataViewField; isSelected: boolean }>;
/**
* Callback to edit a runtime field from index pattern
* Callback to edit a field from data view
* @param fieldName name of the field to edit
*/
onEditField?: (fieldName: string) => void;
/**
* Callback to delete a runtime field from index pattern
* Callback to delete a runtime field from data view
* @param fieldName name of the field to delete
*/
onDeleteField?: (fieldName: string) => void;

View file

@ -9,6 +9,7 @@
import { cloneDeep, each } from 'lodash';
import { ReactWrapper } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
import { Action } from '@kbn/ui-actions-plugin/public';
// @ts-expect-error
import realHits from '../../../../__fixtures__/real_hits';
@ -29,6 +30,16 @@ import { BehaviorSubject } from 'rxjs';
import { FetchStatus } from '../../../types';
import { AvailableFields$ } from '../../hooks/use_saved_search';
const mockGetActions = jest.fn<Promise<Array<Action<object>>>, [string, { fieldName: string }]>(
() => Promise.resolve([])
);
jest.mock('../../../../kibana_services', () => ({
getUiActions: () => ({
getTriggerCompatibleActions: mockGetActions,
}),
}));
function getCompProps(): DiscoverSidebarProps {
const indexPattern = stubLogstashIndexPattern;
const hits = each(cloneDeep(realHits), (hit) =>
@ -73,6 +84,7 @@ function getCompProps(): DiscoverSidebarProps {
createNewDataView: jest.fn(),
onDataViewCreated: jest.fn(),
availableFields$,
useNewFieldsApi: true,
};
}
@ -105,4 +117,80 @@ describe('discover sidebar', function () {
findTestSubject(comp, 'fieldToggle-extension').simulate('click');
expect(props.onRemoveField).toHaveBeenCalledWith('extension');
});
it('should render "Add a field" button', () => {
const addFieldButton = findTestSubject(comp, 'indexPattern-add-field_btn');
expect(addFieldButton.length).toBe(1);
addFieldButton.simulate('click');
expect(props.editField).toHaveBeenCalledWith();
});
it('should render "Edit field" button', () => {
findTestSubject(comp, 'field-bytes').simulate('click');
const editFieldButton = findTestSubject(comp, 'discoverFieldListPanelEdit-bytes');
expect(editFieldButton.length).toBe(1);
editFieldButton.simulate('click');
expect(props.editField).toHaveBeenCalledWith('bytes');
});
it('should not render Add/Edit field buttons in viewer mode', () => {
const compInViewerMode = mountWithIntl(
<KibanaContextProvider services={mockDiscoverServices}>
<DiscoverSidebar {...props} editField={undefined} />
</KibanaContextProvider>
);
const addFieldButton = findTestSubject(compInViewerMode, 'indexPattern-add-field_btn');
expect(addFieldButton.length).toBe(0);
findTestSubject(comp, 'field-bytes').simulate('click');
const editFieldButton = findTestSubject(compInViewerMode, 'discoverFieldListPanelEdit-bytes');
expect(editFieldButton.length).toBe(0);
});
it('should render buttons in data view picker correctly', async () => {
const compWithPicker = mountWithIntl(
<KibanaContextProvider services={mockDiscoverServices}>
<DiscoverSidebar {...props} showDataViewPicker />
</KibanaContextProvider>
);
// open data view picker
findTestSubject(compWithPicker, 'indexPattern-switch-link').simulate('click');
expect(findTestSubject(compWithPicker, 'changeDataViewPopover').length).toBe(1);
// click "Add a field"
const addFieldButtonInDataViewPicker = findTestSubject(
compWithPicker,
'indexPattern-add-field'
);
expect(addFieldButtonInDataViewPicker.length).toBe(1);
addFieldButtonInDataViewPicker.simulate('click');
expect(props.editField).toHaveBeenCalledWith();
// click "Create a data view"
const createDataViewButton = findTestSubject(compWithPicker, 'dataview-create-new');
expect(createDataViewButton.length).toBe(1);
createDataViewButton.simulate('click');
expect(props.createNewDataView).toHaveBeenCalled();
});
it('should not render buttons in data view picker when in viewer mode', async () => {
const compWithPickerInViewerMode = mountWithIntl(
<KibanaContextProvider services={mockDiscoverServices}>
<DiscoverSidebar
{...props}
showDataViewPicker
editField={undefined}
createNewDataView={undefined}
/>
</KibanaContextProvider>
);
// open data view picker
findTestSubject(compWithPickerInViewerMode, 'indexPattern-switch-link').simulate('click');
expect(findTestSubject(compWithPickerInViewerMode, 'changeDataViewPopover').length).toBe(1);
// check that buttons are not present
const addFieldButtonInDataViewPicker = findTestSubject(
compWithPickerInViewerMode,
'indexPattern-add-field'
);
expect(addFieldButtonInDataViewPicker.length).toBe(0);
const createDataViewButton = findTestSubject(compWithPickerInViewerMode, 'dataview-create-new');
expect(createDataViewButton.length).toBe(0);
});
});

View file

@ -68,9 +68,18 @@ export interface DiscoverSidebarProps extends Omit<DiscoverSidebarResponsiveProp
*/
setFieldEditorRef?: (ref: () => void | undefined) => void;
editField: (fieldName?: string) => void;
/**
* Handles "Edit field" action
* Buttons will be hidden if not provided
* @param fieldName
*/
editField?: (fieldName?: string) => void;
createNewDataView: () => void;
/**
* Handles "Create a data view action" action
* Buttons will be hidden if not provided
*/
createNewDataView?: () => void;
/**
* a statistics of the distribution of fields in the given hits
@ -113,9 +122,6 @@ export function DiscoverSidebarComponent({
}: DiscoverSidebarProps) {
const { uiSettings, dataViewFieldEditor } = useDiscoverServices();
const [fields, setFields] = useState<DataViewField[] | null>(null);
const dataViewFieldEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern();
const canEditDataViewField = !!dataViewFieldEditPermission && useNewFieldsApi;
const [scrollContainer, setScrollContainer] = useState<Element | null>(null);
const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE);
const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE);
@ -258,7 +264,7 @@ export function DiscoverSidebarComponent({
const deleteField = useMemo(
() =>
canEditDataViewField && selectedIndexPattern
editField && selectedIndexPattern
? async (fieldName: string) => {
const ref = dataViewFieldEditor.openDeleteModal({
ctx: {
@ -279,7 +285,7 @@ export function DiscoverSidebarComponent({
: undefined,
[
selectedIndexPattern,
canEditDataViewField,
editField,
setFieldEditorRef,
closeFlyout,
onEditRuntimeField,
@ -396,8 +402,8 @@ export function DiscoverSidebarComponent({
selected={true}
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
onEditField={canEditDataViewField ? editField : undefined}
onDeleteField={canEditDataViewField ? deleteField : undefined}
onEditField={editField}
onDeleteField={deleteField}
showFieldStats={showFieldStats}
/>
</li>
@ -456,8 +462,8 @@ export function DiscoverSidebarComponent({
getDetails={getDetailsByField}
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
onEditField={canEditDataViewField ? editField : undefined}
onDeleteField={canEditDataViewField ? deleteField : undefined}
onEditField={editField}
onDeleteField={deleteField}
showFieldStats={showFieldStats}
/>
</li>
@ -485,8 +491,8 @@ export function DiscoverSidebarComponent({
getDetails={getDetailsByField}
trackUiMetric={trackUiMetric}
multiFields={multiFields?.get(field.name)}
onEditField={canEditDataViewField ? editField : undefined}
onDeleteField={canEditDataViewField ? deleteField : undefined}
onEditField={editField}
onDeleteField={deleteField}
showFieldStats={showFieldStats}
/>
</li>
@ -498,18 +504,20 @@ export function DiscoverSidebarComponent({
)}
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconType="indexOpen"
data-test-subj="indexPattern-add-field_btn"
onClick={() => editField()}
size="s"
>
{i18n.translate('discover.fieldChooser.addField.label', {
defaultMessage: 'Add a field',
})}
</EuiButton>
</EuiFlexItem>
{!!editField && (
<EuiFlexItem grow={false}>
<EuiButton
iconType="indexOpen"
data-test-subj="indexPattern-add-field_btn"
onClick={() => editField()}
size="s"
>
{i18n.translate('discover.fieldChooser.addField.label', {
defaultMessage: 'Add a field',
})}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPageSideBar>
);

View file

@ -52,6 +52,11 @@ const mockServices = {
},
},
docLinks: { links: { discover: { fieldTypeHelp: '' } } },
dataViewEditor: {
userPermissions: {
editDataView: jest.fn(() => true),
},
},
} as unknown as DiscoverServices;
const mockfieldCounts: Record<string, number> = {};
@ -111,6 +116,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
onEditRuntimeField: jest.fn(),
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
onDataViewCreated: jest.fn(),
useNewFieldsApi: true,
};
}
@ -160,4 +166,31 @@ describe('discover responsive sidebar', function () {
expect(findTestSubject(comp, 'fieldList-unpopular').children().length).toBe(4);
expect(mockCalcFieldCounts.mock.calls.length).toBe(1);
});
it('should show "Add a field" button to create a runtime field', () => {
expect(mockServices.dataViewEditor.userPermissions.editDataView).toHaveBeenCalled();
expect(findTestSubject(comp, 'indexPattern-add-field_btn').length).toBe(1);
});
it('should not show "Add a field" button in viewer mode', () => {
const mockedServicesInViewerMode = {
...mockServices,
dataViewEditor: {
...mockServices.dataViewEditor,
userPermissions: {
...mockServices.dataViewEditor.userPermissions,
editDataView: jest.fn(() => false),
},
},
};
const compInViewerMode = mountWithIntl(
<KibanaContextProvider services={mockedServicesInViewerMode}>
<DiscoverSidebarResponsive {...props} />
</KibanaContextProvider>
);
expect(
mockedServicesInViewerMode.dataViewEditor.userPermissions.editDataView
).toHaveBeenCalled();
expect(findTestSubject(compInViewerMode, 'indexPattern-add-field_btn').length).toBe(0);
});
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { UiCounterMetricType } from '@kbn/analytics';
@ -89,7 +89,7 @@ export interface DiscoverSidebarResponsiveProps {
/**
* Read from the Fields API
*/
useNewFieldsApi?: boolean;
useNewFieldsApi: boolean;
/**
* callback to execute on edit runtime field
*/
@ -115,7 +115,7 @@ export interface DiscoverSidebarResponsiveProps {
*/
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
const services = useDiscoverServices();
const { selectedIndexPattern, onEditRuntimeField, useNewFieldsApi, onDataViewCreated } = props;
const { selectedIndexPattern, onEditRuntimeField, onDataViewCreated } = props;
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
/**
@ -178,6 +178,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
const { dataViewFieldEditor, dataViewEditor } = services;
const { availableFields$ } = props;
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
useEffect(
() => {
// For an external embeddable like the Field stats
@ -203,57 +205,56 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
]
);
const editField = useCallback(
(fieldName?: string) => {
const indexPatternFieldEditPermission =
dataViewFieldEditor?.userPermissions.editIndexPattern();
const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi;
if (!canEditIndexPatternField || !selectedIndexPattern) {
return;
}
const ref = dataViewFieldEditor.openEditor({
ctx: {
dataView: selectedIndexPattern,
},
fieldName,
onSave: async () => {
onEditRuntimeField();
},
});
if (setFieldEditorRef) {
setFieldEditorRef(ref);
}
if (closeFlyout) {
closeFlyout();
}
},
const editField = useMemo(
() =>
canEditDataView && selectedIndexPattern
? (fieldName?: string) => {
const ref = dataViewFieldEditor.openEditor({
ctx: {
dataView: selectedIndexPattern,
},
fieldName,
onSave: async () => {
onEditRuntimeField();
},
});
if (setFieldEditorRef) {
setFieldEditorRef(ref);
}
if (closeFlyout) {
closeFlyout();
}
}
: undefined,
[
canEditDataView,
closeFlyout,
dataViewFieldEditor,
selectedIndexPattern,
setFieldEditorRef,
onEditRuntimeField,
useNewFieldsApi,
]
);
const createNewDataView = useCallback(() => {
const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView;
if (!indexPatternFieldEditPermission) {
return;
}
const ref = dataViewEditor.openEditor({
onSave: async (dataView) => {
onDataViewCreated(dataView);
},
});
if (setDataViewEditorRef) {
setDataViewEditorRef(ref);
}
if (closeFlyout) {
closeFlyout();
}
}, [dataViewEditor, setDataViewEditorRef, closeFlyout, onDataViewCreated]);
const createNewDataView = useMemo(
() =>
canEditDataView
? () => {
const ref = dataViewEditor.openEditor({
onSave: async (dataView) => {
onDataViewCreated(dataView);
},
});
if (setDataViewEditorRef) {
setDataViewEditorRef(ref);
}
if (closeFlyout) {
closeFlyout();
}
}
: undefined,
[canEditDataView, dataViewEditor, setDataViewEditorRef, closeFlyout, onDataViewCreated]
);
if (!selectedIndexPattern) {
return null;

View file

@ -27,7 +27,6 @@ export type DiscoverTopNavProps = Pick<
resetSavedSearch: () => void;
onChangeIndexPattern: (indexPattern: string) => void;
onEditRuntimeField: () => void;
useNewFieldsApi?: boolean;
};
export const DiscoverTopNav = ({
@ -43,7 +42,6 @@ export const DiscoverTopNav = ({
resetSavedSearch,
onChangeIndexPattern,
onEditRuntimeField,
useNewFieldsApi = false,
}: DiscoverTopNavProps) => {
const history = useHistory();
const showDatePicker = useMemo(
@ -52,11 +50,9 @@ export const DiscoverTopNav = ({
);
const services = useDiscoverServices();
const { dataViewEditor, navigation, dataViewFieldEditor, data } = services;
const editPermission = useMemo(
() => dataViewFieldEditor.userPermissions.editIndexPattern(),
[dataViewFieldEditor]
);
const canEditDataViewField = !!editPermission && useNewFieldsApi;
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const closeFieldEditor = useRef<() => void | undefined>();
const closeDataViewEditor = useRef<() => void | undefined>();
@ -87,7 +83,7 @@ export const DiscoverTopNav = ({
const editField = useMemo(
() =>
canEditDataViewField
canEditDataView
? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => {
if (indexPattern?.id) {
const indexPatternInstance = await data.dataViews.get(indexPattern.id);
@ -103,33 +99,29 @@ export const DiscoverTopNav = ({
}
}
: undefined,
[
canEditDataViewField,
indexPattern?.id,
data.dataViews,
dataViewFieldEditor,
onEditRuntimeField,
]
[canEditDataView, indexPattern?.id, data.dataViews, dataViewFieldEditor, onEditRuntimeField]
);
const addField = useMemo(
() => (canEditDataViewField && editField ? () => editField(undefined, 'add') : undefined),
[editField, canEditDataViewField]
() => (canEditDataView && editField ? () => editField(undefined, 'add') : undefined),
[editField, canEditDataView]
);
const createNewDataView = useCallback(() => {
const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView;
if (!indexPatternFieldEditPermission) {
return;
}
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataView) => {
if (dataView.id) {
onChangeIndexPattern(dataView.id);
}
},
});
}, [dataViewEditor, onChangeIndexPattern]);
const createNewDataView = useMemo(
() =>
canEditDataView
? () => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataView) => {
if (dataView.id) {
onChangeIndexPattern(dataView.id);
}
},
});
}
: undefined,
[canEditDataView, dataViewEditor, onChangeIndexPattern]
);
const topNavMenu = useMemo(
() =>

View file

@ -116,7 +116,7 @@ describe('DataView component', () => {
await act(async () => {
const component = mount(wrapDataViewComponentInContext(props, true));
findTestSubject(component, 'dataview-trigger').simulate('click');
expect(component.find('[data-test-subj="idataview-create-new"]').length).toBe(0);
expect(component.find('[data-test-subj="dataview-create-new"]').length).toBe(0);
});
});

View file

@ -409,7 +409,7 @@ describe('Lens App', () => {
expect.objectContaining({
currentDataViewId: 'mockip',
onChangeDataView: expect.any(Function),
onDataViewCreated: expect.any(Function),
onDataViewCreated: undefined,
onAddField: undefined,
})
);
@ -417,7 +417,7 @@ describe('Lens App', () => {
it('calls the nav component with the correct dataview picker props if permissions are given', async () => {
const { instance, lensStore, services } = await mountWith({ preloadedState: {} });
services.dataViewFieldEditor.userPermissions.editIndexPattern = () => true;
services.dataViewEditor.userPermissions.editDataView = () => true;
const document = {
savedObjectId: defaultSavedObjectId,
state: {

View file

@ -244,7 +244,7 @@ export const LensTopNavMenu = ({
const [indexPatterns, setIndexPatterns] = useState<DataView[]>([]);
const [currentIndexPattern, setCurrentIndexPattern] = useState<DataView>();
const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState<string[]>([]);
const editPermission = dataViewFieldEditor.userPermissions.editIndexPattern();
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const closeFieldEditor = useRef<() => void | undefined>();
const closeDataViewEditor = useRef<() => void | undefined>();
@ -644,7 +644,7 @@ export const LensTopNavMenu = ({
const editField = useMemo(
() =>
editPermission
canEditDataView
? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => {
if (currentIndexPattern?.id) {
const indexPatternInstance = await data.dataViews.get(currentIndexPattern?.id);
@ -660,39 +660,54 @@ export const LensTopNavMenu = ({
}
}
: undefined,
[editPermission, currentIndexPattern?.id, data.dataViews, dataViewFieldEditor, refreshFieldList]
[
canEditDataView,
currentIndexPattern?.id,
data.dataViews,
dataViewFieldEditor,
refreshFieldList,
]
);
const addField = useMemo(
() => (editPermission && editField ? () => editField(undefined, 'add') : undefined),
[editField, editPermission]
() => (canEditDataView && editField ? () => editField(undefined, 'add') : undefined),
[editField, canEditDataView]
);
const createNewDataView = useCallback(() => {
const dataViewEditPermission = dataViewEditor.userPermissions.editDataView;
if (!dataViewEditPermission) {
return;
}
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataView) => {
if (dataView.id) {
handleIndexPatternChange({
activeDatasources: Object.keys(datasourceStates).reduce(
(acc, datasourceId) => ({
...acc,
[datasourceId]: datasourceMap[datasourceId],
}),
{}
),
datasourceStates,
indexPatternId: dataView.id,
setDatasourceState,
});
refreshFieldList();
}
},
});
}, [dataViewEditor, datasourceMap, datasourceStates, refreshFieldList, setDatasourceState]);
const createNewDataView = useMemo(
() =>
canEditDataView
? () => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataView) => {
if (dataView.id) {
handleIndexPatternChange({
activeDatasources: Object.keys(datasourceStates).reduce(
(acc, datasourceId) => ({
...acc,
[datasourceId]: datasourceMap[datasourceId],
}),
{}
),
datasourceStates,
indexPatternId: dataView.id,
setDatasourceState,
});
refreshFieldList();
}
},
});
}
: undefined,
[
dataViewEditor,
canEditDataView,
datasourceMap,
datasourceStates,
refreshFieldList,
setDatasourceState,
]
);
const dataViewPickerProps = {
trigger: {