mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Merge remote-tracking branch 'upstream/master' into saved-objects-client-provider
This commit is contained in:
commit
0eeca7d1b4
289 changed files with 6621 additions and 3626 deletions
|
@ -1,5 +1,8 @@
|
|||
[[release-notes]]
|
||||
= {kib} Release Notes
|
||||
++++
|
||||
<titleabbrev>Release Notes</titleabbrev>
|
||||
++++
|
||||
|
||||
[partintro]
|
||||
--
|
||||
|
|
BIN
docs/images/management-index-management.png
Normal file
BIN
docs/images/management-index-management.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 178 KiB |
|
@ -77,4 +77,6 @@ include::development.asciidoc[]
|
|||
|
||||
include::limitations.asciidoc[]
|
||||
|
||||
include::release-notes/highlights.asciidoc[]
|
||||
|
||||
include::CHANGELOG.asciidoc[]
|
|
@ -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[]
|
||||
|
|
24
docs/management/managing-indices.asciidoc
Normal file
24
docs/management/managing-indices.asciidoc
Normal 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.
|
9
docs/release-notes/highlights-7.0.0.asciidoc
Normal file
9
docs/release-notes/highlights-7.0.0.asciidoc
Normal 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>>.
|
16
docs/release-notes/highlights.asciidoc
Normal file
16
docs/release-notes/highlights.asciidoc
Normal 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[]
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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'));
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/*']);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { resolve, join } from 'path';
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
import { prepareExternalProjectDependencies } from './prepare_project_dependencies';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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}}`
|
||||
);
|
||||
|
||||
|
|
11
packages/kbn-pm/src/utils/log.ts
Normal file
11
packages/kbn-pm/src/utils/log.ts
Normal 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);
|
||||
},
|
||||
};
|
|
@ -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)) {
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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`
|
||||
)
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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`);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
2
scripts/check_file_casing.js
Normal file
2
scripts/check_file_casing.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
require('../src/babel-register');
|
||||
require('../src/dev/run_check_file_casing');
|
|
@ -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: ''
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { queryDsl as default } from './dsl';
|
137
src/core_plugins/console/api_server/es_6_0/query/templates.js
Normal file
137
src/core_plugins/console/api_server/es_6_0/query/templates.js
Normal 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',
|
||||
},
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -121,6 +121,7 @@ export class DashboardPanel extends React.Component {
|
|||
>
|
||||
<PanelHeader
|
||||
panelId={panel.panelIndex}
|
||||
embeddable={this.embeddable}
|
||||
/>
|
||||
|
||||
{this.renderContent()}
|
||||
|
|
|
@ -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}`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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),
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
155
src/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
Normal file
155
src/core_plugins/kibana/public/dashboard/top_nav/add_panel.js
Normal 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,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
.addPanelFlyout {
|
||||
width: 33vw;
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -7,7 +7,7 @@ import {
|
|||
|
||||
import {
|
||||
DashboardCloneModal,
|
||||
} from '../top_nav/clone_modal';
|
||||
} from './clone_modal';
|
||||
|
||||
let onClone;
|
||||
let onClose;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
.synopsis {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.synopsis:hover {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -2,4 +2,7 @@
|
|||
add-base-path="addBasePath"
|
||||
directories="directories"
|
||||
recently-accessed="recentlyAccessed"
|
||||
get-config="getConfig"
|
||||
set-config="setConfig"
|
||||
clear-index-patterns-cache="clearIndexPatternsCache"
|
||||
/>
|
||||
|
|
|
@ -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 |
96
src/core_plugins/kibana/public/home/sample_data_sets.js
Normal file
96
src/core_plugins/kibana/public/home/sample_data_sets.js
Normal 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 |
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ describe('CreateIndexPatternWizardRender', () => {
|
|||
|
||||
it('should call render', () => {
|
||||
renderCreateIndexPatternWizard(
|
||||
'',
|
||||
'',
|
||||
{
|
||||
es: {},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
export const EmptyState = ({
|
||||
loadingDataDocUrl,
|
||||
onRefresh,
|
||||
}) => (
|
||||
<EuiPanel paddingSize="l">
|
||||
|
@ -33,10 +32,15 @@ export const EmptyState = ({
|
|||
</EuiTextColor>
|
||||
|
||||
<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,
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue