Merge branch 'master' of github.com:elastic/kibana into pr/43529

This commit is contained in:
spalger 2019-12-10 21:09:48 -07:00
commit f72dd9850e
256 changed files with 12573 additions and 6452 deletions

View file

@ -44,6 +44,7 @@ tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \
x-pack/legacy/plugins/*/node_modules \
x-pack/legacy/plugins/reporting/.chromium \
test/plugin_functional/plugins/*/node_modules \
examples/*/node_modules \
.es \
.chromedriver \
.geckodriver;

View file

@ -65,6 +65,8 @@ connects to this Kibana instance.
`elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List
of Kibana client-side headers to send to Elasticsearch. To send *no* client-side
headers, set this value to [] (an empty list).
Removing the `authorization` header from being whitelisted means that you cannot
use <<basic-authentication, basic authentication>> in Kibana.
`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait
for responses from the back end or Elasticsearch. This value must be a positive

8
examples/README.md Normal file
View file

@ -0,0 +1,8 @@
## Example plugins
This folder contains example plugins. To run the plugins in this folder, use the `--run-examples` flag, via
```
yarn start --run-examples
```

View file

@ -0,0 +1,8 @@
## Demo search strategy
This example registers a custom search strategy that simply takes a name string in the request and returns the
string `Hello {name}`
To see the demo search strategy in action, navigate to the `Search explorer` app.
To run these examples, use the command `yarn start --run-examples`.

View file

@ -17,10 +17,7 @@
* under the License.
*/
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
} from '../../../../../src/plugins/data/public';
import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public';
export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY';

View file

@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"kbn": "node ../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {

View file

@ -22,8 +22,8 @@ import {
ISearchContext,
SYNC_SEARCH_STRATEGY,
ISearchGeneric,
} from '../../../../../src/plugins/data/public';
import { TSearchStrategyProvider, ISearchStrategy } from '../../../../../src/plugins/data/public';
} from '../../../src/plugins/data/public';
import { TSearchStrategyProvider, ISearchStrategy } from '../../../src/plugins/data/public';
import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common';

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public';
import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../src/core/public';
import { DataPublicPluginSetup } from '../../../src/plugins/data/public';
import { Plugin, CoreSetup, PluginInitializerContext } from '../../../src/core/public';
import { DEMO_SEARCH_STRATEGY } from '../common';
import { demoClientSearchStrategyProvider } from './demo_search_strategy';
import { IDemoRequest, IDemoResponse } from '../common';
@ -36,7 +36,7 @@ interface DemoDataSearchSetupDependencies {
* If the caller does not pass in the right `request` shape, typescript will
* complain. The caller will also get a typed response.
*/
declare module '../../../../../src/plugins/data/public' {
declare module '../../../src/plugins/data/public' {
export interface IRequestTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoRequest;
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { TSearchStrategyProvider } from 'src/plugins/data/server';
import { TSearchStrategyProvider } from '../../../src/plugins/data/server';
import { DEMO_SEARCH_STRATEGY } from '../common';
export const demoSearchStrategyProvider: TSearchStrategyProvider<typeof DEMO_SEARCH_STRATEGY> = () => {

View file

@ -35,7 +35,7 @@ interface IDemoSearchExplorerDeps {
* If the caller does not pass in the right `request` shape, typescript will
* complain. The caller will also get a typed response.
*/
declare module '../../../../../src/plugins/data/server' {
declare module '../../../src/plugins/data/server' {
export interface IRequestTypesMap {
[DEMO_SEARCH_STRATEGY]: IDemoRequest;
}

View file

@ -1,5 +1,5 @@
{
"extends": "../../../../tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
@ -10,7 +10,7 @@
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*"
"../../typings/**/*"
],
"exclude": []
}

View file

@ -0,0 +1,8 @@
## Search explorer
This example search explorer app shows how to use different search strategies in order to retrieve data.
One demo uses the built in elasticsearch search strategy, and runs a search against data in elasticsearch. The
other demo uses the custom demo search strategy, a custom search strategy registerd inside the [demo_search plugin](../demo_search).
To run this example, use the command `yarn start --run-examples`.

View file

@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"kbn": "node ../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {

View file

@ -28,7 +28,7 @@ import {
EuiSideNav,
} from '@elastic/eui';
import { AppMountContext, AppMountParameters } from '../../../../../src/core/public';
import { AppMountContext, AppMountParameters } from '../../../src/core/public';
import { EsSearchTest } from './es_strategy';
import { Page } from './page';
import { DemoStrategy } from './demo_strategy';

View file

@ -25,7 +25,7 @@ import {
EuiFlexGroup,
EuiFieldText,
} from '@elastic/eui';
import { ISearchGeneric } from '../../../../../src/plugins/data/public';
import { ISearchGeneric } from '../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';

View file

@ -21,10 +21,7 @@ import React from 'react';
import { EuiButton, EuiCodeBlock, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui';
import { EuiProgress } from '@elastic/eui';
import { Observable } from 'rxjs';
import {
IKibanaSearchResponse,
IKibanaSearchRequest,
} from '../../../../../src/plugins/data/public';
import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../../src/plugins/data/public';
interface Props {
request: IKibanaSearchRequest;

View file

@ -29,19 +29,19 @@ import {
ISearchGeneric,
IEsSearchResponse,
IEsSearchRequest,
} from '../../../../../src/plugins/data/public';
} from '../../../src/plugins/data/public';
import { DoSearch } from './do_search';
import { GuideSection } from './guide_section';
// @ts-ignore
import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_service';
import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_service';
// @ts-ignore
import serverStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_strategy';
import serverStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_strategy';
// @ts-ignore
import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_service';
import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_service';
// @ts-ignore
import publicStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_strategy';
import publicStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_strategy';
interface Props {
search: ISearchGeneric;

View file

@ -18,7 +18,7 @@
*/
import { Plugin, CoreSetup } from 'kibana/public';
import { ISearchAppMountContext } from '../../../../../src/plugins/data/public';
import { ISearchAppMountContext } from '../../../src/plugins/data/public';
declare module 'kibana/public' {
interface AppMountContext {

View file

@ -20,22 +20,22 @@ import React from 'react';
import { GuideSection } from './guide_section';
// @ts-ignore
import publicSetupContract from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_setup';
import publicSetupContract from '!!raw-loader!./../../../src/plugins/data/public/search/i_search_setup';
// @ts-ignore
import publicSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_strategy';
import publicSearchStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/i_search_strategy';
// @ts-ignore
import publicSearch from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search';
import publicSearch from '!!raw-loader!./../../../src/plugins/data/public/search/i_search';
// @ts-ignore
import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/search_service';
import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/search_service';
// @ts-ignore
import serverSetupContract from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_setup';
import serverSetupContract from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_setup';
// @ts-ignore
import serverSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_strategy';
import serverSearchStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_strategy';
// @ts-ignore
import serverSearch from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search';
import serverSearch from '!!raw-loader!./../../../src/plugins/data/server/search/i_search';
// @ts-ignore
import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/search_service';
import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/search_service';
export const SearchApiPage = () => (
<GuideSection

View file

@ -1,5 +1,5 @@
{
"extends": "../../../../tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
@ -9,7 +9,7 @@
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*",
"../../typings/**/*",
],
"exclude": []
}

View file

@ -97,6 +97,7 @@
"packages/*",
"x-pack",
"x-pack/legacy/plugins/*",
"examples/*",
"test/plugin_functional/plugins/*",
"test/interpreter_functional/plugins/*"
],

View file

@ -1,95 +1,95 @@
/*
* 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.
*/
const { resolve } = require('path');
const del = require('del');
const supportsColor = require('supports-color');
const { run, withProcRunner } = require('@kbn/dev-utils');
const ROOT_DIR = resolve(__dirname, '..');
const BUILD_DIR = resolve(ROOT_DIR, 'target');
const padRight = (width, str) =>
str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`;
run(
async ({ log, flags }) => {
await withProcRunner(log, async proc => {
log.info('Deleting old output');
await del(BUILD_DIR);
const cwd = ROOT_DIR;
const env = { ...process.env };
if (supportsColor.stdout) {
env.FORCE_COLOR = 'true';
}
log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`);
await Promise.all([
...['web', 'node'].map(subTask =>
proc.run(padRight(10, `babel:${subTask}`), {
cmd: 'babel',
args: [
'src',
'--config-file',
require.resolve('../babel.config.js'),
'--out-dir',
resolve(BUILD_DIR, subTask),
'--extensions',
'.ts,.js,.tsx',
...(flags.watch ? ['--watch'] : ['--quiet']),
...(flags['source-maps'] ? ['--source-map', 'inline'] : []),
],
wait: true,
env: {
...env,
BABEL_ENV: subTask,
},
cwd,
})
),
proc.run(padRight(10, 'tsc'), {
cmd: 'tsc',
args: [
'--emitDeclarationOnly',
...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []),
...(flags['source-maps'] ? ['--declarationMap', 'true'] : []),
],
wait: true,
env,
cwd,
}),
]);
log.success('Complete');
});
},
{
description: 'Simple build tool for @kbn/analytics package',
flags: {
boolean: ['watch', 'source-maps'],
help: `
--watch Run in watch mode
--source-maps Include sourcemaps
`,
},
}
);
/*
* 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.
*/
const { resolve } = require('path');
const del = require('del');
const supportsColor = require('supports-color');
const { run, withProcRunner } = require('@kbn/dev-utils');
const ROOT_DIR = resolve(__dirname, '..');
const BUILD_DIR = resolve(ROOT_DIR, 'target');
const padRight = (width, str) =>
str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`;
run(
async ({ log, flags }) => {
await withProcRunner(log, async proc => {
log.info('Deleting old output');
await del(BUILD_DIR);
const cwd = ROOT_DIR;
const env = { ...process.env };
if (supportsColor.stdout) {
env.FORCE_COLOR = 'true';
}
log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`);
await Promise.all([
...['web', 'node'].map(subTask =>
proc.run(padRight(10, `babel:${subTask}`), {
cmd: 'babel',
args: [
'src',
'--config-file',
require.resolve('../babel.config.js'),
'--out-dir',
resolve(BUILD_DIR, subTask),
'--extensions',
'.ts,.js,.tsx',
...(flags.watch ? ['--watch'] : ['--quiet']),
...(flags['source-maps'] ? ['--source-maps', 'inline'] : []),
],
wait: true,
env: {
...env,
BABEL_ENV: subTask,
},
cwd,
})
),
proc.run(padRight(10, 'tsc'), {
cmd: 'tsc',
args: [
'--emitDeclarationOnly',
...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []),
...(flags['source-maps'] ? ['--declarationMap', 'true'] : []),
],
wait: true,
env,
cwd,
}),
]);
log.success('Complete');
});
},
{
description: 'Simple build tool for @kbn/analytics package',
flags: {
boolean: ['watch', 'source-maps'],
help: `
--watch Run in watch mode
--source-maps Include sourcemaps
`,
},
}
);

View file

@ -55,7 +55,7 @@ run(
'--extensions',
'.ts,.js,.tsx',
...(flags.watch ? ['--watch'] : ['--quiet']),
...(flags['source-maps'] ? ['--source-map', 'inline'] : []),
...(flags['source-maps'] ? ['--source-maps', 'inline'] : []),
],
wait: true,
env: {

View file

@ -23185,6 +23185,7 @@ function getProjectPaths(rootPath, options = {}) {
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*'));
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*'));
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*'));
if (!ossOnly) {
projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack'));

View file

@ -44,6 +44,7 @@ export function getProjectPaths(rootPath: string, options: IProjectPathOptions =
// correct and the expect behavior.
projectPaths.push(resolve(rootPath, 'test/plugin_functional/plugins/*'));
projectPaths.push(resolve(rootPath, 'test/interpreter_functional/plugins/*'));
projectPaths.push(resolve(rootPath, 'examples/*'));
if (!ossOnly) {
projectPaths.push(resolve(rootPath, 'x-pack'));

View file

@ -13,6 +13,7 @@
'x-pack/package.json',
'x-pack/legacy/plugins/*/package.json',
'packages/*/package.json',
'examples/*/package.json',
'test/plugin_functional/plugins/*/package.json',
'test/interpreter_functional/plugins/*/package.json',
],

View file

@ -24,4 +24,5 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/plugin_functional/config.js'),
require.resolve('../test/interpreter_functional/config.ts'),
require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'),
require.resolve('../test/examples/config.js')
]);

View file

@ -144,6 +144,11 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
set('plugins.paths', _.compact([].concat(
get('plugins.paths'),
opts.pluginPath,
opts.runExamples ? [
// Ideally this would automatically include all plugins in the examples dir
fromRoot('examples/demo_search'),
fromRoot('examples/search_explorer'),
] : [],
XPACK_INSTALLED && !opts.oss
? [XPACK_DIR]
@ -201,7 +206,8 @@ export default function (program) {
if (!IS_KIBANA_DISTRIBUTABLE) {
command
.option('--oss', 'Start Kibana without X-Pack');
.option('--oss', 'Start Kibana without X-Pack')
.option('--run-examples', 'Adds plugin paths for all the Kibana example plugins and runs with no base path');
}
if (CAN_CLUSTER) {
@ -238,7 +244,12 @@ export default function (program) {
silent: !!opts.silent,
watch: !!opts.watch,
repl: !!opts.repl,
basePath: !!opts.basePath,
// We want to run without base path when the `--run-examples` flag is given so that we can use local
// links in other documentation sources, like "View this tutorial [here](http://localhost:5601/app/tutorial/xyz)".
// We can tell users they only have to run with `yarn start --run-examples` to get those
// local links to work. Similar to what we do for "View in Console" links in our
// elastic.co links.
basePath: opts.runExamples ? false : !!opts.basePath,
optimize: !!opts.optimize,
oss: !!opts.oss
},

View file

@ -58,6 +58,15 @@ function getKbnPrecommitGitHookScript(rootPath, nodeHome, platform) {
set -euo pipefail
# Make it possible to terminate pre commit hook
# using ctrl-c so nothing else would happen or be
# sent to the output.
#
# The correct exit code on that situation
# according the linux documentation project is 130
# https://www.tldp.org/LDP/abs/html/exitcodes.html
trap "exit 130" SIGINT
has_node() {
command -v node >/dev/null 2>&1
}

View file

@ -28,6 +28,7 @@ export const PACKAGE_GLOBS = [
'x-pack/package.json',
'x-pack/legacy/plugins/*/package.json',
'packages/*/package.json',
'examples/*/package.json',
'test/plugin_functional/plugins/*/package.json',
'test/interpreter_functional/plugins/*/package.json',
];

View file

@ -40,6 +40,9 @@ export const PROJECTS = [
...glob
.sync('packages/*/tsconfig.json', { cwd: REPO_ROOT })
.map(path => new Project(resolve(REPO_ROOT, path))),
...glob
.sync('examples/*/tsconfig.json', { cwd: REPO_ROOT })
.map(path => new Project(resolve(REPO_ROOT, path))),
...glob
.sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT })
.map(path => new Project(resolve(REPO_ROOT, path))),

View file

@ -193,78 +193,160 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = `
textComponent={Symbol(react.fragment)}
>
<PseudoLocaleWrapper>
<EuiIcon
color="subdued"
size="xxl"
type="dashboardApp"
<EuiPage
className="dshStartScreen"
restrictWidth="36em"
>
<EuiIconEmpty
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
style={null}
>
<svg
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
<h2>
This dashboard is empty. Lets fill it up!
</h2>
<p>
<span>
Click the
<EuiLink
aria-label="Add visualization"
data-test-subj="emptyDashboardAddPanelButton"
onClick={[MockFunction]}
>
<button
aria-label="Add visualization"
className="euiLink euiLink--primary"
data-test-subj="emptyDashboardAddPanelButton"
onClick={[MockFunction]}
type="button"
>
Add
</button>
</EuiLink>
button in the menu bar above to add a visualization to the dashboard.
</span>
</p>
<p
className="linkToVisualizeParagraph"
>
<FormattedMessage
defaultMessage="If you haven't set up any visualizations yet, {visualizeAppLink} to create your first visualization"
id="kbn.dashboard.addVisualizationDescription3"
values={
<div
className="euiPage euiPage--restrictWidth-custom dshStartScreen"
style={
Object {
"visualizeAppLink": <a
className="euiLink"
href="#/visualize"
>
visit the Visualize app
</a>,
"maxWidth": "36em",
}
}
>
If you haven't set up any visualizations yet,
<a
className="euiLink"
href="#/visualize"
<EuiPageBody
component="main"
restrictWidth={false}
>
visit the Visualize app
</a>
to create your first visualization
</FormattedMessage>
</p>
<main
className="euiPageBody"
>
<EuiPageContent
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<EuiPanel
className="euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter"
paddingSize="l"
>
<div
className="euiPanel euiPanel--paddingLarge euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter"
>
<EuiIcon
color="subdued"
size="xxl"
type="dashboardApp"
>
<EuiIconEmpty
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
style={null}
>
<svg
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
<EuiSpacer
size="s"
>
<div
className="euiSpacer euiSpacer--s"
/>
</EuiSpacer>
<EuiText
grow={true}
>
<div
className="euiText euiText--medium"
>
<h2
key="0.5"
>
This dashboard is empty. Lets fill it up!
</h2>
</div>
</EuiText>
<EuiSpacer
size="m"
>
<div
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<EuiText
size="m"
>
<div
className="euiText euiText--medium"
>
<p>
Click the
<EuiLink
aria-label="Add visualization"
data-test-subj="emptyDashboardAddPanelButton"
onClick={[MockFunction]}
>
<button
aria-label="Add visualization"
className="euiLink euiLink--primary"
data-test-subj="emptyDashboardAddPanelButton"
onClick={[MockFunction]}
type="button"
>
Add
</button>
</EuiLink>
button in the menu bar above to add a visualization to the dashboard.
</p>
</div>
</EuiText>
<EuiSpacer
size="m"
>
<div
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<EuiText
data-test-subj="linkToVisualizeParagraph"
>
<div
className="euiText euiText--medium"
data-test-subj="linkToVisualizeParagraph"
>
<p>
<FormattedMessage
defaultMessage="If you haven't set up any visualizations yet, {visualizeAppLink} to create your first visualization"
id="kbn.dashboard.addVisualizationDescription3"
values={
Object {
"visualizeAppLink": <a
className="euiLink"
href="#/visualize"
>
visit the Visualize app
</a>,
}
}
>
If you haven't set up any visualizations yet,
<a
className="euiLink"
href="#/visualize"
>
visit the Visualize app
</a>
to create your first visualization
</FormattedMessage>
</p>
</div>
</EuiText>
</div>
</EuiPanel>
</EuiPageContent>
</main>
</EuiPageBody>
</div>
</EuiPage>
</PseudoLocaleWrapper>
</IntlProvider>
</I18nProvider>
@ -464,51 +546,119 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`]
textComponent={Symbol(react.fragment)}
>
<PseudoLocaleWrapper>
<EuiIcon
color="subdued"
size="xxl"
type="dashboardApp"
<EuiPage
className="dshStartScreen"
restrictWidth="36em"
>
<EuiIconEmpty
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
style={null}
<div
className="euiPage euiPage--restrictWidth-custom dshStartScreen"
style={
Object {
"maxWidth": "36em",
}
}
>
<svg
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
<h2>
This dashboard is empty. Lets fill it up!
</h2>
<p>
<span>
Click the
<EuiLink
aria-label="Edit dashboard"
data-test-subj=""
onClick={[MockFunction]}
<EuiPageBody
component="main"
restrictWidth={false}
>
<button
aria-label="Edit dashboard"
className="euiLink euiLink--primary"
data-test-subj=""
onClick={[MockFunction]}
type="button"
<main
className="euiPageBody"
>
Edit
</button>
</EuiLink>
button in the menu bar above to start working on your new dashboard.
</span>
</p>
<EuiPageContent
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<EuiPanel
className="euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter"
paddingSize="l"
>
<div
className="euiPanel euiPanel--paddingLarge euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter"
>
<EuiIcon
color="subdued"
size="xxl"
type="dashboardApp"
>
<EuiIconEmpty
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
style={null}
>
<svg
className="euiIcon euiIcon--xxLarge euiIcon--subdued euiIcon--app euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</EuiIconEmpty>
</EuiIcon>
<EuiSpacer
size="s"
>
<div
className="euiSpacer euiSpacer--s"
/>
</EuiSpacer>
<EuiText
grow={true}
>
<div
className="euiText euiText--medium"
>
<h2
key="0.5"
>
This dashboard is empty. Lets fill it up!
</h2>
</div>
</EuiText>
<EuiSpacer
size="m"
>
<div
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<EuiText
size="m"
>
<div
className="euiText euiText--medium"
>
<p>
Click the
<EuiLink
aria-label="Edit dashboard"
data-test-subj=""
onClick={[MockFunction]}
>
<button
aria-label="Edit dashboard"
className="euiLink euiLink--primary"
data-test-subj=""
onClick={[MockFunction]}
type="button"
>
Edit
</button>
</EuiLink>
button in the menu bar above to start working on your new dashboard.
</p>
</div>
</EuiText>
</div>
</EuiPanel>
</EuiPageContent>
</main>
</EuiPageBody>
</div>
</EuiPage>
</PseudoLocaleWrapper>
</IntlProvider>
</I18nProvider>

View file

@ -18,7 +18,9 @@
*/
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { DashboardEmptyScreen, Props } from '../dashboard_empty_screen';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from '../dashboard_empty_screen';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
describe('DashboardEmptyScreen', () => {
const defaultProps = {
@ -26,7 +28,7 @@ describe('DashboardEmptyScreen', () => {
onLinkClick: jest.fn(),
};
function mountComponent(props?: Props) {
function mountComponent(props?: DashboardEmptyScreenProps) {
const compProps = props || defaultProps;
const comp = mountWithIntl(<DashboardEmptyScreen {...compProps} />);
return comp;
@ -35,14 +37,14 @@ describe('DashboardEmptyScreen', () => {
test('renders correctly with visualize paragraph', () => {
const component = mountComponent();
expect(component).toMatchSnapshot();
const paragraph = component.find('.linkToVisualizeParagraph');
const paragraph = findTestSubject(component, 'linkToVisualizeParagraph');
expect(paragraph.length).toBe(1);
});
test('renders correctly without visualize paragraph', () => {
const component = mountComponent({ ...defaultProps, ...{ showLinkToVisualize: false } });
expect(component).toMatchSnapshot();
const paragraph = component.find('.linkToVisualizeParagraph');
const paragraph = findTestSubject(component, 'linkToVisualizeParagraph');
expect(paragraph.length).toBe(0);
});
});

View file

@ -6,9 +6,5 @@
.dshStartScreen {
text-align: center;
padding: $euiSize;
> * {
max-width: 36em !important;
}
padding: $euiSizeS;
}

View file

@ -131,7 +131,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav
'app/dashboard/State',
'app/dashboard/ConfirmModal',
'app/dashboard/icon',
'app/dashboard/emptyScreen',
]);
return dashboardAngularModule;
}

View file

@ -48,24 +48,6 @@
>
</kbn-top-nav>
<div ng-show="getShouldShowEditHelp() || getShouldShowViewHelp()" class="dshStartScreen">
<div class="euiPanel euiPanel--paddingLarge euiPageContent euiPageContent--horizontalCenter">
<br><br>
<div ng-show="getShouldShowEditHelp()" class="euiText">
<dashboard-empty-screen on-link-click="showAddPanel"
show-link-to-visualize="true"
/>
</div>
<div ng-show="getShouldShowViewHelp()" class="euiText">
<dashboard-empty-screen show-link-to-visualize="false"
on-link-click="enterEditMode"
/>
</div>
</div>
</div>
<h1 class="euiScreenReaderOnly">{{screenTitle}}</h1>
<div id="dashboardViewport"></div>

View file

@ -21,9 +21,10 @@ import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import React from 'react';
import angular from 'angular';
import { uniq } from 'lodash';
import { uniq, noop } from 'lodash';
import { Subscription } from 'rxjs';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen';
import {
subscribeWithScope,
@ -35,14 +36,11 @@ import {
AppStateClass as TAppStateClass,
KbnUrl,
SaveOptions,
SavedObjectFinder,
unhashUrl,
} from './legacy_imports';
import { FilterStateManager, IndexPattern } from '../../../data/public';
import { Query, SavedQuery, IndexPatternsContract } from '../../../../../plugins/data/public';
import './dashboard_empty_screen_directive';
import {
DashboardContainer,
DASHBOARD_CONTAINER_TYPE,
@ -71,6 +69,10 @@ import { DashboardAppScope } from './dashboard_app';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize/embeddable';
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
import { RenderDeps } from './application';
import {
SavedObjectFinderProps,
SavedObjectFinderUi,
} from '../../../../../plugins/kibana_react/public';
export interface DashboardAppControllerDependencies extends RenderDeps {
$scope: DashboardAppScope;
@ -115,7 +117,7 @@ export class DashboardAppController {
timefilter: { timefilter },
},
},
core: { notifications, overlays, chrome, injectedMetadata },
core: { notifications, overlays, chrome, injectedMetadata, uiSettings, savedObjects },
}: DashboardAppControllerDependencies) {
new FilterStateManager(globalState, getAppState, filterManager);
const queryFilter = filterManager;
@ -143,6 +145,16 @@ export class DashboardAppController {
}
$scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean;
$scope.getShouldShowEditHelp = () =>
!dashboardStateManager.getPanels().length &&
dashboardStateManager.getIsEditMode() &&
!dashboardConfig.getHideWriteControls();
$scope.getShouldShowViewHelp = () =>
!dashboardStateManager.getPanels().length &&
dashboardStateManager.getIsViewMode() &&
!dashboardConfig.getHideWriteControls();
const updateIndexPatterns = (container?: DashboardContainer) => {
if (!container || isErrorEmbeddable(container)) {
return;
@ -171,6 +183,17 @@ export class DashboardAppController {
}
};
const getEmptyScreenProps = (shouldShowEditHelp: boolean): DashboardEmptyScreenProps => {
const emptyScreenProps: DashboardEmptyScreenProps = {
onLinkClick: shouldShowEditHelp ? $scope.showAddPanel : $scope.enterEditMode,
showLinkToVisualize: shouldShowEditHelp,
};
if (shouldShowEditHelp) {
emptyScreenProps.onVisualizeClick = noop;
}
return emptyScreenProps;
};
const getDashboardInput = (): DashboardContainerInput => {
const embeddablesMap: {
[key: string]: DashboardPanelState;
@ -182,6 +205,8 @@ export class DashboardAppController {
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
expandedPanelId = dashboardContainer.getInput().expandedPanelId;
}
const shouldShowEditHelp = $scope.getShouldShowEditHelp();
const shouldShowViewHelp = $scope.getShouldShowViewHelp();
return {
id: dashboardStateManager.savedDashboard.id || '',
filters: queryFilter.getFilters(),
@ -194,6 +219,7 @@ export class DashboardAppController {
viewMode: dashboardStateManager.getViewMode(),
panels: embeddablesMap,
isFullScreenMode: dashboardStateManager.getFullScreenMode(),
isEmptyState: shouldShowEditHelp || shouldShowViewHelp,
useMargins: dashboardStateManager.getUseMargins(),
lastReloadRequestTime,
title: dashboardStateManager.getTitle(),
@ -234,6 +260,15 @@ export class DashboardAppController {
if (!isErrorEmbeddable(container)) {
dashboardContainer = container;
dashboardContainer.renderEmpty = () => {
const shouldShowEditHelp = $scope.getShouldShowEditHelp();
const shouldShowViewHelp = $scope.getShouldShowViewHelp();
const isEmptyState = shouldShowEditHelp || shouldShowViewHelp;
return isEmptyState ? (
<DashboardEmptyScreen {...getEmptyScreenProps(shouldShowEditHelp)} />
) : null;
};
updateIndexPatterns(dashboardContainer);
outputSubscription = dashboardContainer.getOutput$().subscribe(() => {
@ -334,15 +369,6 @@ export class DashboardAppController {
updateBreadcrumbs();
dashboardStateManager.registerChangeListener(updateBreadcrumbs);
$scope.getShouldShowEditHelp = () =>
!dashboardStateManager.getPanels().length &&
dashboardStateManager.getIsEditMode() &&
!dashboardConfig.getHideWriteControls();
$scope.getShouldShowViewHelp = () =>
!dashboardStateManager.getPanels().length &&
dashboardStateManager.getIsViewMode() &&
!dashboardConfig.getHideWriteControls();
const getChangesFromAppStateForContainerState = () => {
const appStateDashboardInput = getDashboardInput();
if (!dashboardContainer || isErrorEmbeddable(dashboardContainer)) {
@ -718,6 +744,10 @@ export class DashboardAppController {
};
navActions[TopNavIds.ADD] = () => {
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
const SavedObjectFinder = (props: SavedObjectFinderProps) => (
<SavedObjectFinderUi {...props} savedObjects={savedObjects} uiSettings={uiSettings} />
);
openAddPanelFlyout({
embeddable: dashboardContainer,
getAllFactories: embeddables.getEmbeddableFactories,
@ -729,6 +759,8 @@ export class DashboardAppController {
}
};
navActions[TopNavIds.VISUALIZE] = async () => {};
navActions[TopNavIds.OPTIONS] = anchorElement => {
showOptionsPopover({
anchorElement,

View file

@ -18,29 +18,43 @@
*/
import React from 'react';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
import { EuiIcon, EuiLink } from '@elastic/eui';
import {
EuiIcon,
EuiLink,
EuiSpacer,
EuiPageContent,
EuiPageBody,
EuiPage,
EuiText,
} from '@elastic/eui';
import * as constants from './dashboard_empty_screen_constants';
export interface Props {
export interface DashboardEmptyScreenProps {
showLinkToVisualize: boolean;
onLinkClick: () => void;
onVisualizeClick?: () => void;
}
export function DashboardEmptyScreen({ showLinkToVisualize, onLinkClick }: Props) {
export function DashboardEmptyScreen({
showLinkToVisualize,
onLinkClick,
}: DashboardEmptyScreenProps) {
const linkToVisualizeParagraph = (
<p className="linkToVisualizeParagraph">
<FormattedMessage
id="kbn.dashboard.addVisualizationDescription3"
defaultMessage="If you haven't set up any visualizations yet, {visualizeAppLink} to create your first visualization"
values={{
visualizeAppLink: (
<a className="euiLink" href="#/visualize">
{constants.visualizeAppLinkTest}
</a>
),
}}
/>
</p>
<EuiText data-test-subj="linkToVisualizeParagraph">
<p>
<FormattedMessage
id="kbn.dashboard.addVisualizationDescription3"
defaultMessage="If you haven't set up any visualizations yet, {visualizeAppLink} to create your first visualization"
values={{
visualizeAppLink: (
<a className="euiLink" href="#/visualize">
{constants.visualizeAppLinkTest}
</a>
),
}}
/>
</p>
</EuiText>
);
const paragraph = (
description1: string,
@ -50,15 +64,15 @@ export function DashboardEmptyScreen({ showLinkToVisualize, onLinkClick }: Props
dataTestSubj?: string
) => {
return (
<p>
<span>
<EuiText size="m">
<p>
{description1}
<EuiLink onClick={onLinkClick} aria-label={ariaLabel} data-test-subj={dataTestSubj || ''}>
{linkText}
</EuiLink>
{description2}
</span>
</p>
</p>
</EuiText>
);
};
const addVisualizationParagraph = (
@ -70,6 +84,7 @@ export function DashboardEmptyScreen({ showLinkToVisualize, onLinkClick }: Props
constants.addVisualizationLinkAriaLabel,
'emptyDashboardAddPanelButton'
)}
<EuiSpacer size="m" />
{linkToVisualizeParagraph}
</React.Fragment>
);
@ -81,11 +96,19 @@ export function DashboardEmptyScreen({ showLinkToVisualize, onLinkClick }: Props
);
return (
<I18nProvider>
<React.Fragment>
<EuiIcon type="dashboardApp" size="xxl" color="subdued" />
<h2>{constants.fillDashboardTitle}</h2>
{showLinkToVisualize ? addVisualizationParagraph : enterEditModeParagraph}
</React.Fragment>
<EuiPage className="dshStartScreen" restrictWidth={'36em'}>
<EuiPageBody>
<EuiPageContent verticalPosition="center" horizontalPosition="center">
<EuiIcon type="dashboardApp" size="xxl" color="subdued" />
<EuiSpacer size="s" />
<EuiText grow={true}>
<h2 key={0.5}>{constants.fillDashboardTitle}</h2>
</EuiText>
<EuiSpacer size="m" />
{showLinkToVisualize ? addVisualizationParagraph : enterEditModeParagraph}
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</I18nProvider>
);
}

View file

@ -1,30 +0,0 @@
/*
* 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.
*/
// @ts-ignore
import angular from 'angular';
import { DashboardEmptyScreen } from './dashboard_empty_screen';
angular
.module('app/dashboard/emptyScreen', ['react'])
.directive('dashboardEmptyScreen', function(reactDirective: any) {
return reactDirective(DashboardEmptyScreen, [
['showLinkToVisualize', { watchDepth: 'value' }],
['onLinkClick', { watchDepth: 'reference' }],
]);
});

View file

@ -65,4 +65,3 @@ export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_mon
export { ensureDefaultIndexPattern } from 'ui/legacy_compat';
export { unhashUrl } from '../../../../../plugins/kibana_utils/public';
export { IInjector } from 'ui/chrome';
export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';

View file

@ -26,4 +26,5 @@ export const TopNavIds = {
ENTER_EDIT_MODE: 'enterEditMode',
CLONE: 'clone',
FULL_SCREEN: 'fullScreenMode',
VISUALIZE: 'visualize',
};

View file

@ -26,7 +26,7 @@ exports[`render 1`] = `
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<SavedObjectFinder
<SavedObjectFinderUi
noItemsMessage={
<FormattedMessage
defaultMessage="No matching searches found."
@ -44,6 +44,8 @@ exports[`render 1`] = `
},
]
}
savedObjects={Object {}}
uiSettings={Object {}}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -32,11 +32,16 @@ import {
EuiFlyoutBody,
EuiTitle,
} from '@elastic/eui';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import { SavedObjectFinderUi } from '../../../../../../../plugins/kibana_react/public';
import { getServices } from '../../kibana_services';
const SEARCH_OBJECT_TYPE = 'search';
export function OpenSearchPanel(props) {
const {
core: { uiSettings, savedObjects },
} = getServices();
return (
<EuiFlyout ownFocus onClose={props.onClose} data-test-subj="loadSearchForm">
<EuiFlyoutHeader hasBorder>
@ -50,7 +55,7 @@ export function OpenSearchPanel(props) {
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<SavedObjectFinder
<SavedObjectFinderUi
noItemsMessage={
<FormattedMessage
id="kbn.discover.topNav.openSearchPanel.noSearchesFoundDescription"
@ -70,6 +75,8 @@ export function OpenSearchPanel(props) {
window.location.assign(props.makeUrl(id));
props.onClose();
}}
uiSettings={uiSettings}
savedObjects={savedObjects}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -23,7 +23,7 @@ import { shallow } from 'enzyme';
jest.mock('../../kibana_services', () => {
return {
getServices: () => ({
SavedObjectFinder: jest.fn()
core: { uiSettings: {}, savedObjects: {} },
}),
};
});

View file

@ -199,7 +199,8 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
editorParams: ['addToDashboard'],
},
npStart.core.http.basePath.prepend,
npStart.core.uiSettings
npStart.core.uiSettings,
npStart.core.savedObjects
);
}
return undefined;

View file

@ -45,7 +45,6 @@ export { PrivateProvider } from 'ui/private/private';
export { SavedObjectRegistryProvider } from 'ui/saved_objects';
export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
export { subscribeWithScope } from 'ui/utils/subscribe_with_scope';

View file

@ -16,6 +16,7 @@
vis-types-registry="listingController.visTypeRegistry"
add-base-path="listingController.addBasePath"
ui-settings="listingController.uiSettings"
saved-objects="listingController.savedObjects"
></new-vis-modal>
</div>

View file

@ -34,6 +34,7 @@ export function initListingDirective(app) {
['onClose', { watchDepth: 'reference' }],
['addBasePath', { watchDepth: 'reference' }],
['uiSettings', { watchDepth: 'reference' }],
['savedObjects', { watchDepth: 'reference' }],
'isOpen',
])
);
@ -54,7 +55,7 @@ export function VisualizeListingController($injector, createNewVis) {
toastNotifications,
uiSettings,
visualizations,
core: { docLinks },
core: { docLinks, savedObjects },
} = getServices();
const kbnUrl = $injector.get('kbnUrl');
@ -64,6 +65,7 @@ export function VisualizeListingController($injector, createNewVis) {
this.showNewVisModal = false;
this.addBasePath = addBasePath;
this.uiSettings = uiSettings;
this.savedObjects = savedObjects;
this.createNewVis = () => {
this.showNewVisModal = true;

View file

@ -108,6 +108,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
}
isOpen={true}
onClose={[Function]}
savedObjects={Object {}}
uiSettings={
Object {
"get": [MockFunction] {
@ -1413,6 +1414,7 @@ exports[`NewVisModal should render as expected 1`] = `
}
isOpen={true}
onClose={[Function]}
savedObjects={Object {}}
uiSettings={
Object {
"get": [MockFunction] {

View file

@ -29,6 +29,7 @@ jest.mock('../legacy_imports', () => ({
}));
import { NewVisModal } from './new_vis_modal';
import { SavedObjectsStart } from 'kibana/public';
describe('NewVisModal', () => {
const defaultVisTypeParams = {
@ -76,6 +77,7 @@ describe('NewVisModal', () => {
visTypesRegistry={visTypes}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
expect(wrapper).toMatchSnapshot();
@ -89,6 +91,7 @@ describe('NewVisModal', () => {
visTypesRegistry={visTypes}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
expect(wrapper.find('[data-test-subj="visType-vis"]').exists()).toBe(true);
@ -104,6 +107,7 @@ describe('NewVisModal', () => {
visTypesRegistry={visTypes}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
const visButton = wrapper.find('button[data-test-subj="visType-vis"]');
@ -121,6 +125,7 @@ describe('NewVisModal', () => {
editorParams={['foo=true', 'bar=42']}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
const visButton = wrapper.find('button[data-test-subj="visType-vis"]');
@ -138,6 +143,7 @@ describe('NewVisModal', () => {
visTypesRegistry={visTypes}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
const searchBox = wrapper.find('input[data-test-subj="filterVisType"]');
@ -156,6 +162,7 @@ describe('NewVisModal', () => {
visTypesRegistry={visTypes}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
expect(wrapper.find('[data-test-subj="visType-visExp"]').exists()).toBe(false);
@ -170,6 +177,7 @@ describe('NewVisModal', () => {
visTypesRegistry={visTypes}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={{} as SavedObjectsStart}
/>
);
expect(wrapper.find('[data-test-subj="visType-visExp"]').exists()).toBe(true);

View file

@ -22,7 +22,7 @@ import React from 'react';
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public';
import { VisType } from '../legacy_imports';
import { VisualizeConstants } from '../visualize_constants';
import { createUiStatsReporter, METRIC_TYPE } from '../../../../ui_metric/public';
@ -37,6 +37,7 @@ interface TypeSelectionProps {
editorParams?: string[];
addBasePath: (path: string) => string;
uiSettings: IUiSettingsClient;
savedObjects: SavedObjectsStart;
}
interface TypeSelectionState {
@ -81,7 +82,12 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
const selectionModal =
this.state.showSearchVisModal && this.state.visType ? (
<EuiModal onClose={this.onCloseModal} className="visNewVisSearchDialog">
<SearchSelection onSearchSelected={this.onSearchSelected} visType={this.state.visType} />
<SearchSelection
onSearchSelected={this.onSearchSelected}
visType={this.state.visType}
uiSettings={this.props.uiSettings}
savedObjects={this.props.savedObjects}
/>
</EuiModal>
) : (
<EuiModal

View file

@ -21,12 +21,16 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public';
import { SavedObjectFinder, VisType } from '../../legacy_imports';
import { SavedObjectFinderUi } from '../../../../../../../plugins/kibana_react/public';
import { VisType } from '../../legacy_imports';
interface SearchSelectionProps {
onSearchSelected: (searchId: string, searchType: string) => void;
visType: VisType;
uiSettings: IUiSettingsClient;
savedObjects: SavedObjectsStart;
}
export class SearchSelection extends React.Component<SearchSelectionProps> {
@ -50,7 +54,7 @@ export class SearchSelection extends React.Component<SearchSelectionProps> {
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<SavedObjectFinder
<SavedObjectFinderUi
key="searchSavedObjectFinder"
onChoose={this.props.onSearchSelected}
showFilter
@ -83,6 +87,8 @@ export class SearchSelection extends React.Component<SearchSelectionProps> {
},
]}
fixedPageSize={this.fixedPageSize}
uiSettings={this.props.uiSettings}
savedObjects={this.props.savedObjects}
/>
</EuiModalBody>
</React.Fragment>

View file

@ -21,7 +21,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { IUiSettingsClient } from 'kibana/public';
import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public';
import { NewVisModal } from './new_vis_modal';
import { TypesStart } from '../../../../visualizations/public/np_ready/public/types';
@ -33,7 +33,8 @@ export function showNewVisModal(
visTypeRegistry: TypesStart,
{ editorParams = [] }: ShowNewVisModalParams = {},
addBasePath: (path: string) => string,
uiSettings: IUiSettingsClient
uiSettings: IUiSettingsClient,
savedObjects: SavedObjectsStart
) {
const container = document.createElement('div');
const onClose = () => {
@ -51,6 +52,7 @@ export function showNewVisModal(
editorParams={editorParams}
addBasePath={addBasePath}
uiSettings={uiSettings}
savedObjects={savedObjects}
/>
</I18nProvider>
);

File diff suppressed because one or more lines are too long

View file

@ -38,7 +38,24 @@ exports[`TelemetryForm renders as expected when allows to change optIn status 1`
"defVal": true,
"description": <React.Fragment>
<p>
Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.
<FormattedMessage
defaultMessage="Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details."
id="telemetry.telemetryConfigAndLinkDescription"
values={
Object {
"privacyStatementLink": <ForwardRef
href="https://www.elastic.co/legal/privacy-statement"
target="_blank"
>
<FormattedMessage
defaultMessage="Privacy Statement"
id="telemetry.readOurUsageDataPrivacyStatementLinkText"
values={Object {}}
/>
</ForwardRef>,
}
}
/>
</p>
<p>
<ForwardRef
@ -51,18 +68,6 @@ exports[`TelemetryForm renders as expected when allows to change optIn status 1`
/>
</ForwardRef>
</p>
<p>
<ForwardRef
href="https://www.elastic.co/legal/privacy-statement"
target="_blank"
>
<FormattedMessage
defaultMessage="Read our usage data privacy statement"
id="telemetry.readOurUsageDataPrivacyStatementLinkText"
values={Object {}}
/>
</ForwardRef>
</p>
</React.Fragment>,
"type": "boolean",
"value": false,

View file

@ -29,7 +29,7 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { getConfigTelemetryDesc, PRIVACY_STATEMENT_URL } from '../../common/constants';
import { PRIVACY_STATEMENT_URL } from '../../common/constants';
import { OptInExampleFlyout } from './opt_in_details_component';
import { Field } from 'ui/management';
import { FormattedMessage } from '@kbn/i18n/react';
@ -162,7 +162,23 @@ export class TelemetryForm extends Component {
renderDescription = () => (
<Fragment>
<p>{getConfigTelemetryDesc()}</p>
<p>
<FormattedMessage
id="telemetry.telemetryConfigAndLinkDescription"
defaultMessage="Enabling data usage collection helps us manage and improve our products and services.
See our {privacyStatementLink} for more details."
values={{
privacyStatementLink: (
<EuiLink href={PRIVACY_STATEMENT_URL} target="_blank">
<FormattedMessage
id="telemetry.readOurUsageDataPrivacyStatementLinkText"
defaultMessage="Privacy Statement"
/>
</EuiLink>
)
}}
/>
</p>
<p>
<EuiLink onClick={this.toggleExample}>
<FormattedMessage
@ -171,14 +187,6 @@ export class TelemetryForm extends Component {
/>
</EuiLink>
</p>
<p>
<EuiLink href={PRIVACY_STATEMENT_URL} target="_blank">
<FormattedMessage
id="telemetry.readOurUsageDataPrivacyStatementLinkText"
defaultMessage="Read our usage data privacy statement"
/>
</EuiLink>
</p>
</Fragment>
)

View file

@ -1,108 +0,0 @@
/*
* 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 React from 'react';
import { npStart } from 'ui/new_platform';
import { IconType } from '@elastic/eui';
import { SavedObjectAttributes } from 'src/core/server';
import { SimpleSavedObject } from 'src/core/public';
import { SavedObjectFinder as SavedObjectFinderNP } from '../../../../../plugins/kibana_react/public';
/**
* DO NOT USE THIS COMPONENT, IT IS DEPRECATED.
* Use the one in `src/plugins/kibana_react` instead.
*/
export interface SavedObjectMetaData<T extends SavedObjectAttributes> {
type: string;
name: string;
getIconForSavedObject(savedObject: SimpleSavedObject<T>): IconType;
getTooltipForSavedObject?(savedObject: SimpleSavedObject<T>): string;
showSavedObject?(savedObject: SimpleSavedObject<T>): boolean;
}
interface BaseSavedObjectFinder {
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
onChoose?: (
id: SimpleSavedObject<SavedObjectAttributes>['id'],
type: SimpleSavedObject<SavedObjectAttributes>['type'],
name: string
) => void;
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
noItemsMessage?: React.ReactNode;
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
savedObjectMetaData: Array<SavedObjectMetaData<SavedObjectAttributes>>;
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
showFilter?: boolean;
}
interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder {
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
initialPageSize?: undefined;
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
fixedPageSize: number;
}
interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder {
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
initialPageSize?: 5 | 10 | 15 | 25;
/**
* @deprecated
*
* Use component in `src/plugins/kibana_react` instead.
*/
fixedPageSize?: undefined;
}
type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize;
export const SavedObjectFinder: React.FC<SavedObjectFinderProps> = props => (
<SavedObjectFinderNP
savedObjects={npStart.core.savedObjects}
uiSettings={npStart.core.uiSettings}
{...props}
/>
);

View file

@ -90,6 +90,8 @@ export type DashboardReactContext = KibanaReactContext<DashboardContainerOptions
export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
public readonly type = DASHBOARD_CONTAINER_TYPE;
public renderEmpty?: undefined | (() => React.ReactNode);
constructor(
initialInput: DashboardContainerInput,
private readonly options: DashboardContainerOptions,
@ -124,7 +126,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
ReactDOM.render(
<I18nProvider>
<KibanaContextProvider services={this.options}>
<DashboardViewport container={this} />
<DashboardViewport renderEmpty={this.renderEmpty} container={this} />
</KibanaContextProvider>
</I18nProvider>,
dom

View file

@ -34,7 +34,7 @@
.dshLayout-isMaximizedPanel {
height: 100% !important; /* 1. */
width: 100%;
position: absolute;
position: absolute !important;
}
/**

View file

@ -121,6 +121,24 @@ test('renders DashboardViewport with no visualizations', () => {
component.unmount();
});
test('renders DashboardEmptyScreen', () => {
const renderEmptyScreen = jest.fn();
const { props, options } = getProps({ renderEmpty: renderEmptyScreen });
props.container.updateInput({ isEmptyState: true });
const component = mount(
<I18nProvider>
<KibanaContextProvider services={options}>
<DashboardViewport {...props} />
</KibanaContextProvider>
</I18nProvider>
);
const dashboardEmptyScreenDiv = component.find('.dshDashboardEmptyScreen');
expect(dashboardEmptyScreenDiv.length).toBe(1);
expect(renderEmptyScreen).toHaveBeenCalled();
component.unmount();
});
test('renders exit full screen button when in full screen mode', async () => {
const { props, options } = getProps();
props.container.updateInput({ isFullScreenMode: true });
@ -153,6 +171,39 @@ test('renders exit full screen button when in full screen mode', async () => {
component.unmount();
});
test('renders exit full screen button when in full screen mode and empty screen', async () => {
const renderEmptyScreen = jest.fn();
renderEmptyScreen.mockReturnValue(React.createElement('div'));
const { props, options } = getProps({ renderEmpty: renderEmptyScreen });
props.container.updateInput({ isEmptyState: true, isFullScreenMode: true });
const component = mount(
<I18nProvider>
<KibanaContextProvider services={options}>
<DashboardViewport {...props} />
</KibanaContextProvider>
</I18nProvider>
);
expect(
(component
.find('.dshDashboardEmptyScreen')
.childAt(0)
.type() as any).name
).toBe('ExitFullScreenButton');
props.container.updateInput({ isFullScreenMode: false });
component.update();
await nextTick();
expect(
(component
.find('.dshDashboardEmptyScreen')
.childAt(0)
.type() as any).name
).not.toBe('ExitFullScreenButton');
component.unmount();
});
test('DashboardViewport unmount unsubscribes', async done => {
const { props, options } = getProps();
const component = mount(

View file

@ -26,6 +26,7 @@ import { context } from '../../../../kibana_react/public';
export interface DashboardViewportProps {
container: DashboardContainer;
renderEmpty?: () => React.ReactNode;
}
interface State {
@ -34,6 +35,7 @@ interface State {
title: string;
description?: string;
panels: { [key: string]: PanelState };
isEmptyState?: boolean;
}
export class DashboardViewport extends React.Component<DashboardViewportProps, State> {
@ -44,26 +46,40 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
private mounted: boolean = false;
constructor(props: DashboardViewportProps) {
super(props);
const { isFullScreenMode, panels, useMargins, title } = this.props.container.getInput();
const {
isFullScreenMode,
panels,
useMargins,
title,
isEmptyState,
} = this.props.container.getInput();
this.state = {
isFullScreenMode,
panels,
useMargins,
title,
isEmptyState,
};
}
public componentDidMount() {
this.mounted = true;
this.subscription = this.props.container.getInput$().subscribe(() => {
const { isFullScreenMode, useMargins, title, description } = this.props.container.getInput();
const {
isFullScreenMode,
useMargins,
title,
description,
isEmptyState,
} = this.props.container.getInput();
if (this.mounted) {
this.setState({
isFullScreenMode,
description,
useMargins,
title,
isEmptyState,
});
}
});
@ -82,19 +98,33 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
});
};
public render() {
private renderEmptyScreen() {
const { renderEmpty } = this.props;
const { isFullScreenMode } = this.state;
return (
<div className="dshDashboardEmptyScreen">
{isFullScreenMode && (
<this.context.services.ExitFullScreenButton
onExitFullScreenMode={this.onExitFullScreenMode}
/>
)}
{renderEmpty && renderEmpty()}
</div>
);
}
private renderContainerScreen() {
const { container } = this.props;
const { isFullScreenMode, panels, title, description, useMargins } = this.state;
return (
<div
data-shared-items-count={Object.values(this.state.panels).length}
data-shared-items-count={Object.values(panels).length}
data-shared-items-container
data-title={this.state.title}
data-description={this.state.description}
className={
this.state.useMargins ? 'dshDashboardViewport-withMargins' : 'dshDashboardViewport'
}
data-title={title}
data-description={description}
className={useMargins ? 'dshDashboardViewport-withMargins' : 'dshDashboardViewport'}
>
{this.state.isFullScreenMode && (
{isFullScreenMode && (
<this.context.services.ExitFullScreenButton
onExitFullScreenMode={this.onExitFullScreenMode}
/>
@ -103,4 +133,13 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
</div>
);
}
public render() {
return (
<React.Fragment>
{this.state.isEmptyState ? this.renderEmptyScreen() : null}
{this.renderContainerScreen()}
</React.Fragment>
);
}
}

View file

@ -27,7 +27,7 @@ import { ExpandPanelAction, ReplacePanelAction } from '.';
import { DashboardContainerFactory } from './embeddable/dashboard_container_factory';
import { Start as InspectorStartContract } from '../../../plugins/inspector/public';
import {
SavedObjectFinder as SavedObjectFinderUi,
SavedObjectFinderUi,
SavedObjectFinderProps,
ExitFullScreenButton as ExitFullScreenButtonUi,
ExitFullScreenButtonProps,

View file

@ -240,6 +240,7 @@ export abstract class Container<
...this.input.panels,
[panelState.explicitInput.id]: panelState,
},
isEmptyState: false,
} as Partial<TContainerInput>);
return await this.untilEmbeddableLoaded<TEmbeddable>(panelState.explicitInput.id);

View file

@ -28,7 +28,7 @@ export interface EmbeddableInput {
id: string;
lastReloadRequestTime?: number;
hidePanelTitles?: boolean;
isEmptyState?: boolean;
/**
* List of action IDs that this embeddable should not render.
*/

View file

@ -35,7 +35,7 @@ import { IconType } from '@elastic/eui';
import { shallow } from 'enzyme';
import React from 'react';
import * as sinon from 'sinon';
import { SavedObjectFinder } from './saved_object_finder';
import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder';
// eslint-disable-next-line
import { coreMock } from '../../../../core/public/mocks';

View file

@ -46,6 +46,7 @@ import { i18n } from '@kbn/i18n';
import { SavedObjectAttributes } from '../../../../core/server';
import { SimpleSavedObject, CoreStart } from '../../../../core/public';
import { useKibana } from '../context';
// TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted
const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent<
@ -104,12 +105,18 @@ interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder {
initialPageSize?: 5 | 10 | 15 | 25;
fixedPageSize?: undefined;
}
export type SavedObjectFinderProps = {
export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize;
export type SavedObjectFinderUiProps = {
savedObjects: CoreStart['savedObjects'];
uiSettings: CoreStart['uiSettings'];
} & (SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize);
} & SavedObjectFinderProps;
class SavedObjectFinder extends React.Component<SavedObjectFinderProps, SavedObjectFinderState> {
class SavedObjectFinderUi extends React.Component<
SavedObjectFinderUiProps,
SavedObjectFinderState
> {
public static propTypes = {
onChoose: PropTypes.func,
noItemsMessage: PropTypes.node,
@ -174,7 +181,7 @@ class SavedObjectFinder extends React.Component<SavedObjectFinderProps, SavedObj
}
}, 300);
constructor(props: SavedObjectFinderProps) {
constructor(props: SavedObjectFinderUiProps) {
super(props);
this.state = {
@ -523,4 +530,15 @@ class SavedObjectFinder extends React.Component<SavedObjectFinderProps, SavedObj
}
}
export { SavedObjectFinder };
const SavedObjectFinder = (props: SavedObjectFinderProps) => {
const { services } = useKibana();
return (
<SavedObjectFinderUi
{...props}
savedObjects={services.savedObject}
uiSettings={services.uiSettings}
/>
);
};
export { SavedObjectFinder, SavedObjectFinderUi };

23
test/examples/README.md Normal file
View file

@ -0,0 +1,23 @@
# Example plugin functional tests
This folder contains functional tests for the example plugins.
## Run the test
To run these tests during development you can use the following commands:
```
# Start the test server (can continue running)
node scripts/functional_tests_server.js --config test/examples/config.js
# Start a test run
node scripts/functional_test_runner.js --config test/examples/config.js
```
## Run Kibana with a test plugin
In case you want to start Kibana with the example plugins, you can just run:
```
yarn start --run-examples
```

55
test/examples/config.js Normal file
View file

@ -0,0 +1,55 @@
/*
* 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 path from 'path';
import { services } from '../plugin_functional/services';
export default async function ({ readConfigFile }) {
const functionalConfig = await readConfigFile(require.resolve('../functional/config'));
return {
testFiles: [
require.resolve('./search'),
],
services: {
...functionalConfig.get('services'),
...services,
},
pageObjects: functionalConfig.get('pageObjects'),
servers: functionalConfig.get('servers'),
esTestCluster: functionalConfig.get('esTestCluster'),
apps: functionalConfig.get('apps'),
esArchiver: {
directory: path.resolve(__dirname, '../es_archives')
},
screenshots: functionalConfig.get('screenshots'),
junit: {
reportName: 'Example plugin functional tests',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
'--run-examples',
// Required to run examples
'--env.name=development',
],
},
};
}

View file

@ -78,7 +78,6 @@ export default function ({ getService, getPageObjects }) {
const logoButton = await PageObjects.dashboard.getExitFullScreenLogoButton();
await logoButton.moveMouseTo();
await PageObjects.dashboard.clickExitFullScreenTextButton();
await retry.try(async () => {
const isChromeVisible = await PageObjects.common.isChromeVisible();
expect(isChromeVisible).to.be(true);

View file

@ -33,7 +33,6 @@ export default async function ({ readConfigFile }) {
require.resolve('./test_suites/app_plugins'),
require.resolve('./test_suites/custom_visualizations'),
require.resolve('./test_suites/panel_actions'),
require.resolve('./test_suites/search'),
/**
* @todo Work on re-enabling this test suite after this is merged. These tests pass

View file

@ -22,7 +22,6 @@ import 'uiExports/embeddableFactories';
import 'uiExports/embeddableActions';
import { npSetup, npStart } from 'ui/new_platform';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import { ExitFullScreenButton } from 'ui/exit_full_screen';
import uiRoutes from 'ui/routes';
// @ts-ignore
@ -39,7 +38,6 @@ export const setup = pluginInstance.setup(npSetup.core, {
embeddable: npSetup.plugins.embeddable,
inspector: npSetup.plugins.inspector,
__LEGACY: {
SavedObjectFinder,
ExitFullScreenButton,
},
});
@ -64,7 +62,6 @@ export const start = pluginInstance.start(npStart.core, {
inspector: npStart.plugins.inspector,
uiActions: npStart.plugins.uiActions,
__LEGACY: {
SavedObjectFinder,
ExitFullScreenButton,
onRenderComplete: (renderCompleteListener: () => void) => {
if (rendered) {

View file

@ -38,6 +38,10 @@ import {
ContactCardEmbeddableFactory,
} from './embeddable_api';
import { App } from './app';
import {
SavedObjectFinderProps,
SavedObjectFinderUi,
} from '../../../../../../../src/plugins/kibana_react/public/saved_objects';
import {
IEmbeddableStart,
IEmbeddableSetup,
@ -47,7 +51,6 @@ export interface SetupDependencies {
embeddable: IEmbeddableSetup;
inspector: InspectorSetupContract;
__LEGACY: {
SavedObjectFinder: React.ComponentType<any>;
ExitFullScreenButton: React.ComponentType<any>;
};
}
@ -57,7 +60,6 @@ interface StartDependencies {
uiActions: IUiActionsStart;
inspector: InspectorStartContract;
__LEGACY: {
SavedObjectFinder: React.ComponentType<any>;
ExitFullScreenButton: React.ComponentType<any>;
onRenderComplete: (onRenderComplete: () => void) => void;
};
@ -99,6 +101,13 @@ export class EmbeddableExplorerPublicPlugin
plugins.__LEGACY.onRenderComplete(() => {
const root = document.getElementById(REACT_ROOT_ID);
const SavedObjectFinder = (props: SavedObjectFinderProps) => (
<SavedObjectFinderUi
{...props}
savedObjects={core.savedObjects}
uiSettings={core.uiSettings}
/>
);
ReactDOM.render(
<App
getActions={plugins.uiActions.getTriggerCompatibleActions}
@ -107,7 +116,7 @@ export class EmbeddableExplorerPublicPlugin
notifications={core.notifications}
overlays={core.overlays}
inspector={plugins.inspector}
SavedObjectFinder={plugins.__LEGACY.SavedObjectFinder}
SavedObjectFinder={SavedObjectFinder}
I18nContext={core.i18n.Context}
/>,
root

View file

@ -16,8 +16,11 @@ import {
} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { start } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
import { EmbeddableExpression } from '../expression_types/embeddable';
import { SavedObjectFinder } from '../../../../../../src/legacy/ui/public/saved_objects/components/saved_object_finder';
import { RendererStrings } from '../../i18n';
import {
SavedObjectFinderProps,
SavedObjectFinderUi,
} from '../../../../../../src/plugins/kibana_react/public';
const { embeddable: strings } = RendererStrings;
@ -34,6 +37,13 @@ interface Handlers {
}
const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) => {
const SavedObjectFinder = (props: SavedObjectFinderProps) => (
<SavedObjectFinderUi
{...props}
savedObjects={npStart.core.savedObjects}
uiSettings={npStart.core.uiSettings}
/>
);
return (
<div
className="embeddable"

View file

@ -8,7 +8,7 @@ import React from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui';
import {
SavedObjectFinder,
SavedObjectFinderUi,
SavedObjectMetaData,
} from '../../../../../../../src/plugins/kibana_react/public/saved_objects'; // eslint-disable-line
import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
@ -64,7 +64,7 @@ export class AddEmbeddableFlyout extends React.Component<Props> {
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<SavedObjectFinder
<SavedObjectFinderUi
onChoose={this.onAddPanel}
savedObjectMetaData={availableSavedObjects}
showFilter={true}

View file

@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { CoreStart } from 'src/core/public';
import { SavedObjectFinder } from '../../../../../../src/plugins/kibana_react/public';
import { SavedObjectFinderUi } from '../../../../../../src/plugins/kibana_react/public';
import { IndexPatternSavedObject } from '../types';
export interface SourcePickerProps {
@ -25,7 +25,7 @@ export function SourcePicker({
onIndexPatternSelected,
}: SourcePickerProps) {
return (
<SavedObjectFinder
<SavedObjectFinderUi
savedObjects={savedObjects}
uiSettings={uiSettings}
onChoose={(_id, _type, _name, indexPattern) => {

View file

@ -29,7 +29,7 @@ export type GetLogEntryRateRequestPayload = rt.TypeOf<typeof getLogEntryRateRequ
* response
*/
export const logEntryRateAnomaly = rt.type({
export const logEntryRateAnomalyRT = rt.type({
actualLogEntryRate: rt.number,
anomalyScore: rt.number,
duration: rt.number,
@ -39,22 +39,26 @@ export const logEntryRateAnomaly = rt.type({
export const logEntryRatePartitionRT = rt.type({
analysisBucketCount: rt.number,
anomalies: rt.array(logEntryRateAnomaly),
anomalies: rt.array(logEntryRateAnomalyRT),
averageActualLogEntryRate: rt.number,
maximumAnomalyScore: rt.number,
numberOfLogEntries: rt.number,
partitionId: rt.string,
});
export const logEntryRateHistogramBucket = rt.type({
export type LogEntryRatePartition = rt.TypeOf<typeof logEntryRatePartitionRT>;
export const logEntryRateHistogramBucketRT = rt.type({
partitions: rt.array(logEntryRatePartitionRT),
startTime: rt.number,
});
export type LogEntryRateHistogramBucket = rt.TypeOf<typeof logEntryRateHistogramBucketRT>;
export const getLogEntryRateSuccessReponsePayloadRT = rt.type({
data: rt.type({
bucketDuration: rt.number,
histogramBuckets: rt.array(logEntryRateHistogramBucket),
histogramBuckets: rt.array(logEntryRateHistogramBucketRT),
totalNumberOfLogEntries: rt.number,
}),
});

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './indices';
export * from './log_entry_rate_indices';

View file

@ -6,14 +6,24 @@
import * as rt from 'io-ts';
export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices';
export const LOG_ANALYSIS_VALIDATE_INDICES_PATH =
'/api/infra/log_analysis/validation/log_entry_rate_indices';
/**
* Request types
*/
export const validationIndicesFieldSpecificationRT = rt.type({
name: rt.string,
validTypes: rt.array(rt.string),
});
export type ValidationIndicesFieldSpecification = rt.TypeOf<
typeof validationIndicesFieldSpecificationRT
>;
export const validationIndicesRequestPayloadRT = rt.type({
data: rt.type({
timestampField: rt.string,
fields: rt.array(validationIndicesFieldSpecificationRT),
indices: rt.array(rt.string),
}),
});

View file

@ -4,19 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { JobType } from './log_analysis';
import * as rt from 'io-ts';
export const bucketSpan = 900000;
export const partitionField = 'event.dataset';
export const getJobIdPrefix = (spaceId: string, sourceId: string) =>
`kibana-logs-ui-${spaceId}-${sourceId}-`;
export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) =>
export const getJobId = (spaceId: string, sourceId: string, jobType: string) =>
`${getJobIdPrefix(spaceId, sourceId)}${jobType}`;
export const getDatafeedId = (spaceId: string, sourceId: string, jobType: JobType) =>
export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) =>
`datafeed-${getJobId(spaceId, sourceId, jobType)}`;
export const getAllModuleJobIds = (spaceId: string, sourceId: string) => [
getJobId(spaceId, sourceId, 'log-entry-rate'),
];
export const jobSourceConfigurationRT = rt.type({
indexPattern: rt.string,
timestampField: rt.string,
bucketSpan: rt.number,
});
export type JobSourceConfiguration = rt.TypeOf<typeof jobSourceConfigurationRT>;

View file

@ -4,14 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import url from 'url';
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { encode } from 'rison-node';
import chrome from 'ui/chrome';
import { QueryString } from 'ui/utils/query_string';
import { encode } from 'rison-node';
import { TimeRange } from '../../../../../common/http_api/shared/time_range';
import url from 'url';
import { TimeRange } from '../../../../common/http_api/shared/time_range';
export const AnalyzeInMlButton: React.FunctionComponent<{
jobId: string;

View file

@ -0,0 +1,7 @@
/*
* 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 * from './analyze_in_ml_button';

View file

@ -6,11 +6,30 @@
import * as rt from 'io-ts';
import { jobSourceConfigurationRT } from '../../../../../common/log_analysis';
export const jobCustomSettingsRT = rt.partial({
job_revision: rt.number,
logs_source_config: rt.partial({
indexPattern: rt.string,
timestampField: rt.string,
bucketSpan: rt.number,
}),
logs_source_config: rt.partial(jobSourceConfigurationRT.props),
});
export const getMlCapabilitiesResponsePayloadRT = rt.type({
capabilities: rt.type({
canGetJobs: rt.boolean,
canCreateJob: rt.boolean,
canDeleteJob: rt.boolean,
canOpenJob: rt.boolean,
canCloseJob: rt.boolean,
canForecastJob: rt.boolean,
canGetDatafeeds: rt.boolean,
canStartStopDatafeed: rt.boolean,
canUpdateJob: rt.boolean,
canUpdateDatafeed: rt.boolean,
canPreviewDatafeed: rt.boolean,
}),
isPlatinumOrTrialLicense: rt.boolean,
mlFeatureEnabledInSpace: rt.boolean,
upgradeInProgress: rt.boolean,
});
export type GetMlCapabilitiesResponsePayload = rt.TypeOf<typeof getMlCapabilitiesResponsePayloadRT>;

View file

@ -9,17 +9,22 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { kfetch } from 'ui/kfetch';
import { getAllModuleJobIds, getDatafeedId } from '../../../../../common/log_analysis';
import { getDatafeedId, getJobId } from '../../../../../common/log_analysis';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
export const callDeleteJobs = async (spaceId: string, sourceId: string) => {
export const callDeleteJobs = async <JobType extends string>(
spaceId: string,
sourceId: string,
jobTypes: JobType[]
) => {
// NOTE: Deleting the jobs via this API will delete the datafeeds at the same time
const deleteJobsResponse = await kfetch({
method: 'POST',
pathname: '/api/ml/jobs/delete_jobs',
body: JSON.stringify(
deleteJobsRequestPayloadRT.encode({
jobIds: getAllModuleJobIds(spaceId, sourceId),
jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)),
})
),
});
@ -42,15 +47,24 @@ export const callGetJobDeletionTasks = async () => {
);
};
export const callStopDatafeed = async (spaceId: string, sourceId: string) => {
export const callStopDatafeeds = async <JobType extends string>(
spaceId: string,
sourceId: string,
jobTypes: JobType[]
) => {
// Stop datafeed due to https://github.com/elastic/kibana/issues/44652
const stopDatafeedResponse = await kfetch({
method: 'POST',
pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}/_stop`,
pathname: '/api/ml/jobs/stop_datafeeds',
body: JSON.stringify(
stopDatafeedsRequestPayloadRT.encode({
datafeedIds: jobTypes.map(jobType => getDatafeedId(spaceId, sourceId, jobType)),
})
),
});
return pipe(
stopDatafeedResponsePayloadRT.decode(stopDatafeedResponse),
stopDatafeedsResponsePayloadRT.decode(stopDatafeedResponse),
fold(throwErrors(createPlainError), identity)
);
};
@ -68,10 +82,19 @@ export const deleteJobsResponsePayloadRT = rt.record(
})
);
export type DeleteJobsResponsePayload = rt.TypeOf<typeof deleteJobsResponsePayloadRT>;
export const getJobDeletionTasksResponsePayloadRT = rt.type({
jobIds: rt.array(rt.string),
});
export const stopDatafeedResponsePayloadRT = rt.type({
stopped: rt.boolean,
export const stopDatafeedsRequestPayloadRT = rt.type({
datafeedIds: rt.array(rt.string),
});
export const stopDatafeedsResponsePayloadRT = rt.record(
rt.string,
rt.type({
stopped: rt.boolean,
})
);

View file

@ -12,15 +12,19 @@ import { kfetch } from 'ui/kfetch';
import { jobCustomSettingsRT } from './ml_api_types';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
import { getAllModuleJobIds } from '../../../../../common/log_analysis';
import { getJobId } from '../../../../../common/log_analysis';
export const callJobsSummaryAPI = async (spaceId: string, sourceId: string) => {
export const callJobsSummaryAPI = async <JobType extends string>(
spaceId: string,
sourceId: string,
jobTypes: JobType[]
) => {
const response = await kfetch({
method: 'POST',
pathname: '/api/ml/jobs/jobs_summary',
body: JSON.stringify(
fetchJobStatusRequestPayloadRT.encode({
jobIds: getAllModuleJobIds(spaceId, sourceId),
jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)),
})
),
});

View file

@ -12,7 +12,6 @@ import { kfetch } from 'ui/kfetch';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
import { getJobIdPrefix } from '../../../../../common/log_analysis';
import { jobCustomSettingsRT } from './ml_api_types';
export const callSetupMlModuleAPI = async (
moduleId: string,
@ -21,8 +20,8 @@ export const callSetupMlModuleAPI = async (
spaceId: string,
sourceId: string,
indexPattern: string,
timeField: string,
bucketSpan: number
jobOverrides: SetupMlModuleJobOverrides[] = [],
datafeedOverrides: SetupMlModuleDatafeedOverrides[] = []
) => {
const response = await kfetch({
method: 'POST',
@ -34,25 +33,8 @@ export const callSetupMlModuleAPI = async (
indexPatternName: indexPattern,
prefix: getJobIdPrefix(spaceId, sourceId),
startDatafeed: true,
jobOverrides: [
{
job_id: 'log-entry-rate' as const,
analysis_config: {
bucket_span: `${bucketSpan}ms`,
},
data_description: {
time_field: timeField,
},
custom_settings: {
logs_source_config: {
indexPattern,
timestampField: timeField,
bucketSpan,
},
},
},
],
datafeedOverrides: [],
jobOverrides,
datafeedOverrides,
})
),
});
@ -68,23 +50,20 @@ const setupMlModuleTimeParamsRT = rt.partial({
end: rt.number,
});
const setupMlModuleLogEntryRateJobOverridesRT = rt.type({
job_id: rt.literal('log-entry-rate'),
analysis_config: rt.type({
bucket_span: rt.string,
}),
data_description: rt.type({
time_field: rt.string,
}),
custom_settings: jobCustomSettingsRT,
});
const setupMlModuleJobOverridesRT = rt.object;
export type SetupMlModuleJobOverrides = rt.TypeOf<typeof setupMlModuleJobOverridesRT>;
const setupMlModuleDatafeedOverridesRT = rt.object;
export type SetupMlModuleDatafeedOverrides = rt.TypeOf<typeof setupMlModuleDatafeedOverridesRT>;
const setupMlModuleRequestParamsRT = rt.type({
indexPatternName: rt.string,
prefix: rt.string,
startDatafeed: rt.boolean,
jobOverrides: rt.array(setupMlModuleLogEntryRateJobOverridesRT),
datafeedOverrides: rt.array(rt.object),
jobOverrides: rt.array(setupMlModuleJobOverridesRT),
datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT),
});
const setupMlModuleRequestPayloadRT = rt.intersection([

View file

@ -10,20 +10,22 @@ import { identity } from 'fp-ts/lib/function';
import { kfetch } from 'ui/kfetch';
import {
LOG_ANALYSIS_VALIDATION_INDICES_PATH,
LOG_ANALYSIS_VALIDATE_INDICES_PATH,
ValidationIndicesFieldSpecification,
validationIndicesRequestPayloadRT,
validationIndicesResponsePayloadRT,
} from '../../../../../common/http_api';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => {
export const callValidateIndicesAPI = async (
indices: string[],
fields: ValidationIndicesFieldSpecification[]
) => {
const response = await kfetch({
method: 'POST',
pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH,
body: JSON.stringify(
validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } })
),
pathname: LOG_ANALYSIS_VALIDATE_INDICES_PATH,
body: JSON.stringify(validationIndicesRequestPayloadRT.encode({ data: { indices, fields } })),
});
return pipe(

View file

@ -6,7 +6,6 @@
export * from './log_analysis_capabilities';
export * from './log_analysis_cleanup';
export * from './log_analysis_jobs';
export * from './log_analysis_results';
export * from './log_analysis_results_url_state';
export * from './log_analysis_status_state';
export * from './log_analysis_module';
export * from './log_analysis_module_status';
export * from './log_analysis_module_types';

View file

@ -15,7 +15,7 @@ import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import {
getMlCapabilitiesResponsePayloadRT,
GetMlCapabilitiesResponsePayload,
} from './ml_api_types';
} from './api/ml_api_types';
import { throwErrors, createPlainError } from '../../../../common/runtime_types';
export const useLogAnalysisCapabilities = () => {

View file

@ -4,64 +4,46 @@
* you may not use this file except in compliance with the Elastic License.
*/
import createContainer from 'constate';
import { useMemo } from 'react';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callDeleteJobs, callStopDatafeed, callGetJobDeletionTasks } from './api/ml_cleanup';
import { getAllModuleJobIds } from '../../../../common/log_analysis';
import { getJobId } from '../../../../common/log_analysis';
import { callDeleteJobs, callGetJobDeletionTasks, callStopDatafeeds } from './api/ml_cleanup';
export const useLogAnalysisCleanup = ({
sourceId,
spaceId,
}: {
sourceId: string;
spaceId: string;
}) => {
const [cleanupMLResourcesRequest, cleanupMLResources] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
try {
await callStopDatafeed(spaceId, sourceId);
} catch (err) {
// Datefeed has been deleted / doesn't exist, proceed with deleting jobs anyway
if (err && err.res && err.res.status === 404) {
return await deleteJobs(spaceId, sourceId);
} else {
throw err;
}
}
export const cleanUpJobsAndDatafeeds = async <JobType extends string>(
spaceId: string,
sourceId: string,
jobTypes: JobType[]
) => {
try {
await callStopDatafeeds(spaceId, sourceId, jobTypes);
} catch (err) {
// Proceed only if datafeed has been deleted or didn't exist in the first place
if (err?.res?.status !== 404) {
throw err;
}
}
return await deleteJobs(spaceId, sourceId);
},
},
[spaceId, sourceId]
);
const isCleaningUp = useMemo(() => cleanupMLResourcesRequest.state === 'pending', [
cleanupMLResourcesRequest.state,
]);
return {
cleanupMLResources,
isCleaningUp,
};
return await deleteJobs(spaceId, sourceId, jobTypes);
};
export const LogAnalysisCleanup = createContainer(useLogAnalysisCleanup);
const deleteJobs = async (spaceId: string, sourceId: string) => {
const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId);
await waitUntilJobsAreDeleted(spaceId, sourceId);
const deleteJobs = async <JobType extends string>(
spaceId: string,
sourceId: string,
jobTypes: JobType[]
) => {
const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId, jobTypes);
await waitUntilJobsAreDeleted(spaceId, sourceId, jobTypes);
return deleteJobsResponse;
};
const waitUntilJobsAreDeleted = async (spaceId: string, sourceId: string) => {
const waitUntilJobsAreDeleted = async <JobType extends string>(
spaceId: string,
sourceId: string,
jobTypes: JobType[]
) => {
const moduleJobIds = jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType));
while (true) {
const response = await callGetJobDeletionTasks();
const jobIdsBeingDeleted = response.jobIds;
const moduleJobIds = getAllModuleJobIds(spaceId, sourceId);
const { jobIds: jobIdsBeingDeleted } = await callGetJobDeletionTasks();
const needToWait = jobIdsBeingDeleted.some(jobId => moduleJobIds.includes(jobId));
if (needToWait) {
await timeout(1000);
} else {

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