Don't trigger auto-refresh until previous refresh completes (#93410)
This commit is contained in:
parent
d39701fc97
commit
7984745a9d
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md)
|
||||
|
||||
## AutoRefreshDoneFn type
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AutoRefreshDoneFn = () => void;
|
||||
```
|
|
@ -47,6 +47,7 @@
|
|||
| [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | |
|
||||
| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | |
|
||||
| [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | |
|
||||
| [waitUntilNextSessionCompletes$(sessionService, { waitForIdle })](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. |
|
||||
|
||||
## Interfaces
|
||||
|
||||
|
@ -92,6 +93,7 @@
|
|||
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
|
||||
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object |
|
||||
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields |
|
||||
| [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) |
|
||||
|
||||
## Variables
|
||||
|
||||
|
@ -141,6 +143,7 @@
|
|||
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
|
||||
| [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. |
|
||||
| [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* |
|
||||
| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | |
|
||||
| [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | |
|
||||
| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | |
|
||||
| [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | |
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [waitUntilNextSessionCompletes$](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
|
||||
|
||||
## waitUntilNextSessionCompletes$() function
|
||||
|
||||
Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable<SearchSessionState>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| sessionService | <code>ISessionService</code> | [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
|
||||
| { waitForIdle } | <code>WaitUntilNextSessionCompletesOptions</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`import("rxjs").Observable<SearchSessionState>`
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md)
|
||||
|
||||
## WaitUntilNextSessionCompletesOptions interface
|
||||
|
||||
Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface WaitUntilNextSessionCompletesOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) | <code>number</code> | For how long to wait between session state transitions before considering that session completed |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) > [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md)
|
||||
|
||||
## WaitUntilNextSessionCompletesOptions.waitForIdle property
|
||||
|
||||
For how long to wait between session state transitions before considering that session completed
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
waitForIdle?: number;
|
||||
```
|
|
@ -10,7 +10,7 @@ import { History } from 'history';
|
|||
import { merge, Subject, Subscription } from 'rxjs';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { debounceTime, tap } from 'rxjs/operators';
|
||||
import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators';
|
||||
import { useKibana } from '../../../kibana_react/public';
|
||||
import { DashboardConstants } from '../dashboard_constants';
|
||||
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
|
||||
|
@ -30,7 +30,7 @@ import {
|
|||
useSavedDashboard,
|
||||
} from './hooks';
|
||||
|
||||
import { IndexPattern } from '../services/data';
|
||||
import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data';
|
||||
import { EmbeddableRenderer } from '../services/embeddable';
|
||||
import { DashboardContainerInput } from '.';
|
||||
import { leaveConfirmStrings } from '../dashboard_strings';
|
||||
|
@ -209,14 +209,26 @@ export function DashboardApp({
|
|||
);
|
||||
|
||||
subscriptions.add(
|
||||
merge(
|
||||
data.query.timefilter.timefilter.getAutoRefreshFetch$(),
|
||||
searchSessionIdQuery$
|
||||
).subscribe(() => {
|
||||
searchSessionIdQuery$.subscribe(() => {
|
||||
triggerRefresh$.next({ force: true });
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.add(
|
||||
data.query.timefilter.timefilter
|
||||
.getAutoRefreshFetch$()
|
||||
.pipe(
|
||||
tap(() => {
|
||||
triggerRefresh$.next({ force: true });
|
||||
}),
|
||||
switchMap((done) =>
|
||||
// best way on a dashboard to estimate that panels are updated is to rely on search session service state
|
||||
waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
|
||||
dashboardStateManager.registerChangeListener(() => {
|
||||
setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter));
|
||||
// we aren't checking dirty state because there are changes the container needs to know about
|
||||
|
|
|
@ -5,7 +5,7 @@ title: Data services
|
|||
image: https://source.unsplash.com/400x175/?Search
|
||||
summary: The data plugin contains services for searching, querying and filtering.
|
||||
date: 2020-12-02
|
||||
tags: ['kibana','dev', 'contributor', 'api docs']
|
||||
tags: ['kibana', 'dev', 'contributor', 'api docs']
|
||||
---
|
||||
|
||||
# data
|
||||
|
@ -149,7 +149,6 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints:
|
|||
- Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
|
||||
- Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
|
||||
|
||||
|
||||
### Index Patterns API
|
||||
|
||||
Index Patterns REST API allows you to create, retrieve and delete index patterns. I also
|
||||
|
@ -212,11 +211,10 @@ The endpoint returns the created index pattern object.
|
|||
|
||||
```json
|
||||
{
|
||||
"index_pattern": {}
|
||||
"index_pattern": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Fetch an index pattern by ID
|
||||
|
||||
Retrieve an index pattern by its ID.
|
||||
|
@ -229,23 +227,22 @@ Returns an index pattern object.
|
|||
|
||||
```json
|
||||
{
|
||||
"index_pattern": {
|
||||
"id": "...",
|
||||
"version": "...",
|
||||
"title": "...",
|
||||
"type": "...",
|
||||
"intervalName": "...",
|
||||
"timeFieldName": "...",
|
||||
"sourceFilters": [],
|
||||
"fields": {},
|
||||
"typeMeta": {},
|
||||
"fieldFormats": {},
|
||||
"fieldAttrs": {}
|
||||
}
|
||||
"index_pattern": {
|
||||
"id": "...",
|
||||
"version": "...",
|
||||
"title": "...",
|
||||
"type": "...",
|
||||
"intervalName": "...",
|
||||
"timeFieldName": "...",
|
||||
"sourceFilters": [],
|
||||
"fields": {},
|
||||
"typeMeta": {},
|
||||
"fieldFormats": {},
|
||||
"fieldAttrs": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Delete an index pattern by ID
|
||||
|
||||
Delete and index pattern by its ID.
|
||||
|
@ -256,21 +253,21 @@ DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|||
|
||||
Returns an '200 OK` response with empty body on success.
|
||||
|
||||
|
||||
#### Partially update an index pattern by ID
|
||||
|
||||
Update part of an index pattern. Only provided fields will be updated on the
|
||||
index pattern, missing fields will stay as they are persisted.
|
||||
|
||||
These fields can be update partially:
|
||||
- `title`
|
||||
- `timeFieldName`
|
||||
- `intervalName`
|
||||
- `fields` (optionally refresh fields)
|
||||
- `sourceFilters`
|
||||
- `fieldFormatMap`
|
||||
- `type`
|
||||
- `typeMeta`
|
||||
|
||||
- `title`
|
||||
- `timeFieldName`
|
||||
- `intervalName`
|
||||
- `fields` (optionally refresh fields)
|
||||
- `sourceFilters`
|
||||
- `fieldFormatMap`
|
||||
- `type`
|
||||
- `typeMeta`
|
||||
|
||||
Update a title of an index pattern.
|
||||
|
||||
|
@ -318,18 +315,14 @@ This endpoint returns the updated index pattern object.
|
|||
|
||||
```json
|
||||
{
|
||||
"index_pattern": {
|
||||
|
||||
}
|
||||
"index_pattern": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Fields API
|
||||
|
||||
Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`.
|
||||
|
||||
|
||||
#### Update fields
|
||||
|
||||
Update endpoint allows you to update fields presentation metadata, such as `count`,
|
||||
|
@ -383,13 +376,10 @@ This endpoint returns the updated index pattern object.
|
|||
|
||||
```json
|
||||
{
|
||||
"index_pattern": {
|
||||
|
||||
}
|
||||
"index_pattern": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Scripted Fields API
|
||||
|
||||
Scripted Fields API provides CRUD API for scripted fields of an index pattern.
|
||||
|
@ -487,7 +477,7 @@ Returns the field object.
|
|||
|
||||
```json
|
||||
{
|
||||
"field": {}
|
||||
"field": {}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -529,47 +519,86 @@ POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scri
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Query
|
||||
|
||||
The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries.
|
||||
|
||||
It contains sub-services for each of those configurations:
|
||||
- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
|
||||
- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
|
||||
- `data.query.queryString` - Responsible for the query string and query language settings.
|
||||
- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
|
||||
|
||||
Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
|
||||
- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
|
||||
- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
|
||||
- `data.query.queryString` - Responsible for the query string and query language settings.
|
||||
- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
|
||||
|
||||
A simple use case is:
|
||||
Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
|
||||
|
||||
```.ts
|
||||
function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
|
||||
data.query.state$.subscribe(() => {
|
||||
A simple use case is:
|
||||
|
||||
// Constuct the query portion of the search request
|
||||
const query = data.query.getEsQuery(indexPattern);
|
||||
```.ts
|
||||
function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
|
||||
data.query.state$.subscribe(() => {
|
||||
|
||||
// Construct a request
|
||||
const request = {
|
||||
params: {
|
||||
index: indexPattern.title,
|
||||
body: {
|
||||
aggs: aggConfigs.toDsl(),
|
||||
query,
|
||||
},
|
||||
},
|
||||
};
|
||||
// Constuct the query portion of the search request
|
||||
const query = data.query.getEsQuery(indexPattern);
|
||||
|
||||
// Search with the `data.query` config
|
||||
const search$ = data.search.search(request);
|
||||
// Construct a request
|
||||
const request = {
|
||||
params: {
|
||||
index: indexPattern.title,
|
||||
body: {
|
||||
aggs: aggConfigs.toDsl(),
|
||||
query,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
...
|
||||
});
|
||||
}
|
||||
// Search with the `data.query` config
|
||||
const search$ = data.search.search(request);
|
||||
|
||||
```
|
||||
...
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Timefilter
|
||||
|
||||
`data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings.
|
||||
|
||||
#### Autorefresh
|
||||
|
||||
Timefilter provides an API for setting and getting current auto refresh state:
|
||||
|
||||
```ts
|
||||
const { pause, value } = data.query.timefilter.timefilter.getRefreshInterval();
|
||||
|
||||
data.query.timefilter.timefilter.setRefreshInterval({ pause: false, value: 5000 }); // start auto refresh with 5 seconds interval
|
||||
```
|
||||
|
||||
Timefilter API also provides an `autoRefreshFetch$` observables that apps should use to get notified
|
||||
when it is time to refresh data because of auto refresh.
|
||||
This API expects apps to confirm when they are done with reloading the data.
|
||||
The confirmation mechanism is needed to prevent excessive queue of fetches.
|
||||
|
||||
```
|
||||
import { refetchData } from '../my-app'
|
||||
|
||||
const autoRefreshFetch$ = data.query.timefilter.timefilter.getAutoRefreshFetch$()
|
||||
autoRefreshFetch$.subscribe((done) => {
|
||||
try {
|
||||
await refetchData();
|
||||
} finally {
|
||||
// confirm that data fetching was finished
|
||||
done();
|
||||
}
|
||||
})
|
||||
|
||||
function unmount() {
|
||||
// don't forget to unsubscribe when leaving the app
|
||||
autoRefreshFetch$.unsubscribe()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
|
|
|
@ -388,6 +388,8 @@ export {
|
|||
PainlessError,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
SEARCH_SESSIONS_MANAGEMENT_ID,
|
||||
waitUntilNextSessionCompletes$,
|
||||
WaitUntilNextSessionCompletesOptions,
|
||||
} from './search';
|
||||
|
||||
export type {
|
||||
|
@ -467,6 +469,7 @@ export {
|
|||
TimeHistoryContract,
|
||||
QueryStateChange,
|
||||
QueryStart,
|
||||
AutoRefreshDoneFn,
|
||||
} from './query';
|
||||
|
||||
export { AggsStart } from './search/aggs';
|
||||
|
|
|
@ -504,6 +504,11 @@ export interface ApplyGlobalFilterActionContext {
|
|||
// @public (undocumented)
|
||||
export type AutocompleteStart = ReturnType<AutocompleteService['start']>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "AutoRefreshDoneFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type AutoRefreshDoneFn = () => void;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
@ -2647,6 +2652,18 @@ export const UI_SETTINGS: {
|
|||
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "waitUntilNextSessionCompletes$" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable<SearchSessionState>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "WaitUntilNextSessionCompletesOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface WaitUntilNextSessionCompletesOptions {
|
||||
waitForIdle?: number;
|
||||
}
|
||||
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
|
@ -2694,21 +2711,21 @@ export const UI_SETTINGS: {
|
|||
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
export { TimefilterService, TimefilterSetup } from './timefilter_service';
|
||||
|
||||
export * from './types';
|
||||
export { Timefilter, TimefilterContract } from './timefilter';
|
||||
export { Timefilter, TimefilterContract, AutoRefreshDoneFn } from './timefilter';
|
||||
export { TimeHistory, TimeHistoryContract } from './time_history';
|
||||
export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter';
|
||||
export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter';
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* 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 { createAutoRefreshLoop, AutoRefreshDoneFn } from './auto_refresh_loop';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
test('triggers refresh with interval', () => {
|
||||
const { loop$, start, stop } = createAutoRefreshLoop();
|
||||
|
||||
const fn = jest.fn((done) => done());
|
||||
loop$.subscribe(fn);
|
||||
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(fn).not.toBeCalled();
|
||||
|
||||
start(1000);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
stop();
|
||||
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('waits for done() to be called', () => {
|
||||
const { loop$, start } = createAutoRefreshLoop();
|
||||
|
||||
let done!: AutoRefreshDoneFn;
|
||||
const fn = jest.fn((_done) => {
|
||||
done = _done;
|
||||
});
|
||||
loop$.subscribe(fn);
|
||||
start(1000);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(done).toBeInstanceOf(Function);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('waits for done() from multiple subscribers to be called', () => {
|
||||
const { loop$, start } = createAutoRefreshLoop();
|
||||
|
||||
let done1!: AutoRefreshDoneFn;
|
||||
const fn1 = jest.fn((_done) => {
|
||||
done1 = _done;
|
||||
});
|
||||
loop$.subscribe(fn1);
|
||||
|
||||
let done2!: AutoRefreshDoneFn;
|
||||
const fn2 = jest.fn((_done) => {
|
||||
done2 = _done;
|
||||
});
|
||||
loop$.subscribe(fn2);
|
||||
|
||||
start(1000);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
expect(done1).toBeInstanceOf(Function);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
done1();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
done2();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('unsubscribe() resets the state', () => {
|
||||
const { loop$, start } = createAutoRefreshLoop();
|
||||
|
||||
let done1!: AutoRefreshDoneFn;
|
||||
const fn1 = jest.fn((_done) => {
|
||||
done1 = _done;
|
||||
});
|
||||
loop$.subscribe(fn1);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
const sub2 = loop$.subscribe(fn2);
|
||||
|
||||
start(1000);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
expect(done1).toBeInstanceOf(Function);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
done1();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub2.unsubscribe();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('calling done() twice is ignored', () => {
|
||||
const { loop$, start } = createAutoRefreshLoop();
|
||||
|
||||
let done1!: AutoRefreshDoneFn;
|
||||
const fn1 = jest.fn((_done) => {
|
||||
done1 = _done;
|
||||
});
|
||||
loop$.subscribe(fn1);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
loop$.subscribe(fn2);
|
||||
|
||||
start(1000);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
expect(done1).toBeInstanceOf(Function);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
done1();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
done1();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('calling older done() is ignored', () => {
|
||||
const { loop$, start } = createAutoRefreshLoop();
|
||||
|
||||
let done1!: AutoRefreshDoneFn;
|
||||
const fn1 = jest.fn((_done) => {
|
||||
// @ts-ignore
|
||||
if (done1) return;
|
||||
done1 = _done;
|
||||
});
|
||||
loop$.subscribe(fn1);
|
||||
|
||||
start(1000);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
expect(done1).toBeInstanceOf(Function);
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
|
||||
done1();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(1);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(2);
|
||||
|
||||
done1();
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(fn1).toHaveBeenCalledTimes(2);
|
||||
jest.advanceTimersByTime(501);
|
||||
expect(fn1).toHaveBeenCalledTimes(2);
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { defer, Subject } from 'rxjs';
|
||||
import { finalize, map } from 'rxjs/operators';
|
||||
import { once } from 'lodash';
|
||||
|
||||
export type AutoRefreshDoneFn = () => void;
|
||||
|
||||
/**
|
||||
* Creates a loop for timepicker's auto refresh
|
||||
* It has a "confirmation" mechanism:
|
||||
* When auto refresh loop emits, it won't continue automatically,
|
||||
* until each subscriber calls received `done` function.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const createAutoRefreshLoop = () => {
|
||||
let subscribersCount = 0;
|
||||
const tick = new Subject<AutoRefreshDoneFn>();
|
||||
|
||||
let _timeoutHandle: number;
|
||||
let _timeout: number = 0;
|
||||
|
||||
function start() {
|
||||
stop();
|
||||
if (_timeout === 0) return;
|
||||
const timeoutHandle = window.setTimeout(() => {
|
||||
let pendingDoneCount = subscribersCount;
|
||||
const done = () => {
|
||||
if (timeoutHandle !== _timeoutHandle) return;
|
||||
|
||||
pendingDoneCount--;
|
||||
if (pendingDoneCount === 0) {
|
||||
start();
|
||||
}
|
||||
};
|
||||
tick.next(done);
|
||||
}, _timeout);
|
||||
|
||||
_timeoutHandle = timeoutHandle;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
window.clearTimeout(_timeoutHandle);
|
||||
_timeoutHandle = -1;
|
||||
}
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
_timeout = 0;
|
||||
stop();
|
||||
},
|
||||
start: (timeout: number) => {
|
||||
_timeout = timeout;
|
||||
if (subscribersCount > 0) {
|
||||
start();
|
||||
}
|
||||
},
|
||||
loop$: defer(() => {
|
||||
subscribersCount++;
|
||||
start(); // restart the loop on a new subscriber
|
||||
return tick.pipe(map((doneCb) => once(doneCb))); // each subscriber allowed to call done only once
|
||||
}).pipe(
|
||||
finalize(() => {
|
||||
subscribersCount--;
|
||||
if (subscribersCount === 0) {
|
||||
stop();
|
||||
} else {
|
||||
start(); // restart the loop to potentially unblock the interval
|
||||
}
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
|
@ -10,7 +10,7 @@ jest.useFakeTimers();
|
|||
|
||||
import sinon from 'sinon';
|
||||
import moment from 'moment';
|
||||
import { Timefilter } from './timefilter';
|
||||
import { AutoRefreshDoneFn, Timefilter } from './timefilter';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { TimeRange, RefreshInterval } from '../../../common';
|
||||
import { createNowProviderMock } from '../../now_provider/mocks';
|
||||
|
@ -121,7 +121,7 @@ describe('setRefreshInterval', () => {
|
|||
beforeEach(() => {
|
||||
update = sinon.spy();
|
||||
fetch = sinon.spy();
|
||||
autoRefreshFetch = sinon.spy();
|
||||
autoRefreshFetch = sinon.spy((done) => done());
|
||||
timefilter.setRefreshInterval({
|
||||
pause: false,
|
||||
value: 0,
|
||||
|
@ -344,3 +344,44 @@ describe('calculateBounds', () => {
|
|||
expect(() => timefilter.calculateBounds(timeRange)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRefreshFetch$', () => {
|
||||
test('next auto refresh loop starts after "done" called', () => {
|
||||
const autoRefreshFetch = jest.fn();
|
||||
let doneCb: AutoRefreshDoneFn | undefined;
|
||||
timefilter.getAutoRefreshFetch$().subscribe((done) => {
|
||||
autoRefreshFetch();
|
||||
doneCb = done;
|
||||
});
|
||||
timefilter.setRefreshInterval({ pause: false, value: 1000 });
|
||||
|
||||
expect(autoRefreshFetch).toBeCalledTimes(0);
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(autoRefreshFetch).toBeCalledTimes(1);
|
||||
|
||||
if (doneCb) doneCb();
|
||||
|
||||
jest.advanceTimersByTime(1005);
|
||||
expect(autoRefreshFetch).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test('new getAutoRefreshFetch$ subscription restarts refresh loop', () => {
|
||||
const autoRefreshFetch = jest.fn();
|
||||
const fetch$ = timefilter.getAutoRefreshFetch$();
|
||||
const sub1 = fetch$.subscribe((done) => {
|
||||
autoRefreshFetch();
|
||||
// this done will be never called, but loop will be reset by another subscription
|
||||
});
|
||||
timefilter.setRefreshInterval({ pause: false, value: 1000 });
|
||||
|
||||
expect(autoRefreshFetch).toBeCalledTimes(0);
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(autoRefreshFetch).toBeCalledTimes(1);
|
||||
|
||||
fetch$.subscribe(autoRefreshFetch);
|
||||
expect(autoRefreshFetch).toBeCalledTimes(1);
|
||||
sub1.unsubscribe();
|
||||
jest.advanceTimersByTime(1005);
|
||||
expect(autoRefreshFetch).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,9 @@ import {
|
|||
TimeRange,
|
||||
} from '../../../common';
|
||||
import { TimeHistoryContract } from './time_history';
|
||||
import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop';
|
||||
|
||||
export { AutoRefreshDoneFn };
|
||||
|
||||
// TODO: remove!
|
||||
|
||||
|
@ -32,8 +35,6 @@ export class Timefilter {
|
|||
private timeUpdate$ = new Subject();
|
||||
// Fired when a user changes the the autorefresh settings
|
||||
private refreshIntervalUpdate$ = new Subject();
|
||||
// Used when an auto refresh is triggered
|
||||
private autoRefreshFetch$ = new Subject();
|
||||
private fetch$ = new Subject();
|
||||
|
||||
private _time: TimeRange;
|
||||
|
@ -45,11 +46,12 @@ export class Timefilter {
|
|||
private _isTimeRangeSelectorEnabled: boolean = false;
|
||||
private _isAutoRefreshSelectorEnabled: boolean = false;
|
||||
|
||||
private _autoRefreshIntervalId: number = 0;
|
||||
|
||||
private readonly timeDefaults: TimeRange;
|
||||
private readonly refreshIntervalDefaults: RefreshInterval;
|
||||
|
||||
// Used when an auto refresh is triggered
|
||||
private readonly autoRefreshLoop = createAutoRefreshLoop();
|
||||
|
||||
constructor(
|
||||
config: TimefilterConfig,
|
||||
timeHistory: TimeHistoryContract,
|
||||
|
@ -86,9 +88,13 @@ export class Timefilter {
|
|||
return this.refreshIntervalUpdate$.asObservable();
|
||||
};
|
||||
|
||||
public getAutoRefreshFetch$ = () => {
|
||||
return this.autoRefreshFetch$.asObservable();
|
||||
};
|
||||
/**
|
||||
* Get an observable that emits when it is time to refetch data due to refresh interval
|
||||
* Each subscription to this observable resets internal interval
|
||||
* Emitted value is a callback {@link AutoRefreshDoneFn} that must be called to restart refresh interval loop
|
||||
* Apps should use this callback to start next auto refresh loop when view finished updating
|
||||
*/
|
||||
public getAutoRefreshFetch$ = () => this.autoRefreshLoop.loop$;
|
||||
|
||||
public getFetch$ = () => {
|
||||
return this.fetch$.asObservable();
|
||||
|
@ -166,13 +172,9 @@ export class Timefilter {
|
|||
}
|
||||
}
|
||||
|
||||
// Clear the previous auto refresh interval and start a new one (if not paused)
|
||||
clearInterval(this._autoRefreshIntervalId);
|
||||
if (!newRefreshInterval.pause) {
|
||||
this._autoRefreshIntervalId = window.setInterval(
|
||||
() => this.autoRefreshFetch$.next(),
|
||||
newRefreshInterval.value
|
||||
);
|
||||
this.autoRefreshLoop.stop();
|
||||
if (!newRefreshInterval.pause && newRefreshInterval.value !== 0) {
|
||||
this.autoRefreshLoop.start(newRefreshInterval.value);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ const createSetupContractMock = () => {
|
|||
getEnabledUpdated$: jest.fn(),
|
||||
getTimeUpdate$: jest.fn(),
|
||||
getRefreshIntervalUpdate$: jest.fn(),
|
||||
getAutoRefreshFetch$: jest.fn(() => new Observable<unknown>()),
|
||||
getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()),
|
||||
getFetch$: jest.fn(),
|
||||
getTime: jest.fn(),
|
||||
setTime: jest.fn(),
|
||||
|
|
|
@ -45,6 +45,8 @@ export {
|
|||
ISessionsClient,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
SEARCH_SESSIONS_MANAGEMENT_ID,
|
||||
waitUntilNextSessionCompletes$,
|
||||
WaitUntilNextSessionCompletesOptions,
|
||||
} from './session';
|
||||
export { getEsPreference } from './es_search';
|
||||
|
||||
|
|
|
@ -11,3 +11,7 @@ export { SearchSessionState } from './search_session_state';
|
|||
export { SessionsClient, ISessionsClient } from './sessions_client';
|
||||
export { noSearchSessionStorageCapabilityMessage } from './i18n';
|
||||
export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
|
||||
export {
|
||||
waitUntilNextSessionCompletes$,
|
||||
WaitUntilNextSessionCompletesOptions,
|
||||
} from './session_helpers';
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { waitUntilNextSessionCompletes$ } from './session_helpers';
|
||||
import { ISessionService, SessionService } from './session_service';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { SearchSessionState } from './search_session_state';
|
||||
import { NowProviderInternalContract } from '../../now_provider';
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
import { createNowProviderMock } from '../../now_provider/mocks';
|
||||
import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
|
||||
import { getSessionsClientMock } from './mocks';
|
||||
|
||||
let sessionService: ISessionService;
|
||||
let state$: BehaviorSubject<SearchSessionState>;
|
||||
let nowProvider: jest.Mocked<NowProviderInternalContract>;
|
||||
let currentAppId$: BehaviorSubject<string>;
|
||||
|
||||
beforeEach(() => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext();
|
||||
const startService = coreMock.createSetup().getStartServices;
|
||||
nowProvider = createNowProviderMock();
|
||||
currentAppId$ = new BehaviorSubject('app');
|
||||
sessionService = new SessionService(
|
||||
initializerContext,
|
||||
() =>
|
||||
startService().then(([coreStart, ...rest]) => [
|
||||
{
|
||||
...coreStart,
|
||||
application: {
|
||||
...coreStart.application,
|
||||
currentAppId$,
|
||||
capabilities: {
|
||||
...coreStart.application.capabilities,
|
||||
management: {
|
||||
kibana: {
|
||||
[SEARCH_SESSIONS_MANAGEMENT_ID]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
]),
|
||||
getSessionsClientMock(),
|
||||
nowProvider,
|
||||
{ freezeState: false } // needed to use mocks inside state container
|
||||
);
|
||||
state$ = new BehaviorSubject<SearchSessionState>(SearchSessionState.None);
|
||||
sessionService.state$.subscribe(state$);
|
||||
});
|
||||
|
||||
describe('waitUntilNextSessionCompletes$', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
test('emits when next session starts', () => {
|
||||
sessionService.start();
|
||||
let untrackSearch = sessionService.trackSearch({ abort: () => {} });
|
||||
untrackSearch();
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
waitUntilNextSessionCompletes$(sessionService).subscribe({ next, complete });
|
||||
expect(next).not.toBeCalled();
|
||||
|
||||
sessionService.start();
|
||||
expect(next).not.toBeCalled();
|
||||
|
||||
untrackSearch = sessionService.trackSearch({ abort: () => {} });
|
||||
untrackSearch();
|
||||
|
||||
expect(next).not.toBeCalled();
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(next).not.toBeCalled();
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(next).toBeCalledTimes(1);
|
||||
expect(complete).toBeCalled();
|
||||
});
|
||||
});
|
48
src/plugins/data/public/search/session/session_helpers.ts
Normal file
48
src/plugins/data/public/search/session/session_helpers.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { debounceTime, first, skipUntil } from 'rxjs/operators';
|
||||
import { ISessionService } from './session_service';
|
||||
import { SearchSessionState } from './search_session_state';
|
||||
|
||||
/**
|
||||
* Options for {@link waitUntilNextSessionCompletes$}
|
||||
*/
|
||||
export interface WaitUntilNextSessionCompletesOptions {
|
||||
/**
|
||||
* For how long to wait between session state transitions before considering that session completed
|
||||
*/
|
||||
waitForIdle?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that emits when next search session completes.
|
||||
* This utility is helpful to use in the application to delay some tasks until next session completes.
|
||||
*
|
||||
* @param sessionService - {@link ISessionService}
|
||||
* @param opts - {@link WaitUntilNextSessionCompletesOptions}
|
||||
*/
|
||||
export function waitUntilNextSessionCompletes$(
|
||||
sessionService: ISessionService,
|
||||
{ waitForIdle = 1000 }: WaitUntilNextSessionCompletesOptions = { waitForIdle: 1000 }
|
||||
) {
|
||||
return sessionService.state$.pipe(
|
||||
// wait until new session starts
|
||||
skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.None))),
|
||||
// wait until new session starts loading
|
||||
skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.Loading))),
|
||||
// debounce to ignore quick switches from loading <-> completed.
|
||||
// that could happen between sequential search requests inside a single session
|
||||
debounceTime(waitForIdle),
|
||||
// then wait until it finishes
|
||||
first(
|
||||
(state) =>
|
||||
state === SearchSessionState.Completed || state === SearchSessionState.BackgroundCompleted
|
||||
)
|
||||
);
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { merge, Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { debounceTime, tap, filter } from 'rxjs/operators';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state';
|
||||
import { RequestAdapter } from '../../../../inspector/public';
|
||||
|
@ -393,12 +393,11 @@ function discoverController($route, $scope) {
|
|||
$scope.state.index = $scope.indexPattern.id;
|
||||
$scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern);
|
||||
|
||||
$scope.opts.fetch = $scope.fetch = function () {
|
||||
$scope.opts.fetch = $scope.fetch = async function () {
|
||||
$scope.fetchCounter++;
|
||||
$scope.fetchError = undefined;
|
||||
if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
|
||||
$scope.resultState = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort any in-progress requests before fetching again
|
||||
|
@ -494,11 +493,19 @@ function discoverController($route, $scope) {
|
|||
showUnmappedFields,
|
||||
};
|
||||
|
||||
// handler emitted by `timefilter.getAutoRefreshFetch$()`
|
||||
// to notify when data completed loading and to start a new autorefresh loop
|
||||
let autoRefreshDoneCb;
|
||||
const fetch$ = merge(
|
||||
refetch$,
|
||||
filterManager.getFetches$(),
|
||||
timefilter.getFetch$(),
|
||||
timefilter.getAutoRefreshFetch$(),
|
||||
timefilter.getAutoRefreshFetch$().pipe(
|
||||
tap((done) => {
|
||||
autoRefreshDoneCb = done;
|
||||
}),
|
||||
filter(() => $scope.fetchStatus !== fetchStatuses.LOADING)
|
||||
),
|
||||
data.query.queryString.getUpdates$(),
|
||||
searchSessionManager.newSearchSessionIdFromURL$
|
||||
).pipe(debounceTime(100));
|
||||
|
@ -508,7 +515,16 @@ function discoverController($route, $scope) {
|
|||
$scope,
|
||||
fetch$,
|
||||
{
|
||||
next: $scope.fetch,
|
||||
next: async () => {
|
||||
try {
|
||||
await $scope.fetch();
|
||||
} finally {
|
||||
// if there is a saved `autoRefreshDoneCb`, notify auto refresh service that
|
||||
// the last fetch is completed so it starts the next auto refresh loop if needed
|
||||
autoRefreshDoneCb?.();
|
||||
autoRefreshDoneCb = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
(error) => addFatalError(core.fatalErrors, error)
|
||||
)
|
||||
|
|
|
@ -118,12 +118,15 @@ export class ExpressionLoader {
|
|||
return this.execution ? (this.execution.inspect() as Adapters) : undefined;
|
||||
}
|
||||
|
||||
update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void {
|
||||
async update(
|
||||
expression?: string | ExpressionAstExpression,
|
||||
params?: IExpressionLoaderParams
|
||||
): Promise<void> {
|
||||
this.setParams(params);
|
||||
|
||||
this.loadingSubject.next(true);
|
||||
if (expression) {
|
||||
this.loadData(expression, this.params);
|
||||
await this.loadData(expression, this.params);
|
||||
} else if (this.data) {
|
||||
this.render(this.data);
|
||||
}
|
||||
|
|
|
@ -367,8 +367,8 @@ export class VisualizeEmbeddable
|
|||
}
|
||||
}
|
||||
|
||||
public reload = () => {
|
||||
this.handleVisUpdate();
|
||||
public reload = async () => {
|
||||
await this.handleVisUpdate();
|
||||
};
|
||||
|
||||
private async updateHandler() {
|
||||
|
@ -395,13 +395,13 @@ export class VisualizeEmbeddable
|
|||
});
|
||||
|
||||
if (this.handler && !abortController.signal.aborted) {
|
||||
this.handler.update(this.expression, expressionParams);
|
||||
await this.handler.update(this.expression, expressionParams);
|
||||
}
|
||||
}
|
||||
|
||||
private handleVisUpdate = async () => {
|
||||
this.handleChanges();
|
||||
this.updateHandler();
|
||||
await this.updateHandler();
|
||||
};
|
||||
|
||||
private uiStateChangeHandler = () => {
|
||||
|
|
|
@ -183,8 +183,12 @@ const TopNav = ({
|
|||
useEffect(() => {
|
||||
const autoRefreshFetchSub = services.data.query.timefilter.timefilter
|
||||
.getAutoRefreshFetch$()
|
||||
.subscribe(() => {
|
||||
visInstance.embeddableHandler.reload();
|
||||
.subscribe(async (done) => {
|
||||
try {
|
||||
await visInstance.embeddableHandler.reload();
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
autoRefreshFetchSub.unsubscribe();
|
||||
|
|
|
@ -155,11 +155,7 @@ function createMockTimefilter() {
|
|||
getBounds: jest.fn(() => timeFilter),
|
||||
getRefreshInterval: () => {},
|
||||
getRefreshIntervalDefaults: () => {},
|
||||
getAutoRefreshFetch$: () => ({
|
||||
subscribe: ({ next }: { next: () => void }) => {
|
||||
return next;
|
||||
},
|
||||
}),
|
||||
getAutoRefreshFetch$: () => new Observable(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Toast } from 'kibana/public';
|
|||
import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
|
||||
import { Datatable } from 'src/plugins/expressions/public';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import { finalize, switchMap, tap } from 'rxjs/operators';
|
||||
import { downloadMultipleAs } from '../../../../../src/plugins/share/public';
|
||||
import {
|
||||
createKbnUrlStateStorage,
|
||||
|
@ -37,6 +38,7 @@ import {
|
|||
Query,
|
||||
SavedQuery,
|
||||
syncQueryStateWithUrl,
|
||||
waitUntilNextSessionCompletes$,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common';
|
||||
import { LensAppProps, LensAppServices, LensAppState } from './types';
|
||||
|
@ -193,14 +195,19 @@ export function App({
|
|||
|
||||
const autoRefreshSubscription = data.query.timefilter.timefilter
|
||||
.getAutoRefreshFetch$()
|
||||
.subscribe({
|
||||
next: () => {
|
||||
.pipe(
|
||||
tap(() => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
searchSessionId: data.search.session.start(),
|
||||
}));
|
||||
},
|
||||
});
|
||||
}),
|
||||
switchMap((done) =>
|
||||
// best way in lens to estimate that all panels are updated is to rely on search session service state
|
||||
waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
|
|
Loading…
Reference in a new issue