Merge remote-tracking branch 'upstream/master' into saved-objects-client-provider

This commit is contained in:
kobelb 2018-05-25 11:27:20 -04:00
commit 0eeca7d1b4
289 changed files with 6621 additions and 3626 deletions

View file

@ -1,5 +1,8 @@
[[release-notes]]
= {kib} Release Notes
++++
<titleabbrev>Release Notes</titleabbrev>
++++
[partintro]
--

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View file

@ -77,4 +77,6 @@ include::development.asciidoc[]
include::limitations.asciidoc[]
include::release-notes/highlights.asciidoc[]
include::CHANGELOG.asciidoc[]

View file

@ -19,6 +19,8 @@ include::management/index-patterns.asciidoc[]
include::management/managing-fields.asciidoc[]
include::management/managing-indices.asciidoc[]
include::management/advanced-options.asciidoc[]
include::management/managing-saved-objects.asciidoc[]

View file

@ -0,0 +1,24 @@
[[managing-indices]]
== Managing Indices
The *Index management* UI enables you to view index settings,
mappings, and statistics and perform management operations.
These include refreshing, flushing, clearing the cache, merging segments,
and closing or deleting indices. The UI provides a convenient way to
perform bulk operations on multiple indices.
[role="screenshot"]
image::images/management-index-management.png[Index Management UI]
Click the name of an index to display the index summary and access
the index settings, mapping, and statistics. The *Manage* menu in the
lower right of the index pane enables you to manage
the selected index.
To perform bulk operations, select the checkboxes of the indices you want to
modify and choose an operation from the *Manage indices* menu
next to the query bar. To select all
indices, select the checkbox in the *Name* header.
For information about the available management operations,
see {ref}/indices.html[Indices APIs] in the Elasticsearch Reference.

View file

@ -0,0 +1,9 @@
[[release-highlights-7.0.0]]
== 7.0.0 release highlights
++++
<titleabbrev>7.0.0</titleabbrev>
++++
coming[7.0.0]
See also <<breaking-changes-7.0>> and <<release-notes-7.0.0>>.

View file

@ -0,0 +1,16 @@
[[release-highlights]]
= {kib} Release Highlights
++++
<titleabbrev>Release Highlights</titleabbrev>
++++
[partintro]
--
This section summarizes the most important changes in each release. For the
full list, see <<release-notes>> and <<breaking-changes>>.
* <<release-highlights-7.0.0>>
--
include::highlights-7.0.0.asciidoc[]

View file

@ -232,6 +232,8 @@
"@types/glob": "^5.0.35",
"@types/listr": "^0.13.0",
"@types/minimatch": "^2.0.29",
"@types/react": "^16.3.14",
"@types/react-dom": "^16.0.5",
"angular-mocks": "1.4.7",
"babel-eslint": "8.1.2",
"babel-jest": "^22.4.3",
@ -252,7 +254,7 @@
"eslint-plugin-import": "2.8.0",
"eslint-plugin-jest": "^21.6.2",
"eslint-plugin-mocha": "4.11.0",
"eslint-plugin-no-unsanitized": "^2.0.2",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-prefer-object-spread": "1.2.1",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "7.5.1",
@ -261,7 +263,7 @@
"faker": "1.1.0",
"geckodriver": "1.11.0",
"getopts": "2.0.0",
"globby": "^8.0.0",
"globby": "^8.0.1",
"grunt": "1.0.1",
"grunt-cli": "0.1.13",
"grunt-contrib-watch": "^1.0.0",

View file

@ -21,7 +21,7 @@
"eslint-plugin-import": "^2.6.0",
"eslint-plugin-jest": "^21.0.0",
"eslint-plugin-mocha": "^4.9.0",
"eslint-plugin-no-unsanitized": "^2.0.2",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-react": "^7.1.0"
}

View file

@ -1,17 +1,18 @@
import getopts from 'getopts';
import dedent from 'dedent';
import chalk from 'chalk';
import dedent from 'dedent';
import getopts from 'getopts';
import { resolve } from 'path';
import { commands } from './commands';
import { runCommand } from './run';
import { log } from './utils/log';
function help() {
const availableCommands = Object.keys(commands)
.map(commandName => commands[commandName])
.map(command => `${command.name} - ${command.description}`);
console.log(dedent`
log.write(dedent`
usage: kbn <command> [<args>]
By default commands are run for Kibana itself, all packages in the 'packages/'
@ -35,7 +36,7 @@ export async function run(argv: string[]) {
// starts forwarding the `--` directly to this script, see
// https://github.com/yarnpkg/yarn/blob/b2d3e1a8fe45ef376b716d597cc79b38702a9320/src/cli/index.js#L174-L182
if (argv.includes('--')) {
console.log(
log.write(
chalk.red(
`Using "--" is not allowed, as it doesn't work with 'yarn kbn'.`
)
@ -45,9 +46,9 @@ export async function run(argv: string[]) {
const options = getopts(argv, {
alias: {
e: 'exclude',
h: 'help',
i: 'include',
e: 'exclude',
},
});
@ -69,7 +70,7 @@ export async function run(argv: string[]) {
const command = commands[commandName];
if (command === undefined) {
console.log(
log.write(
chalk.red(`[${commandName}] is not a valid command, see 'kbn --help'`)
);
process.exit(1);

View file

@ -7,18 +7,18 @@ import {
absolutePathSnapshotSerializer,
stripAnsiSnapshotSerializer,
} from '../test_helpers';
import { BootstrapCommand } from './bootstrap';
import { PackageJson } from '../utils/package_json';
import { linkProjectExecutables } from '../utils/link_project_executables';
import { IPackageJson } from '../utils/package_json';
import { Project } from '../utils/project';
import { buildProjectGraph } from '../utils/projects';
import { installInDir, runScriptInPackageStreaming } from '../utils/scripts';
import { linkProjectExecutables } from '../utils/link_project_executables';
import { BootstrapCommand } from './bootstrap';
const mockInstallInDir = installInDir as jest.Mock;
const mockRunScriptInPackageStreaming = runScriptInPackageStreaming as jest.Mock;
const mockLinkProjectExecutables = linkProjectExecutables as jest.Mock;
const createProject = (packageJson: PackageJson, path = '.') =>
const createProject = (packageJson: IPackageJson, path = '.') =>
new Project(
{
name: 'kibana',
@ -31,7 +31,9 @@ const createProject = (packageJson: PackageJson, path = '.') =>
expect.addSnapshotSerializer(absolutePathSnapshotSerializer);
expect.addSnapshotSerializer(stripAnsiSnapshotSerializer);
const noop = () => {};
const noop = () => {
// noop
};
afterEach(() => {
jest.resetAllMocks();
@ -45,19 +47,19 @@ test('handles dependencies of dependencies', async () => {
});
const foo = createProject(
{
name: 'foo',
dependencies: {
bar: 'link:../bar',
},
name: 'foo',
},
'packages/foo'
);
const bar = createProject(
{
name: 'bar',
dependencies: {
baz: 'link:../baz',
},
name: 'bar',
},
'packages/bar'
);

View file

@ -1,13 +1,14 @@
import chalk from 'chalk';
import { topologicallyBatchProjects } from '../utils/projects';
import { linkProjectExecutables } from '../utils/link_project_executables';
import { log } from '../utils/log';
import { parallelizeBatches } from '../utils/parallelize';
import { Command } from './';
import { topologicallyBatchProjects } from '../utils/projects';
import { ICommand } from './';
export const BootstrapCommand: Command = {
name: 'bootstrap',
export const BootstrapCommand: ICommand = {
description: 'Install dependencies and crosslink projects',
name: 'bootstrap',
async run(projects, projectGraph, { options }) {
const batchedProjects = topologicallyBatchProjects(projects, projectGraph);
@ -15,7 +16,7 @@ export const BootstrapCommand: Command = {
const frozenLockfile = options['frozen-lockfile'] === true;
const extraArgs = frozenLockfile ? ['--frozen-lockfile'] : [];
console.log(chalk.bold('\nRunning installs in topological order:'));
log.write(chalk.bold('\nRunning installs in topological order:'));
for (const batch of batchedProjects) {
for (const project of batch) {
@ -25,7 +26,7 @@ export const BootstrapCommand: Command = {
}
}
console.log(
log.write(
chalk.bold('\nInstalls completed, linking package executables:\n')
);
await linkProjectExecutables(projects, projectGraph);
@ -36,7 +37,7 @@ export const BootstrapCommand: Command = {
* transpiled before they can be used. Ideally we shouldn't do this unless we
* have to, as it will slow down the bootstrapping process.
*/
console.log(
log.write(
chalk.bold(
'\nLinking executables completed, running `kbn:bootstrap` scripts\n'
)
@ -47,6 +48,6 @@ export const BootstrapCommand: Command = {
}
});
console.log(chalk.green.bold('\nBootstrapping completed!\n'));
log.write(chalk.green.bold('\nBootstrapping completed!\n'));
},
};

View file

@ -1,15 +1,16 @@
import del from 'del';
import chalk from 'chalk';
import { relative } from 'path';
import del from 'del';
import ora from 'ora';
import { relative } from 'path';
import { isDirectory } from '../utils/fs';
import { Command } from './';
import { log } from '../utils/log';
import { ICommand } from './';
export const CleanCommand: Command = {
name: 'clean',
export const CleanCommand: ICommand = {
description:
'Remove the node_modules and target directories from all projects.',
name: 'clean',
async run(projects, projectGraph, { rootPath }) {
const directoriesToDelete = [];
@ -24,9 +25,9 @@ export const CleanCommand: Command = {
}
if (directoriesToDelete.length === 0) {
console.log(chalk.bold.green('\n\nNo directories to delete'));
log.write(chalk.bold.green('\n\nNo directories to delete'));
} else {
console.log(chalk.bold.red('\n\nDeleting directories:\n'));
log.write(chalk.bold.red('\n\nDeleting directories:\n'));
for (const dir of directoriesToDelete) {
const deleting = del(dir, { force: true });

View file

@ -1,19 +1,19 @@
import { ProjectGraph, ProjectMap } from '../utils/projects';
export interface CommandConfig {
export interface ICommandConfig {
extraArgs: string[];
options: { [key: string]: any };
rootPath: string;
}
export interface Command {
export interface ICommand {
name: string;
description: string;
run: (
projects: ProjectMap,
projectGraph: ProjectGraph,
config: CommandConfig
config: ICommandConfig
) => Promise<void>;
}
@ -22,7 +22,7 @@ import { CleanCommand } from './clean';
import { RunCommand } from './run';
import { WatchCommand } from './watch';
export const commands: { [key: string]: Command } = {
export const commands: { [key: string]: ICommand } = {
bootstrap: BootstrapCommand,
clean: CleanCommand,
run: RunCommand,

View file

@ -1,26 +1,27 @@
import chalk from 'chalk';
import { topologicallyBatchProjects } from '../utils/projects';
import { log } from '../utils/log';
import { parallelizeBatches } from '../utils/parallelize';
import { Command } from './';
import { topologicallyBatchProjects } from '../utils/projects';
import { ICommand } from './';
export const RunCommand: Command = {
name: 'run',
export const RunCommand: ICommand = {
description:
'Run script defined in package.json in each package that contains that script.',
name: 'run',
async run(projects, projectGraph, { extraArgs }) {
const batchedProjects = topologicallyBatchProjects(projects, projectGraph);
if (extraArgs.length === 0) {
console.log(chalk.red.bold('\nNo script specified'));
log.write(chalk.red.bold('\nNo script specified'));
process.exit(1);
}
const scriptName = extraArgs[0];
const scriptArgs = extraArgs.slice(1);
console.log(
log.write(
chalk.bold(
`\nRunning script [${chalk.green(
scriptName

View file

@ -1,8 +1,9 @@
import chalk from 'chalk';
import { topologicallyBatchProjects, ProjectMap } from '../utils/projects';
import { log } from '../utils/log';
import { parallelizeBatches } from '../utils/parallelize';
import { ProjectMap, topologicallyBatchProjects } from '../utils/projects';
import { waitUntilWatchIsReady } from '../utils/watch';
import { Command } from './';
import { ICommand } from './';
/**
* Name of the script in the package/project package.json file to run during `kbn watch`.
@ -24,9 +25,9 @@ const kibanaProjectName = 'kibana';
* the `kbn:watch` script and eventually for the entire batch. Currently we support completion "markers" for
* `webpack` and `tsc` only, for the rest we rely on predefined timeouts.
*/
export const WatchCommand: Command = {
name: 'watch',
export const WatchCommand: ICommand = {
description: 'Runs `kbn:watch` script for every project.',
name: 'watch',
async run(projects, projectGraph) {
const projectsToWatch: ProjectMap = new Map();
@ -38,7 +39,7 @@ export const WatchCommand: Command = {
}
if (projectsToWatch.size === 0) {
console.log(
log.write(
chalk.red(
`\nThere are no projects to watch found. Make sure that projects define 'kbn:watch' script in 'package.json'.\n`
)
@ -47,7 +48,7 @@ export const WatchCommand: Command = {
}
const projectNames = Array.from(projectsToWatch.keys());
console.log(
log.write(
chalk.bold(
chalk.green(
`Running ${watchScriptName} scripts for [${projectNames.join(', ')}].`
@ -73,7 +74,7 @@ export const WatchCommand: Command = {
pkg.runScriptStreaming(watchScriptName).stdout
);
console.log(
log.write(
chalk.bold(
`[${chalk.green(
pkg.name

View file

@ -1,14 +1,17 @@
import { resolve } from 'path';
export type ProjectPathOptions = {
export interface IProjectPathOptions {
'skip-kibana-extra'?: boolean;
'oss'?: boolean;
};
oss?: boolean;
}
/**
* Returns all the paths where plugins are located
*/
export function getProjectPaths(rootPath: string, options: ProjectPathOptions) {
export function getProjectPaths(
rootPath: string,
options: IProjectPathOptions
) {
const skipKibanaExtra = Boolean(options['skip-kibana-extra']);
const ossOnly = Boolean(options.oss);

View file

@ -1,22 +1,22 @@
import del from 'del';
import { relative, resolve, join } from 'path';
import copy from 'cpy';
import del from 'del';
import { join, relative, resolve } from 'path';
import { getProjectPaths } from '../config';
import {
getProjects,
buildProjectGraph,
topologicallyBatchProjects,
ProjectMap,
includeTransitiveProjects,
} from '../utils/projects';
import { isDirectory, isFile } from '../utils/fs';
import { log } from '../utils/log';
import {
createProductionPackageJson,
writePackageJson,
readPackageJson,
writePackageJson,
} from '../utils/package_json';
import { isDirectory, isFile } from '../utils/fs';
import { Project } from '../utils/project';
import {
buildProjectGraph,
getProjects,
includeTransitiveProjects,
topologicallyBatchProjects,
} from '../utils/projects';
export async function buildProductionProjects({
kibanaRoot,
@ -30,7 +30,7 @@ export async function buildProductionProjects({
const batchedProjects = topologicallyBatchProjects(projects, projectGraph);
const projectNames = [...projects.values()].map(project => project.name);
console.log(`Preparing production build for [${projectNames.join(', ')}]`);
log.write(`Preparing production build for [${projectNames.join(', ')}]`);
for (const batch of batchedProjects) {
for (const project of batch) {
@ -99,9 +99,9 @@ async function copyToBuild(
await copy(['**/*', '!node_modules/**'], buildProjectPath, {
cwd: project.getIntermediateBuildDirectory(),
parents: true,
nodir: true,
dot: true,
nodir: true,
parents: true,
});
// If a project is using an intermediate build directory, we special-case our

View file

@ -1,16 +1,16 @@
import tempy from 'tempy';
import copy from 'cpy';
import { resolve, relative, join } from 'path';
import globby from 'globby';
import { join, resolve } from 'path';
import tempy from 'tempy';
import { buildProductionProjects } from '../build_production_projects';
import { getProjects } from '../../utils/projects';
import { readPackageJson } from '../../utils/package_json';
import { getProjects } from '../../utils/projects';
import { buildProductionProjects } from '../build_production_projects';
describe('kbn-pm production', function() {
describe('kbn-pm production', () => {
test(
'builds and copies projects for production',
async function() {
async () => {
const tmpDir = tempy.directory();
const buildRoot = tempy.directory();
const fixturesPath = resolve(__dirname, '__fixtures__');
@ -18,9 +18,9 @@ describe('kbn-pm production', function() {
// Copy all the test fixtures into a tmp dir, as we will be mutating them
await copy(['**/*'], tmpDir, {
cwd: fixturesPath,
parents: true,
nodir: true,
dot: true,
nodir: true,
parents: true,
});
const projects = await getProjects(tmpDir, ['.', './packages/*']);

View file

@ -1,4 +1,4 @@
import { resolve, join } from 'path';
import { join, resolve } from 'path';
import { prepareExternalProjectDependencies } from './prepare_project_dependencies';

View file

@ -1,5 +1,5 @@
import { Project } from '../utils/project';
import { isLinkDependency } from '../utils/package_json';
import { Project } from '../utils/project';
/**
* All external projects are located within `../kibana-extra/{plugin}` relative

View file

@ -1,7 +1,7 @@
import { resolve } from 'path';
import { ICommand, ICommandConfig } from './commands';
import { runCommand } from './run';
import { Project } from './utils/project';
import { Command, CommandConfig } from './commands';
const rootPath = resolve(`${__dirname}/utils/__fixtures__/kibana`);
@ -25,12 +25,12 @@ function getExpectedProjectsAndGraph(runMock: any) {
return { projects, graph };
}
let command: Command;
let config: CommandConfig;
let command: ICommand;
let config: ICommandConfig;
beforeEach(() => {
command = {
name: 'test name',
description: 'test description',
name: 'test name',
run: jest.fn(),
};
@ -41,7 +41,9 @@ beforeEach(() => {
};
// Reduce the noise that comes from the run command.
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {
// noop
});
});
test('passes all found projects to the command if no filter is specified', async () => {
@ -102,9 +104,9 @@ test('respects both `include` and `exclude` filters if specified at the same tim
});
test('does not run command if all projects are filtered out', async () => {
let mockProcessExit = jest
.spyOn(process, 'exit')
.mockImplementation(() => {});
const mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
// noop
});
await runCommand(command, {
...config,

View file

@ -1,16 +1,17 @@
import chalk from 'chalk';
import wrapAnsi from 'wrap-ansi';
import indentString from 'indent-string';
import wrapAnsi from 'wrap-ansi';
import { ICommand, ICommandConfig } from './commands';
import { getProjectPaths, IProjectPathOptions } from './config';
import { CliError } from './utils/errors';
import { getProjects, buildProjectGraph } from './utils/projects';
import { log } from './utils/log';
import { buildProjectGraph, getProjects } from './utils/projects';
import { renderProjectsTree } from './utils/projects_tree';
import { getProjectPaths, ProjectPathOptions } from './config';
import { Command, CommandConfig } from './commands';
export async function runCommand(command: Command, config: CommandConfig) {
export async function runCommand(command: ICommand, config: ICommandConfig) {
try {
console.log(
log.write(
chalk.bold(
`Running [${chalk.green(command.name)}] command from [${chalk.yellow(
config.rootPath
@ -20,7 +21,7 @@ export async function runCommand(command: Command, config: CommandConfig) {
const projectPaths = getProjectPaths(
config.rootPath,
config.options as ProjectPathOptions
config.options as IProjectPathOptions
);
const projects = await getProjects(config.rootPath, projectPaths, {
@ -29,7 +30,7 @@ export async function runCommand(command: Command, config: CommandConfig) {
});
if (projects.size === 0) {
console.log(
log.write(
chalk.red(
`There are no projects found. Double check project name(s) in '-i/--include' and '-e/--exclude' filters.\n`
)
@ -39,18 +40,18 @@ export async function runCommand(command: Command, config: CommandConfig) {
const projectGraph = buildProjectGraph(projects);
console.log(
log.write(
chalk.bold(`Found [${chalk.green(projects.size.toString())}] projects:\n`)
);
console.log(renderProjectsTree(config.rootPath, projects));
log.write(renderProjectsTree(config.rootPath, projects));
await command.run(projects, projectGraph, config);
} catch (e) {
console.log(chalk.bold.red(`\n[${command.name}] failed:\n`));
log.write(chalk.bold.red(`\n[${command.name}] failed:\n`));
if (e instanceof CliError) {
const msg = chalk.red(`CliError: ${e.message}\n`);
console.log(wrapAnsi(msg, 80));
log.write(wrapAnsi(msg, 80));
const keys = Object.keys(e.meta);
if (keys.length > 0) {
@ -59,11 +60,11 @@ export async function runCommand(command: Command, config: CommandConfig) {
return `${key}: ${value}`;
});
console.log('Additional debugging info:\n');
console.log(indentString(metaOutput.join('\n'), 3));
log.write('Additional debugging info:\n');
log.write(indentString(metaOutput.join('\n'), 3));
}
} else {
console.log(e.stack);
log.write(e.stack);
}
process.exit(1);

View file

@ -1,5 +1,5 @@
import { resolve, sep as pathSep } from 'path';
import cloneDeepWith from 'lodash.clonedeepwith';
import { resolve, sep as pathSep } from 'path';
const repoRoot = resolve(__dirname, '../../../../');
@ -16,8 +16,8 @@ const normalizePaths = (value: any) => {
});
return {
didReplacement,
clone,
didReplacement,
};
};

View file

@ -1,7 +1,7 @@
import execa from 'execa';
import chalk from 'chalk';
import logTransformer from 'strong-log-transformer';
import execa from 'execa';
import logSymbols from 'log-symbols';
import logTransformer from 'strong-log-transformer';
function generateColors() {
const colorWheel = [
@ -42,8 +42,8 @@ export function spawnStreaming(
const color = nextColor();
const prefixedStdout = logTransformer({ tag: `${color.bold(prefix)}:` });
const prefixedStderr = logTransformer({
tag: `${logSymbols.error} ${color.bold(prefix)}:`,
mergeMultiline: true,
tag: `${logSymbols.error} ${color.bold(prefix)}:`,
});
spawned.stdout.pipe(prefixedStdout).pipe(process.stdout);

View file

@ -1,8 +1,8 @@
import fs from 'fs';
import { relative, dirname } from 'path';
import { promisify } from 'util';
import cmdShimCb from 'cmd-shim';
import fs from 'fs';
import mkdirpCb from 'mkdirp';
import { dirname, relative } from 'path';
import { promisify } from 'util';
const stat = promisify(fs.stat);
const readFile = promisify(fs.readFile);

View file

@ -15,10 +15,10 @@ const projectsByName = new Map([
'foo',
new Project(
{
name: 'foo',
dependencies: {
bar: 'link:../bar',
},
name: 'foo',
},
resolve(__dirname, 'foo')
),
@ -27,8 +27,8 @@ const projectsByName = new Map([
'bar',
new Project(
{
name: 'bar',
bin: 'bin/bar.js',
name: 'bar',
},
resolve(__dirname, 'bar')
),
@ -37,10 +37,10 @@ const projectsByName = new Map([
'baz',
new Project(
{
name: 'baz',
devDependencies: {
bar: 'link:../bar',
},
name: 'baz',
},
resolve(__dirname, 'baz')
),
@ -82,7 +82,9 @@ describe('bin script points to a file', () => {
const fs = require('./fs');
fs.isFile.mockReturnValue(true);
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {
// noop
});
await linkProjectExecutables(projectsByName, projectGraph);
logMock.mockRestore();

View file

@ -1,8 +1,9 @@
import { resolve, relative, dirname, sep } from 'path';
import { dirname, relative, resolve, sep } from 'path';
import chalk from 'chalk';
import { createSymlink, isFile, chmod, mkdirp } from './fs';
import { chmod, createSymlink, isFile, mkdirp } from './fs';
import { log } from './log';
import { ProjectGraph, ProjectMap } from './projects';
/**
@ -39,7 +40,7 @@ export async function linkProjectExecutables(
.split(sep)
.join('/');
console.log(
log.write(
chalk`{dim [${project.name}]} ${name} -> {dim ${projectRelativePath}}`
);

View file

@ -0,0 +1,11 @@
export const log = {
/**
* Log something to the console. Ideally we would use a real logger in
* kbn-pm, but that's a pretty big change for now.
* @param ...args
*/
write(...args: any[]) {
// tslint:disable no-console
console.log(...args);
},
};

View file

@ -1,20 +1,25 @@
import readPkg from 'read-pkg';
import writePkg from 'write-pkg';
import path from 'path';
export type PackageJson = { [key: string]: any };
export type PackageDependencies = { [key: string]: string };
export type PackageScripts = { [key: string]: string };
export interface IPackageJson {
[key: string]: any;
}
export interface IPackageDependencies {
[key: string]: string;
}
export interface IPackageScripts {
[key: string]: string;
}
export function readPackageJson(dir: string) {
return readPkg(dir, { normalize: false });
}
export function writePackageJson(path: string, json: PackageJson) {
export function writePackageJson(path: string, json: IPackageJson) {
return writePkg(path, json);
}
export const createProductionPackageJson = (pkgJson: PackageJson) => ({
export const createProductionPackageJson = (pkgJson: IPackageJson) => ({
...pkgJson,
dependencies: transformDependencies(pkgJson.dependencies),
});
@ -31,8 +36,8 @@ export const isLinkDependency = (depVersion: string) =>
* will then _copy_ the `file:` dependencies into `node_modules` instead of
* symlinking like we do in development.
*/
export function transformDependencies(dependencies: PackageDependencies = {}) {
const newDeps: PackageDependencies = {};
export function transformDependencies(dependencies: IPackageDependencies = {}) {
const newDeps: IPackageDependencies = {};
for (const name of Object.keys(dependencies)) {
const depVersion = dependencies[name];
if (isLinkDependency(depVersion)) {

View file

@ -106,9 +106,9 @@ test('rejects if any promise rejects', async () => {
function createPromiseWithResolve() {
let resolve: (val?: any) => void;
let reject: (err?: any) => void;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve: resolve!, reject: reject!, called: false };
}

View file

@ -1,11 +1,11 @@
import { resolve, join } from 'path';
import { join, resolve } from 'path';
import { PackageJson } from './package_json';
import { IPackageJson } from './package_json';
import { Project } from './project';
const rootPath = resolve(`${__dirname}/__fixtures__/kibana`);
const createProjectWith = (packageJson: PackageJson, path = '') =>
const createProjectWith = (packageJson: IPackageJson, path = '') =>
new Project(
{
name: 'kibana',
@ -25,12 +25,12 @@ describe('fromPath', () => {
test('fields', async () => {
const kibana = createProjectWith({
scripts: {
test: 'jest',
},
dependencies: {
foo: '1.2.3',
},
scripts: {
test: 'jest',
},
});
expect(kibana.name).toBe('kibana');

View file

@ -1,49 +1,50 @@
import path from 'path';
import { inspect } from 'util';
import chalk from 'chalk';
import { relative, resolve as resolvePath } from 'path';
import { inspect } from 'util';
import { CliError } from './errors';
import { log } from './log';
import {
IPackageDependencies,
IPackageJson,
IPackageScripts,
isLinkDependency,
readPackageJson,
} from './package_json';
import {
installInDir,
runScriptInPackage,
runScriptInPackageStreaming,
} from './scripts';
import {
PackageJson,
PackageDependencies,
PackageScripts,
isLinkDependency,
readPackageJson,
} from './package_json';
import { CliError } from './errors';
interface BuildConfig {
interface IBuildConfig {
skip?: boolean;
intermediateBuildDirectory?: string;
}
export class Project {
static async fromPath(path: string) {
public static async fromPath(path: string) {
const pkgJson = await readPackageJson(path);
return new Project(pkgJson, path);
}
public readonly json: PackageJson;
public readonly json: IPackageJson;
public readonly packageJsonLocation: string;
public readonly nodeModulesLocation: string;
public readonly targetLocation: string;
public readonly path: string;
public readonly allDependencies: PackageDependencies;
public readonly productionDependencies: PackageDependencies;
public readonly devDependencies: PackageDependencies;
public readonly scripts: PackageScripts;
public readonly allDependencies: IPackageDependencies;
public readonly productionDependencies: IPackageDependencies;
public readonly devDependencies: IPackageDependencies;
public readonly scripts: IPackageScripts;
constructor(packageJson: PackageJson, projectPath: string) {
constructor(packageJson: IPackageJson, projectPath: string) {
this.json = Object.freeze(packageJson);
this.path = projectPath;
this.packageJsonLocation = path.resolve(this.path, 'package.json');
this.nodeModulesLocation = path.resolve(this.path, 'node_modules');
this.targetLocation = path.resolve(this.path, 'target');
this.packageJsonLocation = resolvePath(this.path, 'package.json');
this.nodeModulesLocation = resolvePath(this.path, 'node_modules');
this.targetLocation = resolvePath(this.path, 'target');
this.productionDependencies = this.json.dependencies || {};
this.devDependencies = this.json.devDependencies || {};
@ -59,9 +60,9 @@ export class Project {
return this.json.name;
}
ensureValidProjectDependency(project: Project) {
public ensureValidProjectDependency(project: Project) {
const relativePathToProject = normalizePath(
path.relative(this.path, project.path)
relative(this.path, project.path)
);
const versionInPackageJson = this.allDependencies[project.name];
@ -73,9 +74,9 @@ export class Project {
const updateMsg = 'Update its package.json to the expected value below.';
const meta = {
package: `${this.name} (${this.packageJsonLocation})`,
expected: `"${project.name}": "${expectedVersionInPackageJson}"`,
actual: `"${project.name}": "${versionInPackageJson}"`,
expected: `"${project.name}": "${expectedVersionInPackageJson}"`,
package: `${this.name} (${this.packageJsonLocation})`,
};
if (isLinkDependency(versionInPackageJson)) {
@ -95,7 +96,7 @@ export class Project {
);
}
getBuildConfig(): BuildConfig {
public getBuildConfig(): IBuildConfig {
return (this.json.kibana && this.json.kibana.build) || {};
}
@ -104,18 +105,18 @@ export class Project {
* This config can be specified to only include the project's build artifacts
* instead of everything located in the project directory.
*/
getIntermediateBuildDirectory() {
return path.resolve(
public getIntermediateBuildDirectory() {
return resolvePath(
this.path,
this.getBuildConfig().intermediateBuildDirectory || '.'
);
}
hasScript(name: string) {
public hasScript(name: string) {
return name in this.scripts;
}
getExecutables(): { [key: string]: string } {
public getExecutables(): { [key: string]: string } {
const raw = this.json.bin;
if (!raw) {
@ -124,14 +125,14 @@ export class Project {
if (typeof raw === 'string') {
return {
[this.name]: path.resolve(this.path, raw),
[this.name]: resolvePath(this.path, raw),
};
}
if (typeof raw === 'object') {
const binsConfig: { [k: string]: string } = {};
for (const binName of Object.keys(raw)) {
binsConfig[binName] = path.resolve(this.path, raw[binName]);
binsConfig[binName] = resolvePath(this.path, raw[binName]);
}
return binsConfig;
}
@ -140,14 +141,14 @@ export class Project {
`[${this.name}] has an invalid "bin" field in its package.json, ` +
`expected an object or a string`,
{
package: `${this.name} (${this.packageJsonLocation})`,
binConfig: inspect(raw),
package: `${this.name} (${this.packageJsonLocation})`,
}
);
}
async runScript(scriptName: string, args: string[] = []) {
console.log(
public async runScript(scriptName: string, args: string[] = []) {
log.write(
chalk.bold(
`\n\nRunning script [${chalk.green(scriptName)}] in [${chalk.green(
this.name
@ -157,16 +158,16 @@ export class Project {
return runScriptInPackage(scriptName, args, this);
}
runScriptStreaming(scriptName: string, args: string[] = []) {
public runScriptStreaming(scriptName: string, args: string[] = []) {
return runScriptInPackageStreaming(scriptName, args, this);
}
hasDependencies() {
public hasDependencies() {
return Object.keys(this.allDependencies).length > 0;
}
async installDependencies({ extraArgs }: { extraArgs: string[] }) {
console.log(
public async installDependencies({ extraArgs }: { extraArgs: string[] }) {
log.write(
chalk.bold(
`\n\nInstalling dependencies in [${chalk.green(this.name)}]:\n`
)

View file

@ -1,15 +1,15 @@
import { resolve } from 'path';
import {
getProjects,
buildProjectGraph,
topologicallyBatchProjects,
includeTransitiveProjects,
ProjectMap,
ProjectGraph,
} from './projects';
import { Project } from './project';
import { getProjectPaths } from '../config';
import { Project } from './project';
import {
buildProjectGraph,
getProjects,
includeTransitiveProjects,
ProjectGraph,
ProjectMap,
topologicallyBatchProjects,
} from './projects';
const rootPath = resolve(`${__dirname}/__fixtures__/kibana`);
@ -164,12 +164,12 @@ describe('#getProjects', () => {
describe('#buildProjectGraph', () => {
test('builds full project graph', async () => {
const projects = await getProjects(rootPath, [
const allProjects = await getProjects(rootPath, [
'.',
'packages/*',
'../plugins/*',
]);
const graph = buildProjectGraph(projects);
const graph = buildProjectGraph(allProjects);
const expected: { [k: string]: string[] } = {};
for (const [projectName, projects] of graph.entries()) {

View file

@ -9,12 +9,15 @@ const glob = promisify(globSync);
export type ProjectMap = Map<string, Project>;
export type ProjectGraph = Map<string, Project[]>;
export type ProjectsOptions = { include?: string[]; exclude?: string[] };
export interface IProjectsOptions {
include?: string[];
exclude?: string[];
}
export async function getProjects(
rootPath: string,
projectsPathsPatterns: string[],
{ include = [], exclude = [] }: ProjectsOptions = {}
{ include = [], exclude = [] }: IProjectsOptions = {}
) {
const projects: ProjectMap = new Map();

View file

@ -1,9 +1,9 @@
import { resolve } from 'path';
import { stripAnsiSnapshotSerializer } from '../test_helpers';
import { renderProjectsTree } from './projects_tree';
import { getProjects } from './projects';
import { getProjectPaths } from '../config';
import { stripAnsiSnapshotSerializer } from '../test_helpers';
import { getProjects } from './projects';
import { renderProjectsTree } from './projects_tree';
const rootPath = resolve(`${__dirname}/__fixtures__/kibana`);

View file

@ -1,5 +1,5 @@
import path from 'path';
import chalk from 'chalk';
import path from 'path';
import { Project } from './project';
@ -13,41 +13,44 @@ export function renderProjectsTree(
return treeToString(createTreeStructure(projectsTree));
}
type Tree = {
interface ITree {
name?: string;
children?: TreeChildren;
};
interface TreeChildren extends Array<Tree> {}
children?: ITreeChildren;
}
interface ITreeChildren extends Array<ITree> {}
type DirOrProjectName = string | typeof projectKey;
type ProjectsTree = Map<DirOrProjectName, ProjectsTreeValue | string>;
interface ProjectsTreeValue extends ProjectsTree {}
function treeToString(tree: Tree) {
return [tree.name].concat(childrenToString(tree.children, '')).join('\n');
interface IProjectsTree extends Map<DirOrProjectName, string | IProjectsTree> {}
function treeToString(tree: ITree) {
return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n');
}
function childrenToString(tree: TreeChildren | undefined, treePrefix: string) {
function childrenToStrings(
tree: ITreeChildren | undefined,
treePrefix: string
) {
if (tree === undefined) {
return [];
}
let string: string[] = [];
let strings: string[] = [];
tree.forEach((node, index) => {
const isLastNode = tree.length - 1 === index;
const nodePrefix = isLastNode ? '└── ' : '├── ';
const childPrefix = isLastNode ? ' ' : '│ ';
const childrenPrefix = treePrefix + childPrefix;
string.push(`${treePrefix}${nodePrefix}${node.name}`);
string = string.concat(childrenToString(node.children, childrenPrefix));
strings.push(`${treePrefix}${nodePrefix}${node.name}`);
strings = strings.concat(childrenToStrings(node.children, childrenPrefix));
});
return string;
return strings;
}
function createTreeStructure(tree: ProjectsTree): Tree {
function createTreeStructure(tree: IProjectsTree): ITree {
let name: string | undefined;
const children: TreeChildren = [];
const children: ITreeChildren = [];
for (const [dir, project] of tree.entries()) {
// This is a leaf node (aka a project)
@ -63,8 +66,8 @@ function createTreeStructure(tree: ProjectsTree): Tree {
if (project.size === 1 && project.has(projectKey)) {
const projectName = project.get(projectKey)! as string;
children.push({
name: dirOrProjectName(dir, projectName),
children: [],
name: dirOrProjectName(dir, projectName),
});
continue;
}
@ -77,8 +80,8 @@ function createTreeStructure(tree: ProjectsTree): Tree {
const projectName = subtree.name;
children.push({
name: dirOrProjectName(dir, projectName),
children: subtree.children,
name: dirOrProjectName(dir, projectName),
});
continue;
}
@ -91,15 +94,15 @@ function createTreeStructure(tree: ProjectsTree): Tree {
const newName = chalk.dim(path.join(dir.toString(), child.name!));
children.push({
name: newName,
children: child.children,
name: newName,
});
continue;
}
children.push({
name: chalk.dim(dir.toString()),
children: subtree.children,
name: chalk.dim(dir.toString()),
});
}
@ -113,7 +116,7 @@ function dirOrProjectName(dir: DirOrProjectName, projectName: string) {
}
function buildProjectsTree(rootPath: string, projects: Map<string, Project>) {
const tree: ProjectsTree = new Map();
const tree: IProjectsTree = new Map();
for (const project of projects.values()) {
if (rootPath === project.path) {
@ -128,7 +131,7 @@ function buildProjectsTree(rootPath: string, projects: Map<string, Project>) {
}
function addProjectToTree(
tree: ProjectsTree,
tree: IProjectsTree,
pathParts: string[],
project: Project
) {
@ -141,7 +144,7 @@ function addProjectToTree(
tree.set(currentDir, new Map());
}
const subtree = tree.get(currentDir) as ProjectsTree;
const subtree = tree.get(currentDir) as IProjectsTree;
addProjectToTree(subtree, rest, project);
}
}

View file

@ -14,7 +14,7 @@ const defaultHandlerReadinessTimeout = 2000;
/**
* Describes configurable watch options.
*/
interface WatchOptions {
interface IWatchOptions {
/**
* Number of milliseconds to wait before we fall back to default watch handler.
*/
@ -33,7 +33,7 @@ function getWatchHandlers(
{
handlerDelay = defaultHandlerDelay,
handlerReadinessTimeout = defaultHandlerReadinessTimeout,
}: WatchOptions
}: IWatchOptions
) {
const typescriptHandler = buildOutput$
.first(data => data.includes('$ tsc'))
@ -60,7 +60,7 @@ function getWatchHandlers(
export function waitUntilWatchIsReady(
stream: NodeJS.EventEmitter,
opts: WatchOptions = {}
opts: IWatchOptions = {}
) {
const buildOutput$ = new Subject<string>();
const onDataListener = (data: Buffer) =>

View file

@ -1,19 +0,0 @@
extends: ../../tslint.yaml
rules:
max-classes-per-file: false
interface-name: false
variable-name: false
no-empty: false
object-literal-sort-keys: false
member-ordering: false
no-console: false
only-arrow-functions: false
no-shadowed-variable: false
no-empty-interface: false
ordered-imports: false
interface-over-type-literal: false
prettier: false
prefer-const: false
member-access: false
no-unused-variable: false

View file

@ -1,16 +1,22 @@
/* tslint:disable max-classes-per-file */
import { System } from './system';
import { KibanaSystem } from './system_types';
test('can get exposed values after starting', () => {
type CoreType = { bar: string };
type DepsType = { quux: string };
type ExposedType = {
core: CoreType;
deps: DepsType;
};
interface ICoreType {
bar: string;
}
interface IDepsType {
quux: string;
}
interface IExposedType {
core: ICoreType;
deps: IDepsType;
}
class FooSystem extends KibanaSystem<CoreType, DepsType, ExposedType> {
start() {
class FooSystem extends KibanaSystem<ICoreType, IDepsType, IExposedType> {
public start() {
return {
core: this.kibana,
deps: this.deps,
@ -39,7 +45,7 @@ test('can get exposed values after starting', () => {
test('throws if start returns a promise', () => {
class FooSystem extends KibanaSystem<any, any, any> {
async start() {
public async start() {
return 'foo';
}
}
@ -55,9 +61,11 @@ test('throws if start returns a promise', () => {
test('throws if stop returns a promise', () => {
class FooSystem extends KibanaSystem<any, any, any> {
start() {}
public start() {
// noop
}
async stop() {
public async stop() {
return 'stop';
}
}

View file

@ -1,9 +1,9 @@
import {
SystemsType,
SystemName,
SystemMetadata,
KibanaSystemClassStatic,
IKibanaSystemClassStatic,
ISystemMetadata,
ISystemsType,
KibanaSystem,
SystemName,
} from './system_types';
function isPromise(obj: any) {
@ -12,45 +12,45 @@ function isPromise(obj: any) {
);
}
export class System<C, M extends SystemMetadata, D extends SystemsType, E> {
readonly name: SystemName;
readonly dependencies: SystemName[];
readonly metadata?: M;
export class System<C, M extends ISystemMetadata, D extends ISystemsType, E> {
public readonly name: SystemName;
public readonly dependencies: SystemName[];
public readonly metadata?: M;
private readonly _systemClass: KibanaSystemClassStatic<C, D, E>;
private _systemInstance?: KibanaSystem<C, D, E>;
private _exposedValues?: E;
private readonly systemClass: IKibanaSystemClassStatic<C, D, E>;
private systemInstance?: KibanaSystem<C, D, E>;
private exposedValues?: E;
constructor(
name: SystemName,
config: {
metadata?: M;
dependencies?: SystemName[];
implementation: KibanaSystemClassStatic<C, D, E>;
implementation: IKibanaSystemClassStatic<C, D, E>;
}
) {
this.name = name;
this.dependencies = config.dependencies || [];
this.metadata = config.metadata;
this._systemClass = config.implementation;
this.systemClass = config.implementation;
}
getExposedValues(): E {
if (this._systemInstance === undefined) {
public getExposedValues(): E {
if (this.systemInstance === undefined) {
throw new Error(
'trying to get the exposed value of a system that is NOT running'
);
}
return this._exposedValues!;
return this.exposedValues!;
}
start(kibanaValues: C, dependenciesValues: D) {
this._systemInstance = new this._systemClass(
public start(kibanaValues: C, dependenciesValues: D) {
this.systemInstance = new this.systemClass(
kibanaValues,
dependenciesValues
);
const exposedValues = this._systemInstance.start();
const exposedValues = this.systemInstance.start();
if (isPromise(exposedValues)) {
throw new Error(
@ -60,15 +60,15 @@ export class System<C, M extends SystemMetadata, D extends SystemsType, E> {
);
}
this._exposedValues =
this.exposedValues =
exposedValues === undefined ? ({} as E) : exposedValues;
}
stop() {
const stoppedResponse = this._systemInstance && this._systemInstance.stop();
public stop() {
const stoppedResponse = this.systemInstance && this.systemInstance.stop();
this._exposedValues = undefined;
this._systemInstance = undefined;
this.exposedValues = undefined;
this.systemInstance = undefined;
if (isPromise(stoppedResponse)) {
throw new Error(

View file

@ -1,44 +1,53 @@
/* tslint:disable max-classes-per-file */
import { System } from './system';
import { KibanaSystemApiFactory, SystemLoader } from './system_loader';
import { KibanaSystem } from './system_types';
import { SystemLoader, KibanaSystemApiFactory } from './system_loader';
// To make types simpler in the tests
type CoreType = void;
const createCoreValues = () => {};
const createCoreValues = () => {
// noop
};
test('starts system with core api', () => {
expect.assertions(1);
type KibanaCoreApi = { fromCore: boolean; name: string };
type Metadata = { configPath?: string };
interface IKibanaCoreApi {
fromCore: boolean;
name: string;
}
interface IMetadata {
configPath?: string;
}
class FooSystem extends KibanaSystem<KibanaCoreApi, {}> {
start() {
class FooSystem extends KibanaSystem<IKibanaCoreApi, {}> {
public start() {
expect(this.kibana).toEqual({
name: 'foo',
fromCore: true,
metadata: {
configPath: 'config.path.foo',
},
name: 'foo',
});
}
}
const foo = new System('foo', {
implementation: FooSystem,
metadata: {
configPath: 'config.path.foo',
},
implementation: FooSystem,
});
const createSystemApi: KibanaSystemApiFactory<KibanaCoreApi, Metadata> = (
const createSystemApi: KibanaSystemApiFactory<IKibanaCoreApi, IMetadata> = (
name,
metadata
) => {
return {
name,
metadata,
fromCore: true,
metadata,
name,
};
};
@ -51,22 +60,22 @@ test('starts system with core api', () => {
test('system can expose a value', () => {
expect.assertions(1);
type Foo = {
interface IFoo {
foo: {
value: string;
};
};
}
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
start() {
class FooSystem extends KibanaSystem<CoreType, {}, IFoo['foo']> {
public start() {
return {
value: 'my-value',
};
}
}
class BarSystem extends KibanaSystem<CoreType, Foo> {
start() {
class BarSystem extends KibanaSystem<CoreType, IFoo> {
public start() {
expect(this.deps.foo).toEqual({ value: 'my-value' });
}
}
@ -89,22 +98,22 @@ test('system can expose a value', () => {
test('system can expose a function', () => {
expect.assertions(2);
type Foo = {
interface IFoo {
foo: {
fn: (val: string) => string;
};
};
}
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
start(): Foo['foo'] {
class FooSystem extends KibanaSystem<CoreType, {}, IFoo['foo']> {
public start(): IFoo['foo'] {
return {
fn: val => `test-${val}`,
};
}
}
class BarSystem extends KibanaSystem<CoreType, Foo> {
start() {
class BarSystem extends KibanaSystem<CoreType, IFoo> {
public start() {
expect(this.deps.foo).toBeDefined();
expect(this.deps.foo.fn('some-value')).toBe('test-some-value');
}
@ -128,36 +137,36 @@ test('system can expose a function', () => {
test('can expose value with same name across multiple systems', () => {
expect.assertions(2);
type Foo = {
interface IFoo {
foo: {
value: string;
};
};
}
type Bar = {
interface IBar {
bar: {
value: string;
};
};
}
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
start(): Foo['foo'] {
class FooSystem extends KibanaSystem<CoreType, {}, IFoo['foo']> {
public start(): IFoo['foo'] {
return {
value: 'value-foo',
};
}
}
class BarSystem extends KibanaSystem<CoreType, {}, Bar['bar']> {
start(): Bar['bar'] {
class BarSystem extends KibanaSystem<CoreType, {}, IBar['bar']> {
public start(): IBar['bar'] {
return {
value: 'value-bar',
};
}
}
class QuuxSystem extends KibanaSystem<CoreType, Foo & Bar> {
start() {
class QuuxSystem extends KibanaSystem<CoreType, IFoo & IBar> {
public start() {
expect(this.deps.foo).toEqual({ value: 'value-foo' });
expect(this.deps.bar).toEqual({ value: 'value-bar' });
}
@ -186,32 +195,36 @@ test('can expose value with same name across multiple systems', () => {
test('receives values from dependencies but not transitive dependencies', () => {
expect.assertions(3);
type Grandchild = {
interface IGrandchild {
grandchild: {
value: string;
};
};
}
type Child = {
interface IChild {
child: {
value: string;
};
};
}
class GrandchildSystem extends KibanaSystem<
CoreType,
{},
Grandchild['grandchild']
IGrandchild['grandchild']
> {
start() {
public start() {
return {
value: 'grandchild',
};
}
}
class ChildSystem extends KibanaSystem<CoreType, Grandchild, Child['child']> {
start() {
class ChildSystem extends KibanaSystem<
CoreType,
IGrandchild,
IChild['child']
> {
public start() {
expect(this.deps.grandchild).toEqual({ value: 'grandchild' });
return {
@ -220,8 +233,8 @@ test('receives values from dependencies but not transitive dependencies', () =>
}
}
class ParentSystem extends KibanaSystem<CoreType, Grandchild & Child> {
start() {
class ParentSystem extends KibanaSystem<CoreType, IGrandchild & IChild> {
public start() {
expect(this.deps.child).toEqual({ value: 'child' });
expect(this.deps.grandchild).toBeUndefined();
}
@ -251,24 +264,24 @@ test('receives values from dependencies but not transitive dependencies', () =>
test('keeps reference on registered value', () => {
expect.assertions(1);
type Child = {
interface IChild {
child: {
value: {};
};
};
}
const myRef = {};
class ChildSystem extends KibanaSystem<CoreType, {}, Child['child']> {
start() {
class ChildSystem extends KibanaSystem<CoreType, {}, IChild['child']> {
public start() {
return {
value: myRef,
};
}
}
class ParentSystem extends KibanaSystem<CoreType, Child> {
start() {
class ParentSystem extends KibanaSystem<CoreType, IChild> {
public start() {
expect(this.deps.child.value).toBe(myRef);
}
}
@ -291,15 +304,15 @@ test('keeps reference on registered value', () => {
test('can register multiple values in single system', () => {
expect.assertions(1);
type Child = {
interface IChild {
child: {
value1: number;
value2: number;
};
};
}
class ChildSystem extends KibanaSystem<CoreType, {}, Child['child']> {
start() {
class ChildSystem extends KibanaSystem<CoreType, {}, IChild['child']> {
public start() {
return {
value1: 1,
value2: 2,
@ -307,8 +320,8 @@ test('can register multiple values in single system', () => {
}
}
class ParentSystem extends KibanaSystem<CoreType, Child> {
start() {
class ParentSystem extends KibanaSystem<CoreType, IChild> {
public start() {
expect(this.deps.child).toEqual({
value1: 1,
value2: 2,
@ -333,7 +346,9 @@ test('can register multiple values in single system', () => {
test("throws if starting a system that depends on a system that's not present", () => {
class FooSystem extends KibanaSystem<CoreType, {}> {
start() {}
public start() {
// noop
}
}
const foo = new System('foo', {
@ -352,7 +367,9 @@ test("throws if starting a system that depends on a system that's not present",
test("throws if adding that has the same name as a system that's already added", () => {
class FooSystem extends KibanaSystem<CoreType, {}> {
start() {}
public start() {
// noop
}
}
const foo = new System('foo', {
@ -371,19 +388,19 @@ test('stops systems in reverse order of their starting order', () => {
const events: string[] = [];
class FooSystem extends KibanaSystem<CoreType, {}> {
start() {
public start() {
events.push('start foo');
}
stop() {
public stop() {
events.push('stop foo');
}
}
class BarSystem extends KibanaSystem<CoreType, {}> {
start() {
public start() {
events.push('start bar');
}
stop() {
public stop() {
events.push('stop bar');
}
}
@ -409,18 +426,18 @@ test('stops systems in reverse order of their starting order', () => {
test('can add systems before adding its dependencies', () => {
expect.assertions(1);
type Foo = {
interface IFoo {
foo: string;
};
}
class FooSystem extends KibanaSystem<CoreType, {}, Foo['foo']> {
start() {
class FooSystem extends KibanaSystem<CoreType, {}, IFoo['foo']> {
public start() {
return 'value';
}
}
class BarSystem extends KibanaSystem<CoreType, Foo> {
start() {
class BarSystem extends KibanaSystem<CoreType, IFoo> {
public start() {
expect(this.deps.foo).toBe('value');
}
}
@ -447,13 +464,13 @@ test('can add multiple system specs at the same time', () => {
const spy = jest.fn();
class FooSystem extends KibanaSystem<CoreType, {}> {
start() {
public start() {
spy();
}
}
class BarSystem extends KibanaSystem<CoreType, {}> {
start() {
public start() {
spy();
}
}

View file

@ -1,15 +1,15 @@
import { System } from './system';
import { SystemName, SystemMetadata, SystemsType } from './system_types';
import { getSortedSystemNames } from './sorted_systems';
import { System } from './system';
import { ISystemMetadata, ISystemsType, SystemName } from './system_types';
export type KibanaSystemApiFactory<C, M> = (
name: SystemName,
metadata?: M
) => C;
export class SystemLoader<C, M extends SystemMetadata> {
private readonly _systems = new Map<SystemName, System<C, M, any, any>>();
private _startedSystems: SystemName[] = [];
export class SystemLoader<C, M extends ISystemMetadata> {
private readonly systems = new Map<SystemName, System<C, M, any, any>>();
private startedSystems: SystemName[] = [];
constructor(
/**
@ -17,37 +17,54 @@ export class SystemLoader<C, M extends SystemMetadata> {
* information about a system before it's started, and the return value will
* be injected into the system at startup.
*/
private readonly _kibanaSystemApiFactory: KibanaSystemApiFactory<C, M>
private readonly kibanaSystemApiFactory: KibanaSystemApiFactory<C, M>
) {}
addSystems(systemSpecs: System<C, M, any, any>[]) {
public addSystems(systemSpecs: Array<System<C, M, any, any>>) {
systemSpecs.forEach(systemSpec => {
this.addSystem(systemSpec);
});
}
addSystem<D extends SystemsType, E = void>(system: System<C, M, D, E>) {
if (this._systems.has(system.name)) {
public addSystem<D extends ISystemsType, E = void>(
system: System<C, M, D, E>
) {
if (this.systems.has(system.name)) {
throw new Error(`a system named [${system.name}] has already been added`);
}
this._systems.set(system.name, system);
this.systems.set(system.name, system);
}
startSystems() {
public startSystems() {
this._ensureAllSystemDependenciesCanBeResolved();
getSortedSystemNames(this._systems)
.map(systemName => this._systems.get(systemName)!)
getSortedSystemNames(this.systems)
.map(systemName => this.systems.get(systemName)!)
.forEach(systemSpec => {
this.startSystem(systemSpec);
});
}
/**
* Stop all systems in the reverse order of when they were started
*/
public stopSystems() {
this.startedSystems
.map(systemName => this.systems.get(systemName)!)
.reverse()
.forEach(system => {
system.stop();
this.systems.delete(system.name);
});
this.startedSystems = [];
}
private _ensureAllSystemDependenciesCanBeResolved() {
for (const [systemName, system] of this._systems) {
for (const [systemName, system] of this.systems) {
for (const systemDependency of system.dependencies) {
if (!this._systems.has(systemDependency)) {
if (!this.systems.has(systemDependency)) {
throw new Error(
`System [${systemName}] depends on [${systemDependency}], which is not present`
);
@ -56,38 +73,23 @@ export class SystemLoader<C, M extends SystemMetadata> {
}
}
private startSystem<D extends SystemsType, E = void>(
private startSystem<D extends ISystemsType, E = void>(
system: System<C, M, D, E>
) {
const dependenciesValues = {} as D;
for (const dependency of system.dependencies) {
dependenciesValues[dependency] = this._systems
dependenciesValues[dependency] = this.systems
.get(dependency)!
.getExposedValues();
}
const kibanaSystemApi = this._kibanaSystemApiFactory(
const kibanaSystemApi = this.kibanaSystemApiFactory(
system.name,
system.metadata
);
system.start(kibanaSystemApi, dependenciesValues);
this._startedSystems.push(system.name);
}
/**
* Stop all systems in the reverse order of when they were started
*/
stopSystems() {
this._startedSystems
.map(systemName => this._systems.get(systemName)!)
.reverse()
.forEach(system => {
system.stop();
this._systems.delete(system.name);
});
this._startedSystems = [];
this.startedSystems.push(system.name);
}
}

View file

@ -1,18 +1,18 @@
export type SystemName = string;
export type SystemMetadata = {
export interface ISystemMetadata {
[key: string]: any;
};
}
export type SystemsType = {
export interface ISystemsType {
[systemName: string]: any;
};
}
export abstract class KibanaSystem<C, D extends SystemsType, E = void> {
export abstract class KibanaSystem<C, D extends ISystemsType, E = void> {
constructor(readonly kibana: C, readonly deps: D) {}
abstract start(): E;
public abstract start(): E;
stop() {
public stop() {
// default implementation of stop does nothing
}
}
@ -27,6 +27,6 @@ export abstract class KibanaSystem<C, D extends SystemsType, E = void> {
*
* See https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes
*/
export interface KibanaSystemClassStatic<C, D extends SystemsType, E = void> {
export interface IKibanaSystemClassStatic<C, D extends ISystemsType, E = void> {
new (kibana: C, deps: D): KibanaSystem<C, D, E>;
}

View file

@ -8,7 +8,7 @@ test('returns a topologically ordered sequence', () => {
['d', ['a']],
]);
let sorted = topologicalSort(nodes);
const sorted = topologicalSort(nodes);
expect(sorted).toBeDefined();
@ -23,7 +23,7 @@ test('handles multiple "roots" with no deps', () => {
['d', ['a']],
]);
let sorted = topologicalSort(nodes);
const sorted = topologicalSort(nodes);
expect(sorted).toBeDefined();

View file

@ -1,14 +0,0 @@
extends: ../../tslint.yaml
rules:
max-classes-per-file: false
interface-name: false
variable-name: false
no-empty: false
object-literal-sort-keys: false
member-ordering: false
member-access: false
ordered-imports: false
interface-over-type-literal: false
array-type: false
prefer-const: false

View file

@ -0,0 +1,2 @@
require('../src/babel-register');
require('../src/dev/run_check_file_casing');

View file

@ -1,5 +1,18 @@
import _ from 'lodash';
import {
spanFirstTemplate,
spanNearTemplate,
spanOrTemplate,
spanNotTemplate,
spanTermTemplate,
spanContainingTemplate,
spanWithinTemplate,
wildcardTemplate,
fuzzyTemplate,
prefixTemplate,
rangeTemplate,
regexpTemplate
} from './templates';
const matchOptions = {
cutoff_frequency: 0.001,
query: '',
@ -52,30 +65,72 @@ const innerHits = {
__one_of: ['true', 'false'],
},
};
const SPAN_QUERIES = {
const SPAN_QUERIES_NO_FIELD_MASK = {
// TODO add one_of for objects
span_first: {
__template: spanFirstTemplate,
__scope_link: '.span_first',
},
span_near: {
__template: spanNearTemplate,
__scope_link: '.span_near',
},
span_or: {
__template: spanOrTemplate,
__scope_link: '.span_or',
},
span_not: {
__template: spanNotTemplate,
__scope_link: '.span_not',
},
span_term: {
__template: spanTermTemplate,
__scope_link: '.span_term',
},
span_containing: {
__template: spanContainingTemplate,
__scope_link: '.span_containing',
},
span_within: {
__template: spanWithinTemplate,
__scope_link: '.span_within',
},
};
const SPAN_QUERIES = {
...SPAN_QUERIES_NO_FIELD_MASK,
field_masking_span: {
__template: {
query: {
SPAN_QUERY: {}
}
},
query: SPAN_QUERIES_NO_FIELD_MASK,
field: ''
}
};
const SPAN_MULTI_QUERIES = {
wildcard: {
__template: wildcardTemplate,
__scope_link: '.wildcard'
},
fuzzy: {
__template: fuzzyTemplate,
__scope_link: '.fuzzy'
},
prefix: {
__template: prefixTemplate,
__scope_link: '.prefix'
},
range: {
__template: rangeTemplate,
__scope_link: '.range'
},
regexp: {
__template: regexpTemplate,
__scope_link: '.regexp'
}
};
const DECAY_FUNC_DESC = {
__template: {
@ -130,7 +185,7 @@ const SCORING_FUNCS = {
},
};
export default function (api) {
export function queryDsl(api) {
api.addGlobalAutocompleteRules('query', {
match: {
__template: {
@ -166,9 +221,7 @@ export default function (api) {
},
},
regexp: {
__template: {
FIELD: 'REGEXP',
},
__template: regexpTemplate,
'{field}': {
value: '',
flags: {
@ -279,6 +332,7 @@ export default function (api) {
},
},
fuzzy: {
__template: fuzzyTemplate,
'{field}': {
value: '',
boost: 1.0,
@ -353,11 +407,7 @@ export default function (api) {
__scope_link: '.more_like_this',
},
prefix: {
__template: {
FIELD: {
value: '',
},
},
__template: prefixTemplate,
'{field}': {
value: '',
boost: 1.0,
@ -416,12 +466,7 @@ export default function (api) {
lenient: { __one_of: [true, false] },
},
range: {
__template: {
FIELD: {
gte: 10,
lte: 20,
},
},
__template: rangeTemplate,
'{field}': {
__template: {
gte: 10,
@ -437,30 +482,19 @@ export default function (api) {
},
},
span_first: {
__template: {
match: {
span_term: {
FIELD: 'VALUE',
},
},
end: 3,
},
__template: spanFirstTemplate,
match: SPAN_QUERIES,
},
span_near: {
span_multi: {
__template: {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
slop: 12,
in_order: false,
match: {
MULTI_TERM_QUERY: {}
}
},
match: SPAN_MULTI_QUERIES
},
span_near: {
__template: spanNearTemplate,
clauses: [SPAN_QUERIES],
slop: 12,
in_order: {
@ -471,117 +505,28 @@ export default function (api) {
},
},
span_term: {
__template: {
FIELD: {
value: 'VALUE',
},
},
__template: spanTermTemplate,
'{field}': {
value: '',
boost: 2.0,
},
},
span_not: {
__template: {
include: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
exclude: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
},
__template: spanNotTemplate,
include: SPAN_QUERIES,
exclude: SPAN_QUERIES,
},
span_or: {
__template: {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
},
__template: spanOrTemplate,
clauses: [SPAN_QUERIES],
},
span_containing: {
__template: {
little: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
big: {
span_near: {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
slop: 5,
in_order: false,
},
},
},
__template: spanContainingTemplate,
little: SPAN_QUERIES,
big: SPAN_QUERIES,
},
span_within: {
__template: {
little: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
big: {
span_near: {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
slop: 5,
in_order: false,
},
},
},
__template: spanWithinTemplate,
little: SPAN_QUERIES,
big: SPAN_QUERIES,
},
@ -603,11 +548,7 @@ export default function (api) {
'{field}': [''],
},
wildcard: {
__template: {
FIELD: {
value: 'VALUE',
},
},
__template: wildcardTemplate,
'{field}': {
value: '',
boost: 2.0,
@ -625,6 +566,22 @@ export default function (api) {
__one_of: ['avg', 'total', 'max', 'none'],
},
},
percolate: {
__template: {
field: '',
document: {}
},
field: '',
document: {},
name: '',
documents: [{}],
document_type: '',
index: '',
type: '',
id: '',
routing: '',
preference: ''
},
common: {
__template: {
FIELD: {
@ -722,5 +679,11 @@ export default function (api) {
//populated by a global rule
},
},
wrapper: {
__template: {
query: 'QUERY_BASE64_ENCODED',
},
query: ''
}
});
}

View file

@ -0,0 +1 @@
export { queryDsl as default } from './dsl';

View file

@ -0,0 +1,137 @@
export const regexpTemplate = {
FIELD: 'REGEXP',
};
export const fuzzyTemplate = {
FIELD: {},
};
export const prefixTemplate = {
FIELD: {
value: '',
},
};
export const rangeTemplate = {
FIELD: {
gte: 10,
lte: 20,
},
};
export const spanFirstTemplate = {
match: {
span_term: {
FIELD: 'VALUE',
},
},
end: 3,
};
export const spanNearTemplate = {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
slop: 12,
in_order: false,
};
export const spanTermTemplate = {
FIELD: {
value: 'VALUE',
},
};
export const spanNotTemplate = {
include: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
exclude: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
};
export const spanOrTemplate = {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
};
export const spanContainingTemplate = {
little: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
big: {
span_near: {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
slop: 5,
in_order: false,
},
},
};
export const spanWithinTemplate = {
little: {
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
big: {
span_near: {
clauses: [
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
{
span_term: {
FIELD: {
value: 'VALUE',
},
},
},
],
slop: 5,
in_order: false,
},
},
};
export const wildcardTemplate = {
FIELD: {
value: 'VALUE',
},
};

View file

@ -1,6 +1,7 @@
import { createAction } from 'redux-actions';
export const updateViewMode = createAction('UPDATE_VIEW_MODE');
export const setVisibleContextMenuPanelId = createAction('SET_VISIBLE_CONTEXT_MENU_PANEL_ID');
export const maximizePanel = createAction('MAXIMIZE_PANEl');
export const minimizePanel = createAction('MINIMIZE_PANEL');
export const updateIsFullScreenMode = createAction('UPDATE_IS_FULL_SCREEN_MODE');

View file

@ -7,6 +7,8 @@ import { toastNotifications } from 'ui/notify';
import 'ui/query_bar';
import { panelActionsStore } from './store/panel_actions_store';
import { getDashboardTitle } from './dashboard_strings';
import { DashboardViewMode } from './dashboard_view_mode';
import { TopNavIds } from './top_nav/top_nav_ids';
@ -19,10 +21,14 @@ import { VisualizeConstants } from '../visualize/visualize_constants';
import { DashboardStateManager } from './dashboard_state_manager';
import { saveDashboard } from './lib';
import { showCloneModal } from './top_nav/show_clone_modal';
import { showAddPanel } from './top_nav/show_add_panel';
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
import * as filterActions from 'ui/doc_table/actions/filter';
import { FilterManagerProvider } from 'ui/filter_manager';
import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry';
import { DashboardPanelActionsRegistryProvider } from 'ui/dashboard_panel_actions/dashboard_panel_actions_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider';
@ -53,12 +59,18 @@ app.directive('dashboardApp', function ($injector) {
return {
restrict: 'E',
controllerAs: 'dashboardApp',
controller: function ($scope, $rootScope, $route, $routeParams, $location, getAppState, $compile, dashboardConfig, localStorage) {
controller: function ($scope, $rootScope, $route, $routeParams, $location, getAppState, dashboardConfig, localStorage) {
const filterManager = Private(FilterManagerProvider);
const filterBar = Private(FilterBarQueryFilterProvider);
const docTitle = Private(DocTitleProvider);
const notify = new Notifier({ location: 'Dashboard' });
const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider);
const panelActionsRegistry = Private(DashboardPanelActionsRegistryProvider);
panelActionsStore.initializeFromRegistry(panelActionsRegistry);
const savedObjectsClient = Private(SavedObjectsClientProvider);
const visTypes = Private(VisTypesRegistryProvider);
$scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType];
const dash = $scope.dash = $route.current.locals.dash;
@ -178,25 +190,6 @@ app.directive('dashboardApp', function ($injector) {
$scope.refresh();
};
// called by the saved-object-finder when a user clicks a vis
$scope.addVis = function (hit, showToast = true) {
dashboardStateManager.addNewPanel(hit.id, 'visualization');
if (showToast) {
toastNotifications.addSuccess({
title: 'Visualization was added to your dashboard',
'data-test-subj': 'addVisualizationToDashboardSuccess',
});
}
};
$scope.addSearch = function (hit) {
dashboardStateManager.addNewPanel(hit.id, 'search');
toastNotifications.addSuccess({
title: 'Saved search was added to your dashboard',
'data-test-subj': 'addSavedSearchToDashboardSuccess',
});
};
$scope.$watch('model.hidePanelTitles', () => {
dashboardStateManager.setHidePanelTitles($scope.model.hidePanelTitles);
});
@ -306,7 +299,7 @@ app.directive('dashboardApp', function ($injector) {
$scope.showAddPanel = () => {
dashboardStateManager.setFullScreenMode(false);
$scope.kbnTopNav.open('add');
$scope.kbnTopNav.click(TopNavIds.ADD);
};
$scope.enterEditMode = () => {
dashboardStateManager.setFullScreenMode(false);
@ -341,6 +334,19 @@ app.directive('dashboardApp', function ($injector) {
showCloneModal(onClone, currentTitle);
};
navActions[TopNavIds.ADD] = () => {
const addNewVis = () => {
kbnUrl.change(
`${VisualizeConstants.WIZARD_STEP_1_PAGE_PATH}?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`);
// Function is called outside of angular. Must apply digest cycle to trigger URL update
$scope.$apply();
};
const isLabsEnabled = config.get('visualize:enableLabs');
const listingLimit = config.get('savedObjects:listingLimit');
showAddPanel(savedObjectsClient, dashboardStateManager.addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes);
};
updateViewMode(dashboardStateManager.getViewMode());
// update root source when filters update
@ -375,27 +381,16 @@ app.directive('dashboardApp', function ($injector) {
}
if ($route.current.params && $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) {
// Hide the toast message since they will already see a notification from saving the visualization,
// and one is sufficient (especially given how the screen jumps down a bit for each unique notification).
const showToast = false;
$scope.addVis({ id: $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] }, showToast);
dashboardStateManager.addNewPanel($route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM], 'visualization');
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM);
}
const addNewVis = function addNewVis() {
kbnUrl.change(
`${VisualizeConstants.WIZARD_STEP_1_PAGE_PATH}?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`);
};
$scope.opts = {
displayName: dash.getDisplayName(),
dashboard: dash,
save: $scope.save,
addVis: $scope.addVis,
addNewVis,
addSearch: $scope.addSearch,
timefilter: $scope.timefilter
};
}

View file

@ -453,7 +453,7 @@ export class DashboardStateManager {
* @param {number} id
* @param {string} type
*/
addNewPanel(id, type) {
addNewPanel = (id, type) => {
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
const newPanel = createPanelState(id, type, maxPanelIndex, this.getPanels());
this.getPanels().push(newPanel);

View file

@ -25,7 +25,7 @@ exports[`DashboardPanel matches snapshot 1`] = `
>
<div
class="euiPopover euiPopover--anchorDownRight dashboardPanelPopOver euiPopover--withTitle"
id="panelContextMenu"
id="dashboardPanelContextMenu"
>
<button
aria-label="Panel options"

View file

@ -121,6 +121,7 @@ export class DashboardPanel extends React.Component {
>
<PanelHeader
panelId={panel.panelIndex}
embeddable={this.embeddable}
/>
{this.renderContent()}

View file

@ -0,0 +1,109 @@
import _ from 'lodash';
/**
* Loops through allActions and extracts those that belong on the given contextMenuPanelId
* @param {string} contextMenuPanelId
* @param {Array.<DashboardPanelAction>} allActions
*/
function getActionsForPanel(contextMenuPanelId, allActions) {
return allActions.filter(action => action.parentPanelId === contextMenuPanelId);
}
/**
* @param {String} contextMenuPanelId
* @param {Array.<DashboardPanelAction>} actions
* @param {Embeddable} embeddable
* @param {ContainerState} containerState
* @return {{
* Array.<EuiContextMenuPanelItemShape> items - panel actions converted into the items expected to be on an
* EuiContextMenuPanel,
* Array.<EuiContextMenuPanelShape> childPanels - extracted child panels, if any actions also open a panel. They
* need to be moved to the top level for EUI.
* }}
*/
function buildEuiContextMenuPanelItemsAndChildPanels({ contextMenuPanelId, actions, embeddable, containerState }) {
const items = [];
const childPanels = [];
const actionsForPanel = getActionsForPanel(contextMenuPanelId, actions);
actionsForPanel.forEach(action => {
const isVisible = action.isVisible({ embeddable, containerState });
if (!isVisible) {
return;
}
if (action.childContextMenuPanel) {
childPanels.push(
...buildEuiContextMenuPanels({
contextMenuPanel: action.childContextMenuPanel,
actions,
embeddable,
containerState
}));
}
items.push(convertPanelActionToContextMenuItem(
{
action,
containerState,
embeddable
}));
});
return { items, childPanels };
}
/**
* Transforms a DashboardContextMenuPanel to the shape EuiContextMenuPanel expects, inserting any registered pluggable
* panel actions.
* @param {DashboardContextMenuPanel} contextMenuPanel
* @param {Array.<DashboardPanelAction>} actions to build the context menu with
* @param {Embeddable} embeddable
* @param {ContainerState} containerState
* @return {Object} An object that conforms to EuiContextMenuPanelShape in elastic/eui
*/
export function buildEuiContextMenuPanels(
{
contextMenuPanel,
actions,
embeddable,
containerState
}) {
const euiContextMenuPanel = {
id: contextMenuPanel.id,
title: contextMenuPanel.title,
items: [],
content: contextMenuPanel.getContent({ embeddable, containerState }),
};
const contextMenuPanels = [euiContextMenuPanel];
const { items, childPanels } =
buildEuiContextMenuPanelItemsAndChildPanels({
contextMenuPanelId: contextMenuPanel.id,
actions,
embeddable,
containerState
});
euiContextMenuPanel.items = items;
return contextMenuPanels.concat(childPanels);
}
/**
*
* @param {DashboardPanelAction} action
* @param {ContainerState} containerState
* @param {Embeddable} embeddable
* @return {Object} See EuiContextMenuPanelItemShape in @elastic/eui
*/
function convertPanelActionToContextMenuItem({ action, containerState, embeddable }) {
return {
id: action.id || action.displayName.replace(/\s/g, ''),
name: action.displayName,
icon: action.icon,
panel: _.get(action, 'childContextMenuPanel.id'),
onClick: () => action.onClick({ containerState, embeddable }),
disabled: action.isDisabled({ containerState, embeddable }),
'data-test-subj': `dashboardPanelAction-${action.id}`,
};
}

View file

@ -0,0 +1,35 @@
import React from 'react';
import {
EuiIcon,
} from '@elastic/eui';
import { PanelOptionsMenuForm } from '../panel_options_menu_form';
import { DashboardPanelAction, DashboardContextMenuPanel } from 'ui/dashboard_panel_actions';
import { DashboardViewMode } from '../../../dashboard_view_mode';
/**
*
* @param {function} onResetPanelTitle
* @param {function} onUpdatePanelTitle
* @param {string} title
* @param {function} closeContextMenu
* @return {DashboardPanelAction}
*/
export function getCustomizePanelAction({ onResetPanelTitle, onUpdatePanelTitle, title, closeContextMenu }) {
return new DashboardPanelAction({
displayName: 'Customize panel',
id: 'customizePanel',
parentPanelId: 'mainMenu',
icon: <EuiIcon type="pencil" />,
isVisible: ({ containerState }) => (containerState.viewMode === DashboardViewMode.EDIT),
childContextMenuPanel: new DashboardContextMenuPanel({
id: 'panelSubOptionsMenu',
title: 'Customize panel',
getContent: () => (<PanelOptionsMenuForm
onReset={onResetPanelTitle}
onUpdatePanelTitle={onUpdatePanelTitle}
title={title}
onClose={closeContextMenu}
/>),
}),
});
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import {
EuiIcon,
} from '@elastic/eui';
import { DashboardPanelAction } from 'ui/dashboard_panel_actions';
import { DashboardViewMode } from '../../../dashboard_view_mode';
/**
*
* @return {DashboardPanelAction}
*/
export function getEditPanelAction() {
return new DashboardPanelAction({
displayName: 'Edit visualization',
id: 'editPanel',
icon: <EuiIcon type="pencil" />,
parentPanelId: 'mainMenu',
onClick: ({ embeddable }) => { window.location = embeddable.metadata.editUrl; },
isVisible: ({ containerState }) => (containerState.viewMode === DashboardViewMode.EDIT),
isDisabled: ({ embeddable }) => (!embeddable || !embeddable.metadata || !embeddable.metadata.editUrl),
});
}

View file

@ -0,0 +1,25 @@
import React from 'react';
import {
EuiIcon,
} from '@elastic/eui';
import { DashboardPanelAction } from 'ui/dashboard_panel_actions';
import { DashboardViewMode } from '../../../dashboard_view_mode';
/**
*
* @param {function} onDeletePanel
* @return {DashboardPanelAction}
*/
export function getRemovePanelAction(onDeletePanel) {
return new DashboardPanelAction({
displayName: 'Delete from dashboard',
id: 'deletePanel',
parentPanelId: 'mainMenu',
icon: <EuiIcon type="trash" />,
isVisible: ({ containerState }) => (
containerState.viewMode === DashboardViewMode.EDIT && !containerState.isPanelExpanded
),
onClick: onDeletePanel,
});
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import {
EuiIcon,
} from '@elastic/eui';
import { DashboardPanelAction } from 'ui/dashboard_panel_actions';
/**
* Returns an action that toggles the panel into maximized or minimized state.
* @param {boolean} isExpanded
* @param {function} toggleExpandedPanel
* @return {DashboardPanelAction}
*/
export function getToggleExpandPanelAction({ isExpanded, toggleExpandedPanel }) {
return new DashboardPanelAction({
displayName: isExpanded ? 'Minimize' : 'Full screen',
id: 'togglePanel',
parentPanelId: 'mainMenu',
// TODO: Update to minimize icon when https://github.com/elastic/eui/issues/837 is complete.
icon: <EuiIcon type={isExpanded ? 'expand' : 'expand'} />,
onClick: toggleExpandedPanel,
});
}

View file

@ -0,0 +1,5 @@
export { getEditPanelAction } from './get_edit_panel_action';
export { getRemovePanelAction } from './get_remove_panel_action';
export { buildEuiContextMenuPanels } from './build_context_menu';
export { getCustomizePanelAction } from './get_customize_panel_action';
export { getToggleExpandPanelAction } from './get_toggle_expand_panel_action';

View file

@ -1,12 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { embeddableShape } from 'ui/embeddable';
import { PanelOptionsMenuContainer } from './panel_options_menu_container';
export function PanelHeader({ title, actions, isViewOnlyMode, hidePanelTitles }) {
export function PanelHeader({ title, panelId, embeddable, isViewOnlyMode, hidePanelTitles }) {
if (isViewOnlyMode && (!title || hidePanelTitles)) {
return (
<div className="panel-heading-floater">
<div className="kuiMicroButtonGroup">
{actions}
<PanelOptionsMenuContainer panelId={panelId} embeddable={embeddable} />
</div>
</div>
);
@ -24,7 +26,7 @@ export function PanelHeader({ title, actions, isViewOnlyMode, hidePanelTitles })
</span>
<div className="kuiMicroButtonGroup">
{actions}
<PanelOptionsMenuContainer panelId={panelId} embeddable={embeddable} />
</div>
</div>
);
@ -33,6 +35,7 @@ export function PanelHeader({ title, actions, isViewOnlyMode, hidePanelTitles })
PanelHeader.propTypes = {
isViewOnlyMode: PropTypes.bool,
title: PropTypes.string,
actions: PropTypes.node,
hidePanelTitles: PropTypes.bool.isRequired,
embeddable: embeddableShape,
panelId: PropTypes.string.isRequired,
};

View file

@ -1,18 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { embeddableShape } from 'ui/embeddable';
import { PanelHeader } from './panel_header';
import { PanelOptionsMenuContainer } from './panel_options_menu_container';
import { PanelMaximizeIcon } from './panel_maximize_icon';
import { PanelMinimizeIcon } from './panel_minimize_icon';
import { DashboardViewMode } from '../../dashboard_view_mode';
import {
maximizePanel,
minimizePanel,
} from '../../actions';
import {
getPanel,
getMaximizedPanelId,
@ -33,39 +25,11 @@ const mapStateToProps = ({ dashboard }, { panelId }) => {
};
};
const mapDispatchToProps = (dispatch, { panelId }) => ({
onMaximizePanel: () => dispatch(maximizePanel(panelId)),
onMinimizePanel: () => dispatch(minimizePanel()),
});
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { isExpanded, isViewOnlyMode, title, hidePanelTitles } = stateProps;
const { onMaximizePanel, onMinimizePanel } = dispatchProps;
const { panelId } = ownProps;
let actions;
if (isViewOnlyMode) {
actions = isExpanded ?
<PanelMinimizeIcon onMinimize={onMinimizePanel} /> :
<PanelMaximizeIcon onMaximize={onMaximizePanel} />;
} else {
actions = <PanelOptionsMenuContainer panelId={panelId} />;
}
return {
title,
actions,
isViewOnlyMode,
hidePanelTitles,
};
};
export const PanelHeaderContainer = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
)(PanelHeader);
PanelHeaderContainer.propTypes = {
panelId: PropTypes.string.isRequired,
embeddable: embeddableShape,
};

View file

@ -12,6 +12,7 @@ import {
setPanelTitle,
resetPanelTitle,
embeddableIsInitialized,
updateTimeRange,
} from '../../actions';
import { findTestSubject } from '@elastic/eui/lib/test';
@ -25,6 +26,7 @@ function getProps(props = {}) {
let component;
beforeAll(() => {
store.dispatch(updateTimeRange({ to: 'now', from: 'now-15m' }));
store.dispatch(updateViewMode(DashboardViewMode.EDIT));
store.dispatch(setPanels({ 'foo1': { panelIndex: 'foo1' } }));
const metadata = { title: 'my embeddable title', editUrl: 'editme' };

View file

@ -1,22 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export function PanelMaximizeIcon({ onMaximize }) {
return (
<button
className="kuiMicroButton viewModeExpandPanelToggle"
aria-label="Maximize panel"
data-test-subj="dashboardPanelExpandIcon"
onClick={onMaximize}
>
<span
aria-hidden="true"
className="kuiIcon fa-expand"
/>
</button>
);
}
PanelMaximizeIcon.propTypes = {
onMaximize: PropTypes.func.isRequired
};

View file

@ -1,22 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export function PanelMinimizeIcon({ onMinimize }) {
return (
<button
className="kuiMicroButton viewModeExpandPanelToggle"
aria-label="Minimize panel"
data-test-subj="dashboardPanelExpandIcon"
onClick={onMinimize}
>
<span
aria-hidden="true"
className="kuiIcon fa-compress"
/>
</button>
);
}
PanelMinimizeIcon.propTypes = {
onMinimize: PropTypes.func.isRequired
};

View file

@ -1,143 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PanelOptionsMenuForm } from './panel_options_menu_form';
import {
EuiContextMenu,
EuiPopover,
EuiIcon,
EuiButtonIcon,
} from '@elastic/eui';
export class PanelOptionsMenu extends React.Component {
state = {
isPopoverOpen: false
};
export function PanelOptionsMenu({ toggleContextMenu, isPopoverOpen, closeContextMenu, panels }) {
const button = (
<EuiButtonIcon
iconType="gear"
aria-label="Panel options"
data-test-subj="dashboardPanelToggleMenuIcon"
onClick={toggleContextMenu}
/>
);
toggleMenu = () => {
this.setState({ isPopoverOpen: !this.state.isPopoverOpen });
};
closePopover = () => this.setState({ isPopoverOpen: false });
onEditPanel = () => {
window.location = this.props.editUrl;
};
onDeletePanel = () => {
if (this.props.onDeletePanel) {
this.props.onDeletePanel();
}
};
onToggleExpandPanel = () => {
this.closePopover();
this.props.toggleExpandedPanel();
};
buildMainMenuPanel() {
const { isExpanded } = this.props;
const mainPanelMenuItems = [
{
name: 'Edit visualization',
'data-test-subj': 'dashboardPanelEditLink',
icon: <EuiIcon
type="pencil"
/>,
onClick: this.onEditPanel,
disabled: !this.props.editUrl,
},
{
name: 'Customize panel',
'data-test-subj': 'dashboardPanelOptionsSubMenuLink',
icon: <EuiIcon
type="pencil"
/>,
panel: 'panelSubOptionsMenu',
},
{
name: isExpanded ? 'Minimize' : 'Full screen',
'data-test-subj': 'dashboardPanelExpandIcon',
icon: <EuiIcon
type={isExpanded ? 'expand' : 'expand'}
/>,
onClick: this.onToggleExpandPanel,
}
];
if (!this.props.isExpanded) {
mainPanelMenuItems.push({
name: 'Delete from dashboard',
'data-test-subj': 'dashboardPanelRemoveIcon',
icon: <EuiIcon
type="trash"
/>,
onClick: this.onDeletePanel,
});
}
return {
title: 'Options',
id: 'mainMenu',
items: mainPanelMenuItems,
};
}
buildPanelOptionsSubMenu() {
return {
title: 'Customize panel',
id: 'panelSubOptionsMenu',
content: <PanelOptionsMenuForm
onReset={this.props.onResetPanelTitle}
onUpdatePanelTitle={this.props.onUpdatePanelTitle}
title={this.props.panelTitle}
onClose={this.closePopover}
/>,
};
}
renderPanels() {
return [
this.buildMainMenuPanel(),
this.buildPanelOptionsSubMenu(),
];
}
render() {
const button = (
<EuiButtonIcon
iconType="gear"
aria-label="Panel options"
data-test-subj="dashboardPanelToggleMenuIcon"
onClick={this.toggleMenu}
return (
<EuiPopover
id="dashboardPanelContextMenu"
className="dashboardPanelPopOver"
button={button}
isOpen={isPopoverOpen}
closePopover={closeContextMenu}
panelPaddingSize="none"
anchorPosition="downRight"
withTitle
>
<EuiContextMenu
initialPanelId="mainMenu"
panels={panels}
/>
);
return (
<EuiPopover
id="panelContextMenu"
className="dashboardPanelPopOver"
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="downRight"
withTitle
>
<EuiContextMenu
initialPanelId="mainMenu"
panels={this.renderPanels()}
/>
</EuiPopover>
);
}
</EuiPopover>
);
}
PanelOptionsMenu.propTypes = {
panelTitle: PropTypes.string,
onUpdatePanelTitle: PropTypes.func.isRequired,
onResetPanelTitle: PropTypes.func.isRequired,
editUrl: PropTypes.string, // May be empty if the embeddable is still loading
toggleExpandedPanel: PropTypes.func.isRequired,
isExpanded: PropTypes.bool.isRequired,
onDeletePanel: PropTypes.func, // Not available when the panel is expanded.
panels: PropTypes.array,
toggleContextMenu: PropTypes.func,
closeContextMenu: PropTypes.func,
isPopoverOpen: PropTypes.bool,
};

View file

@ -1,7 +1,15 @@
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { panelActionsStore } from '../../store/panel_actions_store';
import { embeddableShape } from 'ui/embeddable';
import { PanelOptionsMenu } from './panel_options_menu';
import {
buildEuiContextMenuPanels,
getEditPanelAction,
getRemovePanelAction,
getCustomizePanelAction,
getToggleExpandPanelAction,
} from './panel_actions';
import {
deletePanel,
@ -9,6 +17,7 @@ import {
minimizePanel,
resetPanelTitle,
setPanelTitle,
setVisibleContextMenuPanelId,
} from '../../actions';
import {
@ -16,17 +25,24 @@ import {
getEmbeddableEditUrl,
getMaximizedPanelId,
getPanel,
getEmbeddableTitle
getEmbeddableTitle,
getContainerState,
getVisibleContextMenuPanelId,
} from '../../selectors';
import { DashboardContextMenuPanel } from 'ui/dashboard_panel_actions';
const mapStateToProps = ({ dashboard }, { panelId }) => {
const embeddable = getEmbeddable(dashboard, panelId);
const panel = getPanel(dashboard, panelId);
const embeddableTitle = getEmbeddableTitle(dashboard, panelId);
const containerState = getContainerState(dashboard, panelId);
const visibleContextMenuPanelId = getVisibleContextMenuPanelId(dashboard);
return {
panelTitle: panel.title === undefined ? embeddableTitle : panel.title,
editUrl: embeddable ? getEmbeddableEditUrl(dashboard, panelId) : null,
isExpanded: getMaximizedPanelId(dashboard) === panelId,
containerState,
visibleContextMenuPanelId,
};
};
@ -39,23 +55,71 @@ const mapDispatchToProps = (dispatch, { panelId }) => ({
onDeletePanel: () => {
dispatch(deletePanel(panelId));
},
closeContextMenu: () => dispatch(setVisibleContextMenuPanelId()),
openContextMenu: () => dispatch(setVisibleContextMenuPanelId(panelId)),
onMaximizePanel: () => dispatch(maximizePanel(panelId)),
onMinimizePanel: () => dispatch(minimizePanel()),
onResetPanelTitle: () => dispatch(resetPanelTitle(panelId)),
onUpdatePanelTitle: (newTitle) => dispatch(setPanelTitle(newTitle, panelId)),
});
const mergeProps = (stateProps, dispatchProps) => {
const { isExpanded, editUrl, panelTitle } = stateProps;
const { onMaximizePanel, onMinimizePanel, ...dispatchers } = dispatchProps;
const toggleExpandedPanel = () => isExpanded ? onMinimizePanel() : onMaximizePanel();
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { isExpanded, panelTitle, containerState, visibleContextMenuPanelId } = stateProps;
const isPopoverOpen = visibleContextMenuPanelId === ownProps.panelId;
const {
onMaximizePanel,
onMinimizePanel,
onDeletePanel,
onResetPanelTitle,
onUpdatePanelTitle,
closeContextMenu,
openContextMenu,
} = dispatchProps;
const toggleContextMenu = () => isPopoverOpen ? closeContextMenu() : openContextMenu();
// Outside click handlers will trigger for every closed context menu, we only want to react to clicks external to
// the currently opened menu.
const closeMyContextMenuPanel = () => {
if (isPopoverOpen) {
closeContextMenu();
}
};
const toggleExpandedPanel = () => {
isExpanded ? onMinimizePanel() : onMaximizePanel();
closeMyContextMenuPanel();
};
let panels = [];
// Don't build the panels if the pop over is not open, or this gets expensive - this function is called once for
// every panel, every time any state changes.
if (isPopoverOpen) {
const contextMenuPanel = new DashboardContextMenuPanel({
title: 'Options',
id: 'mainMenu'
});
const actions = [
getEditPanelAction(),
getCustomizePanelAction({
onResetPanelTitle,
onUpdatePanelTitle,
title: panelTitle,
closeContextMenu: closeMyContextMenuPanel
}),
getToggleExpandPanelAction({ isExpanded, toggleExpandedPanel }),
getRemovePanelAction(onDeletePanel),
].concat(panelActionsStore.actions);
panels = buildEuiContextMenuPanels({ contextMenuPanel, actions, embeddable: ownProps.embeddable, containerState });
}
return {
panelTitle,
toggleExpandedPanel,
isExpanded,
editUrl,
...dispatchers,
panels,
toggleContextMenu,
closeContextMenu: closeMyContextMenuPanel,
isPopoverOpen,
};
};
@ -67,4 +131,5 @@ export const PanelOptionsMenuContainer = connect(
PanelOptionsMenuContainer.propTypes = {
panelId: PropTypes.string.isRequired,
embeddable: embeddableShape,
};

View file

@ -7,11 +7,17 @@ import {
updateHidePanelTitles,
updateIsFullScreenMode,
updateTimeRange,
setVisibleContextMenuPanelId,
} from '../actions';
import { DashboardViewMode } from '../dashboard_view_mode';
export const view = handleActions({
[setVisibleContextMenuPanelId]: (state, { payload }) => ({
...state,
visibleContextMenuPanelId: payload
}),
[updateViewMode]: (state, { payload }) => ({
...state,
viewMode: payload

View file

@ -5,6 +5,7 @@ import _ from 'lodash';
* @property {DashboardViewMode} viewMode
* @property {boolean} isFullScreenMode
* @property {string|undefined} maximizedPanelId
* @property {string|undefined} getVisibleContextMenuPanelId
*/
/**
@ -103,6 +104,9 @@ export const getEmbeddableEditUrl = (dashboard, panelId) => {
return embeddable && embeddable.initialized ? embeddable.metadata.editUrl : '';
};
export const getVisibleContextMenuPanelId = dashboard => dashboard.view.visibleContextMenuPanelId;
/**
* @param dashboard {DashboardState}
* @return {boolean}
@ -161,10 +165,12 @@ export const getDescription = dashboard => dashboard.metadata.description;
* This state object is specifically for communicating to embeddables and it's structure is not tied to
* the redux tree structure.
* @typedef {Object} ContainerState
* @property {DashboardViewMode} viewMode - edit or view mode.
* @property {String} timeRange.to - either an absolute time range in utc format or a relative one (e.g. now-15m)
* @property {String} timeRange.from - either an absolute time range in utc format or a relative one (e.g. now-15m)
* @property {Object} embeddableCustomization
* @property {boolean} hidePanelTitles
* @property {boolean} isPanelExpanded
*/
/**
@ -183,6 +189,8 @@ export const getContainerState = (dashboard, panelId) => {
embeddableCustomization: _.cloneDeep(getEmbeddableCustomization(dashboard, panelId) || {}),
hidePanelTitles: getHidePanelTitles(dashboard),
customTitle: getPanel(dashboard, panelId).title,
viewMode: getViewMode(dashboard),
isPanelExpanded: getMaximizedPanelId(dashboard) === panelId,
};
};

View file

@ -0,0 +1,21 @@
class PanelActionsStore {
constructor() {
/**
*
* @type {Array.<DashboardPanelAction>}
*/
this.actions = [];
}
/**
*
* @type {IndexedArray} panelActionsRegistry
*/
initializeFromRegistry(panelActionsRegistry) {
panelActionsRegistry.forEach(panelAction => {
this.actions.push(panelAction);
});
}
}
export const panelActionsStore = new PanelActionsStore();

View file

@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiFlyout
className="addPanelFlyout"
data-test-subj="dashboardAddPanel"
onClose={[Function]}
ownFocus={true}
size="s"
>
<EuiFlyoutBody>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiTitle
size="m"
>
<h2>
Add Panels
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonIcon
aria-label="close add panel"
color="primary"
data-test-subj="closeAddPanelBtn"
iconType="cross"
onClick={[Function]}
type="button"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTabs>
<EuiTab
data-test-subj="addVisualizationTab"
disabled={false}
isSelected={true}
key="vis"
onClick={[Function]}
>
Visualization
</EuiTab>
<EuiTab
data-test-subj="addSavedSearchTab"
disabled={false}
isSelected={false}
key="search"
onClick={[Function]}
>
Saved Search
</EuiTab>
</EuiTabs>
<EuiSpacer
size="s"
/>
<SavedObjectFinder
callToActionButton={
<EuiButton
color="primary"
data-test-subj="addNewSavedObjectLink"
fill={false}
iconSide="left"
onClick={[Function]}
type="button"
>
Add new Visualization
</EuiButton>
}
find={[Function]}
key="visSavedObjectFinder"
noItemsMessage="No matching visualizations found."
onChoose={[Function]}
savedObjectType="visualization"
/>
</EuiFlyoutBody>
</EuiFlyout>
`;

View file

@ -1,47 +0,0 @@
<div
ng-switch="mode"
ng-init="mode = 'visualization'"
>
<h2 class="kuiLocalDropdownTitle" data-test-subj="dashboardAddPanel">
Add Panels
</h2>
<div class="kuiTabs">
<button
ng-class="{ 'kuiTab-isSelected': mode == 'visualization'}"
class="kuiTab"
ng-click="mode='visualization'"
aria-label="List visualizations"
data-test-subj="addVisualizationTab"
>
Visualization
</button>
<button
ng-class="{ 'kuiTab-isSelected': mode == 'search' }"
class="kuiTab"
ng-click="mode='search'"
aria-label="List saved searches"
data-test-subj="addSavedSearchTab"
>
Saved Search
</button>
</div>
<div class="list-group-item list-group-item--noBorder" ng-switch-when="visualization">
<saved-object-finder
use-local-management="true"
type="visualizations"
on-add-new="opts.addNewVis"
on-choose="opts.addVis">
</saved-object-finder>
</div>
<div class="list-group-item list-group-item--noBorder" ng-switch-when="search">
<saved-object-finder
type="searches"
use-local-management="true"
on-choose="opts.addSearch"
></saved-object-finder>
</div>
</div>

View file

@ -0,0 +1,155 @@
import './add_panel.less';
import React from 'react';
import PropTypes from 'prop-types';
import { toastNotifications } from 'ui/notify';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiButton,
EuiButtonIcon,
EuiTabs,
EuiTab,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
const VIS_TAB_ID = 'vis';
const SAVED_SEARCH_TAB_ID = 'search';
export class DashboardAddPanel extends React.Component {
constructor(props) {
super(props);
const addNewVisBtn = (
<EuiButton
onClick={this.props.addNewVis}
data-test-subj="addNewSavedObjectLink"
>
Add new Visualization
</EuiButton>
);
const tabs = [{
id: VIS_TAB_ID,
name: 'Visualization',
dataTestSubj: 'addVisualizationTab',
toastDataTestSubj: 'addVisualizationToDashboardSuccess',
savedObjectFinder: (
<SavedObjectFinder
key="visSavedObjectFinder"
callToActionButton={addNewVisBtn}
onChoose={this.onAddPanel}
find={this.props.find}
noItemsMessage="No matching visualizations found."
savedObjectType="visualization"
/>
)
}, {
id: SAVED_SEARCH_TAB_ID,
name: 'Saved Search',
dataTestSubj: 'addSavedSearchTab',
toastDataTestSubj: 'addSavedSearchToDashboardSuccess',
savedObjectFinder: (
<SavedObjectFinder
key="searchSavedObjectFinder"
onChoose={this.onAddPanel}
find={this.props.find}
noItemsMessage="No matching saved searches found."
savedObjectType="search"
/>
)
}];
this.state = {
tabs: tabs,
selectedTab: tabs[0],
};
}
onSelectedTabChanged = tab => {
this.setState({
selectedTab: tab,
});
}
renderTabs() {
return this.state.tabs.map((tab) => {
return (
<EuiTab
onClick={() => this.onSelectedTabChanged(tab)}
isSelected={tab.id === this.state.selectedTab.id}
key={tab.id}
data-test-subj={tab.dataTestSubj}
>
{tab.name}
</EuiTab>
);
});
}
onAddPanel = (id, type) => {
this.props.addNewPanel(id, type);
// To avoid the clutter of having toast messages cover flyout
// close previous toast message before creating a new one
if (this.lastToast) {
toastNotifications.remove(this.lastToast);
}
this.lastToast = toastNotifications.addSuccess({
title: `${this.state.selectedTab.name} was added to your dashboard`,
'data-test-subj': this.state.selectedTab.toastDataTestSubj,
});
}
render() {
return (
<EuiFlyout
ownFocus
className="addPanelFlyout"
onClose={this.props.onClose}
size="s"
data-test-subj="dashboardAddPanel"
>
<EuiFlyoutBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle>
<h2>Add Panels</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
onClick={this.props.onClose}
aria-label="close add panel"
data-test-subj="closeAddPanelBtn"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTabs>
{this.renderTabs()}
</EuiTabs>
<EuiSpacer size="s" />
{this.state.selectedTab.savedObjectFinder}
</EuiFlyoutBody>
</EuiFlyout>
);
}
}
DashboardAddPanel.propTypes = {
onClose: PropTypes.func.isRequired,
find: PropTypes.func.isRequired,
addNewPanel: PropTypes.func.isRequired,
addNewVis: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,3 @@
.addPanelFlyout {
width: 33vw;
}

View file

@ -0,0 +1,43 @@
import React from 'react';
import sinon from 'sinon';
import { mount, shallow } from 'enzyme';
import {
findTestSubject,
} from '@elastic/eui/lib/test';
import {
DashboardAddPanel,
} from './add_panel';
jest.mock('ui/notify',
() => ({
toastNotifications: {
addDanger: () => {},
}
}), { virtual: true });
let onClose;
beforeEach(() => {
onClose = sinon.spy();
});
test('render', () => {
const component = shallow(<DashboardAddPanel
onClose={onClose}
find={() => {}}
addNewPanel={() => {}}
addNewVis={() => {}}
/>);
expect(component).toMatchSnapshot();
});
test('onClose', () => {
const component = mount(<DashboardAddPanel
onClose={onClose}
find={() => {}}
addNewPanel={() => {}}
addNewVis={() => {}}
/>);
findTestSubject(component, 'closeAddPanelBtn', false).simulate('click');
sinon.assert.calledOnce(onClose);
});

View file

@ -7,7 +7,7 @@ import {
import {
DashboardCloneModal,
} from '../top_nav/clone_modal';
} from './clone_modal';
let onClone;
let onClose;

View file

@ -28,7 +28,7 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
return [
getSaveConfig(),
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
getAddConfig(),
getAddConfig(actions[TopNavIds.ADD]),
getOptionsConfig(),
getShareConfig()];
default:
@ -96,12 +96,12 @@ function getCloneConfig(action) {
/**
* @returns {kbnTopNavConfig}
*/
function getAddConfig() {
function getAddConfig(action) {
return {
key: TopNavIds.ADD,
description: 'Add a panel to the dashboard',
testId: 'dashboardAddPanelButton',
template: require('plugins/kibana/dashboard/top_nav/add_panel.html')
run: action
};
}

View file

@ -0,0 +1,55 @@
import { DashboardAddPanel } from './add_panel';
import React from 'react';
import ReactDOM from 'react-dom';
let isOpen = false;
export function showAddPanel(savedObjectsClient, addNewPanel, addNewVis, listingLimit, isLabsEnabled, visTypes) {
if (isOpen) {
return;
}
isOpen = true;
const container = document.createElement('div');
const onClose = () => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
isOpen = false;
};
const find = async (type, search) => {
const resp = await savedObjectsClient.find({
type: type,
fields: ['title', 'visState'],
search: search ? `${search}*` : undefined,
page: 1,
perPage: listingLimit,
searchFields: ['title^3', 'description']
});
if (type === 'visualization' && !isLabsEnabled) {
resp.savedObjects = resp.savedObjects.filter(savedObject => {
const typeName = JSON.parse(savedObject.attributes.visState).type;
const visType = visTypes.byName[typeName];
return visType.stage !== 'lab';
});
}
return resp;
};
const addNewVisWithCleanup = () => {
onClose();
addNewVis();
};
document.body.appendChild(container);
const element = (
<DashboardAddPanel
onClose={onClose}
find={find}
addNewPanel={addNewPanel}
addNewVis={addNewVisWithCleanup}
/>
);
ReactDOM.render(element, container);
}

View file

@ -168,12 +168,16 @@ exports[`apmUiEnabled 1`] = `
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule
margin="l"
size="full"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
justifyContent="spaceAround"
responsive={true}
wrap={false}
>
@ -184,7 +188,37 @@ exports[`apmUiEnabled 1`] = `
<EuiText
grow={true}
>
<span
<strong
style={
Object {
"height": 38,
}
}
>
Fresh Elastic stack installation?
</strong>
<EuiLink
color="primary"
href="#/home/tutorial_directory/sampleData"
style={
Object {
"marginLeft": 8,
}
}
type="button"
>
Try some sample data sets
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
grow={true}
>
<strong
style={
Object {
"height": 38,
@ -192,7 +226,7 @@ exports[`apmUiEnabled 1`] = `
}
>
Data already in Elasticsearch?
</span>
</strong>
<EuiLink
color="primary"
href="#/management/kibana/index"
@ -349,12 +383,16 @@ exports[`render 1`] = `
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule
margin="l"
size="full"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
justifyContent="spaceAround"
responsive={true}
wrap={false}
>
@ -365,7 +403,37 @@ exports[`render 1`] = `
<EuiText
grow={true}
>
<span
<strong
style={
Object {
"height": 38,
}
}
>
Fresh Elastic stack installation?
</strong>
<EuiLink
color="primary"
href="#/home/tutorial_directory/sampleData"
style={
Object {
"marginLeft": 8,
}
}
type="button"
>
Try some sample data sets
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
grow={true}
>
<strong
style={
Object {
"height": 38,
@ -373,7 +441,7 @@ exports[`render 1`] = `
}
>
Data already in Elasticsearch?
</span>
</strong>
<EuiLink
color="primary"
href="#/management/kibana/index"

View file

@ -13,6 +13,7 @@ import {
EuiText,
EuiCard,
EuiIcon,
EuiHorizontalRule,
} from '@elastic/eui';
export function AddData({ apmUiEnabled }) {
@ -115,12 +116,27 @@ export function AddData({ apmUiEnabled }) {
{renderCards()}
<EuiFlexGroup justifyContent="center">
<EuiHorizontalRule />
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiText>
<span style={{ height: 38 }}>
<strong style={{ height: 38 }}>
Fresh Elastic stack installation?
</strong>
<EuiLink
style={{ marginLeft: 8 }}
href="#/home/tutorial_directory/sampleData"
>
Try some sample data sets
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<strong style={{ height: 38 }}>
Data already in Elasticsearch?
</span>
</strong>
<EuiLink
style={{ marginLeft: 8 }}
href="#/management/kibana/index"
@ -129,6 +145,8 @@ export function AddData({ apmUiEnabled }) {
</EuiLink>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -14,7 +14,14 @@ import { replaceTemplateStrings } from './tutorial/replace_template_strings';
import chrome from 'ui/chrome';
import { recentlyAccessedShape } from './recently_accessed';
export function HomeApp({ addBasePath, directories, recentlyAccessed }) {
export function HomeApp({
addBasePath,
directories,
recentlyAccessed,
getConfig,
setConfig,
clearIndexPatternsCache,
}) {
const isCloudEnabled = chrome.getInjected('isCloudEnabled', false);
const apmUiEnabled = chrome.getInjected('apmUiEnabled', true);
@ -25,6 +32,9 @@ export function HomeApp({ addBasePath, directories, recentlyAccessed }) {
addBasePath={addBasePath}
openTab={props.match.params.tab}
isCloudEnabled={isCloudEnabled}
getConfig={getConfig}
setConfig={setConfig}
clearIndexPatternsCache={clearIndexPatternsCache}
/>
);
};
@ -87,4 +97,7 @@ HomeApp.propTypes = {
category: PropTypes.string.isRequired
})),
recentlyAccessed: PropTypes.arrayOf(recentlyAccessedShape).isRequired,
getConfig: PropTypes.func.isRequired,
setConfig: PropTypes.func.isRequired,
clearIndexPatternsCache: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,157 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiCard,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
} from '@elastic/eui';
import {
installSampleDataSet,
uninstallSampleDataSet
} from '../sample_data_sets';
export class SampleDataSetCard extends React.Component {
constructor(props) {
super(props);
this.state = {
isProcessingRequest: false,
};
}
startRequest = async () => {
const {
getConfig,
setConfig,
id,
name,
onRequestComplete,
defaultIndex,
clearIndexPatternsCache,
} = this.props;
this.setState({
isProcessingRequest: true,
});
if (this.isInstalled()) {
await uninstallSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache);
} else {
await installSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache);
}
onRequestComplete();
this.setState({
isProcessingRequest: false,
});
}
isInstalled = () => {
if (this.props.status === 'installed') {
return true;
}
return false;
}
renderBtn = () => {
switch (this.props.status) {
case 'installed':
return (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
isLoading={this.state.isProcessingRequest}
onClick={this.startRequest}
color="danger"
data-test-subj={`removeSampleDataSet${this.props.id}`}
>
{this.state.isProcessingRequest ? 'Removing' : 'Remove'}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
href={this.props.launchUrl}
data-test-subj={`launchSampleDataSet${this.props.id}`}
>
Launch
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
case 'not_installed':
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
isLoading={this.state.isProcessingRequest}
onClick={this.startRequest}
data-test-subj={`addSampleDataSet${this.props.id}`}
>
{this.state.isProcessingRequest ? 'Adding' : 'Add'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
default: {
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={<p>{`Unable to verify dataset status, error: ${this.props.statusMsg}`}</p>}
>
<EuiButton
isDisabled
data-test-subj={`addSampleDataSet${this.props.id}`}
>
{'Add'}
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}
}
render() {
return (
<EuiCard
image={this.props.previewUrl}
title={this.props.name}
description={this.props.description}
betaBadgeLabel={this.isInstalled() ? 'INSTALLED' : null}
footer={this.renderBtn()}
data-test-subj={`sampleDataSetCard${this.props.id}`}
/>
);
}
}
SampleDataSetCard.propTypes = {
id: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
launchUrl: PropTypes.string.isRequired,
status: PropTypes.oneOf([
'installed',
'not_installed',
'unknown',
]).isRequired,
statusMsg: PropTypes.string,
onRequestComplete: PropTypes.func.isRequired,
getConfig: PropTypes.func.isRequired,
setConfig: PropTypes.func.isRequired,
clearIndexPatternsCache: PropTypes.func.isRequired,
defaultIndex: PropTypes.string.isRequired,
previewUrl: PropTypes.string.isRequired,
};

View file

@ -11,7 +11,7 @@ import {
EuiIcon,
} from '@elastic/eui';
export function Synopsis({ description, iconUrl, iconType, title, url, wrapInPanel }) {
export function Synopsis({ description, iconUrl, iconType, title, url, wrapInPanel, onClick }) {
let optionalImg;
if (iconUrl) {
optionalImg = (
@ -63,6 +63,18 @@ export function Synopsis({ description, iconUrl, iconType, title, url, wrapInPan
);
}
if (onClick) {
return (
<span
onClick={onClick}
className="euiLink synopsis"
data-test-subj={`homeSynopsisLink${title.toLowerCase()}`}
>
{synopsisDisplay}
</span>
);
}
return (
<a
href={url}
@ -79,5 +91,6 @@ Synopsis.propTypes = {
iconUrl: PropTypes.string,
iconType: PropTypes.string,
title: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
url: PropTypes.string,
onClick: PropTypes.func,
};

View file

@ -3,6 +3,7 @@
.synopsis {
display: flex;
flex-grow: 1;
cursor: pointer;
}
.synopsis:hover {

View file

@ -2,6 +2,7 @@ import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { Synopsis } from './synopsis';
import { SampleDataSetCard } from './sample_data_set_card';
import {
EuiPage,
@ -15,8 +16,10 @@ import {
import { getTutorials } from '../load_tutorials';
import { listSampleDataSets } from '../sample_data_sets';
const ALL = 'all';
const ALL_TAB_ID = 'all';
const SAMPLE_DATA_TAB_ID = 'sampleData';
export class TutorialDirectory extends React.Component {
@ -24,7 +27,7 @@ export class TutorialDirectory extends React.Component {
super(props);
this.tabs = [{
id: ALL,
id: ALL_TAB_ID,
name: 'All',
}, {
id: 'logging',
@ -35,30 +38,83 @@ export class TutorialDirectory extends React.Component {
}, {
id: 'security',
name: 'Security Analytics',
}, {
id: SAMPLE_DATA_TAB_ID,
name: 'Sample Data',
}];
let openTab = ALL;
let openTab = ALL_TAB_ID;
if (props.openTab && this.tabs.some(tab => { return tab.id === props.openTab; })) {
openTab = props.openTab;
}
this.state = {
selectedTabId: openTab,
tutorials: []
tutorialCards: [],
sampleDataSets: [],
};
}
async componentWillMount() {
let tutorials = await getTutorials();
componentWillUnmount() {
this._isMounted = false;
}
async componentDidMount() {
this._isMounted = true;
this.loadSampleDataSets();
const tutorialConfigs = await getTutorials();
if (!this._isMounted) {
return;
}
let tutorialCards = tutorialConfigs.map(tutorialConfig => {
return {
category: tutorialConfig.category,
icon: tutorialConfig.euiIconType,
name: tutorialConfig.name,
description: tutorialConfig.shortDescription,
url: this.props.addBasePath(`#/home/tutorial/${tutorialConfig.id}`),
elasticCloud: tutorialConfig.elasticCloud,
};
});
// Add card for sample data that only gets show in "all" tab
tutorialCards.push({
name: 'Sample Data',
description: 'Get started exploring Kibana with these "one click" data sets.',
url: this.props.addBasePath('#/home/tutorial_directory/sampleData'),
elasticCloud: true,
onClick: this.onSelectedTabChanged.bind(null, SAMPLE_DATA_TAB_ID),
});
if (this.props.isCloudEnabled) {
tutorials = tutorials.filter(tutorial => {
tutorialCards = tutorialCards.filter(tutorial => {
return _.has(tutorial, 'elasticCloud');
});
}
tutorials.sort((a, b) => {
tutorialCards.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
this.setState({ // eslint-disable-line react/no-did-mount-set-state
tutorialCards: tutorialCards,
});
}
loadSampleDataSets = async () => {
const sampleDataSets = await listSampleDataSets();
if (!this._isMounted) {
return;
}
this.setState({
tutorials: tutorials,
sampleDataSets: sampleDataSets.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}),
});
}
@ -81,29 +137,58 @@ export class TutorialDirectory extends React.Component {
));
}
renderTutorials = () => {
return this.state.tutorials
renderTab = () => {
if (this.state.selectedTabId === SAMPLE_DATA_TAB_ID) {
return this.renderSampleDataSetsTab();
}
return this.renderTutorialsTab();
}
renderTutorialsTab = () => {
return this.state.tutorialCards
.filter((tutorial) => {
if (this.state.selectedTabId === ALL) {
return true;
}
return this.state.selectedTabId === tutorial.category;
return this.state.selectedTabId === ALL_TAB_ID || this.state.selectedTabId === tutorial.category;
})
.map((tutorial) => {
return (
<EuiFlexItem key={tutorial.name}>
<Synopsis
iconType={tutorial.euiIconType}
description={tutorial.shortDescription}
iconType={tutorial.icon}
description={tutorial.description}
title={tutorial.name}
wrapInPanel
url={this.props.addBasePath(`#/home/tutorial/${tutorial.id}`)}
url={tutorial.url}
onClick={tutorial.onClick}
/>
</EuiFlexItem>
);
});
};
renderSampleDataSetsTab = () => {
return this.state.sampleDataSets.map(sampleDataSet => {
return (
<EuiFlexItem key={sampleDataSet.id}>
<SampleDataSetCard
id={sampleDataSet.id}
description={sampleDataSet.description}
name={sampleDataSet.name}
launchUrl={this.props.addBasePath(`/app/kibana#/dashboard/${sampleDataSet.overviewDashboard}`)}
status={sampleDataSet.status}
statusMsg={sampleDataSet.statusMsg}
onRequestComplete={this.loadSampleDataSets}
getConfig={this.props.getConfig}
setConfig={this.props.setConfig}
clearIndexPatternsCache={this.props.clearIndexPatternsCache}
defaultIndex={sampleDataSet.defaultIndex}
previewUrl={this.props.addBasePath(sampleDataSet.previewImagePath)}
/>
</EuiFlexItem>
);
});
}
render() {
return (
<EuiPage className="home">
@ -123,7 +208,7 @@ export class TutorialDirectory extends React.Component {
</EuiTabs>
<EuiSpacer />
<EuiFlexGrid columns={4}>
{ this.renderTutorials() }
{ this.renderTab() }
</EuiFlexGrid>
</EuiPage>
@ -135,4 +220,7 @@ TutorialDirectory.propTypes = {
addBasePath: PropTypes.func.isRequired,
openTab: PropTypes.string,
isCloudEnabled: PropTypes.bool.isRequired,
getConfig: PropTypes.func.isRequired,
setConfig: PropTypes.func.isRequired,
clearIndexPatternsCache: PropTypes.func.isRequired,
};

View file

@ -2,4 +2,7 @@
add-base-path="addBasePath"
directories="directories"
recently-accessed="recentlyAccessed"
get-config="getConfig"
set-config="setConfig"
clear-index-patterns-cache="clearIndexPatternsCache"
/>

View file

@ -17,13 +17,19 @@ app.directive('homeApp', function (reactDirective) {
function getRoute() {
return {
template,
controller($scope, Private) {
controller($scope, config, indexPatterns, Private) {
$scope.addBasePath = chrome.addBasePath;
$scope.directories = Private(FeatureCatalogueRegistryProvider).inTitleOrder;
$scope.recentlyAccessed = recentlyAccessed.get().map(item => {
item.link = chrome.addBasePath(item.link);
return item;
});
$scope.getConfig = (...args) => config.get(...args);
$scope.setConfig = (...args) => config.set(...args);
$scope.clearIndexPatternsCache = () => {
const getter = indexPatterns.getIds;
getter.clearCache();
};
}
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

View file

@ -0,0 +1,96 @@
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
const sampleDataUrl = chrome.addBasePath('/api/sample_data');
const headers = new Headers({
Accept: 'application/json',
'Content-Type': 'application/json',
'kbn-xsrf': 'kibana',
});
export async function listSampleDataSets() {
try {
const response = await fetch(sampleDataUrl, {
method: 'get',
credentials: 'include',
headers: headers,
});
if (response.status >= 300) {
throw new Error(`Request failed with status code: ${response.status}`);
}
return await response.json();
} catch (err) {
toastNotifications.addDanger({
title: `Unable to load sample data sets list`,
text: `${err.message}`,
});
return [];
}
}
export async function installSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) {
try {
const response = await fetch(`${sampleDataUrl}/${id}`, {
method: 'post',
credentials: 'include',
headers: headers,
});
if (response.status >= 300) {
const body = await response.text();
throw new Error(`Request failed with status code: ${response.status}, message: ${body}`);
}
} catch (err) {
toastNotifications.addDanger({
title: `Unable to install sample data set: ${name}`,
text: `${err.message}`,
});
return;
}
const existingDefaultIndex = await getConfig('defaultIndex');
if (existingDefaultIndex === null) {
await setConfig('defaultIndex', defaultIndex);
}
clearIndexPatternsCache();
toastNotifications.addSuccess({
title: `${name} sample data set successfully installed`,
['data-test-subj']: 'sampleDataSetInstallToast'
});
}
export async function uninstallSampleDataSet(id, name, defaultIndex, getConfig, setConfig, clearIndexPatternsCache) {
try {
const response = await fetch(`${sampleDataUrl}/${id}`, {
method: 'delete',
credentials: 'include',
headers: headers,
});
if (response.status >= 300) {
const body = await response.text();
throw new Error(`Request failed with status code: ${response.status}, message: ${body}`);
}
} catch (err) {
toastNotifications.addDanger({
title: `Unable to uninstall sample data set`,
text: `${err.message}`,
});
return;
}
const existingDefaultIndex = await getConfig('defaultIndex');
if (existingDefaultIndex && existingDefaultIndex === defaultIndex) {
await setConfig('defaultIndex', null);
}
clearIndexPatternsCache();
toastNotifications.addSuccess({
title: `${name} sample data set successfully uninstalled`,
['data-test-subj']: 'sampleDataSetUninstallToast'
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Before After
Before After

View file

@ -16,6 +16,7 @@ import 'uiExports/spyModes';
import 'uiExports/fieldFormats';
import 'uiExports/fieldFormatEditors';
import 'uiExports/navbarExtensions';
import 'uiExports/dashboardPanelActions';
import 'uiExports/managementSections';
import 'uiExports/devTools';
import 'uiExports/docViews';

View file

@ -19,7 +19,9 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
<StepIndexPattern
allIndices={
Array [
Object {},
Object {
"name": "myIndexPattern",
},
]
}
esService={Object {}}
@ -38,7 +40,6 @@ exports[`CreateIndexPatternWizard renders the empty state when there are no indi
onChangeIncludingSystemIndices={[Function]}
/>
<EmptyState
loadingDataDocUrl=""
onRefresh={[Function]}
/>
</div>

View file

@ -69,7 +69,7 @@ describe('CreateIndexPatternWizard', () => {
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [{}],
allIndices: [{ name: 'myIndexPattern' }],
});
await component.update();
@ -87,7 +87,7 @@ describe('CreateIndexPatternWizard', () => {
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [{}],
allIndices: [{ name: 'myIndexPattern' }],
step: 2,
});

View file

@ -14,7 +14,6 @@ describe('CreateIndexPatternWizardRender', () => {
it('should call render', () => {
renderCreateIndexPatternWizard(
'',
'',
{
es: {},

View file

@ -51,11 +51,18 @@ exports[`EmptyState should render normally 1`] = `
 
<EuiLink
color="primary"
href="http://www.elastic.co"
target="_blank"
href="#/home/tutorial_directory"
type="button"
>
Learn how.
Learn how
</EuiLink>
or
<EuiLink
color="primary"
href="#/home/tutorial_directory/sampleData"
type="button"
>
get started with some sample data sets.
</EuiLink>
</p>
</EuiText>

View file

@ -14,7 +14,6 @@ import {
} from '@elastic/eui';
export const EmptyState = ({
loadingDataDocUrl,
onRefresh,
}) => (
<EuiPanel paddingSize="l">
@ -33,10 +32,15 @@ export const EmptyState = ({
</EuiTextColor>
&nbsp;
<EuiLink
href={loadingDataDocUrl}
target="_blank"
href="#/home/tutorial_directory"
>
Learn how.
Learn how
</EuiLink>
{' or '}
<EuiLink
href="#/home/tutorial_directory/sampleData"
>
get started with some sample data sets.
</EuiLink>
</p>
</EuiText>
@ -60,6 +64,5 @@ export const EmptyState = ({
);
EmptyState.propTypes = {
loadingDataDocUrl: PropTypes.string.isRequired,
onRefresh: PropTypes.func.isRequired,
};

View file

@ -13,7 +13,6 @@ import { getIndices } from './lib/get_indices';
export class CreateIndexPatternWizard extends Component {
static propTypes = {
loadingDataDocUrl: PropTypes.string.isRequired,
initialQuery: PropTypes.string,
services: PropTypes.shape({
es: PropTypes.object.isRequired,
@ -106,9 +105,9 @@ export class CreateIndexPatternWizard extends Component {
return <LoadingState />;
}
if (allIndices.length === 0) {
const { loadingDataDocUrl } = this.props;
return <EmptyState loadingDataDocUrl={loadingDataDocUrl} onRefresh={this.fetchIndices} />;
const hasDataIndices = allIndices.some(({ name }) => !name.startsWith('.'));
if (!hasDataIndices) {
return <EmptyState onRefresh={this.fetchIndices} />;
}
if (step === 1) {

View file

@ -2,7 +2,6 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects';
import uiRoutes from 'ui/routes';
import angularTemplate from './angular_template.html';
import 'ui/index_patterns';
import { documentationLinks } from 'ui/documentation_links';
import { renderCreateIndexPatternWizard, destroyCreateIndexPatternWizard } from './render';
@ -26,7 +25,6 @@ uiRoutes.when('/management/kibana/index', {
const initialQuery = $routeParams.id ? decodeURIComponent($routeParams.id) : undefined;
renderCreateIndexPatternWizard(
documentationLinks.indexPatterns.loadingData,
initialQuery,
services
);

View file

@ -5,7 +5,6 @@ import { CreateIndexPatternWizard } from './create_index_pattern_wizard';
const CREATE_INDEX_PATTERN_DOM_ELEMENT_ID = 'createIndexPatternReact';
export function renderCreateIndexPatternWizard(
loadingDataDocUrl,
initialQuery,
services,
) {
@ -16,7 +15,6 @@ export function renderCreateIndexPatternWizard(
render(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
initialQuery={initialQuery}
services={services}
/>,

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