* create xpack plugin skeleton, start to implement management section * add tag creation modal * first implementation of the tags table * use InMemoryTable * add edit modal and delete action * update plugin list * add tag list, fix types * add capabilities check on client-side * add tag combo box component * add missing i18n keys * fix privilege FTR tests * add base structure for FTR tests * fix feature ftr test * use string literals for i18n * create savedObjectsTaggingOss plugin, move API types to oss plugin, start to wire to SO management page. * update plugin list * fix types * allow to use `_find` with multiple references * add FTR test for _find API on references fields * add _find integration tests * update generated doc * start to implement tag filtering on SO management section * update generated docs * wire tagging API to dashboard listing page * fix i18n namespace * fix type & tests * update dashboard listing snapshots * adapt FTR listingTable service to search for parsable queries * wite tagging API to visualize listing * update tagging plugin limits * add server-side and client-side validation for tag create/edit * rename title field to name * fix types * fix types bis * add removeReferencesTo API to SOR/SOC * update generated doc * add server-side unit test for `savedObjectsTagging` plugin * move tagging API types to its own file * add savedObjectsTaggingOss mock * add tags_cache tests * add tests for client-side tag client * extract uiApi to distinct files * various API improvements * add more tests * add link between tag and so management sections + add connection counts * add base functional test suite for tagging * add more FTR tests * improve feature control func test * update codeowners * update generated doc * fix access to proxy modal * adapt SO save modal to allow to add tag field * add SO decorator registry and tag implementation * add unit tests for SO tag decorator * add functional tests for visualize integration * add tag SO read permission for vis/dash feature * add RBAC api integ tests * add API integration tests * add test for getTagConnectionsUrl * add SOM test suite * add dashboard integration suite * remove test line * add missing unit tests * improve API types doc * fix create modal save button label * remove console.log * improve doc * self review * add refresh interval for tag cache * improve page object doc * minor cleanup * address review comments * small layout fixes * add initial focus * use lazy accessor for tag request handler context * adapt SOM export and export route to handle references * remove icon from feature config due to master changes * fix SO table tests * update generated docs * sort tags by name in filter dropdown and listing component * wire SO tagging to dashboard save modal * fix types * - add 'create tag' action in tag selector - add notifications on update/create/delete from management - delete modal wording * add description max length validation * remove real-time validation * fix i18n bundle id * update expected size of savedObjectsTagging plugin * use own useIfMounted * update limit again, contract components cannot be lazy loaded atm. * math is hard * remove single usage of lodash for bundle size * add async imports for create/edit modal * add FTR test for 'create tag' action from tag selector * allow 'create new' option to prepopulate name field * extract savedObjectToTag * add advancedSettings read user for security api_integ suite * add audit login for security client wrapper * use import type when possible * wire SO tagging to lens visualization * fix lens jest test * Fix `create tag` option being selected when closing the selector dropdown * add sorting to tag column from getTableColumnDef * address some of restrry comments * rename tag selector's setSelected option to onTagsSelected * fix audit logging even type for saved_object_remove_references * update plugin size limit to current size * adapt maxlength validation wording * remove selection column until we have batch action menu * remove connections link when user lack read privilege to savedObjectManagement * forbid registering multiple SO decorators with the same priority * add so decorator test * extract getTagFindReferences and create API mock * update audit-logging ascidoc * doc nit * throw conflict error if update returns any failure * use refresh=true as default * wording nits * export: rename `references` to `hasReference` * update generated doc * set description max length to 100 * do not initialize tag cache on anonymous pages * split fetchObjectsToExport into two distinct functions * change tag client `delete` call order * tsdoc nits * more nits * add README for oss plugin * add oss plugin start tests * SavedObject.find: rename `references` to `hasReference` * change section description label * remove url prefix constants * last nits and comments * update generated doc
688 lines
21 KiB
TypeScript
688 lines
21 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
*/
|
|
|
|
import './app.scss';
|
|
|
|
import _ from 'lodash';
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { i18n } from '@kbn/i18n';
|
|
import { NotificationsStart } from 'kibana/public';
|
|
import { EuiBreadcrumb } from '@elastic/eui';
|
|
import {
|
|
createKbnUrlStateStorage,
|
|
withNotifyOnErrors,
|
|
} from '../../../../../src/plugins/kibana_utils/public';
|
|
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
|
import {
|
|
OnSaveProps,
|
|
checkForDuplicateTitle,
|
|
} from '../../../../../src/plugins/saved_objects/public';
|
|
import { injectFilterReferences } from '../persistence';
|
|
import { NativeRenderer } from '../native_renderer';
|
|
import { trackUiEvent } from '../lens_ui_telemetry';
|
|
import {
|
|
esFilters,
|
|
IndexPattern as IndexPatternInstance,
|
|
IndexPatternsContract,
|
|
syncQueryStateWithUrl,
|
|
} from '../../../../../src/plugins/data/public';
|
|
import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common';
|
|
import { LensAppProps, LensAppServices, LensAppState } from './types';
|
|
import { getLensTopNavConfig } from './lens_top_nav';
|
|
import { TagEnhancedSavedObjectSaveModalOrigin } from './tags_saved_object_save_modal_origin_wrapper';
|
|
import {
|
|
LensByReferenceInput,
|
|
LensEmbeddableInput,
|
|
} from '../editor_frame_service/embeddable/embeddable';
|
|
|
|
export function App({
|
|
history,
|
|
onAppLeave,
|
|
redirectTo,
|
|
editorFrame,
|
|
initialInput,
|
|
incomingState,
|
|
redirectToOrigin,
|
|
setHeaderActionMenu,
|
|
initialContext,
|
|
}: LensAppProps) {
|
|
const {
|
|
data,
|
|
chrome,
|
|
overlays,
|
|
navigation,
|
|
uiSettings,
|
|
application,
|
|
notifications,
|
|
attributeService,
|
|
savedObjectsClient,
|
|
savedObjectsTagging,
|
|
getOriginatingAppName,
|
|
|
|
// Temporarily required until the 'by value' paradigm is default.
|
|
dashboardFeatureFlag,
|
|
} = useKibana<LensAppServices>().services;
|
|
|
|
const [state, setState] = useState<LensAppState>(() => {
|
|
const currentRange = data.query.timefilter.timefilter.getTime();
|
|
return {
|
|
query: data.query.queryString.getQuery(),
|
|
filters: data.query.filterManager.getFilters(),
|
|
isLoading: Boolean(initialInput),
|
|
indexPatternsForTopNav: [],
|
|
dateRange: {
|
|
fromDate: currentRange.from,
|
|
toDate: currentRange.to,
|
|
},
|
|
isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp),
|
|
isSaveModalVisible: false,
|
|
indicateNoData: false,
|
|
isSaveable: false,
|
|
};
|
|
});
|
|
|
|
const { lastKnownDoc } = state;
|
|
|
|
const showNoDataPopover = useCallback(() => {
|
|
setState((prevState) => ({ ...prevState, indicateNoData: true }));
|
|
}, [setState]);
|
|
|
|
useEffect(() => {
|
|
if (state.indicateNoData) {
|
|
setState((prevState) => ({ ...prevState, indicateNoData: false }));
|
|
}
|
|
}, [
|
|
setState,
|
|
state.indicateNoData,
|
|
state.query,
|
|
state.filters,
|
|
state.dateRange,
|
|
state.indexPatternsForTopNav,
|
|
]);
|
|
|
|
const onError = useCallback(
|
|
(e: { message: string }) =>
|
|
notifications.toasts.addDanger({
|
|
title: e.message,
|
|
}),
|
|
[notifications.toasts]
|
|
);
|
|
|
|
const getLastKnownDocWithoutPinnedFilters = useCallback(
|
|
function () {
|
|
if (!lastKnownDoc) return undefined;
|
|
const [pinnedFilters, appFilters] = _.partition(
|
|
injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references),
|
|
esFilters.isFilterPinned
|
|
);
|
|
return pinnedFilters?.length
|
|
? {
|
|
...lastKnownDoc,
|
|
state: {
|
|
...lastKnownDoc.state,
|
|
filters: appFilters,
|
|
},
|
|
}
|
|
: lastKnownDoc;
|
|
},
|
|
[lastKnownDoc]
|
|
);
|
|
|
|
const getIsByValueMode = useCallback(
|
|
() =>
|
|
Boolean(
|
|
// Temporarily required until the 'by value' paradigm is default.
|
|
dashboardFeatureFlag.allowByValueEmbeddables &&
|
|
state.isLinkedToOriginatingApp &&
|
|
!(initialInput as LensByReferenceInput)?.savedObjectId
|
|
),
|
|
[dashboardFeatureFlag.allowByValueEmbeddables, state.isLinkedToOriginatingApp, initialInput]
|
|
);
|
|
|
|
useEffect(() => {
|
|
// Clear app-specific filters when navigating to Lens. Necessary because Lens
|
|
// can be loaded without a full page refresh. If the user navigates to Lens from Discover
|
|
// we keep the filters
|
|
if (!initialContext) {
|
|
data.query.filterManager.setAppFilters([]);
|
|
}
|
|
|
|
const filterSubscription = data.query.filterManager.getUpdates$().subscribe({
|
|
next: () => {
|
|
setState((s) => ({ ...s, filters: data.query.filterManager.getFilters() }));
|
|
trackUiEvent('app_filters_updated');
|
|
},
|
|
});
|
|
|
|
const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({
|
|
next: () => {
|
|
const currentRange = data.query.timefilter.timefilter.getTime();
|
|
setState((s) => ({
|
|
...s,
|
|
dateRange: {
|
|
fromDate: currentRange.from,
|
|
toDate: currentRange.to,
|
|
},
|
|
}));
|
|
},
|
|
});
|
|
|
|
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
|
history,
|
|
useHash: uiSettings.get('state:storeInSessionStorage'),
|
|
...withNotifyOnErrors(notifications.toasts),
|
|
});
|
|
const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
|
|
data.query,
|
|
kbnUrlStateStorage
|
|
);
|
|
|
|
return () => {
|
|
stopSyncingQueryServiceStateWithUrl();
|
|
filterSubscription.unsubscribe();
|
|
timeSubscription.unsubscribe();
|
|
};
|
|
}, [
|
|
data.query.filterManager,
|
|
data.query.timefilter.timefilter,
|
|
notifications.toasts,
|
|
uiSettings,
|
|
data.query,
|
|
history,
|
|
initialContext,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
onAppLeave((actions) => {
|
|
// Confirm when the user has made any changes to an existing doc
|
|
// or when the user has configured something without saving
|
|
if (
|
|
application.capabilities.visualize.save &&
|
|
!_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) &&
|
|
(state.isSaveable || state.persistedDoc)
|
|
) {
|
|
return actions.confirm(
|
|
i18n.translate('xpack.lens.app.unsavedWorkMessage', {
|
|
defaultMessage: 'Leave Lens with unsaved work?',
|
|
}),
|
|
i18n.translate('xpack.lens.app.unsavedWorkTitle', {
|
|
defaultMessage: 'Unsaved changes',
|
|
})
|
|
);
|
|
} else {
|
|
return actions.default();
|
|
}
|
|
});
|
|
}, [
|
|
onAppLeave,
|
|
lastKnownDoc,
|
|
state.isSaveable,
|
|
state.persistedDoc,
|
|
getLastKnownDocWithoutPinnedFilters,
|
|
application.capabilities.visualize.save,
|
|
]);
|
|
|
|
// Sync Kibana breadcrumbs any time the saved document's title changes
|
|
useEffect(() => {
|
|
const isByValueMode = getIsByValueMode();
|
|
const breadcrumbs: EuiBreadcrumb[] = [];
|
|
if (state.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) {
|
|
breadcrumbs.push({
|
|
onClick: () => {
|
|
redirectToOrigin();
|
|
},
|
|
text: getOriginatingAppName(),
|
|
});
|
|
}
|
|
if (!isByValueMode) {
|
|
breadcrumbs.push({
|
|
href: application.getUrlForApp('visualize'),
|
|
onClick: (e) => {
|
|
application.navigateToApp('visualize', { path: '/' });
|
|
e.preventDefault();
|
|
},
|
|
text: i18n.translate('xpack.lens.breadcrumbsTitle', {
|
|
defaultMessage: 'Visualize',
|
|
}),
|
|
});
|
|
}
|
|
let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', {
|
|
defaultMessage: 'Create',
|
|
});
|
|
if (state.persistedDoc) {
|
|
currentDocTitle = isByValueMode
|
|
? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' })
|
|
: state.persistedDoc.title;
|
|
}
|
|
breadcrumbs.push({ text: currentDocTitle });
|
|
chrome.setBreadcrumbs(breadcrumbs);
|
|
}, [
|
|
dashboardFeatureFlag.allowByValueEmbeddables,
|
|
state.isLinkedToOriginatingApp,
|
|
getOriginatingAppName,
|
|
state.persistedDoc,
|
|
redirectToOrigin,
|
|
getIsByValueMode,
|
|
initialInput,
|
|
application,
|
|
chrome,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!initialInput ||
|
|
(attributeService.inputIsRefType(initialInput) &&
|
|
initialInput.savedObjectId === state.persistedDoc?.savedObjectId)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
setState((s) => ({ ...s, isLoading: true }));
|
|
attributeService
|
|
.unwrapAttributes(initialInput)
|
|
.then((attributes) => {
|
|
if (!initialInput) {
|
|
return;
|
|
}
|
|
const doc = {
|
|
...initialInput,
|
|
...attributes,
|
|
type: LENS_EMBEDDABLE_TYPE,
|
|
};
|
|
|
|
if (attributeService.inputIsRefType(initialInput)) {
|
|
chrome.recentlyAccessed.add(
|
|
getFullPath(initialInput.savedObjectId),
|
|
attributes.title,
|
|
initialInput.savedObjectId
|
|
);
|
|
}
|
|
getAllIndexPatterns(
|
|
_.uniq(doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)),
|
|
data.indexPatterns,
|
|
notifications
|
|
)
|
|
.then((indexPatterns) => {
|
|
// Don't overwrite any pinned filters
|
|
data.query.filterManager.setAppFilters(
|
|
injectFilterReferences(doc.state.filters, doc.references)
|
|
);
|
|
setState((s) => ({
|
|
...s,
|
|
isLoading: false,
|
|
persistedDoc: doc,
|
|
lastKnownDoc: doc,
|
|
query: doc.state.query,
|
|
indexPatternsForTopNav: indexPatterns,
|
|
}));
|
|
})
|
|
.catch((e) => {
|
|
setState((s) => ({ ...s, isLoading: false }));
|
|
redirectTo();
|
|
});
|
|
})
|
|
.catch((e) => {
|
|
setState((s) => ({ ...s, isLoading: false }));
|
|
notifications.toasts.addDanger(
|
|
i18n.translate('xpack.lens.app.docLoadingError', {
|
|
defaultMessage: 'Error loading saved document',
|
|
})
|
|
);
|
|
|
|
redirectTo();
|
|
});
|
|
}, [
|
|
notifications,
|
|
data.indexPatterns,
|
|
data.query.filterManager,
|
|
initialInput,
|
|
attributeService,
|
|
redirectTo,
|
|
chrome.recentlyAccessed,
|
|
state.persistedDoc?.savedObjectId,
|
|
state.persistedDoc?.state,
|
|
]);
|
|
|
|
const runSave = async (
|
|
saveProps: Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
|
returnToOrigin: boolean;
|
|
onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
|
|
newDescription?: string;
|
|
newTags?: string[];
|
|
},
|
|
options: { saveToLibrary: boolean }
|
|
) => {
|
|
if (!lastKnownDoc) {
|
|
return;
|
|
}
|
|
|
|
let references = lastKnownDoc.references;
|
|
if (savedObjectsTagging && saveProps.newTags) {
|
|
references = savedObjectsTagging.ui.updateTagsReferences(references, saveProps.newTags);
|
|
}
|
|
|
|
const docToSave = {
|
|
...getLastKnownDocWithoutPinnedFilters()!,
|
|
description: saveProps.newDescription,
|
|
title: saveProps.newTitle,
|
|
references,
|
|
};
|
|
|
|
// Required to serialize filters in by value mode until
|
|
// https://github.com/elastic/kibana/issues/77588 is fixed
|
|
if (getIsByValueMode()) {
|
|
docToSave.state.filters.forEach((filter) => {
|
|
if (typeof filter.meta.value === 'function') {
|
|
delete filter.meta.value;
|
|
}
|
|
});
|
|
}
|
|
|
|
const originalInput = saveProps.newCopyOnSave ? undefined : initialInput;
|
|
const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId;
|
|
if (options.saveToLibrary) {
|
|
try {
|
|
await checkForDuplicateTitle(
|
|
{
|
|
id: originalSavedObjectId,
|
|
title: docToSave.title,
|
|
copyOnSave: saveProps.newCopyOnSave,
|
|
lastSavedTitle: lastKnownDoc.title,
|
|
getEsType: () => 'lens',
|
|
getDisplayName: () =>
|
|
i18n.translate('xpack.lens.app.saveModalType', {
|
|
defaultMessage: 'Lens visualization',
|
|
}),
|
|
},
|
|
saveProps.isTitleDuplicateConfirmed,
|
|
saveProps.onTitleDuplicate,
|
|
{
|
|
savedObjectsClient,
|
|
overlays,
|
|
}
|
|
);
|
|
} catch (e) {
|
|
// ignore duplicate title failure, user notified in save modal
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
const newInput = (await attributeService.wrapAttributes(
|
|
docToSave,
|
|
options.saveToLibrary,
|
|
originalInput
|
|
)) as LensEmbeddableInput;
|
|
|
|
if (saveProps.returnToOrigin && redirectToOrigin) {
|
|
// disabling the validation on app leave because the document has been saved.
|
|
onAppLeave((actions) => {
|
|
return actions.default();
|
|
});
|
|
redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
|
|
return;
|
|
}
|
|
|
|
notifications.toasts.addSuccess(
|
|
i18n.translate('xpack.lens.app.saveVisualization.successNotificationText', {
|
|
defaultMessage: `Saved '{visTitle}'`,
|
|
values: {
|
|
visTitle: docToSave.title,
|
|
},
|
|
})
|
|
);
|
|
|
|
if (
|
|
attributeService.inputIsRefType(newInput) &&
|
|
newInput.savedObjectId !== originalSavedObjectId
|
|
) {
|
|
chrome.recentlyAccessed.add(
|
|
getFullPath(newInput.savedObjectId),
|
|
docToSave.title,
|
|
newInput.savedObjectId
|
|
);
|
|
setState((s) => ({
|
|
...s,
|
|
isSaveModalVisible: false,
|
|
isLinkedToOriginatingApp: false,
|
|
}));
|
|
redirectTo(newInput.savedObjectId);
|
|
return;
|
|
}
|
|
|
|
const newDoc = {
|
|
...docToSave,
|
|
...newInput,
|
|
};
|
|
setState((s) => ({
|
|
...s,
|
|
persistedDoc: newDoc,
|
|
lastKnownDoc: newDoc,
|
|
isSaveModalVisible: false,
|
|
isLinkedToOriginatingApp: false,
|
|
}));
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-console
|
|
console.dir(e);
|
|
trackUiEvent('save_failed');
|
|
setState((s) => ({ ...s, isSaveModalVisible: false }));
|
|
}
|
|
};
|
|
|
|
const { TopNavMenu } = navigation.ui;
|
|
|
|
const savingPermitted = Boolean(state.isSaveable && application.capabilities.visualize.save);
|
|
const topNavConfig = getLensTopNavConfig({
|
|
showSaveAndReturn: Boolean(
|
|
state.isLinkedToOriginatingApp &&
|
|
// Temporarily required until the 'by value' paradigm is default.
|
|
(dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput))
|
|
),
|
|
isByValueMode: getIsByValueMode(),
|
|
showCancel: Boolean(state.isLinkedToOriginatingApp),
|
|
savingPermitted,
|
|
actions: {
|
|
saveAndReturn: () => {
|
|
if (savingPermitted && lastKnownDoc) {
|
|
// disabling the validation on app leave because the document has been saved.
|
|
onAppLeave((actions) => {
|
|
return actions.default();
|
|
});
|
|
runSave(
|
|
{
|
|
newTitle: lastKnownDoc.title,
|
|
newCopyOnSave: false,
|
|
isTitleDuplicateConfirmed: false,
|
|
returnToOrigin: true,
|
|
},
|
|
{
|
|
saveToLibrary:
|
|
(initialInput && attributeService.inputIsRefType(initialInput)) ?? false,
|
|
}
|
|
);
|
|
}
|
|
},
|
|
showSaveModal: () => {
|
|
if (savingPermitted) {
|
|
setState((s) => ({ ...s, isSaveModalVisible: true }));
|
|
}
|
|
},
|
|
cancel: () => {
|
|
if (redirectToOrigin) {
|
|
redirectToOrigin();
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
const tagsIds =
|
|
state.persistedDoc && savedObjectsTagging
|
|
? savedObjectsTagging.ui.getTagIdsFromReferences(state.persistedDoc.references)
|
|
: [];
|
|
|
|
return (
|
|
<>
|
|
<div className="lnsApp">
|
|
<div className="lnsApp__header">
|
|
<TopNavMenu
|
|
setMenuMountPoint={setHeaderActionMenu}
|
|
config={topNavConfig}
|
|
showSearchBar={true}
|
|
showDatePicker={true}
|
|
showQueryBar={true}
|
|
showFilterBar={true}
|
|
indexPatterns={state.indexPatternsForTopNav}
|
|
showSaveQuery={Boolean(application.capabilities.visualize.saveQuery)}
|
|
savedQuery={state.savedQuery}
|
|
data-test-subj="lnsApp_topNav"
|
|
screenTitle={'lens'}
|
|
appName={'lens'}
|
|
onQuerySubmit={(payload) => {
|
|
const { dateRange, query } = payload;
|
|
if (
|
|
dateRange.from !== state.dateRange.fromDate ||
|
|
dateRange.to !== state.dateRange.toDate
|
|
) {
|
|
data.query.timefilter.timefilter.setTime(dateRange);
|
|
trackUiEvent('app_date_change');
|
|
} else {
|
|
trackUiEvent('app_query_change');
|
|
}
|
|
setState((s) => ({
|
|
...s,
|
|
dateRange: {
|
|
fromDate: dateRange.from,
|
|
toDate: dateRange.to,
|
|
},
|
|
query: query || s.query,
|
|
}));
|
|
}}
|
|
onSaved={(savedQuery) => {
|
|
setState((s) => ({ ...s, savedQuery }));
|
|
}}
|
|
onSavedQueryUpdated={(savedQuery) => {
|
|
const savedQueryFilters = savedQuery.attributes.filters || [];
|
|
const globalFilters = data.query.filterManager.getGlobalFilters();
|
|
data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]);
|
|
setState((s) => ({
|
|
...s,
|
|
savedQuery: { ...savedQuery }, // Shallow query for reference issues
|
|
dateRange: savedQuery.attributes.timefilter
|
|
? {
|
|
fromDate: savedQuery.attributes.timefilter.from,
|
|
toDate: savedQuery.attributes.timefilter.to,
|
|
}
|
|
: s.dateRange,
|
|
}));
|
|
}}
|
|
onClearSavedQuery={() => {
|
|
data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters());
|
|
setState((s) => ({
|
|
...s,
|
|
savedQuery: undefined,
|
|
filters: data.query.filterManager.getGlobalFilters(),
|
|
query: data.query.queryString.getDefaultQuery(),
|
|
}));
|
|
}}
|
|
query={state.query}
|
|
dateRangeFrom={state.dateRange.fromDate}
|
|
dateRangeTo={state.dateRange.toDate}
|
|
indicateNoData={state.indicateNoData}
|
|
/>
|
|
</div>
|
|
{(!state.isLoading || state.persistedDoc) && (
|
|
<NativeRenderer
|
|
className="lnsApp__frame"
|
|
render={editorFrame.mount}
|
|
nativeProps={{
|
|
dateRange: state.dateRange,
|
|
query: state.query,
|
|
filters: state.filters,
|
|
savedQuery: state.savedQuery,
|
|
doc: state.persistedDoc,
|
|
onError,
|
|
showNoDataPopover,
|
|
initialContext,
|
|
onChange: ({ filterableIndexPatterns, doc, isSaveable }) => {
|
|
if (isSaveable !== state.isSaveable) {
|
|
setState((s) => ({ ...s, isSaveable }));
|
|
}
|
|
if (!_.isEqual(state.persistedDoc, doc)) {
|
|
setState((s) => ({ ...s, lastKnownDoc: doc }));
|
|
}
|
|
|
|
// Update the cached index patterns if the user made a change to any of them
|
|
if (
|
|
state.indexPatternsForTopNav.length !== filterableIndexPatterns.length ||
|
|
filterableIndexPatterns.some(
|
|
(id) =>
|
|
!state.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id)
|
|
)
|
|
) {
|
|
getAllIndexPatterns(
|
|
filterableIndexPatterns,
|
|
data.indexPatterns,
|
|
notifications
|
|
).then((indexPatterns) => {
|
|
if (indexPatterns) {
|
|
setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns }));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
{lastKnownDoc && state.isSaveModalVisible && (
|
|
<TagEnhancedSavedObjectSaveModalOrigin
|
|
savedObjectsTagging={savedObjectsTagging}
|
|
initialTags={tagsIds}
|
|
originatingApp={incomingState?.originatingApp}
|
|
onSave={(props) => runSave(props, { saveToLibrary: true })}
|
|
onClose={() => {
|
|
setState((s) => ({ ...s, isSaveModalVisible: false }));
|
|
}}
|
|
getAppNameFromId={() => getOriginatingAppName()}
|
|
documentInfo={{
|
|
id: lastKnownDoc.savedObjectId,
|
|
title: lastKnownDoc.title || '',
|
|
description: lastKnownDoc.description || '',
|
|
}}
|
|
returnToOriginSwitchLabel={
|
|
getIsByValueMode() && initialInput
|
|
? i18n.translate('xpack.lens.app.updatePanel', {
|
|
defaultMessage: 'Update panel on {originatingAppName}',
|
|
values: { originatingAppName: getOriginatingAppName() },
|
|
})
|
|
: undefined
|
|
}
|
|
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
|
defaultMessage: 'Lens visualization',
|
|
})}
|
|
data-test-subj="lnsApp_saveModalOrigin"
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export async function getAllIndexPatterns(
|
|
ids: string[],
|
|
indexPatternsService: IndexPatternsContract,
|
|
notifications: NotificationsStart
|
|
): Promise<IndexPatternInstance[]> {
|
|
try {
|
|
return await Promise.all(ids.map((id) => indexPatternsService.get(id)));
|
|
} catch (e) {
|
|
notifications.toasts.addDanger(
|
|
i18n.translate('xpack.lens.app.indexPatternLoadingError', {
|
|
defaultMessage: 'Error loading index patterns',
|
|
})
|
|
);
|
|
|
|
throw new Error(e);
|
|
}
|
|
}
|