[Lens][Dashboard] Share session between lens and dashboard (#100214)

This commit is contained in:
Joe Reuter 2021-05-31 11:14:31 +02:00 committed by GitHub
parent 2281e9da5e
commit 79fa4f405b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 154 additions and 84 deletions

View file

@ -295,13 +295,6 @@ export function DashboardApp({
};
}, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]);
// clear search session when leaving dashboard route
useEffect(() => {
return () => {
data.search.session.clear();
};
}, [data.search.session]);
return (
<>
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (

View file

@ -198,8 +198,14 @@ export async function mountApp({
return <DashboardNoMatch history={routeProps.history} />;
};
// make sure the index pattern list is up to date
await dataStart.indexPatterns.clearCache();
const hasEmbeddableIncoming = Boolean(
dashboardServices.embeddable
.getStateTransfer()
.getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false)
);
if (!hasEmbeddableIncoming) {
dataStart.indexPatterns.clearCache();
}
// dispatch synthetic hash change event to update hash history objects
// this is necessary because hash updates triggered by using popState won't trigger this event naturally.
@ -242,7 +248,6 @@ export async function mountApp({
}
render(app, element);
return () => {
dataStart.search.session.clear();
unlistenParentHistory();
unmountComponentAtNode(element);
appUnMounted();

View file

@ -85,6 +85,7 @@ export const useDashboardContainer = ({
let canceled = false;
let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined;
(async function createContainer() {
const existingSession = searchSession.getSessionId();
pendingContainer = await dashboardFactory.create(
getDashboardContainerInput({
isEmbeddedExternally: Boolean(isEmbeddedExternally),
@ -92,7 +93,9 @@ export const useDashboardContainer = ({
dashboardStateManager,
incomingEmbeddable,
query,
searchSessionId: searchSessionIdFromURL ?? searchSession.start(),
searchSessionId:
searchSessionIdFromURL ??
(existingSession && incomingEmbeddable ? existingSession : searchSession.start()),
})
);

View file

@ -83,6 +83,11 @@ export const DashboardListing = ({
};
}, [title, savedObjectsClient, redirectTo, data.query, kbnUrlStateStorage]);
// clear dangling session because they are not required here
useEffect(() => {
data.search.session.clear();
}, [data.search.session]);
const hideWriteControls = dashboardCapabilities.hideWriteControls;
const listingLimit = savedObjects.settings.getListingLimit();
const defaultFilter = title ? `"${title}"` : '';

View file

@ -39,12 +39,16 @@ describe('build query', () => {
{ query: 'extension:jpg', language: 'kuery' },
{ query: 'bar:baz', language: 'lucene' },
] as Query[];
const filters = [
{
match_all: {},
meta: { type: 'match_all' },
} as MatchAllFilter,
];
const filters = {
match: {
a: 'b',
},
meta: {
alias: '',
disabled: false,
negate: false,
},
};
const config = {
allowLeadingWildcards: true,
queryStringOptions: {},
@ -56,7 +60,11 @@ describe('build query', () => {
must: [decorateQuery(luceneStringToDsl('bar:baz'), config.queryStringOptions)],
filter: [
toElasticsearchQuery(fromKueryExpression('extension:jpg'), indexPattern),
{ match_all: {} },
{
match: {
a: 'b',
},
},
],
should: [],
must_not: [],
@ -71,9 +79,15 @@ describe('build query', () => {
it('should accept queries and filters as either single objects or arrays', () => {
const queries = { query: 'extension:jpg', language: 'lucene' } as Query;
const filters = {
match_all: {},
meta: { type: 'match_all' },
} as MatchAllFilter;
match: {
a: 'b',
},
meta: {
alias: '',
disabled: false,
negate: false,
},
};
const config = {
allowLeadingWildcards: true,
queryStringOptions: {},
@ -83,7 +97,13 @@ describe('build query', () => {
const expectedResult = {
bool: {
must: [decorateQuery(luceneStringToDsl('extension:jpg'), config.queryStringOptions)],
filter: [{ match_all: {} }],
filter: [
{
match: {
a: 'b',
},
},
],
should: [],
must_not: [],
},
@ -94,6 +114,49 @@ describe('build query', () => {
expect(result).toEqual(expectedResult);
});
it('should remove match_all clauses', () => {
const filters = [
{
match_all: {},
meta: { type: 'match_all' },
} as MatchAllFilter,
{
match: {
a: 'b',
},
meta: {
alias: '',
disabled: false,
negate: false,
},
},
];
const config = {
allowLeadingWildcards: true,
queryStringOptions: {},
ignoreFilterIfFieldNotInIndex: false,
};
const expectedResult = {
bool: {
must: [],
filter: [
{
match: {
a: 'b',
},
},
],
should: [],
must_not: [],
},
};
const result = buildEsQuery(indexPattern, [], filters, config);
expect(result).toEqual(expectedResult);
});
it('should use the default time zone set in the Advanced Settings in queries and filters', () => {
const queries = [
{ query: '@timestamp:"2019-03-23T13:18:00"', language: 'kuery' },
@ -122,7 +185,6 @@ describe('build query', () => {
indexPattern,
config
),
{ match_all: {} },
],
should: [],
must_not: [],

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { groupBy, has } from 'lodash';
import { groupBy, has, isEqual } from 'lodash';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
@ -21,6 +21,12 @@ export interface EsQueryConfig {
dateFormatTZ?: string;
}
function removeMatchAll<T>(filters: T[]) {
return filters.filter(
(filter) => !filter || typeof filter !== 'object' || !isEqual(filter, { match_all: {} })
);
}
/**
* @param indexPattern
* @param queries - a query object or array of query objects. Each query has a language property and a query property.
@ -63,9 +69,9 @@ export function buildEsQuery(
return {
bool: {
must: [...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must],
filter: [...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter],
should: [...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should],
must: removeMatchAll([...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must]),
filter: removeMatchAll([...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter]),
should: removeMatchAll([...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should]),
must_not: [...kueryQuery.must_not, ...luceneQuery.must_not, ...filterQuery.must_not],
},
};

View file

@ -98,14 +98,6 @@ describe('Session service', () => {
expect(nowProvider.reset).toHaveBeenCalled();
});
it("Can't clear other apps' session", async () => {
sessionService.start();
expect(sessionService.getSessionId()).not.toBeUndefined();
currentAppId$.next('change');
sessionService.clear();
expect(sessionService.getSessionId()).not.toBeUndefined();
});
it("Can start a new session in case there is other apps' stale session", async () => {
const s1 = sessionService.start();
expect(sessionService.getSessionId()).not.toBeUndefined();

View file

@ -128,21 +128,6 @@ export class SessionService {
this.subscription.add(
coreStart.application.currentAppId$.subscribe((newAppName) => {
this.currentApp = newAppName;
if (!this.getSessionId()) return;
// Apps required to clean up their sessions before unmounting
// Make sure that apps don't leave sessions open by throwing an error in DEV mode
const message = `Application '${
this.state.get().appName
}' had an open session while navigating`;
if (initializerContext.env.mode.dev) {
coreStart.fatalErrors.add(message);
} else {
// this should never happen in prod because should be caught in dev mode
// in case this happen we don't want to throw fatal error, as most likely possible bugs are not that critical
// eslint-disable-next-line no-console
console.warn(message);
}
})
);
});
@ -230,18 +215,6 @@ export class SessionService {
* Cleans up current state
*/
public clear() {
// make sure apps can't clear other apps' sessions
const currentSessionApp = this.state.get().appName;
if (currentSessionApp && currentSessionApp !== this.currentApp) {
// eslint-disable-next-line no-console
console.warn(
`Skip clearing session "${this.getSessionId()}" because it belongs to a different app. current: "${
this.currentApp
}", owner: "${currentSessionApp}"`
);
return;
}
this.state.transitions.clear();
this.searchSessionInfoProvider = undefined;
this.searchSessionIndicatorUiConfig = undefined;

View file

@ -147,7 +147,7 @@ export interface Bool {
bool?: Bool;
must?: DslQuery[];
filter?: Filter[];
should?: never[];
should?: Filter[];
must_not?: Filter[];
}

View file

@ -12,12 +12,13 @@ import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddab
const defaultSavedObjectId = '1234';
describe('Mounter', () => {
const byValueFlag = { allowByValueEmbeddables: true };
describe('loadDocument', () => {
it('does not load a document if there is no initial input', async () => {
const services = makeDefaultServices();
const redirectCallback = jest.fn();
const lensStore = mockLensStore({ data: services.data });
await loadDocument(redirectCallback, undefined, services, lensStore);
await loadDocument(redirectCallback, undefined, services, lensStore, undefined, byValueFlag);
expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled();
});
@ -39,7 +40,9 @@ describe('Mounter', () => {
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore
lensStore,
undefined,
byValueFlag
);
});
@ -76,7 +79,9 @@ describe('Mounter', () => {
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore
lensStore,
undefined,
byValueFlag
);
});
@ -85,7 +90,9 @@ describe('Mounter', () => {
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore
lensStore,
undefined,
byValueFlag
);
});
@ -96,7 +103,9 @@ describe('Mounter', () => {
redirectCallback,
{ savedObjectId: '5678' } as LensEmbeddableInput,
services,
lensStore
lensStore,
undefined,
byValueFlag
);
});
@ -116,7 +125,9 @@ describe('Mounter', () => {
redirectCallback,
{ savedObjectId: defaultSavedObjectId } as LensEmbeddableInput,
services,
lensStore
lensStore,
undefined,
byValueFlag
);
});
expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({
@ -136,7 +147,9 @@ describe('Mounter', () => {
redirectCallback,
({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput,
services,
lensStore
lensStore,
undefined,
byValueFlag
);
});

View file

@ -17,6 +17,7 @@ import { i18n } from '@kbn/i18n';
import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public';
import { Provider } from 'react-redux';
import { uniq, isEqual } from 'lodash';
import { EmbeddableEditorState } from 'src/plugins/embeddable/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry';
@ -71,6 +72,8 @@ export async function mountApp(
const historyLocationState = params.history.location.state as HistoryLocationState;
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
const dashboardFeatureFlag = await getByValueFeatureFlag();
const lensServices: LensAppServices = {
data,
storage,
@ -92,7 +95,7 @@ export async function mountApp(
},
// Temporarily required until the 'by value' paradigm is default.
dashboardFeatureFlag: await getByValueFeatureFlag(),
dashboardFeatureFlag,
};
addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks);
@ -172,7 +175,6 @@ export async function mountApp(
if (!initialContext) {
data.query.filterManager.setAppFilters([]);
}
const preloadedState = getPreloadedState({
query: data.query.queryString.getQuery(),
// Do not use app-specific filters from previous app,
@ -180,7 +182,7 @@ export async function mountApp(
filters: !initialContext
? data.query.filterManager.getGlobalFilters()
: data.query.filterManager.getFilters(),
searchSessionId: data.search.session.start(),
searchSessionId: data.search.session.getSessionId(),
resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter),
isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp),
});
@ -197,7 +199,14 @@ export async function mountApp(
);
trackUiEvent('loaded');
const initialInput = getInitialInput(props.id, props.editByValue);
loadDocument(redirectCallback, initialInput, lensServices, lensStore);
loadDocument(
redirectCallback,
initialInput,
lensServices,
lensStore,
embeddableEditorIncomingState,
dashboardFeatureFlag
);
return (
<Provider store={lensStore}>
<App
@ -265,7 +274,6 @@ export async function mountApp(
params.element
);
return () => {
data.search.session.clear();
unmountComponentAtNode(params.element);
unlistenParentHistory();
lensStore.dispatch(navigateAway());
@ -276,7 +284,9 @@ export function loadDocument(
redirectCallback: (savedObjectId?: string) => void,
initialInput: LensEmbeddableInput | undefined,
lensServices: LensAppServices,
lensStore: LensRootStore
lensStore: LensRootStore,
embeddableEditorIncomingState: EmbeddableEditorState | undefined,
dashboardFeatureFlag: DashboardFeatureFlagConfig
) {
const { attributeService, chrome, notifications, data } = lensServices;
const { persistedDoc } = lensStore.getState().app;
@ -317,12 +327,20 @@ export function loadDocument(
data.query.filterManager.setAppFilters(
injectFilterReferences(doc.state.filters, doc.references)
);
const currentSessionId = data.search.session.getSessionId();
lensStore.dispatch(
setState({
query: doc.state.query,
isAppLoading: false,
indexPatternsForTopNav: indexPatterns,
lastKnownDoc: doc,
searchSessionId:
dashboardFeatureFlag.allowByValueEmbeddables &&
Boolean(embeddableEditorIncomingState?.originatingApp) &&
!(initialInput as LensByReferenceInput)?.savedObjectId &&
currentSessionId
? currentSessionId
: data.search.session.start(),
...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null),
})
);

View file

@ -237,7 +237,7 @@ const initialState: IndexPatternPrivateState = {
isFirstExistenceFetch: false,
};
const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } };
const dslQuery = { bool: { must: [], filter: [], should: [], must_not: [] } };
describe('IndexPattern Data Panel', () => {
let defaultProps: Parameters<typeof InnerIndexPatternDataPanel>[0] & {

View file

@ -164,7 +164,7 @@ describe('IndexPattern Field Item', () => {
body: JSON.stringify({
dslQuery: {
bool: {
must: [{ match_all: {} }],
must: [],
filter: [],
should: [],
must_not: [],

View file

@ -155,7 +155,7 @@ describe('Hosts - rendering', () => {
myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters }));
wrapper.update();
expect(wrapper.find(HostsTabs).props().filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'
'{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'
);
});
});

View file

@ -159,7 +159,7 @@ describe('Network page - rendering', () => {
myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters }));
wrapper.update();
expect(wrapper.find(NetworkRoutes).props().filterQuery).toEqual(
'{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'
'{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}'
);
});
});

View file

@ -255,7 +255,7 @@ describe('Combined Queries', () => {
isEventViewer,
})
).toEqual({
filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}',
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',
});
});
@ -299,7 +299,7 @@ describe('Combined Queries', () => {
})
).toEqual({
filterQuery:
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}',
'{"bool":{"must":[],"filter":[{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}',
});
});

View file

@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }) {
//Source should be correct
expect(
mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0].startsWith(
`/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape`
`/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape`
)
).to.equal(true);

View file

@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }) {
//Source should be correct
expect(
mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0].startsWith(
`/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point`
`/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point`
)
).to.equal(true);