[UnifiedSearch] Allow editing ad-hoc data views without permissions (#142723)

* allow editing ad-hoc data views without permissions

* [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs'

* fxi tests

* fix test

* allow field editing from discover table

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Joe Reuter 2022-10-06 12:15:22 +02:00 committed by GitHub
parent 41b28155d4
commit 0a01a92368
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 118 additions and 145 deletions

View file

@ -99,6 +99,8 @@ const IndexPatternEditorFlyoutContentComponent = ({
services: { application, http, dataViews, uiSettings, overlays },
} = useKibana<DataViewEditorContext>();
const canSave = dataViews.getCanSaveSync();
const { form } = useForm<IndexPatternConfig, FormInternal>({
// Prefill with data if editData exists
defaultValue: {
@ -447,6 +449,7 @@ const IndexPatternEditorFlyoutContentComponent = ({
isEdit={!!editData}
isPersisted={Boolean(editData && editData.isPersisted())}
allowAdHoc={allowAdHoc}
canSave={canSave}
/>
</FlyoutPanels.Item>
<FlyoutPanels.Item>

View file

@ -24,6 +24,7 @@ interface FooterProps {
isEdit: boolean;
isPersisted: boolean;
allowAdHoc: boolean;
canSave: boolean;
}
const closeButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutCloseButtonLabel', {
@ -56,6 +57,7 @@ export const Footer = ({
isEdit,
allowAdHoc,
isPersisted,
canSave,
}: FooterProps) => {
const submitPersisted = () => {
onSubmit(false);
@ -96,21 +98,23 @@ export const Footer = ({
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
onClick={submitPersisted}
data-test-subj="saveIndexPatternButton"
fill
disabled={submitDisabled}
>
{isEdit
? isPersisted
? editButtonLabel
: editUnpersistedButtonLabel
: saveButtonLabel}
</EuiButton>
</EuiFlexItem>
{(canSave || (isEdit && !isPersisted)) && (
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
onClick={submitPersisted}
data-test-subj="saveIndexPatternButton"
fill
disabled={submitDisabled}
>
{isEdit
? isPersisted
? editButtonLabel
: editUnpersistedButtonLabel
: saveButtonLabel}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -182,7 +182,8 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
const { dataViewFieldEditor, dataViewEditor } = services;
const { availableFields$ } = props;
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const canEditDataView =
Boolean(dataViewEditor?.userPermissions.editDataView()) || !selectedDataView?.isPersisted();
useEffect(
() => {
@ -241,25 +242,19 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
]
);
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]
);
const createNewDataView = useCallback(() => {
const ref = dataViewEditor.openEditor({
onSave: async (dataView) => {
onDataViewCreated(dataView);
},
});
if (setDataViewEditorRef) {
setDataViewEditorRef(ref);
}
if (closeFlyout) {
closeFlyout();
}
}, [dataViewEditor, setDataViewEditorRef, closeFlyout, onDataViewCreated]);
if (!selectedDataView) {
return null;

View file

@ -68,7 +68,8 @@ export const DiscoverTopNav = ({
const services = useDiscoverServices();
const { dataViewEditor, navigation, dataViewFieldEditor, data, uiSettings, dataViews } = services;
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const canEditDataView =
Boolean(dataViewEditor?.userPermissions.editDataView()) || !dataView.isPersisted();
const closeFieldEditor = useRef<() => void | undefined>();
const closeDataViewEditor = useRef<() => void | undefined>();
@ -124,22 +125,16 @@ export const DiscoverTopNav = ({
[editField, canEditDataView]
);
const createNewDataView = useMemo(
() =>
canEditDataView
? () => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataViewToSave) => {
if (dataViewToSave.id) {
onChangeDataView(dataViewToSave.id);
}
},
allowAdHocDataView: true,
});
}
: undefined,
[canEditDataView, dataViewEditor, onChangeDataView]
);
const createNewDataView = useCallback(() => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataViewToSave) => {
if (dataViewToSave.id) {
onChangeDataView(dataViewToSave.id);
}
},
allowAdHocDataView: true,
});
}, [dataViewEditor, onChangeDataView]);
const onCreateDefaultAdHocDataView = useCallback(
async (pattern: string) => {

View file

@ -29,7 +29,8 @@ export const buildEditFieldButton = ({
}
const { canEdit: canEditField } = getFieldCapabilities(dataView, field);
const canEditDataView = Boolean(services.dataViewEditor?.userPermissions?.editDataView());
const canEditDataView =
Boolean(services.dataViewEditor?.userPermissions?.editDataView()) || !dataView.isPersisted();
if (!canEditField || !canEditDataView) {
return null;

View file

@ -13,6 +13,7 @@ import { mountWithIntl as mount } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { ChangeDataView } from './change_dataview';
import { DataViewPickerPropsExtended, TextBasedLanguages } from '.';
@ -44,6 +45,8 @@ describe('DataView component', () => {
storageValue: boolean,
uiSettingValue: boolean = false
) {
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
(dataViewEditorMock.userPermissions.editDataView as jest.Mock).mockReturnValue(true);
let dataMock = dataPluginMock.createStartContract();
dataMock = {
...dataMock,
@ -56,6 +59,7 @@ describe('DataView component', () => {
const services = {
data: dataMock,
storage: getStorage(storageValue),
dataViewEditor: dataViewEditorMock,
uiSettings: {
get: jest.fn(() => uiSettingValue),
},

View file

@ -190,31 +190,35 @@ export function ChangeDataView({
defaultMessage: 'Add a field to this data view',
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="manage"
icon="indexSettings"
data-test-subj="indexPattern-manage-field"
onClick={async () => {
if (onEditDataView) {
const dataView = await dataViews.get(currentDataViewId!);
dataViewEditor.openEditor({
editData: dataView,
onSave: (updatedDataView) => {
onEditDataView(updatedDataView);
},
});
} else {
application.navigateToApp('management', {
path: `/kibana/indexPatterns/patterns/${currentDataViewId}`,
});
}
setPopoverIsOpen(false);
}}
>
{i18n.translate('unifiedSearch.query.queryBar.indexPattern.manageFieldButton', {
defaultMessage: 'Manage this data view',
})}
</EuiContextMenuItem>,
onEditDataView || dataViewEditor.userPermissions.editDataView() ? (
<EuiContextMenuItem
key="manage"
icon="indexSettings"
data-test-subj="indexPattern-manage-field"
onClick={async () => {
if (onEditDataView) {
const dataView = await dataViews.get(currentDataViewId!);
dataViewEditor.openEditor({
editData: dataView,
onSave: (updatedDataView) => {
onEditDataView(updatedDataView);
},
});
} else {
application.navigateToApp('management', {
path: `/kibana/indexPatterns/patterns/${currentDataViewId}`,
});
}
setPopoverIsOpen(false);
}}
>
{i18n.translate('unifiedSearch.query.queryBar.indexPattern.manageFieldButton', {
defaultMessage: 'Manage this data view',
})}
</EuiContextMenuItem>
) : (
<React.Fragment />
),
<EuiHorizontalRule margin="none" />
);
}

View file

@ -10,6 +10,7 @@ import React from 'react';
import SearchBar from './search_bar';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { I18nProvider } from '@kbn/i18n-react';
import { coreMock } from '@kbn/core/public/mocks';
@ -83,6 +84,9 @@ function wrapSearchBarInContext(testProps: any) {
intl: null as any,
};
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
(dataViewEditorMock.userPermissions.editDataView as jest.Mock).mockReturnValue(true);
const services = {
uiSettings: startMock.uiSettings,
savedObjects: startMock.savedObjects,
@ -111,6 +115,7 @@ function wrapSearchBarInContext(testProps: any) {
}),
},
},
dataViewEditor: dataViewEditorMock,
dataViews: {
getIdsWithTitle: jest.fn(() => []),
},

View file

@ -405,39 +405,6 @@ describe('Lens App', () => {
});
describe('TopNavMenu#dataViewPickerProps', () => {
it('calls the nav component with the correct dataview picker props if no permissions are given', async () => {
const { instance, lensStore } = await mountWith({ preloadedState: {} });
const document = {
savedObjectId: defaultSavedObjectId,
state: {
query: 'fake query',
filters: [{ query: { match_phrase: { src: 'test' } } }],
},
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
} as unknown as Document;
act(() => {
lensStore.dispatch(
setState({
query: 'fake query' as unknown as Query,
persistedDoc: document,
})
);
});
instance.update();
const props = instance
.find('[data-test-subj="lnsApp_topNav"]')
.prop('dataViewPickerComponentProps') as TopNavMenuData[];
expect(props).toEqual(
expect.objectContaining({
currentDataViewId: 'mockip',
onChangeDataView: expect.any(Function),
onDataViewCreated: undefined,
onAddField: undefined,
})
);
});
it('calls the nav component with the correct dataview picker props if permissions are given', async () => {
const { instance, lensStore, services } = await mountWith({ preloadedState: {} });
services.dataViewEditor.userPermissions.editDataView = () => true;

View file

@ -288,7 +288,8 @@ export const LensTopNavMenu = ({
]
);
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const canEditDataView =
Boolean(dataViewEditor?.userPermissions.editDataView()) || !currentIndexPattern?.isPersisted();
const closeFieldEditor = useRef<() => void | undefined>();
const closeDataViewEditor = useRef<() => void | undefined>();
@ -756,39 +757,32 @@ export const LensTopNavMenu = ({
[editField, canEditDataView]
);
const createNewDataView = useMemo(
() =>
canEditDataView
? () => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataView) => {
if (dataView.id) {
if (isOnTextBasedMode) {
dispatch(
switchAndCleanDatasource({
newDatasourceId: 'indexpattern',
visualizationId: visualization?.activeId,
currentIndexPatternId: dataView?.id,
})
);
}
dispatchChangeIndexPattern(dataView);
setCurrentIndexPattern(dataView);
}
},
allowAdHocDataView: true,
});
const createNewDataView = useCallback(() => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (dataView) => {
if (dataView.id) {
if (isOnTextBasedMode) {
dispatch(
switchAndCleanDatasource({
newDatasourceId: 'indexpattern',
visualizationId: visualization?.activeId,
currentIndexPatternId: dataView?.id,
})
);
}
: undefined,
[
canEditDataView,
dataViewEditor,
dispatch,
dispatchChangeIndexPattern,
isOnTextBasedMode,
visualization?.activeId,
]
);
dispatchChangeIndexPattern(dataView);
setCurrentIndexPattern(dataView);
}
},
allowAdHocDataView: true,
});
}, [
dataViewEditor,
dispatch,
dispatchChangeIndexPattern,
isOnTextBasedMode,
visualization?.activeId,
]);
const onCreateDefaultAdHocDataView = useCallback(
async (pattern: string) => {

View file

@ -329,7 +329,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
const fieldInfoUnavailable =
existenceFetchFailed || existenceFetchTimeout || currentIndexPattern?.hasRestrictions;
const editPermission = indexPatternFieldEditor.userPermissions.editIndexPattern();
const editPermission =
indexPatternFieldEditor.userPermissions.editIndexPattern() || !currentIndexPattern.isPersisted;
const unfilteredFieldGroups: FieldGroups = useMemo(() => {
const containsData = (field: IndexPatternField) => {