[embeddable refactor] initializeTimeRange utility (#179379)

Move timeRange$ logic into reusable `initializeTimeRange` method.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-03-25 18:18:17 -06:00 committed by GitHub
parent c04de5edaf
commit 61047aead0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 247 additions and 37 deletions

View file

@ -9,9 +9,7 @@
import { EuiCallOut } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import fastIsEqual from 'fast-deep-equal';
import { initializeTimeRange, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import React, { useEffect, useState } from 'react';
import { BehaviorSubject } from 'rxjs';
import { SEARCH_EMBEDDABLE_ID } from './constants';
@ -24,54 +22,39 @@ export const getSearchEmbeddableFactory = (services: Services) => {
deserializeState: (state) => {
return state.rawState as State;
},
buildEmbeddable: async (state, buildApi) => {
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
const {
appliedTimeRange$,
cleanupTimeRange,
serializeTimeRange,
timeRangeApi,
timeRangeComparators,
} = initializeTimeRange(state, parentApi);
const defaultDataView = await services.dataViews.getDefaultDataView();
const timeRange$ = new BehaviorSubject<TimeRange | undefined>(state.timeRange);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(
defaultDataView ? [defaultDataView] : undefined
);
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
function setTimeRange(nextTimeRange: TimeRange | undefined) {
timeRange$.next(nextTimeRange);
}
const api = buildApi(
{
...timeRangeApi,
dataViews: dataViews$,
timeRange$,
setTimeRange,
dataLoading: dataLoading$,
serializeState: () => {
return {
rawState: {
timeRange: timeRange$.value,
...serializeTimeRange(),
},
references: [],
};
},
},
{
timeRange: [timeRange$, setTimeRange, fastIsEqual],
...timeRangeComparators,
}
);
const appliedTimeRange$ = new BehaviorSubject(
timeRange$.value ?? api.parentApi?.timeRange$?.value
);
const subscriptions = api.timeRange$.subscribe((timeRange) => {
appliedTimeRange$.next(timeRange ?? api.parentApi?.timeRange$?.value);
});
if (api.parentApi?.timeRange$) {
subscriptions.add(
api.parentApi?.timeRange$.subscribe((parentTimeRange) => {
if (timeRange$?.value) {
return;
}
appliedTimeRange$.next(parentTimeRange);
})
);
}
return {
api,
Component: () => {
@ -85,7 +68,7 @@ export const getSearchEmbeddableFactory = (services: Services) => {
useEffect(() => {
return () => {
subscriptions.unsubscribe();
cleanupTimeRange();
};
}, []);

View file

@ -76,7 +76,8 @@ export {
type PublishesTimeRange,
type PublishesUnifiedSearch,
type PublishesWritableUnifiedSearch,
} from './interfaces/publishes_unified_search';
} from './interfaces/unified_search/publishes_unified_search';
export { initializeTimeRange } from './interfaces/unified_search/initialize_time_range';
export {
apiPublishesSavedObjectId,
useSavedObjectId,

View file

@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TimeRange } from '@kbn/es-query';
import { BehaviorSubject, skip } from 'rxjs';
import { initializeTimeRange } from './initialize_time_range';
describe('initialize time range', () => {
describe('appliedTimeRange$', () => {
let timeRange: TimeRange | undefined;
const parentApi = {
timeRange$: new BehaviorSubject<TimeRange | undefined>(undefined),
};
describe('local and parent time range', () => {
beforeEach(() => {
timeRange = {
from: 'now-15m',
to: 'now',
};
parentApi.timeRange$.next({
from: 'now-24h',
to: 'now',
});
});
it('should return local time range', () => {
const { appliedTimeRange$ } = initializeTimeRange({ timeRange }, parentApi);
expect(appliedTimeRange$.value).toEqual({
from: 'now-15m',
to: 'now',
});
});
it('should emit when local time range changes', async () => {
let emitCount = 0;
const { appliedTimeRange$, timeRangeApi } = initializeTimeRange({ timeRange }, parentApi);
const subscribe = appliedTimeRange$.pipe(skip(1)).subscribe(() => {
emitCount++;
});
timeRangeApi.setTimeRange({
from: 'now-16m',
to: 'now',
});
await new Promise(process.nextTick);
expect(emitCount).toBe(1);
expect(appliedTimeRange$.value).toEqual({
from: 'now-16m',
to: 'now',
});
subscribe.unsubscribe();
});
it('should not emit when parent time range changes', async () => {
let emitCount = 0;
const { appliedTimeRange$ } = initializeTimeRange({ timeRange }, parentApi);
const subscribe = appliedTimeRange$.pipe(skip(1)).subscribe(() => {
emitCount++;
});
parentApi.timeRange$.next({
from: 'now-25h',
to: 'now',
});
await new Promise(process.nextTick);
expect(emitCount).toBe(0);
expect(appliedTimeRange$.value).toEqual({
from: 'now-15m',
to: 'now',
});
subscribe.unsubscribe();
});
it('should emit parent time range when local time range is cleared', async () => {
let emitCount = 0;
const { appliedTimeRange$, timeRangeApi } = initializeTimeRange({ timeRange }, parentApi);
const subscribe = appliedTimeRange$.pipe(skip(1)).subscribe(() => {
emitCount++;
});
timeRangeApi.setTimeRange(undefined);
await new Promise(process.nextTick);
expect(emitCount).toBe(1);
expect(appliedTimeRange$.value).toEqual({
from: 'now-24h',
to: 'now',
});
subscribe.unsubscribe();
});
});
describe('only parent time range', () => {
beforeEach(() => {
timeRange = undefined;
parentApi.timeRange$.next({
from: 'now-24h',
to: 'now',
});
});
it('should return parent time range', () => {
const { appliedTimeRange$ } = initializeTimeRange({ timeRange }, parentApi);
expect(appliedTimeRange$.value).toEqual({
from: 'now-24h',
to: 'now',
});
});
it('should emit when parent time range changes', async () => {
let emitCount = 0;
const { appliedTimeRange$ } = initializeTimeRange({ timeRange }, parentApi);
const subscribe = appliedTimeRange$.pipe(skip(1)).subscribe(() => {
emitCount++;
});
parentApi.timeRange$.next({
from: 'now-25h',
to: 'now',
});
await new Promise(process.nextTick);
expect(emitCount).toBe(1);
expect(appliedTimeRange$.value).toEqual({
from: 'now-25h',
to: 'now',
});
subscribe.unsubscribe();
});
});
});
});

View file

@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject } from 'rxjs';
import fastIsEqual from 'fast-deep-equal';
import { TimeRange } from '@kbn/es-query';
import { PublishingSubject } from '../../publishing_subject';
import { StateComparators } from '../../comparators';
import {
apiPublishesTimeRange,
PublishesTimeRange,
PublishesWritableTimeRange,
} from './publishes_unified_search';
export interface SerializedTimeRange {
timeRange: TimeRange | undefined;
}
export const initializeTimeRange = (
rawState: SerializedTimeRange,
parentApi?: unknown
): {
appliedTimeRange$: PublishingSubject<TimeRange | undefined>;
cleanupTimeRange: () => void;
serializeTimeRange: () => SerializedTimeRange;
timeRangeApi: PublishesWritableTimeRange;
timeRangeComparators: StateComparators<SerializedTimeRange>;
} => {
const timeRange$ = new BehaviorSubject<TimeRange | undefined>(rawState.timeRange);
function setTimeRange(nextTimeRange: TimeRange | undefined) {
timeRange$.next(nextTimeRange);
}
const appliedTimeRange$ = new BehaviorSubject(
timeRange$.value ?? (parentApi as Partial<PublishesTimeRange>)?.timeRange$?.value
);
const subscriptions = timeRange$.subscribe((timeRange) => {
appliedTimeRange$.next(
timeRange ?? (parentApi as Partial<PublishesTimeRange>)?.timeRange$?.value
);
});
if (apiPublishesTimeRange(parentApi)) {
subscriptions.add(
parentApi?.timeRange$.subscribe((parentTimeRange) => {
if (timeRange$?.value) {
return;
}
appliedTimeRange$.next(parentTimeRange);
})
);
}
return {
appliedTimeRange$,
cleanupTimeRange: () => {
subscriptions.unsubscribe();
},
serializeTimeRange: () => ({
timeRange: timeRange$.value,
}),
timeRangeComparators: {
timeRange: [timeRange$, setTimeRange, fastIsEqual],
} as StateComparators<SerializedTimeRange>,
timeRangeApi: {
timeRange$,
setTimeRange,
},
};
};

View file

@ -7,23 +7,27 @@
*/
import { TimeRange, Filter, Query, AggregateQuery } from '@kbn/es-query';
import { PublishingSubject } from '../publishing_subject';
import { PublishingSubject } from '../../publishing_subject';
export interface PublishesTimeRange {
timeRange$: PublishingSubject<TimeRange | undefined>;
}
export type PublishesWritableTimeRange = PublishesTimeRange & {
setTimeRange: (timeRange: TimeRange | undefined) => void;
};
export type PublishesUnifiedSearch = PublishesTimeRange & {
isCompatibleWithUnifiedSearch?: () => boolean;
filters$: PublishingSubject<Filter[] | undefined>;
query$: PublishingSubject<Query | AggregateQuery | undefined>;
};
export type PublishesWritableUnifiedSearch = PublishesUnifiedSearch & {
setTimeRange: (timeRange: TimeRange | undefined) => void;
setFilters: (filters: Filter[] | undefined) => void;
setQuery: (query: Query | undefined) => void;
};
export type PublishesWritableUnifiedSearch = PublishesUnifiedSearch &
PublishesWritableTimeRange & {
setFilters: (filters: Filter[] | undefined) => void;
setQuery: (query: Query | undefined) => void;
};
export const apiPublishesTimeRange = (
unknownApi: null | unknown