Merge remote-tracking branch 'upstream/master' into EUIfication/data_table

This commit is contained in:
sulemanof 2020-07-29 16:52:32 +03:00
commit a057da9dfd
350 changed files with 6478 additions and 3418 deletions

View file

@ -19,5 +19,6 @@ export interface RouteConfigOptions<Method extends RouteMethod>
| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | <code>boolean &#124; 'optional'</code> | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.<!-- -->Defaults to <code>true</code> if an auth mechanism is registered. |
| [body](./kibana-plugin-core-server.routeconfigoptions.body.md) | <code>Method extends 'get' &#124; 'options' ? undefined : RouteConfigOptionsBody</code> | Additional body options [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md)<!-- -->. |
| [tags](./kibana-plugin-core-server.routeconfigoptions.tags.md) | <code>readonly string[]</code> | Additional metadata tag strings to attach to the route. |
| [timeout](./kibana-plugin-core-server.routeconfigoptions.timeout.md) | <code>number</code> | Timeouts for processing durations. Response timeout is in milliseconds. Default value: 2 minutes |
| [xsrfRequired](./kibana-plugin-core-server.routeconfigoptions.xsrfrequired.md) | <code>Method extends 'get' ? never : boolean</code> | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain <code>kbn-xsrf</code> header. - false. Disables xsrf protection.<!-- -->Set to true by default |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) &gt; [timeout](./kibana-plugin-core-server.routeconfigoptions.timeout.md)
## RouteConfigOptions.timeout property
Timeouts for processing durations. Response timeout is in milliseconds. Default value: 2 minutes
<b>Signature:</b>
```typescript
timeout?: number;
```

View file

@ -9,9 +9,10 @@ Helper to setup two-way syncing of global data and a state container
<b>Signature:</b>
```typescript
connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, queryString, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
time?: boolean;
refreshInterval?: boolean;
filters?: FilterStateStore | boolean;
query?: boolean;
}) => () => void
```

View file

@ -17,6 +17,7 @@ export interface QueryState
| Property | Type | Description |
| --- | --- | --- |
| [filters](./kibana-plugin-plugins-data-public.querystate.filters.md) | <code>Filter[]</code> | |
| [query](./kibana-plugin-plugins-data-public.querystate.query.md) | <code>Query</code> | |
| [refreshInterval](./kibana-plugin-plugins-data-public.querystate.refreshinterval.md) | <code>RefreshInterval</code> | |
| [time](./kibana-plugin-plugins-data-public.querystate.time.md) | <code>TimeRange</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [QueryState](./kibana-plugin-plugins-data-public.querystate.md) &gt; [query](./kibana-plugin-plugins-data-public.querystate.query.md)
## QueryState.query property
<b>Signature:</b>
```typescript
query?: Query;
```

View file

@ -9,7 +9,7 @@ Helper to setup syncing of global data with the URL
<b>Signature:</b>
```typescript
syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'queryString' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
stop: () => void;
hasInheritedQueryFromUrl: boolean;
}

View file

@ -167,7 +167,7 @@ These can be used to automatically update the list of hosts as a cluster is resi
Kibana has a default maximum memory limit of 1.4 GB, and in most cases, we recommend leaving this unconfigured. In some scenarios, such as large reporting jobs,
it may make sense to tweak limits to meet more specific requirements.
You can modify this limit by setting `--max-old-space-size` in the `node.options` config file that can be found inside `kibana/config` folder or any other configured with the environment variable `KIBANA_PATH_CONF` (for example in debian based system would be `/etc/kibana`).
You can modify this limit by setting `--max-old-space-size` in the `node.options` config file that can be found inside `kibana/config` folder or any other configured with the environment variable `KBN_PATH_CONF` (for example in debian based system would be `/etc/kibana`).
The option accepts a limit in MB:
--------

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { History } from 'history';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { Router } from 'react-router-dom';
@ -85,16 +85,9 @@ const App = ({ navigation, data, history, kbnUrlStateStorage }: StateDemoAppDeps
useGlobalStateSyncing(data.query, kbnUrlStateStorage);
useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage);
const onQuerySubmit = useCallback(
({ query }) => {
appStateContainer.set({ ...appState, query });
},
[appStateContainer, appState]
);
const indexPattern = useIndexPattern(data);
if (!indexPattern)
return <div>No index pattern found. Please create an intex patter before loading...</div>;
return <div>No index pattern found. Please create an index patter before loading...</div>;
// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
@ -107,8 +100,6 @@ const App = ({ navigation, data, history, kbnUrlStateStorage }: StateDemoAppDeps
showSearchBar={true}
indexPatterns={[indexPattern]}
useDefaultBehaviors={true}
onQuerySubmit={onQuerySubmit}
query={appState.query}
showSaveQuery={true}
/>
<EuiPage restrictWidth="1000px">
@ -200,7 +191,7 @@ function useAppStateSyncing<AppState extends QueryState>(
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
query,
appStateContainer,
{ filters: esFilters.FilterStateStore.APP_STATE }
{ filters: esFilters.FilterStateStore.APP_STATE, query: true }
);
// sets up syncing app state container with url

View file

@ -459,7 +459,7 @@
"jest-cli": "^25.5.4",
"jest-environment-jsdom-thirteen": "^1.0.1",
"jest-raw-loader": "^1.0.1",
"jimp": "^0.9.6",
"jimp": "^0.14.0",
"json5": "^1.0.1",
"license-checker": "^16.0.0",
"listr": "^0.14.1",

View file

@ -112,7 +112,7 @@ export class DocLinksService {
kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`,
siem: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`,
gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/install-siem.html`,
gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`,
},
query: {
luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`,

View file

@ -51,7 +51,7 @@ describe('core deprecations', () => {
const { messages } = applyCoreDeprecations();
expect(messages).toMatchInlineSnapshot(`
Array [
"Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder",
"Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder",
]
`);
});

View file

@ -23,7 +23,7 @@ import { ConfigDeprecationProvider, ConfigDeprecation } from './types';
const configPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(process.env, 'CONFIG_PATH')) {
log(
`Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder`
`Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder`
);
}
return settings;

View file

@ -992,6 +992,133 @@ describe('body options', () => {
});
});
describe('timeout options', () => {
test('should accept a socket "timeout" which is 3 minutes in milliseconds, "300000" for a POST', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.post(
{
path: '/',
validate: false,
options: { timeout: 300000 },
},
(context, req, res) => {
try {
return res.ok({ body: { timeout: req.route.options.timeout } });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener).post('/').send({ test: 1 }).expect(200, {
timeout: 300000,
});
});
test('should accept a socket "timeout" which is 3 minutes in milliseconds, "300000" for a GET', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.get(
{
path: '/',
validate: false,
options: { timeout: 300000 },
},
(context, req, res) => {
try {
return res.ok({ body: { timeout: req.route.options.timeout } });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener).get('/').expect(200, {
timeout: 300000,
});
});
test('should accept a socket "timeout" which is 3 minutes in milliseconds, "300000" for a DELETE', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.delete(
{
path: '/',
validate: false,
options: { timeout: 300000 },
},
(context, req, res) => {
try {
return res.ok({ body: { timeout: req.route.options.timeout } });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener).delete('/').expect(200, {
timeout: 300000,
});
});
test('should accept a socket "timeout" which is 3 minutes in milliseconds, "300000" for a PUT', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.put(
{
path: '/',
validate: false,
options: { timeout: 300000 },
},
(context, req, res) => {
try {
return res.ok({ body: { timeout: req.route.options.timeout } });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener).put('/').expect(200, {
timeout: 300000,
});
});
test('should accept a socket "timeout" which is 3 minutes in milliseconds, "300000" for a PATCH', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('', logger, enhanceWithContext);
router.patch(
{
path: '/',
validate: false,
options: { timeout: 300000 },
},
(context, req, res) => {
try {
return res.ok({ body: { timeout: req.route.options.timeout } });
} catch (err) {
return res.internalError({ body: err.message });
}
}
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener).patch('/').expect(200, {
timeout: 300000,
});
});
});
test('should return a stream in the body', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);

View file

@ -161,8 +161,10 @@ export class HttpServer {
this.log.debug(`registering route handler for [${route.path}]`);
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
const validate = isSafeMethod(route.method) ? undefined : { payload: true };
const { authRequired, tags, body = {} } = route.options;
const { authRequired, tags, body = {}, timeout } = route.options;
const { accepts: allow, maxBytes, output, parse } = body;
// Hapi does not allow timeouts on payloads to be specified for 'head' or 'get' requests
const payloadTimeout = isSafeMethod(route.method) || timeout == null ? undefined : timeout;
const kibanaRouteState: KibanaRouteState = {
xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method),
@ -181,9 +183,23 @@ export class HttpServer {
// validation applied in ./http_tools#getServerOptions
// (All NP routes are already required to specify their own validation in order to access the payload)
validate,
payload: [allow, maxBytes, output, parse].some((v) => typeof v !== 'undefined')
? { allow, maxBytes, output, parse }
payload: [allow, maxBytes, output, parse, payloadTimeout].some(
(v) => typeof v !== 'undefined'
)
? {
allow,
maxBytes,
output,
parse,
timeout: payloadTimeout,
}
: undefined,
timeout:
timeout != null
? {
socket: timeout + 1, // Hapi server requires the socket to be greater than payload settings so we add 1 millisecond
}
: undefined,
},
});
}

View file

@ -302,6 +302,130 @@ describe('Options', () => {
});
});
});
describe('timeout', () => {
it('should timeout if configured with a small timeout value for a POST', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.post(
{ path: '/a', validate: false, options: { timeout: 1000 } },
async (context, req, res) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return res.ok({});
}
);
router.post({ path: '/b', validate: false }, (context, req, res) => res.ok({}));
await server.start();
expect(supertest(innerServer.listener).post('/a')).rejects.toThrow('socket hang up');
await supertest(innerServer.listener).post('/b').expect(200, {});
});
it('should timeout if configured with a small timeout value for a PUT', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.put(
{ path: '/a', validate: false, options: { timeout: 1000 } },
async (context, req, res) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return res.ok({});
}
);
router.put({ path: '/b', validate: false }, (context, req, res) => res.ok({}));
await server.start();
expect(supertest(innerServer.listener).put('/a')).rejects.toThrow('socket hang up');
await supertest(innerServer.listener).put('/b').expect(200, {});
});
it('should timeout if configured with a small timeout value for a DELETE', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.delete(
{ path: '/a', validate: false, options: { timeout: 1000 } },
async (context, req, res) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
return res.ok({});
}
);
router.delete({ path: '/b', validate: false }, (context, req, res) => res.ok({}));
await server.start();
expect(supertest(innerServer.listener).delete('/a')).rejects.toThrow('socket hang up');
await supertest(innerServer.listener).delete('/b').expect(200, {});
});
it('should timeout if configured with a small timeout value for a GET', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
// Note: There is a bug within Hapi Server where it cannot set the payload timeout for a GET call but it also cannot configure a timeout less than the payload body
// so the least amount of possible time to configure the timeout is 10 seconds.
{ path: '/a', validate: false, options: { timeout: 100000 } },
async (context, req, res) => {
// Cause a wait of 20 seconds to cause the socket hangup
await new Promise((resolve) => setTimeout(resolve, 200000));
return res.ok({});
}
);
router.get({ path: '/b', validate: false }, (context, req, res) => res.ok({}));
await server.start();
expect(supertest(innerServer.listener).get('/a')).rejects.toThrow('socket hang up');
await supertest(innerServer.listener).get('/b').expect(200, {});
});
it('should not timeout if configured with a 5 minute timeout value for a POST', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.post(
{ path: '/a', validate: false, options: { timeout: 300000 } },
async (context, req, res) => res.ok({})
);
await server.start();
await supertest(innerServer.listener).post('/a').expect(200, {});
});
it('should not timeout if configured with a 5 minute timeout value for a PUT', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.put(
{ path: '/a', validate: false, options: { timeout: 300000 } },
async (context, req, res) => res.ok({})
);
await server.start();
await supertest(innerServer.listener).put('/a').expect(200, {});
});
it('should not timeout if configured with a 5 minute timeout value for a DELETE', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.delete(
{ path: '/a', validate: false, options: { timeout: 300000 } },
async (context, req, res) => res.ok({})
);
await server.start();
await supertest(innerServer.listener).delete('/a').expect(200, {});
});
it('should not timeout if configured with a 5 minute timeout value for a GET', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/a', validate: false, options: { timeout: 300000 } },
async (context, req, res) => res.ok({})
);
await server.start();
await supertest(innerServer.listener).get('/a').expect(200, {});
});
});
});
describe('Cache-Control', () => {

View file

@ -197,12 +197,14 @@ export class KibanaRequest<
private getRouteInfo(request: Request): KibanaRequestRoute<Method> {
const method = request.method as Method;
const { parse, maxBytes, allow, output } = request.route.settings.payload || {};
const timeout = request.route.settings.timeout?.socket;
const options = ({
authRequired: this.getAuthRequired(request),
// some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true,
tags: request.route.settings.tags || [],
timeout: typeof timeout === 'number' ? timeout - 1 : undefined, // We are forced to have the timeout be 1 millisecond greater than the server and payload so we subtract one here to give the user consist settings
body: isSafeMethod(method)
? undefined
: {

View file

@ -144,6 +144,12 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
* Additional body options {@link RouteConfigOptionsBody}.
*/
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
/**
* Timeouts for processing durations. Response timeout is in milliseconds.
* Default value: 2 minutes
*/
timeout?: number;
}
/**

View file

@ -25,14 +25,18 @@ import { fromRoot } from '../utils';
const isString = (v: any): v is string => typeof v === 'string';
const CONFIG_PATHS = [
process.env.KBN_PATH_CONF && join(process.env.KBN_PATH_CONF, 'kibana.yml'),
process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'),
process.env.CONFIG_PATH, // deprecated
fromRoot('config/kibana.yml'),
].filter(isString);
const CONFIG_DIRECTORIES = [process.env.KIBANA_PATH_CONF, fromRoot('config'), '/etc/kibana'].filter(
isString
);
const CONFIG_DIRECTORIES = [
process.env.KBN_PATH_CONF,
process.env.KIBANA_PATH_CONF,
fromRoot('config'),
'/etc/kibana',
].filter(isString);
const DATA_PATHS = [
process.env.DATA_PATH, // deprecated

View file

@ -1859,6 +1859,7 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
authRequired?: boolean | 'optional';
body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody;
tags?: readonly string[];
timeout?: number;
xsrfRequired?: Method extends 'get' ? never : boolean;
}

View file

@ -14,7 +14,7 @@ while [ -h "$SCRIPT" ] ; do
done
DIR="$(dirname "${SCRIPT}")/.."
CONFIG_DIR=${KIBANA_PATH_CONF:-"$DIR/config"}
CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"}
NODE="${DIR}/node/bin/node"
test -x "$NODE"
if [ ! -x "$NODE" ]; then

View file

@ -12,8 +12,8 @@ If Not Exist "%NODE%" (
Exit /B 1
)
set CONFIG_DIR=%KIBANA_PATH_CONF%
If [%KIBANA_PATH_CONF%] == [] (
set CONFIG_DIR=%KBN_PATH_CONF%
If [%KBN_PATH_CONF%] == [] (
set CONFIG_DIR=%DIR%\config
)

View file

@ -14,7 +14,7 @@ while [ -h "$SCRIPT" ] ; do
done
DIR="$(dirname "${SCRIPT}")/.."
CONFIG_DIR=${KIBANA_PATH_CONF:-"$DIR/config"}
CONFIG_DIR=${KBN_PATH_CONF:-"$DIR/config"}
NODE="${DIR}/node/bin/node"
test -x "$NODE"
if [ ! -x "$NODE" ]; then

View file

@ -13,8 +13,8 @@ If Not Exist "%NODE%" (
Exit /B 1
)
set CONFIG_DIR=%KIBANA_PATH_CONF%
If [%KIBANA_PATH_CONF%] == [] (
set CONFIG_DIR=%KBN_PATH_CONF%
If [%KBN_PATH_CONF%] == [] (
set CONFIG_DIR=%DIR%\config
)

View file

@ -14,8 +14,8 @@ If Not Exist "%NODE%" (
Exit /B 1
)
set CONFIG_DIR=%KIBANA_PATH_CONF%
If [%KIBANA_PATH_CONF%] == [] (
set CONFIG_DIR=%KBN_PATH_CONF%
If [%KBN_PATH_CONF%] == [] (
set CONFIG_DIR=%DIR%\config
)

View file

@ -31,6 +31,10 @@ case $1 in
--ingroup "<%= group %>" --shell /bin/false "<%= user %>"
fi
if [ -n "$2" ]; then
IS_UPGRADE=true
fi
set_access
;;
abort-deconfigure|abort-upgrade|abort-remove)
@ -47,6 +51,10 @@ case $1 in
-c "kibana service user" "<%= user %>"
fi
if [ "$1" = "2" ]; then
IS_UPGRADE=true
fi
set_access
;;
@ -55,3 +63,9 @@ case $1 in
exit 1
;;
esac
if [ "$IS_UPGRADE" = "true" ]; then
if command -v systemctl >/dev/null; then
systemctl daemon-reload
fi
fi

View file

@ -12,4 +12,4 @@ KILL_ON_STOP_TIMEOUT=0
BABEL_CACHE_PATH="/var/lib/kibana/optimize/.babel_register_cache.json"
KIBANA_PATH_CONF="/etc/kibana"
KBN_PATH_CONF="/etc/kibana"

View file

@ -108,6 +108,7 @@ run(
const reporter = new ErrorReporter();
const messages: Map<string, { message: string }> = new Map();
await list.run({ messages, reporter });
process.exitCode = 0;
} catch (error) {
process.exitCode = 1;
if (error instanceof ErrorReporter) {
@ -117,6 +118,7 @@ run(
log.error(error);
}
}
process.exit();
},
{
flags: {

View file

@ -52,7 +52,10 @@ export interface DashboardAppScope extends ng.IScope {
expandedPanel?: string;
getShouldShowEditHelp: () => boolean;
getShouldShowViewHelp: () => boolean;
updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void;
handleRefresh: (
{ query, dateRange }: { query?: Query; dateRange: TimeRange },
isUpdate?: boolean
) => void;
topNavMenu: any;
showAddPanel: any;
showSaveQuery: boolean;

View file

@ -25,12 +25,11 @@ import React, { useState, ReactElement } from 'react';
import ReactDOM from 'react-dom';
import angular from 'angular';
import { Observable, pipe, Subscription } from 'rxjs';
import { filter, map, mapTo, startWith, switchMap } from 'rxjs/operators';
import { Observable, pipe, Subscription, merge } from 'rxjs';
import { filter, map, debounceTime, mapTo, startWith, switchMap } from 'rxjs/operators';
import { History } from 'history';
import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public';
import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
import { TimeRange } from 'src/plugins/data/public';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen';
import {
@ -38,11 +37,9 @@ import {
esFilters,
IndexPattern,
IndexPatternsContract,
Query,
QueryState,
SavedQuery,
syncQueryStateWithUrl,
UI_SETTINGS,
} from '../../../data/public';
import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../../saved_objects/public';
@ -81,8 +78,8 @@ import {
addFatalError,
AngularHttpError,
KibanaLegacyStart,
migrateLegacyQuery,
subscribeWithScope,
migrateLegacyQuery,
} from '../../../kibana_legacy/public';
export interface DashboardAppControllerDependencies extends RenderDeps {
@ -127,7 +124,6 @@ export class DashboardAppController {
$route,
$routeParams,
dashboardConfig,
localStorage,
indexPatterns,
savedQueryService,
embeddable,
@ -153,8 +149,8 @@ export class DashboardAppController {
navigation,
}: DashboardAppControllerDependencies) {
const filterManager = queryService.filterManager;
const queryFilter = filterManager;
const timefilter = queryService.timefilter.timefilter;
const queryStringManager = queryService.queryString;
const isEmbeddedExternally = Boolean($routeParams.embed);
// url param rules should only apply when embedded (e.g. url?embed=true)
@ -172,6 +168,10 @@ export class DashboardAppController {
chrome.docTitle.change(dash.title);
}
const incomingEmbeddable = embeddable
.getStateTransfer(scopedHistory())
.getIncomingEmbeddablePackage();
const dashboardStateManager = new DashboardStateManager({
savedDashboard: dash,
hideWriteControls: dashboardConfig.getHideWriteControls(),
@ -184,20 +184,30 @@ export class DashboardAppController {
// sync initial app filters from state to filterManager
// if there is an existing similar global filter, then leave it as global
filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters));
queryStringManager.setQuery(migrateLegacyQuery(dashboardStateManager.appState.query));
// setup syncing of app filters between appState and filterManager
const stopSyncingAppFilters = connectToQueryState(
queryService,
{
set: ({ filters }) => dashboardStateManager.setFilters(filters || []),
get: () => ({ filters: dashboardStateManager.appState.filters }),
set: ({ filters, query }) => {
dashboardStateManager.setFilters(filters || []);
dashboardStateManager.setQuery(query || queryStringManager.getDefaultQuery());
},
get: () => ({
filters: dashboardStateManager.appState.filters,
query: dashboardStateManager.getQuery(),
}),
state$: dashboardStateManager.appState$.pipe(
map((state) => ({
filters: state.filters,
query: queryStringManager.formatQuery(state.query),
}))
),
},
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);
@ -327,7 +337,7 @@ export class DashboardAppController {
const isEmptyInReadonlyMode = shouldShowUnauthorizedEmptyState();
return {
id: dashboardStateManager.savedDashboard.id || '',
filters: queryFilter.getFilters(),
filters: filterManager.getFilters(),
hidePanelTitles: dashboardStateManager.getHidePanelTitles(),
query: $scope.model.query,
timeRange: {
@ -352,7 +362,7 @@ export class DashboardAppController {
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
$scope.model = {
query: dashboardStateManager.getQuery(),
filters: queryFilter.getFilters(),
filters: filterManager.getFilters(),
timeRestore: dashboardStateManager.getTimeRestore(),
title: dashboardStateManager.getTitle(),
description: dashboardStateManager.getDescription(),
@ -416,12 +426,12 @@ export class DashboardAppController {
if (
!esFilters.compareFilters(
container.getInput().filters,
queryFilter.getFilters(),
filterManager.getFilters(),
esFilters.COMPARE_ALL_OPTIONS
)
) {
// Add filters modifies the object passed to it, hence the clone deep.
queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
filterManager.addFilters(_.cloneDeep(container.getInput().filters));
dashboardStateManager.applyFilters(
$scope.model.query,
@ -444,21 +454,24 @@ export class DashboardAppController {
refreshDashboardContainer();
});
const incomingState = embeddable
.getStateTransfer(scopedHistory())
.getIncomingEmbeddablePackage();
if (incomingState) {
if ('id' in incomingState) {
container.addOrUpdateEmbeddable<SavedObjectEmbeddableInput>(incomingState.type, {
savedObjectId: incomingState.id,
});
} else if ('input' in incomingState) {
const input = incomingState.input;
if (incomingEmbeddable) {
if ('id' in incomingEmbeddable) {
container.addOrUpdateEmbeddable<SavedObjectEmbeddableInput>(
incomingEmbeddable.type,
{
savedObjectId: incomingEmbeddable.id,
}
);
} else if ('input' in incomingEmbeddable) {
const input = incomingEmbeddable.input;
delete input.id;
const explicitInput = {
savedVis: input,
};
container.addOrUpdateEmbeddable<EmbeddableInput>(incomingState.type, explicitInput);
container.addOrUpdateEmbeddable<EmbeddableInput>(
incomingEmbeddable.type,
explicitInput
);
}
}
}
@ -480,13 +493,8 @@ export class DashboardAppController {
});
dashboardStateManager.applyFilters(
dashboardStateManager.getQuery() || {
query: '',
language:
localStorage.get('kibana.userQueryLanguage') ||
uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
},
queryFilter.getFilters()
dashboardStateManager.getQuery() || queryStringManager.getDefaultQuery(),
filterManager.getFilters()
);
timefilter.disableTimeRangeSelector();
@ -560,21 +568,13 @@ export class DashboardAppController {
}
};
$scope.updateQueryAndFetch = function ({ query, dateRange }) {
if (dateRange) {
timefilter.setTime(dateRange);
}
const oldQuery = $scope.model.query;
if (_.isEqual(oldQuery, query)) {
$scope.handleRefresh = function (_payload, isUpdate) {
if (isUpdate === false) {
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
lastReloadRequestTime = new Date().getTime();
refreshDashboardContainer();
} else {
$scope.model.query = query;
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
}
};
@ -593,7 +593,7 @@ export class DashboardAppController {
// Making this method sync broke the updates.
// Temporary fix, until we fix the complex state in this file.
setTimeout(() => {
queryFilter.setFilters(allFilters);
filterManager.setFilters(allFilters);
}, 0);
};
@ -626,11 +626,6 @@ export class DashboardAppController {
$scope.indexPatterns = [];
$scope.$watch('model.query', (newQuery: Query) => {
const query = migrateLegacyQuery(newQuery) as Query;
$scope.updateQueryAndFetch({ query });
});
$scope.$watch(
() => dashboardCapabilities.saveQuery,
(newCapability) => {
@ -671,18 +666,11 @@ export class DashboardAppController {
showFilterBar,
indexPatterns: $scope.indexPatterns,
showSaveQuery: $scope.showSaveQuery,
query: $scope.model.query,
savedQuery: $scope.savedQuery,
onSavedQueryIdChange,
savedQueryId: dashboardStateManager.getSavedQueryId(),
useDefaultBehaviors: true,
onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }): void => {
if (!payload.query) {
$scope.updateQueryAndFetch({ query: $scope.model.query, dateRange: payload.dateRange });
} else {
$scope.updateQueryAndFetch({ query: payload.query, dateRange: payload.dateRange });
}
},
onQuerySubmit: $scope.handleRefresh,
};
};
const dashboardNavBar = document.getElementById('dashboardChrome');
@ -697,25 +685,11 @@ export class DashboardAppController {
};
$scope.timefilterSubscriptions$ = new Subscription();
const timeChanges$ = merge(timefilter.getRefreshIntervalUpdate$(), timefilter.getTimeUpdate$());
$scope.timefilterSubscriptions$.add(
subscribeWithScope(
$scope,
timefilter.getRefreshIntervalUpdate$(),
{
next: () => {
updateState();
refreshDashboardContainer();
},
},
(error: AngularHttpError | Error | string) => addFatalError(fatalErrors, error)
)
);
$scope.timefilterSubscriptions$.add(
subscribeWithScope(
$scope,
timefilter.getTimeUpdate$(),
timeChanges$,
{
next: () => {
updateState();
@ -1088,13 +1062,21 @@ export class DashboardAppController {
updateViewMode(dashboardStateManager.getViewMode());
const filterChanges = merge(filterManager.getUpdates$(), queryStringManager.getUpdates$()).pipe(
debounceTime(100)
);
// update root source when filters update
const updateSubscription = queryFilter.getUpdates$().subscribe({
const updateSubscription = filterChanges.subscribe({
next: () => {
$scope.model.filters = queryFilter.getFilters();
$scope.model.filters = filterManager.getFilters();
$scope.model.query = queryStringManager.getQuery();
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
if (dashboardContainer) {
dashboardContainer.updateInput({ filters: $scope.model.filters });
dashboardContainer.updateInput({
filters: $scope.model.filters,
query: $scope.model.query,
});
}
},
});

View file

@ -199,10 +199,11 @@ export const castEsToKbnFieldTypeName: (esType: ES_FIELD_TYPES | string) => KBN_
// Warning: (ae-missing-release-tag) "connectToQueryState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export const connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
export const connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, queryString, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
time?: boolean;
refreshInterval?: boolean;
filters?: FilterStateStore | boolean;
query?: boolean;
}) => () => void;
// Warning: (ae-missing-release-tag) "createSavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -1388,6 +1389,8 @@ export interface QueryState {
// (undocumented)
filters?: Filter[];
// (undocumented)
query?: Query;
// (undocumented)
refreshInterval?: RefreshInterval;
// (undocumented)
time?: TimeRange;
@ -1762,7 +1765,7 @@ export type StatefulSearchBarProps = SearchBarOwnProps & {
// Warning: (ae-missing-release-tag) "syncQueryStateWithUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export const syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
export const syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'queryString' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
stop: () => void;
hasInheritedQueryFromUrl: boolean;
};
@ -1911,7 +1914,7 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398: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:41:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts

View file

@ -21,6 +21,7 @@ import { Observable } from 'rxjs';
import { QueryService, QuerySetup, QueryStart } from '.';
import { timefilterServiceMock } from './timefilter/timefilter_service.mock';
import { createFilterManagerMock } from './filter_manager/filter_manager.mock';
import { queryStringManagerMock } from './query_string/query_string_manager.mock';
type QueryServiceClientContract = PublicMethodsOf<QueryService>;
@ -28,6 +29,7 @@ const createSetupContractMock = () => {
const setupContract: jest.Mocked<QuerySetup> = {
filterManager: createFilterManagerMock(),
timefilter: timefilterServiceMock.createSetupContract(),
queryString: queryStringManagerMock.createSetupContract(),
state$: new Observable(),
};
@ -38,6 +40,7 @@ const createStartContractMock = () => {
const startContract: jest.Mocked<QueryStart> = {
addToQueryLog: jest.fn(),
filterManager: createFilterManagerMock(),
queryString: queryStringManagerMock.createStartContract(),
savedQueries: jest.fn() as any,
state$: new Observable(),
timefilter: timefilterServiceMock.createStartContract(),

View file

@ -25,6 +25,7 @@ import { createAddToQueryLog } from './lib';
import { TimefilterService, TimefilterSetup } from './timefilter';
import { createSavedQueryService } from './saved_query/saved_query_service';
import { createQueryStateObservable } from './state_sync/create_global_query_observable';
import { QueryStringManager, QueryStringContract } from './query_string';
/**
* Query Service
@ -45,6 +46,7 @@ interface QueryServiceStartDependencies {
export class QueryService {
filterManager!: FilterManager;
timefilter!: TimefilterSetup;
queryStringManager!: QueryStringContract;
state$!: ReturnType<typeof createQueryStateObservable>;
@ -57,14 +59,18 @@ export class QueryService {
storage,
});
this.queryStringManager = new QueryStringManager(storage, uiSettings);
this.state$ = createQueryStateObservable({
filterManager: this.filterManager,
timefilter: this.timefilter,
queryString: this.queryStringManager,
}).pipe(share());
return {
filterManager: this.filterManager,
timefilter: this.timefilter,
queryString: this.queryStringManager,
state$: this.state$,
};
}
@ -76,6 +82,7 @@ export class QueryService {
uiSettings,
}),
filterManager: this.filterManager,
queryString: this.queryStringManager,
savedQueries: createSavedQueryService(savedObjectsClient),
state$: this.state$,
timefilter: this.timefilter,

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { QueryStringContract, QueryStringManager } from './query_string_manager';

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryStringContract } from '.';
const createSetupContractMock = () => {
const queryStringManagerMock: jest.Mocked<QueryStringContract> = {
getQuery: jest.fn(),
setQuery: jest.fn(),
getUpdates$: jest.fn(),
getDefaultQuery: jest.fn(),
formatQuery: jest.fn(),
clearQuery: jest.fn(),
};
return queryStringManagerMock;
};
export const queryStringManagerMock = {
createSetupContract: createSetupContractMock,
createStartContract: createSetupContractMock,
};

View file

@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { BehaviorSubject } from 'rxjs';
import { CoreStart } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { Query, UI_SETTINGS } from '../../../common';
export class QueryStringManager {
private query$: BehaviorSubject<Query>;
constructor(
private readonly storage: IStorageWrapper,
private readonly uiSettings: CoreStart['uiSettings']
) {
this.query$ = new BehaviorSubject<Query>(this.getDefaultQuery());
}
private getDefaultLanguage() {
return (
this.storage.get('kibana.userQueryLanguage') ||
this.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE)
);
}
public getDefaultQuery() {
return {
query: '',
language: this.getDefaultLanguage(),
};
}
public formatQuery(query: Query | string | undefined): Query {
if (!query) {
return this.getDefaultQuery();
} else if (typeof query === 'string') {
return {
query,
language: this.getDefaultLanguage(),
};
} else {
return query;
}
}
public getUpdates$ = () => {
return this.query$.asObservable();
};
public getQuery = (): Query => {
return this.query$.getValue();
};
/**
* Updates the query.
* @param {Query} query
*/
public setQuery = (query: Query) => {
const curQuery = this.query$.getValue();
if (query?.language !== curQuery.language || query?.query !== curQuery.query) {
this.query$.next(query);
}
};
/**
* Resets the query to the default one.
*/
public clearQuery = () => {
this.setQuery(this.getDefaultQuery());
};
}
export type QueryStringContract = PublicMethodsOf<QueryStringManager>;

View file

@ -48,6 +48,8 @@ setupMock.uiSettings.get.mockImplementation((key: string) => {
switch (key) {
case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT:
return true;
case UI_SETTINGS.SEARCH_QUERY_LANGUAGE:
return 'kuery';
case 'timepicker:timeDefaults':
return { from: 'now-15m', to: 'now' };
case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS:

View file

@ -35,15 +35,24 @@ export const connectToQueryState = <S extends QueryState>(
{
timefilter: { timefilter },
filterManager,
queryString,
state$,
}: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'state$'>,
}: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>,
stateContainer: BaseStateContainer<S>,
syncConfig: { time?: boolean; refreshInterval?: boolean; filters?: FilterStateStore | boolean }
syncConfig: {
time?: boolean;
refreshInterval?: boolean;
filters?: FilterStateStore | boolean;
query?: boolean;
}
) => {
const syncKeys: Array<keyof QueryStateChange> = [];
if (syncConfig.time) {
syncKeys.push('time');
}
if (syncConfig.query) {
syncKeys.push('query');
}
if (syncConfig.refreshInterval) {
syncKeys.push('refreshInterval');
}
@ -133,6 +142,9 @@ export const connectToQueryState = <S extends QueryState>(
if (syncConfig.time && changes.time) {
newState.time = timefilter.getTime();
}
if (syncConfig.query && changes.query) {
newState.query = queryString.getQuery();
}
if (syncConfig.refreshInterval && changes.refreshInterval) {
newState.refreshInterval = timefilter.getRefreshInterval();
}
@ -173,6 +185,13 @@ export const connectToQueryState = <S extends QueryState>(
}
}
if (syncConfig.query) {
const curQuery = state.query || queryString.getQuery();
if (!_.isEqual(curQuery, queryString.getQuery())) {
queryString.setQuery(_.cloneDeep(curQuery));
}
}
if (syncConfig.filters) {
const filters = state.filters || [];
if (syncConfig.filters === true) {

View file

@ -24,23 +24,31 @@ import { FilterManager } from '../filter_manager';
import { QueryState, QueryStateChange } from './index';
import { createStateContainer } from '../../../../kibana_utils/public';
import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '../../../common';
import { QueryStringContract } from '../query_string';
export function createQueryStateObservable({
timefilter: { timefilter },
filterManager,
queryString,
}: {
timefilter: TimefilterSetup;
filterManager: FilterManager;
queryString: QueryStringContract;
}): Observable<{ changes: QueryStateChange; state: QueryState }> {
return new Observable((subscriber) => {
const state = createStateContainer<QueryState>({
time: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
filters: filterManager.getFilters(),
query: queryString.getQuery(),
});
let currentChange: QueryStateChange = {};
const subs: Subscription[] = [
queryString.getUpdates$().subscribe(() => {
currentChange.query = true;
state.set({ ...state.get(), query: queryString.getQuery() });
}),
timefilter.getTimeUpdate$().subscribe(() => {
currentChange.time = true;
state.set({ ...state.get(), time: timefilter.getTime() });

View file

@ -43,6 +43,8 @@ setupMock.uiSettings.get.mockImplementation((key: string) => {
return true;
case 'timepicker:timeDefaults':
return { from: 'now-15m', to: 'now' };
case 'search:queryLanguage':
return 'kuery';
case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS:
return { pause: false, value: 0 };
default:

View file

@ -35,7 +35,7 @@ const GLOBAL_STATE_STORAGE_KEY = '_g';
* @param kbnUrlStateStorage to use for syncing
*/
export const syncQueryStateWithUrl = (
query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'state$'>,
query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'queryString' | 'state$'>,
kbnUrlStateStorage: IKbnUrlStateStorage
) => {
const {

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Filter, RefreshInterval, TimeRange } from '../../../common';
import { Filter, RefreshInterval, TimeRange, Query } from '../../../common';
/**
* All query state service state
@ -26,6 +26,7 @@ export interface QueryState {
time?: TimeRange;
refreshInterval?: RefreshInterval;
filters?: Filter[];
query?: Query;
}
type QueryStateChangePartial = {

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { CoreStart } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { KibanaContextProvider } from '../../../../kibana_react/public';
@ -28,7 +28,8 @@ import { useFilterManager } from './lib/use_filter_manager';
import { useTimefilter } from './lib/use_timefilter';
import { useSavedQuery } from './lib/use_saved_query';
import { DataPublicPluginStart } from '../../types';
import { Filter, Query, TimeRange, UI_SETTINGS } from '../../../common';
import { Filter, Query, TimeRange } from '../../../common';
import { useQueryStringManager } from './lib/use_query_string_manager';
interface StatefulSearchBarDeps {
core: CoreStart;
@ -65,8 +66,7 @@ const defaultOnRefreshChange = (queryService: QueryStart) => {
const defaultOnQuerySubmit = (
props: StatefulSearchBarProps,
queryService: QueryStart,
currentQuery: Query,
setQueryStringState: Function
currentQuery: Query
) => {
if (!props.useDefaultBehaviors) return props.onQuerySubmit;
@ -78,7 +78,11 @@ const defaultOnQuerySubmit = (
!_.isEqual(payload.query, currentQuery);
if (isUpdate) {
timefilter.setTime(payload.dateRange);
setQueryStringState(payload.query);
if (payload.query) {
queryService.queryString.setQuery(payload.query);
} else {
queryService.queryString.clearQuery();
}
} else {
// Refresh button triggered for an update
if (props.onQuerySubmit)
@ -121,30 +125,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
return (props: StatefulSearchBarProps) => {
const { useDefaultBehaviors } = props;
// Handle queries
const queryRef = useRef(props.query);
const onQuerySubmitRef = useRef(props.onQuerySubmit);
const defaultQuery = {
query: '',
language:
storage.get('kibana.userQueryLanguage') ||
core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
};
const [query, setQuery] = useState<Query>(props.query || defaultQuery);
useEffect(() => {
if (props.query !== queryRef.current) {
queryRef.current = props.query;
setQuery(props.query || defaultQuery);
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [defaultQuery, props.query]);
useEffect(() => {
if (props.onQuerySubmit !== onQuerySubmitRef.current) {
onQuerySubmitRef.current = props.onQuerySubmit;
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [props.onQuerySubmit]);
// handle service state updates.
// i.e. filters being added from a visualization directly to filterManager.
@ -152,6 +133,10 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
filters: props.filters,
filterManager: data.query.filterManager,
});
const { query } = useQueryStringManager({
query: props.query,
queryStringManager: data.query.queryString,
});
const { timeRange, refreshInterval } = useTimefilter({
dateRangeFrom: props.dateRangeFrom,
dateRangeTo: props.dateRangeTo,
@ -163,10 +148,8 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
// Fetch and update UI from saved query
const { savedQuery, setSavedQuery, clearSavedQuery } = useSavedQuery({
queryService: data.query,
setQuery,
savedQueryId: props.savedQueryId,
notifications: core.notifications,
defaultLanguage: defaultQuery.language,
});
// Fire onQuerySubmit on query or timerange change
@ -210,7 +193,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
onFiltersUpdated={defaultFiltersUpdated(data.query)}
onRefreshChange={defaultOnRefreshChange(data.query)}
savedQuery={savedQuery}
onQuerySubmit={defaultOnQuerySubmit(props, data.query, query, setQuery)}
onQuerySubmit={defaultOnQuerySubmit(props, data.query, query)}
onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)}
onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)}
onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)}

View file

@ -21,10 +21,8 @@ import { clearStateFromSavedQuery } from './clear_saved_query';
import { dataPluginMock } from '../../../mocks';
import { DataPublicPluginStart } from '../../../types';
import { Query } from '../../..';
describe('clearStateFromSavedQuery', () => {
const DEFAULT_LANGUAGE = 'banana';
let dataMock: jest.Mocked<DataPublicPluginStart>;
beforeEach(() => {
@ -32,19 +30,9 @@ describe('clearStateFromSavedQuery', () => {
});
it('should clear filters and query', async () => {
const setQueryState = jest.fn();
dataMock.query.filterManager.removeAll = jest.fn();
clearStateFromSavedQuery(dataMock.query, setQueryState, DEFAULT_LANGUAGE);
expect(setQueryState).toHaveBeenCalled();
expect(dataMock.query.filterManager.removeAll).toHaveBeenCalled();
});
it('should use search:queryLanguage', async () => {
const setQueryState = jest.fn();
dataMock.query.filterManager.removeAll = jest.fn();
clearStateFromSavedQuery(dataMock.query, setQueryState, DEFAULT_LANGUAGE);
expect(setQueryState).toHaveBeenCalled();
expect((setQueryState.mock.calls[0][0] as Query).language).toBe(DEFAULT_LANGUAGE);
clearStateFromSavedQuery(dataMock.query);
expect(dataMock.query.queryString.clearQuery).toHaveBeenCalled();
expect(dataMock.query.filterManager.removeAll).toHaveBeenCalled();
});
});

View file

@ -18,14 +18,7 @@
*/
import { QueryStart } from '../../../query';
export const clearStateFromSavedQuery = (
queryService: QueryStart,
setQueryStringState: Function,
defaultLanguage: string
) => {
export const clearStateFromSavedQuery = (queryService: QueryStart) => {
queryService.filterManager.removeAll();
setQueryStringState({
query: '',
language: defaultLanguage,
});
queryService.queryString.clearQuery();
};

View file

@ -47,37 +47,34 @@ describe('populateStateFromSavedQuery', () => {
});
it('should set query', async () => {
const setQueryState = jest.fn();
const savedQuery: SavedQuery = {
...baseSavedQuery,
};
populateStateFromSavedQuery(dataMock.query, setQueryState, savedQuery);
expect(setQueryState).toHaveBeenCalled();
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.queryString.setQuery).toHaveBeenCalled();
});
it('should set filters', async () => {
const setQueryState = jest.fn();
const savedQuery: SavedQuery = {
...baseSavedQuery,
};
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
savedQuery.attributes.filters = [f1];
populateStateFromSavedQuery(dataMock.query, setQueryState, savedQuery);
expect(setQueryState).toHaveBeenCalled();
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.queryString.setQuery).toHaveBeenCalled();
expect(dataMock.query.filterManager.setFilters).toHaveBeenCalledWith([f1]);
});
it('should preserve global filters', async () => {
const globalFilter = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
dataMock.query.filterManager.getGlobalFilters = jest.fn().mockReturnValue([globalFilter]);
const setQueryState = jest.fn();
const savedQuery: SavedQuery = {
...baseSavedQuery,
};
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
savedQuery.attributes.filters = [f1];
populateStateFromSavedQuery(dataMock.query, setQueryState, savedQuery);
expect(setQueryState).toHaveBeenCalled();
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.queryString.setQuery).toHaveBeenCalled();
expect(dataMock.query.filterManager.setFilters).toHaveBeenCalledWith([globalFilter, f1]);
});
@ -97,7 +94,7 @@ describe('populateStateFromSavedQuery', () => {
dataMock.query.timefilter.timefilter.setTime = jest.fn();
dataMock.query.timefilter.timefilter.setRefreshInterval = jest.fn();
populateStateFromSavedQuery(dataMock.query, jest.fn(), savedQuery);
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({
from: savedQuery.attributes.timefilter.from,

View file

@ -19,14 +19,11 @@
import { QueryStart, SavedQuery } from '../../../query';
export const populateStateFromSavedQuery = (
queryService: QueryStart,
setQueryStringState: Function,
savedQuery: SavedQuery
) => {
export const populateStateFromSavedQuery = (queryService: QueryStart, savedQuery: SavedQuery) => {
const {
timefilter: { timefilter },
filterManager,
queryString,
} = queryService;
// timefilter
if (savedQuery.attributes.timefilter) {
@ -40,7 +37,7 @@ export const populateStateFromSavedQuery = (
}
// query string
setQueryStringState(savedQuery.attributes.query);
queryString.setQuery(savedQuery.attributes.query);
// filters
const savedQueryFilters = savedQuery.attributes.filters || [];

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useState, useEffect } from 'react';
import { Subscription } from 'rxjs';
import { Query } from '../../..';
import { QueryStringContract } from '../../../query/query_string';
interface UseQueryStringProps {
query?: Query;
queryStringManager: QueryStringContract;
}
export const useQueryStringManager = (props: UseQueryStringProps) => {
// Filters should be either what's passed in the initial state or the current state of the filter manager
const [query, setQuery] = useState<Query>(props.query || props.queryStringManager.getQuery());
useEffect(() => {
const subscriptions = new Subscription();
subscriptions.add(
props.queryStringManager.getUpdates$().subscribe({
next: () => {
const newQuery = props.queryStringManager.getQuery();
setQuery(newQuery);
},
})
);
return () => {
subscriptions.unsubscribe();
};
}, [props.queryStringManager]);
return { query };
};

View file

@ -27,10 +27,8 @@ import { clearStateFromSavedQuery } from './clear_saved_query';
interface UseSavedQueriesProps {
queryService: DataPublicPluginStart['query'];
setQuery: Function;
notifications: CoreStart['notifications'];
savedQueryId?: string;
defaultLanguage: string;
}
interface UseSavedQueriesReturn {
@ -41,7 +39,6 @@ interface UseSavedQueriesReturn {
export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesReturn => {
// Handle saved queries
const defaultLanguage = props.defaultLanguage;
const [savedQuery, setSavedQuery] = useState<SavedQuery | undefined>();
// Effect is used to convert a saved query id into an object
@ -53,12 +50,12 @@ export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesRetur
// Make sure we set the saved query to the most recent one
if (newSavedQuery && newSavedQuery.id === savedQueryId) {
setSavedQuery(newSavedQuery);
populateStateFromSavedQuery(props.queryService, props.setQuery, newSavedQuery);
populateStateFromSavedQuery(props.queryService, newSavedQuery);
}
} catch (error) {
// Clear saved query
setSavedQuery(undefined);
clearStateFromSavedQuery(props.queryService, props.setQuery, defaultLanguage);
clearStateFromSavedQuery(props.queryService);
// notify of saving error
props.notifications.toasts.addWarning({
title: i18n.translate('data.search.unableToGetSavedQueryToastTitle', {
@ -73,23 +70,21 @@ export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesRetur
if (props.savedQueryId) fetchSavedQuery(props.savedQueryId);
else setSavedQuery(undefined);
}, [
defaultLanguage,
props.notifications.toasts,
props.queryService,
props.queryService.savedQueries,
props.savedQueryId,
props.setQuery,
]);
return {
savedQuery,
setSavedQuery: (q: SavedQuery) => {
setSavedQuery(q);
populateStateFromSavedQuery(props.queryService, props.setQuery, q);
populateStateFromSavedQuery(props.queryService, q);
},
clearSavedQuery: () => {
setSavedQuery(undefined);
clearStateFromSavedQuery(props.queryService, props.setQuery, defaultLanguage);
clearStateFromSavedQuery(props.queryService);
},
};
};

View file

@ -6,9 +6,8 @@
app-name="'discover'"
config="topNavMenu"
index-patterns="[indexPattern]"
on-query-submit="updateQuery"
on-query-submit="handleRefresh"
on-saved-query-id-change="updateSavedQueryId"
query="state.query"
saved-query-id="state.savedQuery"
screen-title="screenTitle"
show-date-picker="indexPattern.isTimeBased()"

View file

@ -70,9 +70,7 @@ import {
indexPatterns as indexPatternsUtils,
connectToQueryState,
syncQueryStateWithUrl,
getDefaultQuery,
search,
UI_SETTINGS,
} from '../../../../data/public';
import { getIndexPatternId } from '../helpers/get_index_pattern_id';
import { addFatalError } from '../../../../kibana_legacy/public';
@ -191,16 +189,7 @@ app.directive('discoverApp', function () {
};
});
function discoverController(
$element,
$route,
$scope,
$timeout,
$window,
Promise,
localStorage,
uiCapabilities
) {
function discoverController($element, $route, $scope, $timeout, $window, Promise, uiCapabilities) {
const { isDefault: isDefaultType } = indexPatternsUtils;
const subscriptions = new Subscription();
const $fetchObservable = new Subject();
@ -246,11 +235,15 @@ function discoverController(
// sync initial app filters from state to filterManager
filterManager.setAppFilters(_.cloneDeep(appStateContainer.getState().filters));
data.query.queryString.setQuery(appStateContainer.getState().query);
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
data.query,
appStateContainer,
{ filters: esFilters.FilterStateStore.APP_STATE }
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);
const appStateUnsubscribe = appStateContainer.subscribe(async (newState) => {
@ -262,7 +255,7 @@ function discoverController(
$scope.state = { ...newState };
// detect changes that should trigger fetching of new data
const changes = ['interval', 'sort', 'query'].filter(
const changes = ['interval', 'sort'].filter(
(prop) => !_.isEqual(newStatePartial[prop], oldStatePartial[prop])
);
@ -593,12 +586,7 @@ function discoverController(
};
function getStateDefaults() {
const query =
$scope.searchSource.getField('query') ||
getDefaultQuery(
localStorage.get('kibana.userQueryLanguage') ||
config.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE)
);
const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery();
return {
query,
sort: getSortArray(savedSearch.sort, $scope.indexPattern),
@ -635,12 +623,7 @@ function discoverController(
const init = _.once(() => {
$scope.updateDataSource().then(async () => {
const searchBarChanges = merge(
timefilter.getAutoRefreshFetch$(),
timefilter.getFetch$(),
filterManager.getFetches$(),
$fetchObservable
).pipe(debounceTime(100));
const searchBarChanges = merge(data.query.state$, $fetchObservable).pipe(debounceTime(100));
subscriptions.add(
subscribeWithScope(
@ -824,9 +807,8 @@ function discoverController(
});
};
$scope.updateQuery = function ({ query }, isUpdate = true) {
if (!_.isEqual(query, appStateContainer.getState().query) || isUpdate === false) {
setAppState({ query });
$scope.handleRefresh = function (_payload, isUpdate) {
if (isUpdate === false) {
$fetchObservable.next();
}
};
@ -976,7 +958,7 @@ function discoverController(
config.get(SORT_DEFAULT_ORDER_SETTING)
)
)
.setField('query', $scope.state.query || null)
.setField('query', data.query.queryString.getQuery() || null)
.setField('filter', filterManager.getFilters());
return Promise.resolve();
};

View file

@ -35,12 +35,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
esQuery,
IndexPattern,
Query,
UI_SETTINGS,
} from '../../../../../../../plugins/data/public';
import { esQuery, IndexPattern, Query } from '../../../../../../../plugins/data/public';
import { context as contextType } from '../../../../../../kibana_react/public';
import { IndexPatternManagmentContextValue } from '../../../../types';
import { ExecuteScript } from '../../types';
@ -248,10 +243,7 @@ export class TestScript extends Component<TestScriptProps, TestScriptState> {
showFilterBar={false}
showDatePicker={false}
showQueryInput={true}
query={{
language: this.context.services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
query: '',
}}
query={this.context.services.data.query.queryString.getDefaultQuery()}
onQuerySubmit={this.previewScript}
indexPatterns={[this.props.indexPattern]}
customSubmitButton={

View file

@ -23,7 +23,7 @@ import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useMount } from 'react-use';
import { Query, UI_SETTINGS } from '../../../../data/public';
import { Query } from '../../../../data/public';
import { useKibana } from '../../../../kibana_react/public';
import { FilterRow } from './filter';
import { AggParamEditorProps } from '../agg_param_props';
@ -70,7 +70,7 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps<F
updateFilters([
...filters,
{
input: { query: '', language: services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) },
input: services.data.query.queryString.getDefaultQuery(),
label: '',
id: generateId(),
},

View file

@ -157,7 +157,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
if (!adapters) return;
return this.deps.start().plugins.inspector.open(adapters, {
title: this.getTitle() || '',
title: this.getTitle() || this.title || '',
});
};
@ -215,8 +215,17 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
dirty = true;
}
// propagate the title to the output embeddable
// but only when the visualization is in edit/Visualize mode
if (!this.parent && this.vis.title !== this.output.title) {
this.updateOutput({ title: this.vis.title });
}
// Keep title depending on the output Embeddable to decouple the
// visual appearance of the title and the actual title content (useful in Dashboard)
if (this.output.title !== this.title) {
this.title = this.output.title;
if (this.domNode) {
this.domNode.setAttribute('data-title', this.title || '');
}

View file

@ -18,10 +18,8 @@
*/
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
import { isEqual } from 'lodash';
import { OverlayRef } from 'kibana/public';
import { Query } from 'src/plugins/data/public';
import { useKibana } from '../../../../kibana_react/public';
import {
VisualizeServices,
@ -68,15 +66,13 @@ const TopNav = ({
setInspectorSession(session);
}, [embeddableHandler]);
const updateQuery = useCallback(
({ query }: { query?: Query }) => {
if (!isEqual(currentAppState.query, query)) {
stateContainer.transitions.set('query', query || currentAppState.query);
} else {
const handleRefresh = useCallback(
(_payload: any, isUpdate?: boolean) => {
if (isUpdate === false) {
savedVisInstance.embeddableHandler.reload();
}
},
[currentAppState.query, savedVisInstance.embeddableHandler, stateContainer.transitions]
[savedVisInstance.embeddableHandler]
);
const config = useMemo(() => {
@ -149,8 +145,7 @@ const TopNav = ({
<TopNavMenu
appName={APP_NAME}
config={config}
query={currentAppState.query}
onQuerySubmit={updateQuery}
onQuerySubmit={handleRefresh}
savedQueryId={currentAppState.savedQuery}
onSavedQueryIdChange={stateContainer.transitions.updateSavedQuery}
indexPatterns={indexPattern ? [indexPattern] : undefined}

View file

@ -105,10 +105,16 @@ describe('useEditorUpdates', () => {
to: 'now',
};
mockFilters = ['mockFilters'];
const mockQuery = {
query: '',
language: 'kuery',
};
// @ts-expect-error
mockServices.data.query.timefilter.timefilter.getTime.mockImplementation(() => timeRange);
// @ts-expect-error
mockServices.data.query.filterManager.getFilters.mockImplementation(() => mockFilters);
// @ts-expect-error
mockServices.data.query.queryString.getQuery.mockImplementation(() => mockQuery);
});
test('should set up current app state and render the editor', () => {

View file

@ -20,9 +20,7 @@
import { useEffect, useState } from 'react';
import { isEqual } from 'lodash';
import { EventEmitter } from 'events';
import { merge } from 'rxjs';
import { migrateLegacyQuery } from '../../../../../kibana_legacy/public';
import {
VisualizeServices,
VisualizeAppState,
@ -47,6 +45,8 @@ export const useEditorUpdates = (
const {
timefilter: { timefilter },
filterManager,
queryString,
state$,
} = services.data.query;
const { embeddableHandler, savedVis, savedSearch, vis } = savedVisInstance;
const initialState = appState.getState();
@ -60,7 +60,7 @@ export const useEditorUpdates = (
uiState: vis.uiState,
timeRange: timefilter.getTime(),
filters: filterManager.getFilters(),
query: appState.getState().query,
query: queryString.getQuery(),
linked: !!vis.data.savedSearchId,
savedSearch,
});
@ -68,17 +68,12 @@ export const useEditorUpdates = (
embeddableHandler.updateInput({
timeRange: timefilter.getTime(),
filters: filterManager.getFilters(),
query: appState.getState().query,
query: queryString.getQuery(),
});
}
};
const subscriptions = merge(
timefilter.getTimeUpdate$(),
timefilter.getAutoRefreshFetch$(),
timefilter.getFetch$(),
filterManager.getFetches$()
).subscribe({
const subscriptions = state$.subscribe({
next: reloadVisualization,
error: services.fatalErrors.add,
});
@ -116,10 +111,6 @@ export const useEditorUpdates = (
// and initializing different visualizations
return;
}
const newQuery = migrateLegacyQuery(state.query);
if (!isEqual(state.query, newQuery)) {
appState.transitions.set('query', newQuery);
}
if (!isEqual(state.uiState, vis.uiState.getChanges())) {
vis.uiState.set(state.uiState);

View file

@ -96,6 +96,7 @@ describe('useVisualizeAppState', () => {
);
expect(connectToQueryState).toHaveBeenCalledWith(mockServices.data.query, expect.any(Object), {
filters: 'appState',
query: true,
});
expect(result.current).toEqual({
appState: stateContainer,

View file

@ -24,6 +24,7 @@ import { EventEmitter } from 'events';
import { i18n } from '@kbn/i18n';
import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public';
import { migrateLegacyQuery } from '../../../../../kibana_legacy/public';
import { esFilters, connectToQueryState } from '../../../../../data/public';
import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types';
import { visStateToEditorState } from '../utils';
@ -61,19 +62,35 @@ export const useVisualizeAppState = (
eventEmitter.on('dirtyStateChange', onDirtyStateChange);
const { filterManager } = services.data.query;
// sync initial app filters from state to filterManager
const { filterManager, queryString } = services.data.query;
// sync initial app state from state to managers
filterManager.setAppFilters(cloneDeep(stateContainer.getState().filters));
// setup syncing of app filters between appState and filterManager
queryString.setQuery(migrateLegacyQuery(stateContainer.getState().query));
// setup syncing of app filters between appState and query services
const stopSyncingAppFilters = connectToQueryState(
services.data.query,
{
set: ({ filters }) => stateContainer.transitions.set('filters', filters),
get: () => ({ filters: stateContainer.getState().filters }),
state$: stateContainer.state$.pipe(map((state) => ({ filters: state.filters }))),
set: ({ filters, query }) => {
stateContainer.transitions.set('filters', filters);
stateContainer.transitions.set('query', query);
},
get: () => {
return {
filters: stateContainer.getState().filters,
query: stateContainer.getState().query,
};
},
state$: stateContainer.state$.pipe(
map((state) => ({
filters: state.filters,
query: state.query,
}))
),
},
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);

View file

@ -20,7 +20,7 @@
import { i18n } from '@kbn/i18n';
import { ChromeStart, DocLinksStart } from 'kibana/public';
import { Filter, UI_SETTINGS } from '../../../../data/public';
import { Filter } from '../../../../data/public';
import { VisualizeServices, SavedVisInstance } from '../types';
export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => {
@ -49,12 +49,9 @@ export const addBadgeToAppChrome = (chrome: ChromeStart) => {
});
};
export const getDefaultQuery = ({ localStorage, uiSettings }: VisualizeServices) => ({
query: '',
language:
localStorage.get('kibana.userQueryLanguage') ||
uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
});
export const getDefaultQuery = ({ data }: VisualizeServices) => {
return data.query.queryString.getDefaultQuery();
};
export const visStateToEditorState = (
{ vis, savedVis }: SavedVisInstance,

View file

@ -563,6 +563,10 @@ export default function ({ getService, getPageObjects }) {
it('should display updated scaled label text after time range is changed', async () => {
await PageObjects.visEditor.setInterval('Millisecond');
// Apply interval
await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
const isHelperScaledLabelExists = await find.existsByCssSelector(
'[data-test-subj="currentlyScaledText"]'
);

View file

@ -1,33 +0,0 @@
/*
* 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 React from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
export const AlignmentGuide = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
background: 'magenta',
position: 'absolute',
transform: matrixToCSS(transformMatrix),
};
return (
<div
className="canvasAlignmentGuide canvasInteractable canvasLayoutAnnotation"
style={newStyle}
/>
);
};
AlignmentGuide.propTypes = {
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};

View file

@ -1,4 +0,0 @@
.canvasAlignmentGuide {
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { FC, ReactEventHandler } from 'react';
import PropTypes from 'prop-types';
import {
EuiDescriptionList,
@ -12,7 +12,13 @@ import {
EuiDescriptionListDescription,
} from '@elastic/eui';
export const ArgAdd = ({ onValueAdd, displayName, help }) => {
interface Props {
displayName: string;
help: string;
onValueAdd?: ReactEventHandler;
}
export const ArgAdd: FC<Props> = ({ onValueAdd = () => {}, displayName, help }) => {
return (
<button className="canvasArg__add" onClick={onValueAdd}>
<EuiDescriptionList compressed>
@ -26,7 +32,7 @@ export const ArgAdd = ({ onValueAdd, displayName, help }) => {
};
ArgAdd.propTypes = {
displayName: PropTypes.string,
help: PropTypes.string,
displayName: PropTypes.string.isRequired,
help: PropTypes.string.isRequired,
onValueAdd: PropTypes.func,
};

View file

@ -4,8 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ArgAdd as Component } from './arg_add';
export const ArgAdd = pure(Component);
export { ArgAdd } from './arg_add';

View file

@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { MouseEventHandler, FC } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonIcon } from '@elastic/eui';
// @ts-expect-error untyped local
import { Popover, PopoverChildrenProps } from '../popover';
// @ts-expect-error untyped local
import { ArgAdd } from '../arg_add';
// @ts-expect-error untyped local
import { Arg } from '../../expression_types/arg';
@ -27,8 +26,8 @@ interface Props {
options: ArgOptions[];
}
export const ArgAddPopover = ({ options }: Props) => {
const button = (handleClick: React.MouseEventHandler<HTMLButtonElement>) => (
export const ArgAddPopover: FC<Props> = ({ options }) => {
const button = (handleClick: MouseEventHandler<HTMLButtonElement>) => (
<EuiButtonIcon
iconType="plusInCircle"
aria-label={strings.getAddAriaLabel()}

View file

@ -4,7 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ArgAddPopover as Component } from './arg_add_popover';
export const ArgAddPopover = pure(Component);
export { ArgAddPopover } from './arg_add_popover';

View file

@ -229,545 +229,6 @@ Array [
]
`;
exports[`Storyshots components/Assets/AssetManager redux 1`] = `
Array [
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={1}
/>,
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
>
<div
className="euiModal canvasAssetManager canvasModal--fixedSize"
onKeyDown={[Function]}
style={
Object {
"maxWidth": "1000px",
}
}
tabIndex={0}
>
<button
aria-label="Closes this modal window"
className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="cross"
size="m"
/>
</button>
<div
className="euiModal__flex"
>
<div
className="euiModalHeader canvasAssetManager__modalHeader"
>
<div
className="euiModalHeader__title canvasAssetManager__modalHeaderTitle"
>
Manage workpad assets
</div>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive canvasAssetManager__fileUploadWrapper"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFilePicker euiFilePicker--compressed"
>
<div
className="euiFilePicker__wrap"
>
<input
accept="image/*"
aria-describedby="generated-id"
className="euiFilePicker__input"
multiple={true}
onChange={[Function]}
onDragLeave={[Function]}
onDragOver={[Function]}
onDrop={[Function]}
type="file"
/>
<div
className="euiFilePicker__prompt"
id="generated-id"
>
<div
aria-hidden="true"
className="euiFilePicker__icon"
data-euiicon-type="importAction"
size="m"
/>
<div
className="euiFilePicker__promptText"
>
Select or drag and drop images
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="euiModalBody"
>
<div
className="euiModalBody__overflow"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--subdued"
>
<p>
Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets.
</p>
</div>
</div>
<div
className="euiSpacer euiSpacer--l"
/>
<div
className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiPanel euiPanel--paddingSmall canvasAsset"
>
<div
className="canvasAsset__thumb canvasCheckered"
>
<figure
className="euiImage canvasAsset__img "
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4="
style={Object {}}
/>
</figure>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiText euiText--extraSmall eui-textBreakAll"
>
<p
className="eui-textBreakAll"
>
<strong>
airplane
</strong>
<br />
<span
className="euiTextColor euiTextColor--subdued"
>
<small>
(
1
kb)
</small>
</span>
</p>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-create-image"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="vector"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasDownload"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Download"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="sortDown"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasClipboard"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Copy id to clipboard"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="copyClipboard"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="trash"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
<div
className="euiFlexItem"
>
<div
className="euiPanel euiPanel--paddingSmall canvasAsset"
>
<div
className="canvasAsset__thumb canvasCheckered"
>
<figure
className="euiImage canvasAsset__img "
>
<img
alt="Asset thumbnail"
className="euiImage__img"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4="
style={Object {}}
/>
</figure>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiText euiText--extraSmall eui-textBreakAll"
>
<p
className="eui-textBreakAll"
>
<strong>
marker
</strong>
<br />
<span
className="euiTextColor euiTextColor--subdued"
>
<small>
(
1
kb)
</small>
</span>
</p>
</div>
<div
className="euiSpacer euiSpacer--s"
/>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-create-image"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Create image element"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="vector"
size="m"
/>
</button>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero asset-download"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasDownload"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Download"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="sortDown"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="canvasClipboard"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<button
aria-label="Copy id to clipboard"
className="euiButtonIcon euiButtonIcon--primary"
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="copyClipboard"
size="m"
/>
</button>
</div>
</span>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Delete"
className="euiButtonIcon euiButtonIcon--danger"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="trash"
size="m"
/>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="euiModalFooter canvasAssetManager__modalFooter"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow canvasAssetManager__meterWrapper"
>
<div
className="euiFlexItem"
>
<progress
aria-labelledby="CanvasAssetManagerLabel"
className="euiProgress euiProgress--native euiProgress--s euiProgress--secondary"
max={25000}
value={2}
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero eui-textNoWrap"
>
<div
className="euiText euiText--medium"
id="CanvasAssetManagerLabel"
>
0% space used
</div>
</div>
</div>
<button
className="euiButton euiButton--primary euiButton--small"
onClick={[Function]}
type="button"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Close
</span>
</span>
</button>
</div>
</div>
</div>
</div>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
]
`;
exports[`Storyshots components/Assets/AssetManager two assets 1`] = `
Array [
<div

View file

@ -1,27 +0,0 @@
/*
* 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 React from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
export const BorderConnection = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
position: 'absolute',
transform: matrixToCSS(transformMatrix),
};
return <div className="canvasBorder--connection canvasLayoutAnnotation" style={newStyle} />;
};
BorderConnection.propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
};

View file

@ -1,9 +0,0 @@
.canvasBorder--connection {
position: absolute;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-top: 1px dashed $euiColorLightShade;
border-left: 1px dashed $euiColorLightShade;
}

View file

@ -1,10 +0,0 @@
/*
* 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 { pure } from 'recompose';
import { BorderConnection as Component } from './border_connection';
export const BorderConnection = pure(Component);

View file

@ -1,13 +0,0 @@
.canvasBorderResizeHandle {
@include euiSlightShadow;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
display: block;
position: absolute;
height: 8px;
width: 8px;
margin-left: -4px;
margin-top: -4px;
background-color: $euiColorEmptyShade;
border: 1px solid $euiColorDarkShade;
}

View file

@ -1,10 +0,0 @@
/*
* 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 { pure } from 'recompose';
import { BorderResizeHandle as Component } from './border_resize_handle';
export const BorderResizeHandle = pure(Component);

View file

@ -4,18 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, ReactNode } from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent, ReactNode } from 'react';
import tinycolor from 'tinycolor2';
export interface Props {
/** Any valid CSS color. If not a valid CSS string, the dot will be transparent and checkered */
value?: string;
interface Props {
/** Nodes to display within the dot. Should fit within the constraints. */
children?: ReactNode;
/** Any valid CSS color. If not a valid CSS string, the dot will be transparent and checkered */
value?: string;
}
export const ColorDot: FunctionComponent<Props> = ({ value, children }) => {
export const ColorDot: FC<Props> = ({ value, children }) => {
const tc = tinycolor(value);
let style = {};
@ -34,6 +34,6 @@ export const ColorDot: FunctionComponent<Props> = ({ value, children }) => {
};
ColorDot.propTypes = {
value: PropTypes.string,
children: PropTypes.node,
value: PropTypes.string,
};

View file

@ -4,9 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ColorDot as Component } from './color_dot';
export { Props } from './color_dot';
export const ColorDot = pure(Component);
export { ColorDot } from './color_dot';

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import tinycolor from 'tinycolor2';
import { ColorDot } from '../color_dot/color_dot';
@ -15,17 +15,17 @@ import { ComponentStrings } from '../../../i18n/components';
const { ColorManager: strings } = ComponentStrings;
export interface Props {
/**
* Determines if the add/remove buttons are displayed.
* @default false
*/
hasButtons?: boolean;
/** The function to call when the Add Color button is clicked. The button will be disabled if there is no handler. */
onAddColor?: (value: string) => void;
/** The function to call when the value is changed */
onChange: (value: string) => void;
/** The function to call when the Remove Color button is clicked. The button will be disabled if there is no handler. */
onRemoveColor?: (value: string) => void;
/**
* Determines if the add/remove buttons are displayed.
* @default false
*/
hasButtons?: boolean;
/**
* The value of the color manager. Only honors valid CSS values.
* @default ''
@ -33,12 +33,12 @@ export interface Props {
value?: string;
}
export const ColorManager: FunctionComponent<Props> = ({
value = '',
onAddColor,
onRemoveColor,
onChange,
export const ColorManager: FC<Props> = ({
hasButtons = false,
onAddColor,
onChange,
onRemoveColor,
value = '',
}) => {
const tc = tinycolor(value);
const validColor = tc.isValid();

View file

@ -4,9 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ColorManager as Component } from './color_manager';
export { Props } from './color_manager';
export const ColorManager = pure(Component);
export { ColorManager, Props } from './color_manager';

View file

@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiIcon, EuiLink } from '@elastic/eui';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { EuiIcon, EuiLink } from '@elastic/eui';
import tinycolor from 'tinycolor2';
import { readableColor } from '../../lib/readable_color';
import { ColorDot } from '../color_dot';
import { ItemGrid } from '../item_grid';
export interface Props {
interface Props {
/**
* An array of hexadecimal color values. Non-hex will be ignored.
* @default []
@ -32,7 +32,7 @@ export interface Props {
value?: string;
}
export const ColorPalette: FunctionComponent<Props> = ({
export const ColorPalette: FC<Props> = ({
colors = [],
colorsPerRow = 6,
onChange,

View file

@ -4,8 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ColorPalette as Component } from './color_palette';
export { Props } from './color_palette';
export const ColorPalette = pure(Component);
export { ColorPalette } from './color_palette';

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import tinycolor from 'tinycolor2';
import { ColorManager, Props as ColorManagerProps } from '../color_manager';
import { ColorPalette } from '../color_palette';
@ -18,7 +19,7 @@ export interface Props extends ColorManagerProps {
colors?: string[];
}
export const ColorPicker: FunctionComponent<Props> = ({
export const ColorPicker: FC<Props> = ({
colors = [],
hasButtons = false,
onAddColor,

View file

@ -4,9 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ColorPicker as Component } from './color_picker';
export { Props } from './color_picker';
export const ColorPicker = pure(Component);
export { ColorPicker, Props } from './color_picker';

View file

@ -4,20 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink, PopoverAnchorPosition } from '@elastic/eui';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { EuiLink, PopoverAnchorPosition } from '@elastic/eui';
import tinycolor from 'tinycolor2';
import { ColorDot } from '../color_dot';
import { ColorPicker, Props as ColorPickerProps } from '../color_picker';
import { Popover } from '../popover';
export interface Props extends ColorPickerProps {
anchorPosition: PopoverAnchorPosition;
anchorPosition?: PopoverAnchorPosition;
ariaLabel?: string;
}
export const ColorPickerPopover: FunctionComponent<Props> = (props: Props) => {
export const ColorPickerPopover: FC<Props> = (props: Props) => {
const { value, anchorPosition, ariaLabel, ...rest } = props;
const button = (handleClick: React.MouseEventHandler<HTMLButtonElement>) => (
<EuiLink

View file

@ -4,9 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { ColorPickerPopover as Component } from './color_picker_popover';
export { Props } from './color_picker_popover';
export const ColorPickerPopover = pure(Component);
export { ColorPickerPopover, Props } from './color_picker_popover';

View file

@ -4,13 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiIcon, EuiPagination } from '@elastic/eui';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiIcon, EuiPagination } from '@elastic/eui';
import moment from 'moment';
import { Paginate } from '../paginate';
import { Datatable as DatatableType, DatatableColumn } from '../../../types';
const getIcon = (type) => {
type IconType = 'string' | 'number' | 'date' | 'boolean' | 'null';
const getIcon = (type: IconType) => {
if (type === null) {
return;
}
@ -36,19 +39,31 @@ const getIcon = (type) => {
return <EuiIcon type={icon} color="subdued" />;
};
const getColumnName = (col) => (typeof col === 'string' ? col : col.name);
const getColumnName = (col: DatatableColumn) => (typeof col === 'string' ? col : col.name);
const getColumnType = (col) => col.type || null;
const getColumnType = (col: DatatableColumn) => col.type || null;
const getFormattedValue = (val, type) => {
const getFormattedValue = (val: any, type: any) => {
if (type === 'date') {
return moment(val).format();
}
return String(val);
};
export const Datatable = ({ datatable, perPage, paginate, showHeader }) => (
<Paginate rows={datatable.rows} perPage={perPage || 10}>
interface Props {
datatable: DatatableType;
paginate?: boolean;
perPage?: number;
showHeader?: boolean;
}
export const Datatable: FC<Props> = ({
datatable,
paginate = false,
perPage = 10,
showHeader = false,
}) => (
<Paginate rows={datatable.rows} perPage={perPage}>
{({ rows, setPage, pageNumber, totalPages }) => (
<div className="canvasDataTable">
<div className="canvasDataTable__tableWrapper">
@ -91,7 +106,7 @@ export const Datatable = ({ datatable, perPage, paginate, showHeader }) => (
Datatable.propTypes = {
datatable: PropTypes.object.isRequired,
perPage: PropTypes.number,
paginate: PropTypes.bool,
perPage: PropTypes.number,
showHeader: PropTypes.bool,
};

View file

@ -4,7 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { Datatable as Component } from './datatable';
export const Datatable = pure(Component);
export { Datatable } from './datatable';

View file

@ -1,26 +0,0 @@
/*
* 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 React from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
export const DragBoxAnnotation = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: matrixToCSS(transformMatrix),
};
return <div className="canvasDragBoxAnnotation canvasLayoutAnnotation" style={newStyle} />;
};
DragBoxAnnotation.propTypes = {
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};

View file

@ -1,8 +0,0 @@
.canvasDragBoxAnnotation {
position: absolute;
background: none;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
outline: dashed 1px $euiColorDarkShade;
pointer-events: none;
}

View file

@ -1,10 +0,0 @@
/*
* 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 { pure } from 'recompose';
import { DragBoxAnnotation as Component } from './dragbox_annotation';
export const DragBoxAnnotation = pure(Component);

View file

@ -7,8 +7,8 @@
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { americanTypewriter } from '../../../common/lib/fonts';
import { FontPicker } from './font_picker';
import { americanTypewriter } from '../../../../common/lib/fonts';
import { FontPicker } from '../font_picker';
storiesOf('components/FontPicker', module)
.add('default', () => <FontPicker onSelect={action('onSelect')} />)

View file

@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSuperSelect } from '@elastic/eui';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { EuiSuperSelect } from '@elastic/eui';
import { fonts, FontValue } from '../../../common/lib/fonts';
interface DisplayedFont {
value: string;
label: string;
value: string;
}
interface Props {
@ -19,9 +19,7 @@ interface Props {
value?: FontValue;
}
export const FontPicker: FunctionComponent<Props> = (props) => {
const { value, onSelect } = props;
export const FontPicker: FC<Props> = ({ value, onSelect }) => {
// While fonts are strongly-typed, we also support custom fonts someone might type in.
// So let's cast the fonts and allow for additions.
const displayedFonts: DisplayedFont[] = fonts;
@ -46,10 +44,10 @@ export const FontPicker: FunctionComponent<Props> = (props) => {
};
FontPicker.propTypes = {
/** Initial value of the Font Picker. */
value: PropTypes.string,
/** Function to execute when a Font is selected. */
onSelect: PropTypes.func,
/** Initial value of the Font Picker. */
value: PropTypes.string,
};
FontPicker.displayName = 'FontPicker';

View file

@ -4,8 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { pure } from 'recompose';
import { FontPicker as Component } from './font_picker';
export const FontPicker = pure(Component);
export { FontPicker } from './font_picker';

View file

@ -1,8 +0,0 @@
.canvasHoverAnnotation {
position: absolute;
background: none;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
outline: solid 1px $euiColorVis0;
pointer-events: none;
}

View file

@ -1,10 +0,0 @@
/*
* 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 { pure } from 'recompose';
import { HoverAnnotation as Component } from './hover_annotation';
export const HoverAnnotation = pure(Component);

View file

@ -0,0 +1,37 @@
/*
* 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 React, { FC } from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
import { TransformMatrix3d } from '../../lib/aeroelastic';
interface Props {
height: number;
transformMatrix: TransformMatrix3d;
width: number;
}
export const AlignmentGuide: FC<Props> = ({ transformMatrix, width, height }) => (
<div
className="canvasAlignmentGuide canvasInteractable canvasLayoutAnnotation"
style={{
background: 'magenta',
height,
marginLeft: -width / 2,
marginTop: -height / 2,
position: 'absolute',
transform: matrixToCSS(transformMatrix),
width,
}}
/>
);
AlignmentGuide.propTypes = {
height: PropTypes.number.isRequired,
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
};

View file

@ -0,0 +1,36 @@
/*
* 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 React, { FC } from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
import { TransformMatrix3d } from '../../lib/aeroelastic';
interface Props {
height: number;
transformMatrix: TransformMatrix3d;
width: number;
}
export const BorderConnection: FC<Props> = ({ transformMatrix, width, height }) => (
<div
className="canvasBorderConnection canvasLayoutAnnotation"
style={{
height,
marginLeft: -width / 2,
marginTop: -height / 2,
position: 'absolute',
transform: matrixToCSS(transformMatrix),
width,
}}
/>
);
BorderConnection.propTypes = {
height: PropTypes.number.isRequired,
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
};

View file

@ -4,11 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
import { TransformMatrix3d } from '../../lib/aeroelastic';
export const BorderResizeHandle = ({ transformMatrix, zoomScale }) => (
interface Props {
transformMatrix: TransformMatrix3d;
zoomScale?: number;
}
export const BorderResizeHandle: FC<Props> = ({ transformMatrix, zoomScale = 1 }) => (
<div
className="canvasBorderResizeHandle canvasLayoutAnnotation"
style={{
@ -19,4 +25,5 @@ export const BorderResizeHandle = ({ transformMatrix, zoomScale }) => (
BorderResizeHandle.propTypes = {
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
zoomScale: PropTypes.number,
};

View file

@ -0,0 +1,35 @@
/*
* 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 React, { FC } from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
import { TransformMatrix3d } from '../../lib/aeroelastic';
interface Props {
height: number;
transformMatrix: TransformMatrix3d;
width: number;
}
export const DragBoxAnnotation: FC<Props> = ({ transformMatrix, width, height }) => (
<div
className="canvasDragBoxAnnotation canvasLayoutAnnotation"
style={{
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: matrixToCSS(transformMatrix),
width,
}}
/>
);
DragBoxAnnotation.propTypes = {
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};

View file

@ -4,23 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
import { TransformMatrix3d } from '../../lib/aeroelastic';
export const HoverAnnotation = ({ transformMatrix, width, height }) => {
const newStyle = {
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: matrixToCSS(transformMatrix),
};
return <div className="canvasHoverAnnotation canvasLayoutAnnotation" style={newStyle} />;
};
interface Props {
height: number;
transformMatrix: TransformMatrix3d;
width: number;
}
export const HoverAnnotation: FC<Props> = ({ transformMatrix, width, height }) => (
<div
className="canvasHoverAnnotation canvasLayoutAnnotation"
style={{
width,
height,
marginLeft: -width / 2,
marginTop: -height / 2,
transform: matrixToCSS(transformMatrix),
}}
/>
);
HoverAnnotation.propTypes = {
height: PropTypes.number.isRequired,
transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
};

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export { AlignmentGuide } from './alignment_guide';
export { DragBoxAnnotation } from './dragbox_annotation';
export { HoverAnnotation } from './hover_annotation';
export { TooltipAnnotation } from './tooltip_annotation';
export { RotationHandle } from './rotation_handle';
export { BorderConnection } from './border_connection';
export { BorderResizeHandle } from './border_resize_handle';

View file

@ -0,0 +1,81 @@
.canvasAlignmentGuide {
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
}
.canvasBorderConnection {
position: absolute;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-top: 1px dashed $euiColorLightShade;
border-left: 1px dashed $euiColorLightShade;
}
.canvasBorderResizeHandle {
@include euiSlightShadow;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
display: block;
position: absolute;
height: $euiSizeS;
width: $euiSizeS;
margin-left: -4px;
margin-top: -4px;
background-color: $euiColorEmptyShade;
border: 1px solid $euiColorDarkShade;
}
.canvasDragBoxAnnotation {
position: absolute;
background: none;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
outline: dashed 1px $euiColorDarkShade;
pointer-events: none;
}
.canvasHoverAnnotation {
position: absolute;
background: none;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
outline: solid 1px $euiColorVis0;
pointer-events: none;
}
.canvasRotationHandle {
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
display: block;
position: absolute;
height: $euiSizeL;
width: 0;
margin-left: -1px;
margin-top: -12px;
border-top: 1px dashed $euiColorLightShade;
border-left: 1px dashed $euiColorLightShade;
}
.canvasRotationHandle__handle {
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
display: block;
position: absolute;
height: 9px;
width: 9px;
margin-left: -5px;
margin-top: -6px;
border-radius: 50%;
background-color: $euiColorMediumShade;
}
.tooltipAnnotation {
@include euiToolTipStyle($size: 's');
position: absolute;
transform-origin: center center; /* the default, only for clarity */
transform-style: preserve-3d;
outline: none;
pointer-events: none;
}

Some files were not shown because too many files have changed in this diff Show more