Merge remote-tracking branch 'origin/master' into feature/merge-code

This commit is contained in:
Fuyao Zhao 2019-01-07 21:59:21 -08:00
commit f48000022c
458 changed files with 19239 additions and 5348 deletions

View file

@ -1,7 +1,8 @@
---
name: Accessibility Issue
about: Issues to help Elastic meet WCAG / Section 508 compliance
about: Issues to help Kibana be as keyboard-navigable, screen-readable, and accessible to all, with a focus on WCAG / Section 508 compliance
labels: accessibility
title: (Accessibility)
---
**Steps to reproduce (assumes [ChromeVox](https://chrome.google.com/webstore/detail/chromevox/kgejglhpjiefppelpmljglcjbhoiplfn) or similar)**
@ -9,17 +10,20 @@ about: Issues to help Elastic meet WCAG / Section 508 compliance
1.
2.
3.
4.
[Screenshot here]
**Actual Result**
5.
4.
**Expected Result**
5.
[Link to meta issues here]
4.
**Meta Issue**
**Kibana Version:**
**Relevant WCAG Criteria:** (link to https://www.w3.org/WAI/WCAG21/quickref/?versions=2.0)
**Relevant WCAG Criteria:** [#.#.# WCAG Criterion](link to https://www.w3.org/WAI/WCAG21/quickref/?versions=2.0)

View file

@ -1,13 +1,7 @@
files:
include:
- '{src,x-pack}/**/*.s+(a|c)ss'
ignore:
# _only include_ rollup and security files
- '**/x-pack/plugins/!(rollup|security)/**'
# ignore all of src
- '**/src/**/*'
# ignore all node_modules
- '**/node_modules/**'
- 'x-pack/plugins/rollup/**/*.s+(a|c)ss'
- 'x-pack/plugins/security/**/*.s+(a|c)ss'
rules:
quotes:
- 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View file

@ -78,3 +78,12 @@ You must first stop a rollup job before deleting it.
[role="screenshot"]
image::images/management_rollup_job_details.png[][Rollup job details]
You can start, stop, and delete an existing rollup job, but edits are not supported.
If you want to make any changes, delete the existing job and create a new one with
the updated specifications. Be sure to use a different name for the new rollup job;
reusing the same name could lead to problems with mismatched job configurations.
More about logistical details for the {ref}/rollup-job-config.html[rollup job configuration]
can be found in the {es} documentation.

View file

@ -15,10 +15,15 @@ an item for creating a rollup index pattern, if a rollup index is detected in th
image::images/management_create_rollup_menu.png[Create index pattern menu]
You can match an index pattern to only rolled up data, or mix both rolled up
and raw data to visualize all data together. An index
pattern can match only one rolled up index, not multiple. There is no restriction
on the number of standard indices that an index pattern can match. To match multiple indices, use a comma
to separate the names, with no space after the comma.
and raw data to visualize all data together. An index pattern can match only one
rolled up index, not multiple. There is no restriction on the number of standard
indices that an index pattern can match.
Combination index patterns use the same
notation as other multiple indices in {es}. To match multiple indices to create a
combination index pattern, use a comma to separate the names, with no space after the comma.
The notation for wildcards (`*`) and the ability to "exclude" (`-`) also apply
(for example, `test*,-test3`).
When creating an index pattern, youre asked to set a time field for filtering.
With a rollup index, the time filter field is the same field used for

View file

@ -1,70 +1,14 @@
[role="xpack"]
[[xpack-upgrade-assistant]]
[[upgrade-assistant]]
== Upgrade Assistant
The Upgrade Assistant helps you prepare to upgrade from Elasticsearch 5.x to
Elasticsearch 6.0. It identifies deprecated settings, simplifies reindexing
your pre-5.x indices, and upgrades the internal indices used by Kibana and
X-Pack to the format required in 6.0.
The Upgrade Assistant helps you prepare for your upgrade to {es} 8.0.
To access the assistant, go to *Management > 8.0 Upgrade Assistant*.
To access the Upgrade Assistant, go to **Management** and click the **Upgrade
Assistant** link in the Elasticsearch section.
The assistant identifies the deprecated settings in your cluster and indices
and guides you through the process of resolving issues, including reindexing.
[float]
[[cluster-checkup]]
=== Cluster Checkup
Before upgrading to Elasticsearch 8.0, make sure that you are using the final
7.x minor release to see the most up-to-date deprecation issues.
The first step in preparing to upgrade is to identify any deprecated settings
or features you are using. The Cluster Checkup runs a series of checks
against your cluster and indices and generates a report identifying
any issues that you need to resolve.
To run the Cluster Checkup, go to the **Cluster Checkup** tab in the
Upgrade Assistant. Issues that **must** be resolved before you can upgrade to
Elasticsearch 6.0 appear in red as errors.
If the checkup finds indices that need to be reindexed, you can
manage that process with the Reindex Helper. You can also manually reindex or
simply delete old indices if you are sure you no longer need them.
[float]
[[reindex-helper]]
=== Reindex Helper
If you have indices created in 2.x, you must reindex them before
upgrading to Elasticsearch 6.0. In addition, the internal Kibana and X-Pack
indices must be reindexed to upgrade them to the format required in 6.0.
To reindex indices with the Reindex Helper:
. **Back up your indices using {ref}/modules-snapshots.html[Snapshot and Restore].**
. Go to the **Reindex Helper** tab in the Upgrade Assistant.
. Click the **Reindex** button to reindex an index.
. Monitor the reindex task that is kicked off.
You can run any number of reindex tasks simultaneously. The processing status
is displayed for each index. You can stop a task by clicking **Cancel**. If
any step in the reindex process fails, you can reset the index by clicking
**Reset**.
You can also click **Refresh Indices** to remove any indices that have been
successfully reindexed from the list.
Reindexing tasks continue to run when you leave the Reindex Helper. When you
come back, click **Refresh Indices** to get the latest status of your indices.
NOTE: When you come back to the Reindex Helper, it shows any tasks that are
still running. You can cancel those tasks if you need to, but you won't have
access to the task progress and are blocked from resetting indices that fail
reindexing. This is because the index might have been modified outside of
your Kibana instance.
[float]
[[toggle-deprecation-logger]]
=== Toggle Deprecation Logger
To see the current deprecation logging status, go to the **Toggle Deprecation
Logging** tab in the Upgrade Assistant. Deprecation Logging is enabled by
default from Elasticsearch 5.x onward. If you have disabled deprecation logging, you
can click **Toggle Deprecation Logging** to re-enable it. This logs any
deprecated actions to your log directory so you can see what changes you need
to make to your code before upgrading.
[role="screenshot"]
image::images/management-upgrade-assistant-8.0.png[]

View file

@ -23,6 +23,30 @@ Kibana 7.0 will only use the Node.js distribution included in the package.
*Impact:* There is no expected impact unless Kibana is installed in a non-standard way.
[float]
=== Removed support for using PhantomJS browser for screenshots in Reporting
*Details:* Since the first release of Kibana Reporting, PhantomJS was used as
the headless browser to capture screenshots of Kibana dashboards and
visualizations. In that short time, Chromium has started offering a new
headless browser library and the PhantomJS maintainers abandoned their project.
We started planning for a transition in 6.5.0, when we made Chromium the
default option, but allowed users to continue using Phantom with the
`xpack.reporting.capture.browser.type: phantom` setting. In 7.0, that setting
will still exist for compatibility, but the only valid option will be
`chromium`.
*Impact:* Before upgrading to 7.0, if you have `xpack.reporting.capture.browser.type`
set in kibana.yml, make sure it is set to `chromium`.
[NOTE]
============
Reporting 7.0 uses a version of the Chromium headless browser that RHEL 6,
CentOS 6.x, and other old versions of Linux derived from RHEL 6. This change
effectively removes RHEL 6 OS server support from Kibana Reporting. Users with
RHEL 6 must upgrade to RHEL 7 to use Kibana Reporting starting with version
7.0.0 of the Elastic stack.
============
[float]
=== Advanced setting query:queryString:options no longer applies to filters
*Details:* In previous versions of Kibana the Advanced Setting `query:queryString:options` was applied to both queries

View file

@ -13,17 +13,10 @@ However, these docs will be kept up-to-date to reflect the current implementatio
[float]
[[reporting-nav-bar-extensions]]
=== Nav Bar Extensions
X-Pack uses the `NavBarExtensionsRegistryProvider` to register a navigation bar item for the
Dashboard/Discover/Visualize applications. These all use the `export-config` AngularJS directive to display the
Reporting options. Each of the different exportTypes require the AngularJS controller that the contains the
`export-config` directive to implement certain methods/properties that are used when creating the {reporting} job.
=== Share Menu Extensions
X-Pack uses the `ShareContextMenuExtensionsRegistryProvider` to register actions in the share menu.
If you wish to add Reporting integration via the navigation bar that emulates the way these options are chosen for
Dashboards, Visualize and Discover, you can reuse the `export-config` directive for the time being. Otherwise, you can
provide a custom UI that generates the URL that is used to generate the {reporting} job.
This integration will likely be changing in the near future as we move away from AngularJS towards React.
This integration will likely be changing in the near future as we move towards a unified actions abstraction across {kib}.
[float]
=== Generate Job URL

View file

@ -2,7 +2,7 @@
[[reporting-getting-started]]
== Getting Started
{reporting} is automatically enabled in {kib}.
{reporting} is automatically enabled in {kib}.
To manually generate a report:
@ -14,20 +14,24 @@ information, see <<secure-reporting>>.
. Open the dashboard, visualization, or saved search you want to include
in the report.
. Click *Reporting* in the {kib} toolbar:
. Click *Share* in the {kib} toolbar:
+
--
[role="screenshot"]
image:reporting/images/reporting-button.png["Reporting Button",link="reporting-button.png"]
image:reporting/images/share-button.png["Reporting Button",link="share-button.png"]
--
. Depending on the {kib} application, choose the appropriate options:
.. If you're on Discover, click the *Generate CSV* button.
.. If you're on Discover:
... Select *CSV Reports*
... Click the *Generate CSV* button.
.. If you're on Visualize or Dashboard:
... Select *PDF Reports*
... Select either *Optimize PDF for printing* or *Preserve existing layout in PDF*. For an explanation of the different layout modes, see <<pdf-layout-modes, PDF Layout Modes>>.
... Choose to enable *Optimize for printing* layout mode. For an explanation of the different layout modes, see <<pdf-layout-modes, PDF Layout Modes>>.
... Click the *Generate PDF* button.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View file

@ -13,10 +13,10 @@ NOTE: On Linux, the `libfontconfig` and `libfreetype6` packages and system
fonts are required to generate PDF reports. If no system fonts are available,
labels are not rendered correctly in the reports.
The following Reporting button is found in the {kib} toolbar:
Reporting is located in the share menu from the {kib} toolbar:
[role="screenshot"]
image::reporting/images/reporting-button.png["Reporting"]
image::reporting/images/share-button.png["Share"]
You can also <<automating-report-generation, generate reports automatically>>.
@ -30,4 +30,4 @@ include::pdf-layout-modes.asciidoc[]
include::configuring-reporting.asciidoc[]
include::chromium-sandbox.asciidoc[]
include::reporting-troubleshooting.asciidoc[]
include::development/index.asciidoc[]
include::development/index.asciidoc[]

View file

@ -5,7 +5,7 @@ When creating a PDF report, there are two layout modes *Optimize PDF for printin
--
[role="screenshot"]
image:reporting/images/pdf-reporting.png["PDF Reporting",link="pdf-reporting.png"]
image:reporting/images/preserve-layout-switch.png["PDF Reporting",link="preserve-layout-switch.png"]
--
[float]
@ -26,4 +26,4 @@ This will create a PDF preserving the existing layout and size of the Visualizat
--
[role="screenshot"]
image:reporting/images/preserve-layout.png["Preserve existing layout in PDF",link="preserve-layout.png"]
--
--

View file

@ -22,8 +22,9 @@ There is currently a known limitation with the Data Table visualization that onl
[float]
==== `You must install fontconfig and freetype for Reporting to work'`
Reporting using PhantomJS, the default browser, relies on system packages. Install the appropriate fontconfig and freetype
packages for your distribution.
Reporting uses a headless browser on the Kibana server, which relies on some
system packages. Install the appropriate fontconfig and freetype packages for
your distribution.
[float]
==== `Max attempts reached (3)`
@ -54,11 +55,6 @@ the CAP_SYS_ADMIN capability.
Elastic recommends that you research the feasibility of enabling unprivileged user namespaces before disabling the sandbox. An exception
is if you are running Kibana in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters.
[float]
==== `spawn EACCES`
Ensure that the `phantomjs` binary in your Kibana data directory is owned by the user who is running Kibana, that the user has the execute permission,
and if applicable, that the filesystem is mounted with the `exec` option.
[float]
==== `Caught error spawning Chromium`
Ensure that the `headless_shell` binary located in your Kibana data directory is owned by the user who is running Kibana, that the user has the execute permission,

View file

@ -91,16 +91,8 @@ visualizations, try increasing this value.
Defaults to `3000` (3 seconds).
[[xpack-reporting-browser]]`xpack.reporting.capture.browser.type`::
Specifies the browser to use to capture screenshots. Valid options are `phantom`
and `chromium`. When `chromium` is set, the settings specified in the <<reporting-chromium-settings, Chromium settings>>
are respected. Defaults to `chromium`.
[NOTE]
============
Starting in 7.0, Phantom support will be removed from Kibana, and `chromium`
will be the only valid option for the `xpack.reporting.capture.browser.type` setting.
============
Specifies the browser to use to capture screenshots. This setting exists for
backward compatibility. The only valid option is `chromium`.
[float]
[[reporting-chromium-settings]]

View file

@ -37,16 +37,15 @@
"preinstall": "node ./preinstall_check",
"kbn": "node scripts/kbn",
"es": "node scripts/es",
"elasticsearch": "echo 'use `yarn es snapshot -E path.data=../data/`'",
"test": "grunt test",
"test:dev": "grunt test:dev",
"test:quick": "grunt test:quick",
"test:browser": "grunt test:browser",
"test:jest": "node scripts/jest",
"test:mocha": "grunt test:mocha",
"test:ui": "echo 'use `node scripts/functional_tests`' && false",
"test:ui:server": "echo 'use `node scripts/functional_tests_server`' && false",
"test:ui:runner": "echo 'use `node scripts/functional_test_runner`' && false",
"test:ui": "node scripts/functional_tests",
"test:ui:server": "node scripts/functional_tests_server",
"test:ui:runner": "node scripts/functional_test_runner",
"test:server": "grunt test:server",
"test:coverage": "grunt test:coverage",
"checkLicenses": "grunt licenses --dev",
@ -56,11 +55,12 @@
"debug-break": "node --nolazy --inspect-brk scripts/kibana --dev",
"precommit": "node scripts/precommit_hook",
"karma": "karma start",
"lint": "echo 'use `node scripts/eslint` and/or `node scripts/tslint`' && false",
"lintroller": "echo 'use `node scripts/eslint --fix` and/or `node scripts/tslint --fix`' && false",
"makelogs": "echo 'use `node scripts/makelogs`' && false",
"mocha": "echo 'use `node scripts/mocha`' && false",
"sterilize": "grunt sterilize",
"lint": "yarn run lint:es && yarn run lint:ts && yarn run lint:sass",
"lint:es": "node scripts/eslint",
"lint:ts": "node scripts/tslint",
"lint:sass": "node scripts/sasslint",
"makelogs": "node scripts/makelogs",
"mocha": "node scripts/mocha",
"uiFramework:start": "cd packages/kbn-ui-framework && yarn docSiteStart",
"uiFramework:build": "cd packages/kbn-ui-framework && yarn docSiteBuild",
"uiFramework:createComponent": "cd packages/kbn-ui-framework && yarn createComponent",
@ -397,8 +397,8 @@
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"tslint-plugin-prettier": "^2.0.0",
"tslint-microsoft-contrib": "^6.0.0",
"tslint-plugin-prettier": "^2.0.0",
"typescript": "^3.0.3",
"vinyl-fs": "^3.0.2",
"xml2js": "^0.4.19",

View file

@ -26,7 +26,7 @@ module.exports = {
browsers: [
'last 2 versions',
'> 5%',
'Safari 7', // for PhantomJS support
'Safari 7', // for PhantomJS support: https://github.com/elastic/kibana/issues/27136
],
},
useBuiltIns: true,

View file

@ -52,6 +52,8 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
return parse(expression, parseOptions);
}
// indexPattern isn't required, but if you pass one in, we can be more intelligent
// about how we craft the queries (e.g. scripted fields)
export function toElasticsearchQuery(node, indexPattern) {
if (!node || !node.type || !nodeTypes[node.type]) {
return toElasticsearchQuery(nodeTypes.function.buildNode('and', []));

View file

@ -57,12 +57,21 @@ describe('kuery functions', function () {
expect(_.isEqual(expected, result)).to.be(true);
});
it('should return an ES exists query without an index pattern', function () {
const expected = {
exists: { field: 'response' }
};
const existsNode = nodeTypes.function.buildNode('exists', 'response');
const result = exists.toElasticsearchQuery(existsNode);
expect(_.isEqual(expected, result)).to.be(true);
});
it('should throw an error for scripted fields', function () {
const existsNode = nodeTypes.function.buildNode('exists', 'script string');
expect(exists.toElasticsearchQuery)
.withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/);
});
});
});
});

View file

@ -83,6 +83,14 @@ describe('kuery functions', function () {
expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35');
});
it('should return an ES geo_bounding_box query without an index pattern', function () {
const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params);
const result = geoBoundingBox.toElasticsearchQuery(node);
expect(result).to.have.property('geo_bounding_box');
expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37');
expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35');
});
it('should use the ignore_unmapped parameter', function () {
const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params);
const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern);

View file

@ -91,6 +91,17 @@ describe('kuery functions', function () {
});
});
it('should return an ES geo_polygon query without an index pattern', function () {
const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points);
const result = geoPolygon.toElasticsearchQuery(node);
expect(result).to.have.property('geo_polygon');
expect(result.geo_polygon.geo).to.have.property('points');
result.geo_polygon.geo.points.forEach((point, index) => {
const expectedLatLon = `${points[index].lat}, ${points[index].lon}`;
expect(point).to.be(expectedLatLon);
});
});
it('should use the ignore_unmapped parameter', function () {
const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points);

View file

@ -143,6 +143,21 @@ describe('kuery functions', function () {
expect(result).to.eql(expected);
});
it('should return an ES match query when a concrete fieldName and value are provided without an index pattern', function () {
const expected = {
bool: {
should: [
{ match: { extension: 'jpg' } },
],
minimum_should_match: 1
}
};
const node = nodeTypes.function.buildNode('is', 'extension', 'jpg');
const result = is.toElasticsearchQuery(node);
expect(result).to.eql(expected);
});
it('should support creation of phrase queries', function () {
const expected = {
bool: {

View file

@ -86,6 +86,28 @@ describe('kuery functions', function () {
expect(result).to.eql(expected);
});
it('should return an ES range query without an index pattern', function () {
const expected = {
bool: {
should: [
{
range: {
bytes: {
gt: 1000,
lt: 8000
}
}
}
],
minimum_should_match: 1
}
};
const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
const result = range.toElasticsearchQuery(node);
expect(result).to.eql(expected);
});
it('should support wildcard field names', function () {
const expected = {
bool: {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { get } from 'lodash';
import * as literal from '../node_types/literal';
export function buildNodeParams(fieldName) {
@ -28,7 +29,7 @@ export function buildNodeParams(fieldName) {
export function toElasticsearchQuery(node, indexPattern) {
const { arguments: [ fieldNameArg ] } = node;
const fieldName = literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.find(field => field.name === fieldName);
const field = get(indexPattern, 'fields', []).find(field => field.name === fieldName);
if (field && field.scripted) {
throw new Error(`Exists query does not support scripted fields`);

View file

@ -37,7 +37,7 @@ export function buildNodeParams(fieldName, params) {
export function toElasticsearchQuery(node, indexPattern) {
const [ fieldNameArg, ...args ] = node.arguments;
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.find(field => field.name === fieldName);
const field = _.get(indexPattern, 'fields', []).find(field => field.name === fieldName);
const queryParams = args.reduce((acc, arg) => {
const snakeArgName = _.snakeCase(arg.name);
return {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { get } from 'lodash';
import { nodeTypes } from '../node_types';
import * as ast from '../ast';
@ -35,7 +36,7 @@ export function buildNodeParams(fieldName, points) {
export function toElasticsearchQuery(node, indexPattern) {
const [ fieldNameArg, ...points ] = node.arguments;
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
const field = indexPattern.fields.find(field => field.name === fieldName);
const field = get(indexPattern, 'fields', []).find(field => field.name === fieldName);
const queryParams = {
points: points.map(ast.toElasticsearchQuery)
};

View file

@ -44,6 +44,7 @@ export function buildNodeParams(fieldName, value, isPhrase = false) {
export function toElasticsearchQuery(node, indexPattern) {
const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node;
const fieldName = ast.toElasticsearchQuery(fieldNameArg);
const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg;
const type = isPhraseArg.value ? 'phrase' : 'best_fields';
@ -65,7 +66,7 @@ export function toElasticsearchQuery(node, indexPattern) {
};
}
const fields = getFields(fieldNameArg, indexPattern);
const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : [];
// If no fields are found in the index pattern we send through the given field name as-is. We do this to preserve
// the behaviour of lucene on dashboards where there are panels based on different index patterns that have different
@ -80,7 +81,10 @@ export function toElasticsearchQuery(node, indexPattern) {
}
const isExistsQuery = valueArg.type === 'wildcard' && value === '*';
const isMatchAllQuery = isExistsQuery && fields && fields.length === indexPattern.fields.length;
const isAllFieldsQuery =
(fieldNameArg.type === 'wildcard' && fieldName === '*')
|| (fields && indexPattern && fields.length === indexPattern.fields.length);
const isMatchAllQuery = isExistsQuery && isAllFieldsQuery;
if (isMatchAllQuery) {
return { match_all: {} };

View file

@ -37,7 +37,7 @@ export function buildNodeParams(fieldName, params) {
export function toElasticsearchQuery(node, indexPattern) {
const [ fieldNameArg, ...args ] = node.arguments;
const fields = getFields(fieldNameArg, indexPattern);
const fields = indexPattern ? getFields(fieldNameArg, indexPattern) : [];
const namedArgs = extractArguments(args);
const queryParams = _.mapValues(namedArgs, ast.toElasticsearchQuery);

View file

@ -31,10 +31,16 @@ const canvasPluginDirectoryName = 'canvas_plugin';
const isDirectory = path =>
lstat(path)
.then(stat => stat.isDirectory())
.catch(() => false);
.catch(() => false); // if lstat fails, it doesn't exist and is not a directory
const isDirname = (p, name) => path.basename(p) === name;
const filterDirectories = (paths, { exclude = false } = {}) => {
return Promise.all(paths.map(p => isDirectory(p))).then(directories => {
return paths.filter((p, i) => (exclude ? !directories[i] : directories[i]));
});
};
const getPackagePluginPath = () => {
let basePluginPath = path.resolve(__dirname, '..');
@ -90,19 +96,15 @@ export const getPluginPaths = type => {
return list.concat(dir);
}, [])
)
.then(possibleCanvasPlugins => {
// Check how many are directories. If lstat fails it doesn't exist anyway.
return Promise.all(
// An array
possibleCanvasPlugins.map(pluginPath => isDirectory(pluginPath))
).then(isDirectory => possibleCanvasPlugins.filter((pluginPath, i) => isDirectory[i]));
})
.then(possibleCanvasPlugins => filterDirectories(possibleCanvasPlugins, { exclude: false }))
.then(canvasPluginDirectories => {
return Promise.all(
canvasPluginDirectories.map(dir =>
// Get the full path of all files in the directory
readdir(dir).then(files => files.map(file => path.resolve(dir, file)))
)
).then(flatten);
)
.then(flatten)
.then(files => filterDirectories(files, { exclude: true }));
});
};

View file

@ -2487,17 +2487,17 @@ main {
* 1. Make seamless transition from ToolBar to Table header and contained Menu.
* 1. Make seamless transition from Table to ToolBarFooter header.
*/
.kuiControlledTable .kuiTable {
border-top: none;
/* 1 */ }
.kuiControlledTable .kuiToolBarFooter {
border-top: none;
/* 2 */ }
.kuiControlledTable .kuiMenu--contained {
border-top: none;
/* 1 */ }
.kuiControlledTable {
background: #FFF; }
.kuiControlledTable .kuiTable {
border-top: none;
/* 1 */ }
.kuiControlledTable .kuiToolBarFooter {
border-top: none;
/* 2 */ }
.kuiControlledTable .kuiMenu--contained {
border-top: none;
/* 1 */ }
/**
* 1. Prevent cells from expanding based on content size. This substitutes for table-layout: fixed.

View file

@ -3,6 +3,7 @@
* 1. Make seamless transition from Table to ToolBarFooter header.
*/
.kuiControlledTable {
background: $tableBackgroundColor;
.kuiTable {
border-top: none; /* 1 */
}

View file

@ -4,7 +4,6 @@
padding: 10px;
height: 40px;
background-color: #ffffff;
border: $kuiBorderThin;
}
.kuiToolBarFooterSection {

View file

@ -195,27 +195,21 @@ export const CleanExtraBrowsersTask = {
async run(config, log, build) {
const getBrowserPathsForPlatform = platform => {
const reportingDir = 'node_modules/x-pack/plugins/reporting';
const phantomDir = '.phantom';
const chromiumDir = '.chromium';
const phantomPath = p =>
build.resolvePathForPlatform(platform, reportingDir, phantomDir, p);
const chromiumPath = p =>
build.resolvePathForPlatform(platform, reportingDir, chromiumDir, p);
return platforms => {
const paths = [];
if (platforms.windows) {
paths.push(phantomPath('phantomjs-*-windows.zip'));
paths.push(chromiumPath('chromium-*-win32.zip'));
paths.push(chromiumPath('chromium-*-windows.zip'));
}
if (platforms.darwin) {
paths.push(phantomPath('phantomjs-*-macosx.zip'));
paths.push(chromiumPath('chromium-*-darwin.zip'));
}
if (platforms.linux) {
paths.push(phantomPath('phantomjs-*-linux-x86_64.tar.bz2'));
paths.push(chromiumPath('chromium-*-linux.zip'));
}
return paths;

View file

@ -48,6 +48,10 @@ export class File {
return this.ext === '.ts' || this.ext === '.tsx';
}
public isSass() {
return this.ext === '.sass' || this.ext === '.scss';
}
public isFixture() {
return this.relativePath.split(sep).includes('__fixtures__');
}

View file

@ -20,6 +20,7 @@
import { run, combineErrors } from './run';
import * as Eslint from './eslint';
import * as Tslint from './tslint';
import * as Sasslint from './sasslint';
import { getFilesForCommit, checkFileCasing } from './precommit_hook';
run(async ({ log }) => {
@ -32,7 +33,7 @@ run(async ({ log }) => {
errors.push(error);
}
for (const Linter of [Eslint, Tslint]) {
for (const Linter of [Eslint, Tslint, Sasslint]) {
const filesToLint = Linter.pickFilesToLint(log, files);
if (filesToLint.length > 0) {
try {

View file

@ -17,10 +17,5 @@
* under the License.
*/
import chrome from 'ui/chrome';
const apiPrefix = chrome.addBasePath('/api/kibana');
export async function getRemoteClusters($http) {
const response = await $http.get(`${apiPrefix}/clusters`);
return response.data;
}
export { pickFilesToLint } from './pick_files_to_lint';
export { lintFiles } from './lint_files';

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import sassLint from 'sass-lint';
import path from 'path';
import { createFailError } from '../run';
/**
* Lints a list of files with eslint. eslint reports are written to the log
* and a FailError is thrown when linting errors occur.
*
* @param {ToolingLog} log
* @param {Array<File>} files
* @return {undefined}
*/
export function lintFiles(log, files) {
const paths = files.map(file => file.getRelativePath());
const report = sassLint.lintFiles(
paths.join(', '),
{},
path.resolve(__dirname, '..', '..', '..', '.sass-lint.yml')
);
const failTypes = Object.keys(
report.reduce(
(failTypes, reportEntry) => {
if (reportEntry.warningCount > 0) failTypes.warning = true;
if (reportEntry.errorCount > 0) failTypes.errors = true;
return failTypes;
},
{}
)
);
if (!failTypes.length) {
log.success('[sasslint] %d files linted successfully', files.length);
return;
}
log.error(sassLint.format(report));
throw createFailError(`[sasslint] ${failTypes.join(' & ')}`, 1);
}

View file

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import fs from 'fs';
import { safeLoad } from 'js-yaml';
import { makeRe } from 'minimatch';
import path from 'path';
// load the include globs from .sass-lint.yml and convert them to regular expressions for filtering files
const sassLintPath = path.resolve(__dirname, '..', '..', '..', '.sass-lint.yml');
const sassLintConfig = safeLoad(fs.readFileSync(sassLintPath));
const { files: { include: includeGlobs } } = sassLintConfig;
const includeRegex = includeGlobs.map(glob => makeRe(glob));
function matchesInclude(file) {
for (let i = 0; i < includeRegex.length; i++) {
if (includeRegex[i].test(file.relativePath)) {
return true;
}
}
return false;
}
export function pickFilesToLint(log, files) {
return files
.filter(file => file.isSass())
.filter(matchesInclude);
}

View file

@ -48,8 +48,7 @@ const buildUiExports = _.once(async () => {
* Deletes all indices that start with `.kibana`
*/
export async function deleteKibanaIndices({ client, stats, log }) {
const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' });
const indexNames = kibanaIndices.map(x => x.index);
const indexNames = await fetchKibanaIndices(client);
if (!indexNames.length) {
return;
}
@ -152,6 +151,20 @@ export async function createDefaultSpace({ index, client }) {
});
}
/**
* Migrations mean that the Kibana index will look something like:
* .kibana, .kibana_1, .kibana_323, etc. This finds all indices starting
* with .kibana, then filters out any that aren't actually Kibana's core
* index (e.g. we don't want to remove .kibana_task_manager or the like).
*
* @param {string} index
*/
async function fetchKibanaIndices(client) {
const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' });
const isKibanaIndex = (index) => (/^\.kibana(:?_\d*)?$/).test(index);
return kibanaIndices.map(x => x.index).filter(isKibanaIndex);
}
export async function cleanKibanaIndices({ client, stats, log, kibanaUrl }) {
if (!await isSpacesEnabled({ kibanaUrl })) {
return await deleteKibanaIndices({

View file

@ -75,7 +75,6 @@ describe('server createHandlers', () => {
it('provides helper methods and properties', () => {
expect(handlers).to.have.property('environment', 'server');
expect(handlers).to.have.property('serverUri');
expect(handlers).to.have.property('httpHeaders', mockRequest.headers);
expect(handlers).to.have.property('elasticsearchClient');
});

View file

@ -27,14 +27,10 @@ export const createHandlers = (request, server) => {
return {
environment: 'server',
// TODO: https://github.com/elastic/kibana/issues/27437 - A temporary measure to allow the timelion data source to negotiate secure connections to the Kibana server, to be removed by 6.7
// See https://github.com/elastic/kibana/pull/26809 and https://github.com/elastic/kibana/issues/26812
__dangerouslyUnsupportedSslConfig: server.config().get('server.ssl'),
serverUri:
config.has('server.rewriteBasePath') && config.get('server.rewriteBasePath')
? `${server.info.uri}${config.get('server.basePath')}`
: server.info.uri,
httpHeaders: request.headers,
elasticsearchClient: async (...args) => {
// check if the session is valid because continuing to use it
if (isSecurityEnabled(server)) {

View file

@ -31,7 +31,6 @@ import { managementApi } from './server/routes/api/management';
import { scriptsApi } from './server/routes/api/scripts';
import { registerSuggestionsApi } from './server/routes/api/suggestions';
import { registerKqlTelemetryApi } from './server/routes/api/kql_telemetry';
import { registerClustersRoute } from './server/routes/api/remote_info';
import { registerFieldFormats } from './server/field_formats/register';
import { registerTutorials } from './server/tutorials/register';
import * as systemApi from './server/lib/system_api';
@ -190,7 +189,6 @@ export default function (kibana) {
registerFieldFormats(server);
registerTutorials(server);
makeKQLUsageCollector(server);
registerClustersRoute(server);
server.expose('systemApi', systemApi);
server.expose('handleEsError', handleEsError);
server.injectUiAppVars('kibana', () => injectVars(server));

View file

@ -11,14 +11,14 @@ kbn-management-objects-view {
}
// SASSTODO: Remove when Kibana has a proper background color
.tab-account, .tab-management {
background-color: $euiColorEmptyShade;
kbn-management-objects, kbn-management-app, .tab-management {
background: $euiColorLightestShade;
flex-grow: 1;
}
// SASSTODO: Remove when Kibana has a proper background color
kbn-management-objects, kbn-management-app {
background: $euiColorLightestShade;
min-height: 100vh;
#management-landing {
display: flex;
flex-grow: 1;
}
.kbn-management-tab:first-letter {

View file

@ -1,42 +1,4 @@
<div class="app-container">
<!-- Local nav. -->
<kbn-top-nav name="management-subnav" data-test-subj="managementNav">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Breadcrumbs. -->
<bread-crumbs
data-transclude-slot="topLeftCorner"
omit-current-page="true"
use-links="true"
omit-pages="omitPages"
page-title="pageTitle"
></bread-crumbs>
<!-- Tabs. -->
<div data-transclude-slot="bottomRow" class="kuiLocalTabs" role="tablist" ng-show="!sectionName || section.visibleItems.length > 0">
<h2 class="kuiLocalTab" ng-if="!sectionName" id="tabHeader" tabindex="0" role="tab">
{{::section.display}}
</h2>
<a
role="tab"
ng-if="sectionName"
ng-repeat="item in section.visibleItems"
class="kuiLocalTab"
ng-class="{ 'kuiLocalTab-isSelected': item.active, 'kuiLocalTab-isDisabled': !item.active && (item.disabled || !item.url) }"
kbn-href="{{::item.disabled ? '' : item.url}}"
data-test-subj="{{::item.name}}"
tooltip="{{::item.tooltip}}"
tooltip-placement="bottom"
tooltip-popup-delay="400"
tooltip-append-to-body="1"
aria-selected="{{!!item.active}}"
aria-disabled="{{!!item.disabled}}"
>
{{::item.display}}
</a>
</div>
</div>
</kbn-top-nav>
<main class="management-container" ng-transclude></main>
</div>
<div class="app-container euiPage">
<div id="management-sidenav" class="euiPageSideBar" style="position: static;"></div>
<main class="management-container euiPageBody euiPageBody--restrictWidth-default" ng-transclude></main>
</div>

View file

@ -17,6 +17,10 @@
* under the License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
import './sections';
import 'ui/filters/start_from';
import 'ui/field_editor';
@ -24,11 +28,15 @@ import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import appTemplate from './app.html';
import landingTemplate from './landing.html';
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { management, SidebarNav, MANAGEMENT_BREADCRUMB } from 'ui/management';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { timefilter } from 'ui/timefilter';
import { EuiPageContent, EuiTitle, EuiText, EuiSpacer, EuiIcon, EuiHorizontalRule } from '@elastic/eui';
import 'ui/kbn_top_nav';
const SIDENAV_ID = 'management-sidenav';
const LANDING_ID = 'management-landing';
uiRoutes
.when('/management', {
template: landingTemplate,
@ -46,6 +54,78 @@ require('ui/index_patterns/route_setup/load_default')({
whenMissingRedirectTo: '/management/kibana/index'
});
export function updateLandingPage(version) {
const node = document.getElementById(LANDING_ID);
if (!node) {
return;
}
render(
<EuiPageContent verticalPosition="center" horizontalPosition="center">
<I18nProvider>
<div>
<div className="eui-textCenter">
<EuiIcon type="managementApp" size="xxl" />
<EuiSpacer />
<EuiTitle>
<h1>
<FormattedMessage
id="kbn.management.landing.header"
defaultMessage="Kibana {version} management"
values={{ version }}
/>
</h1>
</EuiTitle>
<EuiText>
<FormattedMessage
id="kbn.management.landing.subhead"
defaultMessage="Manage your indices, index patterns, saved objects, Kibana settings, and more."
/>
</EuiText>
</div>
<EuiHorizontalRule />
<EuiText color="subdued" size="s" textAlign="center">
<p>
<FormattedMessage
id="kbn.management.landing.text"
defaultMessage="A full list of tools can be found in the left menu"
/>
</p>
</EuiText>
</div>
</I18nProvider>
</EuiPageContent>,
node,
);
}
export function updateSidebar(
items, id
) {
const node = document.getElementById(SIDENAV_ID);
if (!node) {
return;
}
render(
<I18nProvider>
<SidebarNav
sections={items}
selectedId={id}
style={{ width: 192 }}
/>
</I18nProvider>,
node,
);
}
export const destroyReact = id => {
const node = document.getElementById(id);
node && unmountComponentAtNode(node);
};
uiModules
.get('apps/management')
.directive('kbnManagementApp', function (Private, $location) {
@ -70,6 +150,13 @@ uiModules
item.active = `#${$location.path()}`.indexOf(item.url) > -1;
});
}
updateSidebar($scope.sections, $scope.section.id);
$scope.$on('$destroy', () => destroyReact(SIDENAV_ID));
management.addListener(() => updateSidebar(management.items.inOrder, $scope.section.id));
updateLandingPage($scope.$root.chrome.getKibanaVersion());
$scope.$on('$destroy', () => destroyReact(LANDING_ID));
}
};
});

View file

@ -1,50 +1,3 @@
<kbn-management-app>
<kbn-management-landing>
<!-- General info -->
<div class="page-row">
<div class="page-row-text">Version: {{::kbnVersion}}</div>
</div>
<!-- Management sections for the ES stack -->
<div
ng-if="section.visibleItems.length > 0"
ng-repeat="section in sections"
class="page-row"
>
<div class="kuiPanel mgtPanel" role="group">
<div class="kuiPanelHeader">
<div class="kuiPanelHeaderSection">
<icon type="'{{::section.icon}}'" size="'l'"></icon>
<h3 class="kuiPanelHeader__title">
{{::section.display}}
</h3>
</div>
</div>
<div class="kuiPanelBody mgtPanel__body">
<div class="row">
<ul class="list-unstyled">
<li
class="col-xs-4 col-md-3 mgtPanel__item"
ng-repeat="item in section.visibleItems"
>
<a
data-test-subj="{{::item.id}}"
class="euiLink euiLink--primary mgtPanel__link"
ng-class="{ 'mgtPanel__link--disabled': item.disabled || !item.url }"
kbn-href="{{::item.disabled ? '' : item.url}}"
tooltip="{{::item.tooltip}}"
tooltip-placement="bottom"
tooltip-popup-delay="400"
tooltip-append-to-body="1"
>
{{::item.display}}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</kbn-management-landing>
<div id="management-landing"></div>
</kbn-management-app>

View file

@ -1,4 +1,4 @@
<kbn-management-app section="kibana">
<kbn-management-app section="kibana/indices">
<kbn-management-indices>
<div id="createIndexPatternReact"></div>
</kbn-management-indices>

View file

@ -1,131 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyState should render normally 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
responsive={true}
wrap={false}
<div>
<EuiCallOut
color="warning"
size="m"
title={
<FormattedMessage
defaultMessage="Couldn't find any Elasticsearch data"
id="kbn.management.createIndexPattern.emptyStateHeader"
values={Object {}}
/>
}
>
<EuiFlexItem
component="div"
grow={false}
<p>
<FormattedMessage
defaultMessage="{needToIndex} {learnHowLink} or {getStartedLink}"
id="kbn.management.createIndexPattern.emptyStateLabel.emptyStateDetail"
values={
Object {
"getStartedLink": <EuiLink
color="primary"
href="#/home/tutorial_directory/sampleData"
type="button"
>
<FormattedMessage
defaultMessage="get started with some sample data sets."
id="kbn.management.createIndexPattern.emptyStateLabel.getStartedLink"
values={Object {}}
/>
</EuiLink>,
"learnHowLink": <EuiLink
color="primary"
href="#/home/tutorial_directory"
type="button"
>
<FormattedMessage
defaultMessage="Learn how"
id="kbn.management.createIndexPattern.emptyStateLabel.learnHowLink"
values={Object {}}
/>
</EuiLink>,
"needToIndex": <EuiTextColor
color="subdued"
component="span"
>
<FormattedMessage
defaultMessage="You'll need to index some data into Elasticsearch before you can create an index pattern."
id="kbn.management.createIndexPattern.emptyStateLabel.needToIndexLabel"
values={Object {}}
/>
</EuiTextColor>,
}
}
/>
</p>
<EuiButton
color="warning"
data-test-subj="refreshIndicesButton"
fill={false}
iconSide="left"
iconType="refresh"
onClick={[Function]}
type="button"
>
<EuiTitle
size="m"
textTransform="none"
>
<EuiTextColor
color="subdued"
component="span"
>
<h2
style={
Object {
"textAlign": "center",
}
}
>
<FormattedMessage
defaultMessage="Couldn't find any Elasticsearch data"
id="kbn.management.createIndexPattern.emptyStateHeader"
values={Object {}}
/>
</h2>
</EuiTextColor>
</EuiTitle>
<EuiSpacer
size="s"
<FormattedMessage
defaultMessage="Check for new data"
id="kbn.management.createIndexPattern.emptyState.checkDataButton"
values={Object {}}
/>
<EuiText
grow={true}
size="m"
>
<p>
<FormattedMessage
defaultMessage="{needToIndex} {learnHowLink} or {getStartedLink}"
id="kbn.management.createIndexPattern.emptyStateLabel.emptyStateDetail"
values={
Object {
"getStartedLink": <EuiLink
color="primary"
href="#/home/tutorial_directory/sampleData"
type="button"
>
<FormattedMessage
defaultMessage="get started with some sample data sets."
id="kbn.management.createIndexPattern.emptyStateLabel.getStartedLink"
values={Object {}}
/>
</EuiLink>,
"learnHowLink": <EuiLink
color="primary"
href="#/home/tutorial_directory"
type="button"
>
<FormattedMessage
defaultMessage="Learn how"
id="kbn.management.createIndexPattern.emptyStateLabel.learnHowLink"
values={Object {}}
/>
</EuiLink>,
"needToIndex": <EuiTextColor
color="subdued"
component="span"
>
<FormattedMessage
defaultMessage="You'll need to index some data into Elasticsearch before you can create an index pattern."
id="kbn.management.createIndexPattern.emptyStateLabel.needToIndexLabel"
values={Object {}}
/>
</EuiTextColor>,
}
}
/>
</p>
</EuiText>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="refreshIndicesButton"
fill={false}
iconSide="left"
iconType="refresh"
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Check for new data"
id="kbn.management.createIndexPattern.emptyState.checkDataButton"
values={Object {}}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiButton>
</EuiCallOut>
</div>
`;

View file

@ -21,13 +21,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiText,
EuiCallOut,
EuiTextColor,
EuiSpacer,
EuiLink,
EuiButton,
} from '@elastic/eui';
@ -37,74 +32,63 @@ import { FormattedMessage } from '@kbn/i18n/react';
export const EmptyState = ({
onRefresh,
}) => (
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle>
<EuiTextColor color="subdued">
<h2 style={{ textAlign: 'center' }}>
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateHeader"
defaultMessage="Couldn't find any Elasticsearch data"
/>
</h2>
</EuiTextColor>
</EuiTitle>
<EuiSpacer size="s"/>
<EuiText>
<p>
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.emptyStateDetail"
defaultMessage="{needToIndex} {learnHowLink} or {getStartedLink}"
values={{
needToIndex: (
<EuiTextColor color="subdued">
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.needToIndexLabel"
defaultMessage="You'll need to index some data into Elasticsearch before you can create an index pattern."
/>
</EuiTextColor>
),
learnHowLink: (
<EuiLink href="#/home/tutorial_directory">
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.learnHowLink"
defaultMessage="Learn how"
/>
</EuiLink>
),
getStartedLink: (
<EuiLink href="#/home/tutorial_directory/sampleData">
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.getStartedLink"
defaultMessage="get started with some sample data sets."
/>
</EuiLink>
)
}}
/>
</p>
</EuiText>
<div>
<EuiCallOut
color="warning"
title={
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateHeader"
defaultMessage="Couldn't find any Elasticsearch data"
/>
}
>
<p>
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.emptyStateDetail"
defaultMessage="{needToIndex} {learnHowLink} or {getStartedLink}"
values={{
needToIndex: (
<EuiTextColor color="subdued">
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.needToIndexLabel"
defaultMessage="You'll need to index some data into Elasticsearch before you can create an index pattern."
/>
</EuiTextColor>
),
learnHowLink: (
<EuiLink href="#/home/tutorial_directory">
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.learnHowLink"
defaultMessage="Learn how"
/>
</EuiLink>
),
getStartedLink: (
<EuiLink href="#/home/tutorial_directory/sampleData">
<FormattedMessage
id="kbn.management.createIndexPattern.emptyStateLabel.getStartedLink"
defaultMessage="get started with some sample data sets."
/>
</EuiLink>
)
}}
/>
</p>
<EuiSpacer size="m"/>
<EuiButton
iconType="refresh"
onClick={onRefresh}
data-test-subj="refreshIndicesButton"
color="warning"
>
<FormattedMessage
id="kbn.management.createIndexPattern.emptyState.checkDataButton"
defaultMessage="Check for new data"
/>
</EuiButton>
</EuiCallOut>
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
iconType="refresh"
onClick={onRefresh}
data-test-subj="refreshIndicesButton"
>
<FormattedMessage
id="kbn.management.createIndexPattern.emptyState.checkDataButton"
defaultMessage="Check for new data"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</div>
);
EmptyState.propTypes = {

View file

@ -1,56 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LoadingState should render normally 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
<EuiFlexGroup
alignItems="center"
component="div"
direction="column"
gutterSize="s"
justifyContent="center"
responsive={true}
wrap={false}
>
<EuiFlexGroup
alignItems="center"
<EuiFlexItem
component="div"
direction="column"
gutterSize="s"
justifyContent="center"
responsive={true}
wrap={false}
grow={false}
>
<EuiFlexItem
component="div"
grow={false}
<EuiTitle
size="s"
textTransform="none"
>
<EuiTitle
size="s"
textTransform="none"
<h2
style={
Object {
"textAlign": "center",
}
}
>
<EuiTextColor
color="subdued"
component="span"
>
<h2
style={
Object {
"textAlign": "center",
}
}
>
<FormattedMessage
defaultMessage="Checking for Elasticsearch data"
id="kbn.management.createIndexPattern.loadingState.checkingLabel"
values={Object {}}
/>
</h2>
</EuiTextColor>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiLoadingSpinner
size="l"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<FormattedMessage
defaultMessage="Checking for Elasticsearch data"
id="kbn.management.createIndexPattern.loadingState.checkingLabel"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiLoadingSpinner
size="l"
/>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -23,32 +23,26 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiPanel,
EuiTextColor,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const LoadingState = () => (
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="center" alignItems="center" direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<EuiTextColor color="subdued">
<h2 style={{ textAlign: 'center' }}>
<FormattedMessage
id="kbn.management.createIndexPattern.loadingState.checkingLabel"
defaultMessage="Checking for Elasticsearch data"
/>
</h2>
</EuiTextColor>
</EuiTitle>
</EuiFlexItem>
<EuiFlexGroup justifyContent="center" alignItems="center" direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2 style={{ textAlign: 'center' }}>
<FormattedMessage
id="kbn.management.createIndexPattern.loadingState.checkingLabel"
defaultMessage="Checking for Elasticsearch data"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l"/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l"/>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -35,7 +35,6 @@ import { MAX_SEARCH_SIZE } from './constants';
import {
ensureMinimumTime,
getIndices,
getRemoteClusters
} from './lib';
export class CreateIndexPatternWizard extends Component {
@ -104,17 +103,18 @@ export class CreateIndexPatternWizard extends Component {
defaultMessage="Failed to load remote clusters"
/>);
const [allIndices, remoteClusters] = await ensureMinimumTime([
this.catchAndWarn(getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), [], indicesFailMsg),
this.catchAndWarn(getRemoteClusters(services.$http), [], clustersFailMsg)
]);
// query local and remote indices, updating state independently
ensureMinimumTime(
this.catchAndWarn(
getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE), [], indicesFailMsg)
).then(allIndices => this.setState({ allIndices, isInitiallyLoadingIndices: false }));
this.setState({
allIndices,
isInitiallyLoadingIndices: false,
remoteClustersExist: remoteClusters.length !== 0
});
}
this.catchAndWarn(
// if we get an error from remote cluster query, supply fallback value that allows user entry.
// ['a'] is fallback value
getIndices(services.es, this.indexPatternCreationType, `*:*`, 1), ['a'], clustersFailMsg
).then(remoteIndices => this.setState({ remoteClustersExist: !!remoteIndices.length }));
};
createIndexPattern = async (timeFieldName, indexPatternId) => {
const { services } = this.props;

View file

@ -46,6 +46,7 @@ export async function getIndices(es, indexPatternCreationType, rawPattern, limit
}
const params = {
ignoreUnavailable: true,
index: pattern,
ignore: [404],
body: {

View file

@ -28,5 +28,3 @@ export { getMatchedIndices } from './get_matched_indices';
export { containsIllegalCharacters } from './contains_illegal_characters';
export { extractTimeFields } from './extract_time_fields';
export { getRemoteClusters } from './get_remote_clusters';

View file

@ -1,4 +1,4 @@
<kbn-management-app section="kibana">
<kbn-management-app section="kibana/indices">
<kbn-management-indices>
<div class="kuiViewContent">
<kbn-management-index-header

View file

@ -1,4 +1,4 @@
<kbn-management-app section="kibana" omit-breadcrumb-pages="['indices']">
<kbn-management-app section="kibana/indices" omit-breadcrumb-pages="['indices']">
<kbn-management-indices>
<div
ng-controller="managementIndicesEdit"
@ -14,32 +14,49 @@
delete="removePattern()"
></kbn-management-index-header>
<p class="kuiText kuiVerticalRhythm" ng-if="::(indexPattern.timeFieldName || (indexPattern.tags && indexPattern.tags.length))">
<div class="euiSpacer euiSpacer--s"></div>
<p ng-if="::(indexPattern.timeFieldName || (indexPattern.tags && indexPattern.tags.length))">
<span ng-if="::indexPattern.timeFieldName">
<span class="label label-success">
<span class="kuiIcon fa-clock-o"></span>
<span i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
<span class="euiBadge euiBadge--warning">
<span class="euiBadge__content">
<span class="euiBadge__text">
<span
i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
i18n-default-message="Time Filter field name: {timeFieldName}"
i18n-values="{ timeFieldName: indexPattern.timeFieldName }"></span>
i18n-values="{ timeFieldName: indexPattern.timeFieldName }">
</span>
</span>
</span>
</span>
&nbsp;
</span>
<span ng-repeat="tag in indexPattern.tags">
<span class="label label-info">{{tag.name}}</span>
<span class="euiBadge euiBadge--hollow">
<span class="euiBadge__content">
<span class="euiBadge__text">
{{tag.name}}
</span>
</span>
</span>
</span>
</p>
<div class="euiSpacer euiSpacer--m"></div>
<p class="kuiText kuiVerticalRhythm">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"
i18n-values="{ html_indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
<a target="_window" class="euiLink euiLink--primary" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink"
i18n-default-message="Mapping API"></span>
<i aria-hidden="true" class="fa-link fa"></i>
</a>
</p>
<div class="euiText">
<p>
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"
i18n-values="{ html_indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
<a target="_window" class="euiLink euiLink--primary" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink"
i18n-default-message="Mapping API"></span>
<i aria-hidden="true" class="fa-link fa"></i>
</a>
</p>
</div>
<div class="euiSpacer euiSpacer--m"></div>
<!-- Alerts -->
<div

View file

@ -2,7 +2,7 @@
<div class="kuiBarSection">
<!-- Index pattern name -->
<h1
class="kuiTitle kuiVerticalRhythm"
class="euiTitle euiTitle--medium"
data-test-subj="indexPatternTitle"
>
<span

View file

@ -1,13 +1,11 @@
<div class="euiPage">
<div class="col-md-2 sidebar-container" role="region" aria-label="{{::'kbn.management.editIndexPatternAria' | i18n: { defaultMessage: 'Index patterns' } }}">
<div class="sidebar-list">
<div id="indexPatternListReact" role="region" aria-label="{{'kbn.management.editIndexPatternLiveRegionAriaLabel' | i18n: { defaultMessage: 'Index patterns' } }}"></div>
</div>
</div>
<div class="col-md-10">
<div class="euiPanel euiPanel--paddingLarge">
<div ng-transclude></div>
</div>
<div class="col-md-2 sidebar-container" role="region" aria-label="{{::'kbn.management.editIndexPatternAria' | i18n: { defaultMessage: 'Index patterns' } }}">
<div class="sidebar-list">
<div id="indexPatternListReact" role="region" aria-label="{{'kbn.management.editIndexPatternLiveRegionAriaLabel' | i18n: { defaultMessage: 'Index patterns' } }}"></div>
</div>
</div>
<div class="col-md-10">
<div class="euiPanel euiPanel--paddingLarge">
<div ng-transclude></div>
</div>
</div>

View file

@ -24,7 +24,7 @@ import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import {
EuiButtonEmpty,
EuiBadge,
EuiCallOut,
EuiText,
EuiSpacer,
} from '@elastic/eui';
@ -105,17 +105,14 @@ class ListUi extends Component {
const { defaultIndex } = this.props;
return !defaultIndex ? (
<div className="indexPatternList__headerWrapper">
<EuiCallOut
color="warning"
size="s"
iconType="alert"
title={(
<EuiText size="xs" color="subdued">
<p>
<FormattedMessage
id="kbn.management.indexPatternList.noDefaultIndexPatternTitle"
defaultMessage="No default index pattern. You must select or create one to continue."
/>
)}
/>
</p>
</EuiText>
</div>
) : null;
}

View file

@ -1,4 +1,4 @@
<kbn-management-app section="kibana" class="kuiView">
<kbn-management-app section="kibana/objects">
<kbn-management-objects>
<div id="reactSavedObjectsTable"></div>
</kbn-management-objects>

View file

@ -1,4 +1,4 @@
<kbn-management-app section="kibana" class="kuiView">
<kbn-management-app section="kibana/objects" class="kuiView">
<kbn-management-objects-view class="kuiViewContent kuiViewContent--constrainedWidth">
<!-- Header -->
<div class="kuiViewContentItem kuiBar kuiVerticalRhythm">

View file

@ -171,106 +171,96 @@ exports[`ObjectsTable relationships should show the flyout 1`] = `
`;
exports[`ObjectsTable should render normally 1`] = `
<EuiPage
restrictWidth={false}
<EuiPageContent
horizontalPosition="center"
panelPaddingSize="l"
style={
Object {
"maxWidth": 1000,
}
}
verticalPosition="center"
>
<EuiPageBody
restrictWidth={false}
>
<EuiPageContent
horizontalPosition="center"
panelPaddingSize="l"
style={
<Header
filteredCount={4}
onExportAll={[Function]}
onImport={[Function]}
onRefresh={[Function]}
/>
<EuiSpacer
size="xs"
/>
<InjectIntl(TableUI)
filterOptions={
Array [
Object {
"marginBottom": 16,
"marginTop": 16,
"maxWidth": 1000,
}
"name": "index-pattern",
"value": "index-pattern",
"view": "index-pattern (0)",
},
Object {
"name": "visualization",
"value": "visualization",
"view": "visualization (0)",
},
Object {
"name": "dashboard",
"value": "dashboard",
"view": "dashboard (0)",
},
Object {
"name": "search",
"value": "search",
"view": "search (0)",
},
]
}
getEditUrl={[Function]}
goInApp={[Function]}
isSearching={false}
itemId="id"
items={
Array [
Object {
"icon": "indexPatternApp",
"id": "1",
"title": "MyIndexPattern*",
"type": "index-pattern",
},
Object {
"icon": "search",
"id": "2",
"title": "MySearch",
"type": "search",
},
Object {
"icon": "dashboardApp",
"id": "3",
"title": "MyDashboard",
"type": "dashboard",
},
Object {
"icon": "visualizeApp",
"id": "4",
"title": "MyViz",
"type": "visualization",
},
]
}
onDelete={[Function]}
onExport={[Function]}
onQueryChange={[Function]}
onShowRelationships={[Function]}
onTableChange={[Function]}
pageIndex={0}
pageSize={15}
selectedSavedObjects={Array []}
selectionConfig={
Object {
"onSelectionChange": [Function],
}
verticalPosition="center"
>
<Header
filteredCount={4}
onExportAll={[Function]}
onImport={[Function]}
onRefresh={[Function]}
/>
<EuiSpacer
size="xs"
/>
<InjectIntl(TableUI)
filterOptions={
Array [
Object {
"name": "index-pattern",
"value": "index-pattern",
"view": "index-pattern (0)",
},
Object {
"name": "visualization",
"value": "visualization",
"view": "visualization (0)",
},
Object {
"name": "dashboard",
"value": "dashboard",
"view": "dashboard (0)",
},
Object {
"name": "search",
"value": "search",
"view": "search (0)",
},
]
}
getEditUrl={[Function]}
goInApp={[Function]}
isSearching={false}
itemId="id"
items={
Array [
Object {
"icon": "indexPatternApp",
"id": "1",
"title": "MyIndexPattern*",
"type": "index-pattern",
},
Object {
"icon": "search",
"id": "2",
"title": "MySearch",
"type": "search",
},
Object {
"icon": "dashboardApp",
"id": "3",
"title": "MyDashboard",
"type": "dashboard",
},
Object {
"icon": "visualizeApp",
"id": "4",
"title": "MyViz",
"type": "visualization",
},
]
}
onDelete={[Function]}
onExport={[Function]}
onQueryChange={[Function]}
onShowRelationships={[Function]}
onTableChange={[Function]}
pageIndex={0}
pageSize={15}
selectedSavedObjects={Array []}
selectionConfig={
Object {
"onSelectionChange": [Function],
}
}
totalItemCount={4}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
}
totalItemCount={4}
/>
</EuiPageContent>
`;

View file

@ -37,9 +37,7 @@ import {
EUI_MODAL_CONFIRM_BUTTON,
EuiCheckboxGroup,
EuiToolTip,
EuiPage,
EuiPageContent,
EuiPageBody,
} from '@elastic/eui';
import {
retrieveAndExportDocs,
@ -593,47 +591,43 @@ class ObjectsTableUI extends Component {
}));
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent
verticalPosition="center"
horizontalPosition="center"
style={{ maxWidth: 1000, marginTop: 16, marginBottom: 16 }}
>
{this.renderFlyout()}
{this.renderRelationships()}
{this.renderDeleteConfirmModal()}
{this.renderExportAllOptionsModal()}
<Header
onExportAll={() =>
this.setState({ isShowingExportAllOptionsModal: true })
}
onImport={this.showImportFlyout}
onRefresh={this.refreshData}
filteredCount={filteredItemCount}
/>
<EuiSpacer size="xs" />
<Table
itemId={'id'}
selectionConfig={selectionConfig}
selectedSavedObjects={selectedSavedObjects}
onQueryChange={this.onQueryChange}
onTableChange={this.onTableChange}
filterOptions={filterOptions}
onExport={this.onExport}
onDelete={this.onDelete}
getEditUrl={this.props.getEditUrl}
goInApp={this.props.goInApp}
pageIndex={page}
pageSize={perPage}
items={savedObjects}
totalItemCount={filteredItemCount}
isSearching={isSearching}
onShowRelationships={this.onShowRelationships}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
<EuiPageContent
verticalPosition="center"
horizontalPosition="center"
style={{ maxWidth: 1000 }}
>
{this.renderFlyout()}
{this.renderRelationships()}
{this.renderDeleteConfirmModal()}
{this.renderExportAllOptionsModal()}
<Header
onExportAll={() =>
this.setState({ isShowingExportAllOptionsModal: true })
}
onImport={this.showImportFlyout}
onRefresh={this.refreshData}
filteredCount={filteredItemCount}
/>
<EuiSpacer size="xs" />
<Table
itemId={'id'}
selectionConfig={selectionConfig}
selectedSavedObjects={selectedSavedObjects}
onQueryChange={this.onQueryChange}
onTableChange={this.onTableChange}
filterOptions={filterOptions}
onExport={this.onExport}
onDelete={this.onDelete}
getEditUrl={this.props.getEditUrl}
goInApp={this.props.goInApp}
pageIndex={page}
pageSize={perPage}
items={savedObjects}
totalItemCount={filteredItemCount}
isSearching={isSearching}
onShowRelationships={this.onShowRelationships}
/>
</EuiPageContent>
);
}
}

View file

@ -2,498 +2,486 @@
exports[`AdvancedSettings should render normally 1`] = `
<I18nProvider>
<EuiPage
restrictWidth={true}
>
<div
className="mgtAdvancedSettings"
<div>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="none"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexGroup
alignItems="stretch"
<EuiFlexItem
component="div"
direction="row"
gutterSize="none"
justifyContent="flexStart"
responsive={true}
wrap={false}
grow={true}
>
<EuiFlexItem
component="div"
grow={true}
>
<advanced_settings_page_title />
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<Search
categories={
Array [
"general",
"elasticsearch",
]
}
onQueryChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [],
"_indexedClauses": Object {
"field": Object {},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "",
}
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<advanced_settings_page_subtitle />
<EuiSpacer
size="m"
/>
<CallOuts />
<EuiSpacer
size="m"
/>
<InjectIntl(FormUI)
categories={
Array [
"general",
"elasticsearch",
]
}
categoryCounts={
Object {
"elasticsearch": 2,
"general": 11,
<advanced_settings_page_title />
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<Search
categories={
Array [
"general",
"elasticsearch",
]
}
onQueryChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [],
"_indexedClauses": Object {
"field": Object {},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "",
}
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<advanced_settings_page_subtitle />
<EuiSpacer
size="m"
/>
<CallOuts />
<EuiSpacer
size="m"
/>
<InjectIntl(FormUI)
categories={
Array [
"general",
"elasticsearch",
]
}
categoryCounts={
Object {
"elasticsearch": 2,
"general": 11,
}
clear={[Function]}
clearQuery={[Function]}
save={[Function]}
settings={
Object {
"elasticsearch": Array [
Object {
"ariaName": "test array setting",
"category": Array [
"elasticsearch",
],
"defVal": Array [
"default_value",
],
"description": "Description for Test array setting",
"displayName": "Test array setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:array:setting",
"options": undefined,
"readonly": false,
"type": "array",
"value": undefined,
},
Object {
"ariaName": "test boolean setting",
"category": Array [
"elasticsearch",
],
"defVal": true,
"description": "Description for Test boolean setting",
"displayName": "Test boolean setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:boolean:setting",
"options": undefined,
"readonly": false,
"type": "boolean",
"value": undefined,
},
],
"general": Array [
Object {
"ariaName": "test customstring setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test custom string setting",
"displayName": "Test custom string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:customstring:setting",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
Object {
"ariaName": "test image setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test image setting",
"displayName": "Test image setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:image:setting",
"options": undefined,
"readonly": false,
"type": "image",
"value": undefined,
},
Object {
"ariaName": "test is overridden json",
"category": Array [
"general",
],
"defVal": "{
}
clear={[Function]}
clearQuery={[Function]}
save={[Function]}
settings={
Object {
"elasticsearch": Array [
Object {
"ariaName": "test array setting",
"category": Array [
"elasticsearch",
],
"defVal": Array [
"default_value",
],
"description": "Description for Test array setting",
"displayName": "Test array setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:array:setting",
"options": undefined,
"readonly": false,
"type": "array",
"value": undefined,
},
Object {
"ariaName": "test boolean setting",
"category": Array [
"elasticsearch",
],
"defVal": true,
"description": "Description for Test boolean setting",
"displayName": "Test boolean setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:boolean:setting",
"options": undefined,
"readonly": false,
"type": "boolean",
"value": undefined,
},
],
"general": Array [
Object {
"ariaName": "test customstring setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test custom string setting",
"displayName": "Test custom string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:customstring:setting",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
Object {
"ariaName": "test image setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test image setting",
"displayName": "Test image setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:image:setting",
"options": undefined,
"readonly": false,
"type": "image",
"value": undefined,
},
Object {
"ariaName": "test is overridden json",
"category": Array [
"general",
],
"defVal": "{
\\"foo\\": \\"bar\\"
}",
"description": "Description for overridden json",
"displayName": "An overridden json",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:json",
"options": undefined,
"readonly": false,
"type": "json",
"value": undefined,
},
Object {
"ariaName": "test is overridden number",
"category": Array [
"general",
],
"defVal": 1234,
"description": "Description for overridden number",
"displayName": "An overridden number",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:number",
"options": undefined,
"readonly": false,
"type": "number",
"value": undefined,
},
Object {
"ariaName": "test is overridden select",
"category": Array [
"general",
],
"defVal": "orange",
"description": "Description for overridden select setting",
"displayName": "Test overridden select setting",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:select",
"options": Array [
"apple",
"orange",
"banana",
],
"readonly": false,
"type": "select",
"value": undefined,
},
Object {
"ariaName": "test is overridden string",
"category": Array [
"general",
],
"defVal": "foo",
"description": "Description for overridden string",
"displayName": "An overridden string",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:string",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
Object {
"ariaName": "test json setting",
"category": Array [
"general",
],
"defVal": "{\\"foo\\": \\"bar\\"}",
"description": "Description for Test json setting",
"displayName": "Test json setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:json:setting",
"options": undefined,
"readonly": false,
"type": "json",
"value": undefined,
},
Object {
"ariaName": "test markdown setting",
"category": Array [
"general",
],
"defVal": "",
"description": "Description for Test markdown setting",
"displayName": "Test markdown setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:markdown:setting",
"options": undefined,
"readonly": false,
"type": "markdown",
"value": undefined,
},
Object {
"ariaName": "test number setting",
"category": Array [
"general",
],
"defVal": 5,
"description": "Description for Test number setting",
"displayName": "Test number setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:number:setting",
"options": undefined,
"readonly": false,
"type": "number",
"value": undefined,
},
Object {
"ariaName": "test select setting",
"category": Array [
"general",
],
"defVal": "orange",
"description": "Description for Test select setting",
"displayName": "Test select setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:select:setting",
"options": Array [
"apple",
"orange",
"banana",
],
"readonly": false,
"type": "select",
"value": undefined,
},
Object {
"ariaName": "test string setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:string:setting",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
],
}
}
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
onQueryMatchChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [],
"_indexedClauses": Object {
"field": Object {},
"is": Object {},
"term": Array [],
},
"description": "Description for overridden json",
"displayName": "An overridden json",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:json",
"options": undefined,
"readonly": false,
"type": "json",
"value": undefined,
},
"syntax": Object {
"parse": [Function],
"print": [Function],
Object {
"ariaName": "test is overridden number",
"category": Array [
"general",
],
"defVal": 1234,
"description": "Description for overridden number",
"displayName": "An overridden number",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:number",
"options": undefined,
"readonly": false,
"type": "number",
"value": undefined,
},
"text": "",
}
Object {
"ariaName": "test is overridden select",
"category": Array [
"general",
],
"defVal": "orange",
"description": "Description for overridden select setting",
"displayName": "Test overridden select setting",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:select",
"options": Array [
"apple",
"orange",
"banana",
],
"readonly": false,
"type": "select",
"value": undefined,
},
Object {
"ariaName": "test is overridden string",
"category": Array [
"general",
],
"defVal": "foo",
"description": "Description for overridden string",
"displayName": "An overridden string",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:string",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
Object {
"ariaName": "test json setting",
"category": Array [
"general",
],
"defVal": "{\\"foo\\": \\"bar\\"}",
"description": "Description for Test json setting",
"displayName": "Test json setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:json:setting",
"options": undefined,
"readonly": false,
"type": "json",
"value": undefined,
},
Object {
"ariaName": "test markdown setting",
"category": Array [
"general",
],
"defVal": "",
"description": "Description for Test markdown setting",
"displayName": "Test markdown setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:markdown:setting",
"options": undefined,
"readonly": false,
"type": "markdown",
"value": undefined,
},
Object {
"ariaName": "test number setting",
"category": Array [
"general",
],
"defVal": 5,
"description": "Description for Test number setting",
"displayName": "Test number setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:number:setting",
"options": undefined,
"readonly": false,
"type": "number",
"value": undefined,
},
Object {
"ariaName": "test select setting",
"category": Array [
"general",
],
"defVal": "orange",
"description": "Description for Test select setting",
"displayName": "Test select setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:select:setting",
"options": Array [
"apple",
"orange",
"banana",
],
"readonly": false,
"type": "select",
"value": undefined,
},
Object {
"ariaName": "test string setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:string:setting",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
],
}
/>
</div>
</EuiPage>
}
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
onQueryMatchChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [],
"_indexedClauses": Object {
"field": Object {},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "",
}
}
/>
</div>
</I18nProvider>
`;
exports[`AdvancedSettings should render specific setting if given setting key 1`] = `
<I18nProvider>
<EuiPage
restrictWidth={true}
>
<div
className="mgtAdvancedSettings"
<div>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="none"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexGroup
alignItems="stretch"
<EuiFlexItem
component="div"
direction="row"
gutterSize="none"
justifyContent="flexStart"
responsive={true}
wrap={false}
grow={true}
>
<EuiFlexItem
component="div"
grow={true}
>
<advanced_settings_page_title />
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<Search
categories={
Array [
"general",
"elasticsearch",
]
}
onQueryChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
"_indexedClauses": Object {
"field": Object {
"ariaName": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "ariaName:\\"test string setting\\"",
}
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<advanced_settings_page_subtitle />
<EuiSpacer
size="m"
/>
<CallOuts />
<EuiSpacer
size="m"
/>
<InjectIntl(FormUI)
categories={
Array [
"general",
"elasticsearch",
]
}
categoryCounts={
Object {
"elasticsearch": 2,
"general": 11,
<advanced_settings_page_title />
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<Search
categories={
Array [
"general",
"elasticsearch",
]
}
}
clear={[Function]}
clearQuery={[Function]}
save={[Function]}
settings={
Object {
"general": Array [
Object {
"ariaName": "test string setting",
"category": Array [
"general",
onQueryChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
"defVal": null,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:string:setting",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
"_indexedClauses": Object {
"field": Object {
"ariaName": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
},
"is": Object {},
"term": Array [],
},
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "ariaName:\\"test string setting\\"",
}
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<advanced_settings_page_subtitle />
<EuiSpacer
size="m"
/>
<CallOuts />
<EuiSpacer
size="m"
/>
<InjectIntl(FormUI)
categories={
Array [
"general",
"elasticsearch",
]
}
categoryCounts={
Object {
"elasticsearch": 2,
"general": 11,
}
}
clear={[Function]}
clearQuery={[Function]}
save={[Function]}
settings={
Object {
"general": Array [
Object {
"ariaName": "test string setting",
"category": Array [
"general",
],
"defVal": null,
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:string:setting",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
],
}
}
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
onQueryMatchChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
}
}
showNoResultsMessage={true}
/>
<advanced_settings_page_footer
onQueryMatchChange={[Function]}
query={
Query {
"ast": _AST {
"_clauses": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
"_indexedClauses": Object {
"field": Object {
"ariaName": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
},
"is": Object {},
"term": Array [],
"_indexedClauses": Object {
"field": Object {
"ariaName": Array [
Object {
"field": "ariaName",
"match": "must",
"operator": "eq",
"type": "field",
"value": "test string setting",
},
],
},
"is": Object {},
"term": Array [],
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "ariaName:\\"test string setting\\"",
}
},
"syntax": Object {
"parse": [Function],
"print": [Function],
},
"text": "ariaName:\\"test string setting\\"",
}
/>
</div>
</EuiPage>
}
/>
</div>
</I18nProvider>
`;

View file

@ -25,7 +25,6 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiPage,
Query,
} from '@elastic/eui';
@ -157,36 +156,34 @@ export class AdvancedSettings extends Component {
return (
<I18nProvider>
<EuiPage restrictWidth>
<div className="mgtAdvancedSettings">
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<PageTitle />
</EuiFlexItem>
<EuiFlexItem>
<Search
query={query}
categories={this.categories}
onQueryChange={this.onQueryChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
<PageSubtitle />
<EuiSpacer size="m" />
<CallOuts />
<EuiSpacer size="m" />
<Form
settings={filteredSettings}
categories={this.categories}
categoryCounts={this.categoryCounts}
clearQuery={this.clearQuery}
save={this.saveConfig}
clear={this.clearConfig}
showNoResultsMessage={!footerQueryMatched}
/>
<PageFooter query={query} onQueryMatchChange={this.onFooterQueryMatchChange} />
</div>
</EuiPage>
<div>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<PageTitle />
</EuiFlexItem>
<EuiFlexItem>
<Search
query={query}
categories={this.categories}
onQueryChange={this.onQueryChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
<PageSubtitle />
<EuiSpacer size="m" />
<CallOuts />
<EuiSpacer size="m" />
<Form
settings={filteredSettings}
categories={this.categories}
categoryCounts={this.categoryCounts}
clearQuery={this.clearQuery}
save={this.saveConfig}
clear={this.clearConfig}
showNoResultsMessage={!footerQueryMatched}
/>
<PageFooter query={query} onQueryMatchChange={this.onFooterQueryMatchChange} />
</div>
</I18nProvider>
);
}

View file

@ -1,19 +1,13 @@
.mgtAdvancedSettings {
padding: $euiSizeL;
background: $euiColorLightestShade;
min-height: calc(100vh - 70px);
.mgtAdvancedSettings__field {
+ * {
margin-top: $euiSize;
}
.mgtAdvancedSettings__field {
+ * {
margin-top: $euiSize;
}
&Wrapper {
width: 640px;
}
&Wrapper {
width: 640px;
}
&Actions {
padding-top: $euiSizeM;
}
&Actions {
padding-top: $euiSizeM;
}
}

View file

@ -1,4 +1,4 @@
<kbn-management-app section="kibana">
<kbn-management-app section="kibana/settings">
<kbn-management-advanced>
<div id="reactAdvancedSettings"></div>
</kbn-management-advanced>

View file

@ -92,8 +92,10 @@ const unescapeTemplateVars = url => {
const DEFAULT_LANGUAGE = 'en';
export class EMSClientV66 {
EMS_LOAD_TIMEOUT = 32000;
constructor({ kbnVersion, manifestServiceUrl, htmlSanitizer, language, landingPageUrl }) {
@ -103,7 +105,6 @@ export class EMSClientV66 {
this._sanitizer = htmlSanitizer ? htmlSanitizer : x => x;
this._manifestServiceUrl = manifestServiceUrl;
this._loadCatalogue = null;
this._loadFileLayers = null;
this._loadTMSServices = null;
this._emsLandingPageUrl = landingPageUrl;
@ -125,11 +126,39 @@ export class EMSClientV66 {
* this internal method is overridden by the tests to simulate custom manifest.
*/
async _getManifest(manifestUrl) {
const url = extendUrl(manifestUrl, { query: this._queryParams });
const result = await fetch(url);
return await result.json();
let result;
try {
const url = extendUrl(manifestUrl, { query: this._queryParams });
result = await this._fetchWithTimeout(url);
} catch (e) {
if (!e) {
e = new Error('Unknown error');
}
if (!(e instanceof Error)) {
e = new Error(e.data || `status ${e.statusText || e.status}`);
}
throw new Error(`Unable to retrieve manifest from ${manifestUrl}: ${e.message}`);
} finally {
return result
? await result.json()
: null;
}
}
_fetchWithTimeout(url) {
return new Promise(
(resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`Request to ${url} timed out`)),
this.EMS_LOAD_TIMEOUT
);
fetch(url)
.then(
response => resolve(response),
err => reject(err)
).finally(() => clearTimeout(timer));
});
}
/**
* Add optional query-parameters to all requests
@ -151,54 +180,42 @@ export class EMSClientV66 {
_invalidateSettings() {
this._loadCatalogue = _.once(async () => {
this._getManifestWithParams = _.once(
async url => this._getManifest(this.extendUrlWithParams(url)));
try {
const url = this.extendUrlWithParams(this._manifestServiceUrl);
return await this._getManifest(url);
} catch (e) {
if (!e) {
e = new Error('Unknown error');
}
if (!(e instanceof Error)) {
e = new Error(e.data || `status ${e.statusText || e.status}`);
}
throw new Error(`Could not retrieve manifest from the tile service: ${e.message}`);
this._getCatalogueService = async serviceType => {
const catalogueManifest = await this._getManifestWithParams(this._manifestServiceUrl);
let service;
if(_.has(catalogueManifest, 'services')) {
service = catalogueManifest.services
.find(s => s.type === serviceType);
}
});
return service || {};
};
this._wrapServiceAttribute = async (manifestUrl, attr, WrapperClass) => {
const manifest = await this._getManifest(manifestUrl);
if (_.has(manifest, attr)) {
return manifest[attr].map(config => {
return new WrapperClass(config, this);
});
}
return [];
};
this._loadFileLayers = _.once(async () => {
const catalogue = await this._loadCatalogue();
const fileService = catalogue.services.find(service => service.type === 'file');
if (!fileService) {
return [];
}
const manifest = await this._getManifest(fileService.manifest, this._queryParams);
return manifest.layers.map(layerConfig => {
return new FileLayer(layerConfig, this);
});
const fileService = await this._getCatalogueService('file');
return _.isEmpty(fileService)
? []
: this._wrapServiceAttribute(fileService.manifest, 'layers', FileLayer);
});
this._loadTMSServices = _.once(async () => {
const catalogue = await this._loadCatalogue();
const tmsService = catalogue.services.find((service) => service.type === 'tms');
if (!tmsService) {
return [];
}
const tmsManifest = await this._getManifest(tmsService.manifest, this._queryParams);
return tmsManifest.services.map(serviceConfig => {
return new TMSService(serviceConfig, this);
});
const tmsService = await this._getCatalogueService('tms');
return _.isEmpty(tmsService)
? []
: await this._wrapServiceAttribute(tmsService.manifest, 'services', TMSService);
});
}
getLandingPageUrl() {

View file

@ -143,6 +143,8 @@ export function BaseMapsVisualizationProvider(serviceSettings, i18n) {
async _updateBaseLayer() {
const DEFAULT_EMS_BASEMAP = 'road_map';
if (!this._kibanaMap) {
return;
}
@ -151,13 +153,11 @@ export function BaseMapsVisualizationProvider(serviceSettings, i18n) {
if (!this._tmsConfigured()) {
try {
const tmsServices = await serviceSettings.getTMSServices();
const firstRoadMapLayer = tmsServices.find((s) => {
return s.id === 'road_map';//first road map layer
});
const fallback = firstRoadMapLayer ? firstRoadMapLayer : tmsServices[0];
if (fallback) {
this._setTmsLayer(firstRoadMapLayer);
}
const userConfiguredTmsLayer = tmsServices[0];
const initBasemapLayer = userConfiguredTmsLayer
? userConfiguredTmsLayer
: tmsServices.find(s => s.id === DEFAULT_EMS_BASEMAP);
if (initBasemapLayer) { this._setTmsLayer(initBasemapLayer); }
} catch (e) {
toastNotifications.addWarning(e.message);
return;

View file

@ -23,7 +23,7 @@ module.exports = {
browsers: [
'last 2 versions',
'> 5%',
'Safari 7' // for PhantomJS support
'Safari 7' // for PhantomJS support: https://github.com/elastic/kibana/issues/27136
]
})
]

View file

@ -240,9 +240,8 @@ export default () => Joi.object({
includeElasticMapsService: Joi.boolean().default(true),
tilemap: tilemapSchema,
regionmap: regionmapSchema,
// manifestServiceUrl: Joi.string().default(' https://catalogue.maps.elastic.co/v2/manifest'),
manifestServiceUrl: Joi.string().default('https://catalogue-staging.maps.elastic.co/v6.6/manifest'),
emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v2'),
manifestServiceUrl: Joi.string().default('https://catalogue.maps.elastic.co/v6.6/manifest'),
emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v6.6'),
}).default(),
tilemap: tilemapSchema.notes('Deprecated'),
regionmap: regionmapSchema.notes('Deprecated'),

View file

@ -21,7 +21,6 @@ import path from 'path';
import { promisify } from 'util';
import fs from 'fs';
import sass from 'node-sass';
import sassLint from 'sass-lint';
import autoprefixer from 'autoprefixer';
import postcss from 'postcss';
@ -64,15 +63,6 @@ export class Build {
async build() {
const outFile = this.outputPath();
const lintResults = sassLint.lintFiles(this.getGlob(), {}, path.resolve(__dirname, '..', '..', '..', '.sass-lint.yml'));
lintResults.forEach(result => {
if (result.messages.length > 0) {
this.log.info(`lint errors in ${result.filePath}`);
this.log.info(JSON.stringify(result.messages, null, 2));
}
});
const rendered = await renderSass({
file: this.source,
outFile,

View file

@ -101,6 +101,15 @@ describe('ManagementSection', () => {
expect(threwException).to.be(true);
});
it('calls listener when item added', () => {
let listerCalled = false;
const listenerFn = () => { listerCalled = true; };
section.addListener(listenerFn);
section.register('about');
expect(listerCalled).to.be(true);
});
});
describe('deregister', () => {
@ -122,6 +131,14 @@ describe('ManagementSection', () => {
expect(section.items).to.have.length(0);
});
it('calls listener when item added', () => {
let listerCalled = false;
const listenerFn = () => { listerCalled = true; };
section.addListener(listenerFn);
section.deregister('about');
expect(listerCalled).to.be(true);
});
});
describe('getSection', () => {

View file

@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Management filters and filters and maps section objects into SidebarNav items 1`] = `
Array [
Object {
"data-test-subj": "activeSection",
"href": undefined,
"icon": null,
"id": "activeSection",
"isSelected": false,
"items": Array [
Object {
"data-test-subj": "item",
"href": undefined,
"icon": null,
"id": "item",
"isSelected": false,
"name": "item",
},
],
"name": "activeSection",
},
]
`;

View file

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

View file

@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexedArray } from '../../indexed_array';
import { sideNavItems } from '../components/sidebar_nav';
const toIndexedArray = (initialSet: any[]) =>
new IndexedArray({
index: ['id'],
order: ['order'],
initialSet,
});
const activeProps = { visible: true, disabled: false };
const disabledProps = { visible: true, disabled: true };
const notVisibleProps = { visible: false, disabled: false };
const visibleItem = { display: 'item', id: 'item', ...activeProps };
const notVisibleSection = {
display: 'Not visible',
id: 'not-visible',
items: toIndexedArray([visibleItem]),
...notVisibleProps,
};
const disabledSection = {
display: 'Disabled',
id: 'disabled',
items: toIndexedArray([visibleItem]),
...disabledProps,
};
const noItemsSection = {
display: 'No items',
id: 'no-items',
items: toIndexedArray([]),
...activeProps,
};
const noActiveItemsSection = {
display: 'No active items',
id: 'no-active-items',
items: toIndexedArray([
{ display: 'disabled', id: 'disabled', ...disabledProps },
{ display: 'notVisible', id: 'notVisible', ...notVisibleProps },
]),
...activeProps,
};
const activeSection = {
display: 'activeSection',
id: 'activeSection',
items: toIndexedArray([visibleItem]),
...activeProps,
};
const managementSections = [
notVisibleSection,
disabledSection,
noItemsSection,
noActiveItemsSection,
activeSection,
];
describe('Management', () => {
it('filters and filters and maps section objects into SidebarNav items', () => {
expect(sideNavItems(managementSections, 'active-item-id')).toMatchSnapshot();
});
});

View file

@ -0,0 +1,94 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EuiIcon, EuiSideNav, IconType } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { IndexedArray } from 'ui/indexed_array';
interface Subsection {
disabled: boolean;
visible: boolean;
id: string;
display: string;
url?: string;
icon?: IconType;
}
interface Section extends Subsection {
items: IndexedArray<Subsection>;
}
const sectionVisible = (section: Subsection) => !section.disabled && section.visible;
const sectionToNav = (selectedId: string) => ({ display, id, url, icon }: Subsection) => ({
id,
name: display,
icon: icon ? <EuiIcon type={icon} /> : null,
isSelected: selectedId === id,
href: url,
'data-test-subj': id,
});
export const sideNavItems = (sections: Section[], selectedId: string) =>
sections
.filter(sectionVisible)
.filter(section => section.items.filter(sectionVisible).length)
.map(section => ({
items: section.items.inOrder.filter(sectionVisible).map(sectionToNav(selectedId)),
...sectionToNav(selectedId)(section),
}));
interface SidebarNavProps {
sections: Section[];
selectedId: string;
}
interface SidebarNavState {
isSideNavOpenOnMobile: boolean;
}
export class SidebarNav extends React.Component<SidebarNavProps, SidebarNavState> {
constructor(props: SidebarNavProps) {
super(props);
this.state = {
isSideNavOpenOnMobile: false,
};
}
public render() {
return (
<EuiSideNav
mobileTitle={this.renderMobileTitle()}
isOpenOnMobile={this.state.isSideNavOpenOnMobile}
toggleOpenOnMobile={this.toggleOpenOnMobile}
items={sideNavItems(this.props.sections, this.props.selectedId)}
style={{ width: 192, paddingBottom: '16px' }}
/>
);
}
private renderMobileTitle() {
return <FormattedMessage id="common.ui.management.nav.menu" defaultMessage="Management menu" />;
}
private toggleOpenOnMobile = () => {
this.setState({
isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile,
});
};
}

View file

@ -17,29 +17,15 @@
* under the License.
*/
import { callWithRequestFactory } from './call_with_request_factory';
import handleEsError from '../../../lib/handle_es_error';
async function fetchRemoteClusters(callWithRequest) {
const options = {
method: 'GET',
path: '_remote/info'
};
const remoteInfo = await callWithRequest('transport.request', options);
return Object.keys(remoteInfo);
}
export function registerClustersRoute(server) {
server.route({
path: '/api/kibana/clusters',
method: 'GET',
handler: async request => {
const callWithRequest = callWithRequestFactory(server, request);
try {
return await fetchRemoteClusters(callWithRequest);
} catch (error) {
throw handleEsError(error);
}
}
});
declare module 'ui/management' {
export const PAGE_TITLE_COMPONENT: string;
export const PAGE_SUBTITLE_COMPONENT: string;
export const PAGE_FOOTER_COMPONENT: string;
export const SidebarNav: React.SFC<any>;
export function registerSettingsComponent(
id: string,
component: string | React.SFC<any>,
allowOverride: boolean
): void;
export const management: any; // TODO - properly provide types
}

View file

@ -27,4 +27,5 @@ export {
} from '../../../legacy/core_plugins/kibana/public/management/sections/settings/components/component_registry';
export { Field } from '../../../legacy/core_plugins/kibana/public/management/sections/settings/components/field/field';
export { management } from './sections_register';
export { SidebarNav } from './components';
export { MANAGEMENT_BREADCRUMB } from './breadcrumbs';

View file

@ -20,8 +20,9 @@
import { assign } from 'lodash';
import { IndexedArray } from '../indexed_array';
export class ManagementSection {
const listeners = [];
export class ManagementSection {
/**
* @param {string} id
* @param {object} options
@ -55,6 +56,16 @@ export class ManagementSection {
return this.items.inOrder.filter(item => item.visible);
}
/**
* Registers a callback that will be executed when management sections are updated
* Globally bound to solve for sidebar nav needs
*
* @param {function} fn
*/
addListener(fn) {
listeners.push(fn);
}
/**
* Registers a sub-section
*
@ -71,6 +82,7 @@ export class ManagementSection {
}
this.items.push(item);
listeners.forEach(fn => fn());
return item;
}
@ -82,6 +94,7 @@ export class ManagementSection {
*/
deregister(id) {
this.items.remove(item => item.id === id);
listeners.forEach(fn => fn(this.items));
}
/**

View file

@ -93,6 +93,7 @@ ObjDefine.prototype.create = function () {
// clone the object on serialization and choose which properties
// to include or trim manually. This is currently only in use in PhantomJS
// due to https://github.com/ariya/phantomjs/issues/11856
// TODO: remove this: https://github.com/elastic/kibana/issues/27136
self.obj.toJSON = function () {
return _.transform(self.obj, function (json, val, key) {
const desc = self.descs[key];

View file

@ -29,6 +29,10 @@ export {
validations,
} from './saved_object';
export {
taskDefinitions
} from './task_definitions';
export {
app,
apps,

View file

@ -17,15 +17,12 @@
* under the License.
*/
import { once } from 'lodash';
import { mergeAtType } from './reduce';
import { alias, wrap, uniqueKeys } from './modify_reduce';
const callWithRequest = once(server => {
const cluster = server.plugins.elasticsearch.getCluster('data');
return cluster.callWithRequest;
});
export const callWithRequestFactory = (server, request) => {
return (...args) => {
return callWithRequest(server)(request, ...args);
};
};
// How plugins define tasks that the task manager can run.
export const taskDefinitions = wrap(
alias('taskDefinitions'),
uniqueKeys(),
mergeAtType,
);

View file

@ -1,58 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { bgRed, white } from 'chalk';
import { execSync } from 'child_process';
import { createInterface } from 'readline';
export default function (grunt) {
grunt.registerTask('sterilize', function () {
const cmd = 'git clean -fdx';
const ignores = [
'.aws-config.json',
'config/kibana.dev.yml'
]
.concat(String(grunt.option('ignore') || '').split(','))
.map(f => `-e "${f.split('"').join('\\"')}"`)
.reduce((all, arg) => `${all} ${arg}`, '');
const stdio = 'inherit';
execSync(`${cmd} -n ${ignores}`, { stdio });
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
const danger = bgRed(white('DANGER'));
rl.on('close', this.async());
rl.question(`\n${danger} Do you really want to delete all of the above files?, [N/y] `, function (resp) {
const yes = resp.toLowerCase().trim()[0] === 'y';
rl.close();
if (yes) {
execSync(`${cmd} ${ignores}`, { stdio });
}
});
});
}

View file

@ -38,19 +38,18 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await find.clickByDisplayedLinkText(text);
}
async clickKibanaSettings() {
await find.clickByDisplayedLinkText('Advanced Settings');
await testSubjects.click('settings');
await PageObjects.header.waitUntilLoadingHasFinished();
// Verify navigation is successful.
await testSubjects.existOrFail('managementSettingsTitle');
}
async clickKibanaSavedObjects() {
await find.clickByDisplayedLinkText('Saved Objects');
await testSubjects.click('objects');
}
async clickKibanaIndices() {
log.debug('clickKibanaIndices link');
await find.clickByDisplayedLinkText('Index Patterns');
await testSubjects.click('indices');
}
async getAdvancedSettings(propertyName) {

View file

@ -64,7 +64,7 @@ export function FindProvider({ getService }) {
return await retry.try(async () => {
const element = await getElementFunction();
// Calling any method forces a staleness check
element.isEnabled();
await element.isEnabled();
return element;
});
}
@ -75,7 +75,7 @@ export function FindProvider({ getService }) {
return await retry.try(async () => {
const element = await getElementFunction(leadfootWithTimeout);
// Calling any method forces a staleness check
element.isEnabled();
await element.isEnabled();
return element;
});
} finally {

View file

@ -23,4 +23,5 @@ declare module '@elastic/eui' {
export const EuiWrappingPopover: React.SFC<any>;
export const EuiCopy: React.SFC<any>;
export const EuiOutsideClickDetector: React.SFC<any>;
export const EuiSideNav: React.SFC<any>;
}

View file

@ -127,18 +127,3 @@ For both of the above commands, it's crucial that you pass in `--config` to spec
Read more about how the scripts work [here](scripts/README.md).
For a deeper dive, read more about the way functional tests and servers work [here](packages/kbn-test/README.md).
### Issues starting dev more of creating builds
You may see an error like this when you are getting started:
```
[14:08:15] Error: Linux x86 checksum failed
at download_phantom.js:42:15
at process._tickDomainCallback (node.js:407:9)
```
That's thanks to the binary Phantom downloads that have to happen, and Bitbucket being annoying with throttling and redirecting or... something. The real issue eludes me, but you have 2 options to resolve it.
1. Just keep re-running the command until it passes. Eventually the downloads will work, and since they are cached, it won't ever be an issue again.
1. Download them by hand [from Bitbucket](https://bitbucket.org/ariya/phantomjs/downloads) and copy them into the `.phantom` path. We're currently using 1.9.8, and you'll need the Window, Mac, and Linux builds.

View file

@ -30,10 +30,12 @@ import { notifications } from './plugins/notifications';
import { kueryAutocomplete } from './plugins/kuery_autocomplete';
import { canvas } from './plugins/canvas';
import { infra } from './plugins/infra';
import { taskManager } from './plugins/task_manager';
import { rollup } from './plugins/rollup';
import { remoteClusters } from './plugins/remote_clusters';
import { crossClusterReplication } from './plugins/cross_cluster_replication';
import { upgradeAssistant } from './plugins/upgrade_assistant';
import { uptime } from './plugins/uptime';
module.exports = function (kibana) {
return [
@ -63,9 +65,11 @@ module.exports = function (kibana) {
indexLifecycleManagement(kibana),
kueryAutocomplete(kibana),
infra(kibana),
taskManager(kibana),
rollup(kibana),
remoteClusters(kibana),
crossClusterReplication(kibana),
upgradeAssistant(kibana),
uptime(kibana),
];
};

View file

@ -143,7 +143,6 @@
"@elastic/javascript-typescript-langserver": "^0.1.7",
"@elastic/lsp-extension": "^0.1.1",
"@elastic/node-crypto": "0.1.2",
"@elastic/node-phantom-simple": "2.2.4",
"@elastic/numeral": "2.3.2",
"@kbn/babel-preset": "1.0.0",
"@kbn/es-query": "1.0.0",
@ -153,10 +152,11 @@
"@samverschueren/stream-to-observable": "^0.3.0",
"@scant/router": "^0.1.0",
"@slack/client": "^4.8.0",
"@turf/boolean-contains": "6.0.1",
"angular-resource": "1.4.9",
"angular-sanitize": "1.6.5",
"angular-ui-ace": "0.2.3",
"apollo-cache-inmemory": "^1.2.7",
"apollo-cache-inmemory": "1.2.7",
"apollo-client": "^2.3.8",
"apollo-link": "^1.2.3",
"apollo-link-http": "^1.5.4",
@ -236,6 +236,7 @@
"nodegit": "git+https://github.com/elastic/nodegit.git#v0.24.0-alpha.6",
"nodemailer": "^4.6.4",
"node-fetch": "^2.1.2",
"nodemailer": "^4.6.4",
"object-path-immutable": "^0.5.3",
"oppsy": "^2.0.0",
"papaparse": "^4.6.0",
@ -288,22 +289,21 @@
"socket.io-client": "^2.1.1",
"squel": "^5.12.2",
"stats-lite": "^2.2.0",
"style-it": "^1.6.12",
"style-it": "2.1.2",
"styled-components": "3.3.3",
"tar-fs": "1.13.0",
"tinycolor2": "1.3.0",
"tinymath": "1.1.1",
"tslib": "^1.9.3",
"topojson-client": "3.0.0",
"tslib": "^1.9.3",
"turf": "3.0.14",
"@turf/boolean-contains": "6.0.1",
"typescript-fsa": "^2.5.0",
"typescript-fsa-reducers": "^0.4.5",
"ui-select": "0.19.4",
"unbzip2-stream": "1.0.9",
"unstated": "^2.1.1",
"uuid": "3.0.1",
"url-parse": "1.3.0",
"uuid": "3.0.1",
"venn.js": "0.2.9",
"vscode-jsonrpc": "^3.6.2",
"vscode-languageserver": "^4.2.1",

View file

@ -144,6 +144,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
toController === 'self'
? `<${DOM_ELEMENT_NAME}><div id="${DOM_ELEMENT_NAME}ReactRoot"></div></${DOM_ELEMENT_NAME}>`
: `<kbn-management-app section="${this.PLUGIN_ID.replace('_', '-')}">
<div id="management-sidenav" class="euiPageSideBar" style="position: static;"></div>
<div id="${DOM_ELEMENT_NAME}ReactRoot" />
</kbn-management-app>`,
// tslint:disable-next-line: max-classes-per-file

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { BeatDetailPage } from './beat/details';
import { BeatDetailsPage } from './beat/index';
import { BeatTagsPage } from './beat/tags';
import { EnforceSecurityPage } from './error/enforce_security';
import { InvalidLicensePage } from './error/invalid_license';
import { NoAccessPage } from './error/no_access';
import { TagsPage } from './overview/configuration_tags';
import { BeatsPage } from './overview/enrolled_beats';
import { MainPage } from './overview/index';
import { TagPage } from './tag';
import { BeatsInitialEnrollmentPage } from './walkthrough/initial/beat';
import { FinishWalkthroughPage } from './walkthrough/initial/finish';
import { InitialWalkthroughPage } from './walkthrough/initial/index';
import { InitialTagPage } from './walkthrough/initial/tag';
export const routeMap = [
{ path: '/tag/:action/:tagid?', component: TagPage },
{
path: '/beat/:beatId',
component: BeatDetailsPage,
routes: [
{ path: '/beat/:beatId/details', component: BeatDetailPage },
{ path: '/beat/:beatId/tags', component: BeatTagsPage },
],
},
{ path: '/error/enforce_security', component: EnforceSecurityPage },
{ path: '/error/invalid_license', component: InvalidLicensePage },
{ path: '/error/no_access', component: NoAccessPage },
{
path: '/overview',
component: MainPage,
routes: [
{ path: '/overview/configuration_tags', component: TagsPage },
{ path: '/overview/enrolled_beats', component: BeatsPage },
],
},
{
path: '/walkthrough/initial',
component: InitialWalkthroughPage,
routes: [
{ path: '/walkthrough/initial/beat', component: BeatsInitialEnrollmentPage },
{ path: '/walkthrough/initial/finish', component: FinishWalkthroughPage },
{ path: '/walkthrough/initial/tag', component: InitialTagPage },
],
},
];

View file

@ -13,18 +13,7 @@ import { BeatsContainer } from './containers/beats';
import { TagsContainer } from './containers/tags';
import { URLStateProps, WithURLState } from './containers/with_url_state';
import { FrontendLibs } from './lib/types';
import { RouteTreeBuilder } from './utils/page_loader/page_loader';
// See ./utils/page_loader/readme.md for details on how this works
// suffice to to say it dynamicly creates routes and pages based on the filesystem
// This is to ensure that the patterns are followed and types assured
// @ts-ignore
const requirePages = require.context('./pages', true, /\.tsx$/);
const routeTreeBuilder = new RouteTreeBuilder(requirePages);
const routesFromFilesystem = routeTreeBuilder.routeTreeFromPaths(requirePages.keys(), {
'/tag': ['action', 'tagid?'],
'/beat': ['beatId'],
});
import { routeMap } from './pages/index';
interface RouterProps {
libs: FrontendLibs;
@ -124,7 +113,7 @@ export class AppRouter extends Component<RouterProps, RouterState> {
<WithURLState>
{(URLProps: URLStateProps) => (
<ChildRoutes
routes={routesFromFilesystem}
routes={routeMap}
{...URLProps}
{...{
libs: this.props.libs,

View file

@ -1,143 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RouteTreeBuilder routeTreeFromPaths Should create a route tree 1`] = `
Array [
Object {
"component": null,
"path": "/tag",
},
Object {
"component": null,
"path": "/beat",
"routes": Array [
Object {
"component": null,
"path": "/beat/detail",
},
Object {
"component": null,
"path": "/beat/tags",
},
],
},
Object {
"component": null,
"path": "/error/enforce_security",
},
Object {
"component": null,
"path": "/error/invalid_license",
},
Object {
"component": null,
"path": "/error/no_access",
},
Object {
"component": null,
"path": "/overview",
"routes": Array [
Object {
"component": null,
"path": "/overview/enrolled_beats",
},
Object {
"component": null,
"path": "/overview/tag_configurations",
},
],
},
Object {
"component": null,
"path": "/walkthrough/initial",
"routes": Array [
Object {
"component": null,
"path": "/walkthrough/initial/beat",
},
Object {
"component": null,
"path": "/walkthrough/initial/finish",
},
Object {
"component": null,
"path": "/walkthrough/initial/tag",
},
],
},
Object {
"component": null,
"path": "*",
},
]
`;
exports[`RouteTreeBuilder routeTreeFromPaths Should create a route tree, with top level route having params 1`] = `
Array [
Object {
"component": null,
"path": "/tag/:action/:tagid?",
},
Object {
"component": null,
"path": "/beat",
"routes": Array [
Object {
"component": null,
"path": "/beat/detail",
},
Object {
"component": null,
"path": "/beat/tags",
},
],
},
Object {
"component": null,
"path": "/error/enforce_security",
},
Object {
"component": null,
"path": "/error/invalid_license",
},
Object {
"component": null,
"path": "/error/no_access",
},
Object {
"component": null,
"path": "/overview",
"routes": Array [
Object {
"component": null,
"path": "/overview/enrolled_beats",
},
Object {
"component": null,
"path": "/overview/tag_configurations",
},
],
},
Object {
"component": null,
"path": "/walkthrough/initial",
"routes": Array [
Object {
"component": null,
"path": "/walkthrough/initial/beat",
},
Object {
"component": null,
"path": "/walkthrough/initial/finish",
},
Object {
"component": null,
"path": "/walkthrough/initial/tag",
},
],
},
Object {
"component": null,
"path": "*",
},
]
`;

View file

@ -1,138 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteTreeBuilder } from './page_loader';
const pages = [
'./_404.tsx',
'./beat/detail.tsx',
'./beat/index.tsx',
'./beat/tags.tsx',
'./error/enforce_security.tsx',
'./error/invalid_license.tsx',
'./error/no_access.tsx',
'./overview/enrolled_beats.tsx',
'./overview/index.tsx',
'./overview/tag_configurations.tsx',
'./tag.tsx',
'./walkthrough/initial/beat.tsx',
'./walkthrough/initial/finish.tsx',
'./walkthrough/initial/index.tsx',
'./walkthrough/initial/tag.tsx',
];
describe('RouteTreeBuilder', () => {
describe('routeTreeFromPaths', () => {
it('Should fail to create a route tree due to no exported *Page component', () => {
const mockRequire = jest.fn(path => ({
path,
testComponent: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
expect(() => {
treeBuilder.routeTreeFromPaths(pages);
}).toThrowError(/in the pages folder does not include an exported/);
});
it('Should create a route tree', () => {
const mockRequire = jest.fn(path => ({
path,
testPage: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
let tree;
expect(() => {
tree = treeBuilder.routeTreeFromPaths(pages);
}).not.toThrow();
expect(tree).toMatchSnapshot();
});
it('Should fail to create a route tree due to no exported custom *Component component', () => {
const mockRequire = jest.fn(path => ({
path,
testComponent: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire, /Component$/);
expect(() => {
treeBuilder.routeTreeFromPaths(pages);
}).not.toThrow();
});
it('Should create a route tree, with top level route having params', () => {
const mockRequire = jest.fn(path => ({
path,
testPage: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
const tree = treeBuilder.routeTreeFromPaths(pages, {
'/tag': ['action', 'tagid?'],
});
expect(tree).toMatchSnapshot();
});
it('Should create a route tree, with a nested route having params', () => {
const mockRequire = jest.fn(path => ({
path,
testPage: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
const tree = treeBuilder.routeTreeFromPaths(pages, {
'/beat': ['beatId'],
});
expect(tree[1].path).toEqual('/beat/:beatId');
});
});
it('Should create a route tree, with a deep nested route having params', () => {
const mockRequire = jest.fn(path => ({
path,
testPage: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
const tree = treeBuilder.routeTreeFromPaths(pages, {
'/beat': ['beatId'],
'/beat/detail': ['other'],
});
expect(tree[1].path).toEqual('/beat/:beatId');
expect(tree[1].routes![0].path).toEqual('/beat/:beatId/detail/:other');
expect(tree[1].routes![1].path).toEqual('/beat/:beatId/tags');
});
it('Should throw an error on invalid mapped path', () => {
const mockRequire = jest.fn(path => ({
path,
testPage: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
expect(() => {
treeBuilder.routeTreeFromPaths(pages, {
'/non-existant-path': ['beatId'],
});
}).toThrowError(/Invalid overrideMap provided to 'routeTreeFromPaths', \/non-existant-path /);
});
it('Should rended 404.tsx as a 404 route not /404', () => {
const mockRequire = jest.fn(path => ({
path,
testPage: null,
}));
const treeBuilder = new RouteTreeBuilder(mockRequire);
const tree = treeBuilder.routeTreeFromPaths(pages);
const firstPath = tree[0].path;
const lastPath = tree[tree.length - 1].path;
expect(firstPath).not.toBe('/_404');
expect(lastPath).toBe('*');
});
});

View file

@ -1,170 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { difference, flatten, last } from 'lodash';
interface PathTree {
[path: string]: string[];
}
export interface RouteConfig {
path: string;
component: React.ComponentType<any>;
routes?: RouteConfig[];
}
interface RouteParamsMap {
[path: string]: string[];
}
export class RouteTreeBuilder {
constructor(
private readonly requireWithContext: any,
private readonly pageComponentPattern: RegExp = /Page$/
) {}
public routeTreeFromPaths(paths: string[], mapParams: RouteParamsMap = {}): RouteConfig[] {
const pathTree = this.buildTree('./', paths);
const allRoutes = Object.keys(pathTree).reduce((routes: any[], filePath) => {
if (pathTree[filePath].includes('index.tsx')) {
routes.push(this.buildRouteWithChildren(filePath, pathTree[filePath], mapParams));
} else {
routes.concat(
pathTree[filePath].map(file => routes.push(this.buildRoute(filePath, file, mapParams)))
);
}
return routes;
}, []);
// Check that no overide maps are ignored due to being invalid
const flatRoutes = this.flatpackRoutes(allRoutes);
const mappedPaths = Object.keys(mapParams);
const invalidOverrides = difference(mappedPaths, flatRoutes);
if (invalidOverrides.length > 0 && flatRoutes.length > 0) {
throw new Error(
`Invalid overrideMap provided to 'routeTreeFromPaths', ${
invalidOverrides[0]
} is not a valid route. Only the following are: ${flatRoutes.join(', ')}`
);
}
// 404 route MUST be last or it gets used first in a switch
return allRoutes.sort((a: RouteConfig) => {
return a.path === '*' ? 1 : 0;
});
}
private flatpackRoutes(arr: RouteConfig[], pre: string = ''): string[] {
return flatten(
[].concat.apply(
[],
arr.map(item => {
const path = (pre + item.path).trim();
// The flattened route based on files without params added
const route = item.path.includes('/:')
? item.path
.split('/')
.filter(s => s.charAt(0) !== ':')
.join('/')
: item.path;
return item.routes ? [route, this.flatpackRoutes(item.routes, path)] : route;
})
)
);
}
private buildRouteWithChildren(dir: string, files: string[], mapParams: RouteParamsMap) {
const childFiles = files.filter(f => f !== 'index.tsx');
const parentConfig = this.buildRoute(dir, 'index.tsx', mapParams);
parentConfig.routes = childFiles.map(cf => this.buildRoute(dir, cf, mapParams));
return parentConfig;
}
private buildRoute(dir: string, file: string, mapParams: RouteParamsMap): RouteConfig {
// Remove the file extension as we dont want that in the URL... also index resolves to parent route
// so remove that... e.g. /beats/index is not the url we want, /beats should resolve to /beats/index
// just like how files resolve in node
const filePath = `${mapParams[dir] || dir}${file.replace('.tsx', '')}`.replace('/index', '');
const page = this.requireWithContext(`.${dir}${file}`);
const cleanDir = dir.replace(/\/$/, '');
// Make sure the expored variable name matches a pattern. By default it will choose the first
// exported variable that matches *Page
const componentExportName = Object.keys(page).find(varName =>
this.pageComponentPattern.test(varName)
);
if (!componentExportName) {
throw new Error(
`${dir}${file} in the pages folder does not include an exported \`${this.pageComponentPattern.toString()}\` component`
);
}
// _404 route is special and maps to a 404 page
if (filePath === '/_404') {
return {
path: '*',
component: page[componentExportName],
};
}
// mapped route has a parent with mapped params, so we map it here too
// e.g. /beat has a beatid param, so /beat/detail, a child of /beat
// should also have that param resulting in /beat/:beatid/detail/:other
if (mapParams[cleanDir] && filePath !== cleanDir) {
const dirWithParams = `${cleanDir}/:${mapParams[cleanDir].join('/:')}`;
const path = `${dirWithParams}/${file.replace('.tsx', '')}${
mapParams[filePath] ? '/:' : ''
}${(mapParams[filePath] || []).join('/:')}`;
return {
path,
component: page[componentExportName],
};
}
// route matches a mapped param exactly
// e.g. /beat has a beatid param, so it becomes /beat/:beatid
if (mapParams[filePath]) {
return {
path: `${filePath}/:${mapParams[filePath].join('/:')}`,
component: page[componentExportName],
};
}
return {
path: filePath,
component: page[componentExportName],
};
}
// Build tree recursively
private buildTree(basePath: string, paths: string[]): PathTree {
return paths.reduce(
(dir: any, p) => {
const path = {
dir:
p
.replace(basePath, '/') // make path absolute
.split('/')
.slice(0, -1) // remove file from path
.join('/')
.replace(/^\/\//, '') + '/', // should end in a slash but not be only //
file: last(p.split('/')),
};
// take each, remove the file name
if (dir[path.dir]) {
dir[path.dir].push(path.file);
} else {
dir[path.dir] = [path.file];
}
return dir;
},
{}
);
}
}

View file

@ -1,21 +0,0 @@
# Page loader
Routing in React is not easy, nether is ensuring a clean and simple api within pages.
This solves for both without massive config files. It also ensure URL paths match our files to make things easier to find
It works like this...
```ts
// Create a webpack context, ensureing all pages in the pages dir are included in the build
const requirePages = require.context('./pages', true, /\.tsx$/);
// Pass the context based require into the RouteTreeBuilder for require the files as-needed
const routeTreeBuilder = new RouteTreeBuilder(requirePages);
// turn the array of file paths from the require context into a nested tree of routes based on folder structure
const routesFromFilesystem = routeTreeBuilder.routeTreeFromPaths(requirePages.keys(), {
'/tag': ['action', 'tagid?'], // add params to a page. In this case /tag turns into /tag/:action/:tagid?
'/beat': ['beatId'],
'/beat/detail': ['action'], // it nests too, in this case, because of the above line, this is /beat/:beatId/detail/:action
});
```
In the above example to allow for flexability, the `./pages/beat.tsx` page would receve a prop of `routes` that is an array of sub-pages

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