Merge branch 'master' of github.com:elastic/kibana into migrate/timelion

This commit is contained in:
Rashid Khan 2016-09-02 09:20:49 -07:00
commit 589ea55f7e
26 changed files with 526 additions and 251 deletions

View file

@ -1,8 +1,8 @@
[[setup-repositories]]
=== Install Kibana using a Linux Package Manager
=== Installing Kibana with apt and yum
Binary packages for Kibana are available for Unix distributions that support the `apt` and `yum` tools. We also have
repositories available for APT and YUM based distributions.
Binary packages for Kibana are available for Unix distributions that support the `apt` and `yum` tools.
We also have repositories available for APT and YUM based distributions.
NOTE: Since the packages are created as part of the Kibana build, source packages are not available.

View file

@ -16,6 +16,7 @@ The {version} release of Kibana requires Elasticsearch {esversion} or later.
* {k4issue}6531[Issue 6531]: Improved warning for URL lengths that approach browser limits.
* {k4issue}6602[Issue 6602]: Improves dark theme support.
* {k4issue}6791[Issue 6791]: Enables composition of custom user toast notifications in Advanced Settings.
* {k4pull}8014[Pull Request 8014]: Changes the UUID config setting from `uuid` to `server.uuid`, and puts UUID storage into data file instead of Elasticsearch.
[float]
[[bugfixes]]

View file

@ -1,6 +1,6 @@
[[setup]]
== Installing Kibana
You can set up Kibana and start exploring your Elasticsearch indices in minutes.
== Setting Up Kibana
You can install Kibana and start exploring your Elasticsearch indices in minutes.
All you need is:
* Elasticsearch {esversion}
@ -9,114 +9,70 @@ All you need is:
** URL of the Elasticsearch instance you want to connect to.
** Which Elasticsearch indices you want to search.
NOTE: If your Elasticsearch installation is protected by http://www.elastic.co/overview/shield/[{scyld}], see
{shield}/kibana.html#using-kibana4-with-shield[{scyld} with Kibana] for additional setup instructions.
=== Upgrading Kibana
Your existing Kibana version is generally compatible with the next minor version release of Elasticsearch.
This means you should upgrade your Elasticsearch cluster(s) before or at the same time as Kibana.
We cannot guarantee compatibility between major version releases so in those cases both Elasticsearch and Kibana must be upgraded together.
To upgrade Kibana:
. Create a https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html[snapshot] of the existing `.kibana` index.
. Back up the `kibana.yml` configuration file.
. Take note of the Kibana plugins that are installed:
* `bin/kibana plugin --list` on 4.x versions of Kibana.
* `bin/kibana-plugin list` on 5.x versions of Kibana.
. To upgrade from an Archive File:
.. Extract the new version of Kibana into a different directory. See steps below.
.. Migrate any custom configuration from your old kibana.yml to your new one
.. Follow other steps below to complete the new installation.
.. Once the new version is fully configured and working with required plugins, remove the previous version of Kibana
. To upgrade using a Linux Package Manager:
.. Uninstall the existing Kibana package: `apt-get remove kibana` or `yum remove kibana`
.. Install the new Kibana package. There have been some installer issues between various version of Kibana so the uninstall and install process is safer than an upgrade.
[float]
[[install]]
=== Install and Start Kibana from an Archive File
=== Install Kibana
To install and start Kibana:
To get Kibana up and running:
. Download the https://www.elastic.co/downloads/kibana[Kibana {version} binary package] for your platform.
. Download the https://www.elastic.co/downloads/kibana[Kibana 4 binary package] for your platform.
. Extract the `.zip` or `tar.gz` archive file.
. If you're upgrading, migrate any configuration changes from the previous `kibana.yml` to the new version.
. Install Kibana plugins (optional).
. Run Kibana from the install directory: `bin/kibana` (Linux/MacOSX) or `bin\kibana.bat` (Windows).
On Unix, you can instead run the package manager suited for your distribution.
////
[float]
include::kibana-repositories.asciidoc[]
////
That's it! Kibana is now running on port 5601.
[float]
[[kibana-dynamic-mapping]]
==== Kibana and Elasticsearch Dynamic Mapping
By default, Elasticsearch enables {ref}dynamic-mapping.html[dynamic mapping] for fields. Kibana needs dynamic mapping
to use fields in visualizations correctly, as well as to manage the `.kibana` index where saved searches,
visualizations, and dashboards are stored.
On Unix, you can also install Kibana using the package manager suited for your distribution. For more
information, see <<setup-repositories, Installing Kibana with apt and yum>>.
If your Elasticsearch use case requires you to disable dynamic mapping, you need to manually provide mappings for
fields that Kibana uses to create visualizations. You also need to manually enable dynamic mapping for the `.kibana`
index.
The following procedure assumes that the `.kibana` index does not already exist in Elasticsearch and that the
`index.mapper.dynamic` setting in `elasticsearch.yml` is set to `false`:
. Start Elasticsearch.
. Create the `.kibana` index with dynamic mapping enabled just for that index:
+
[source,shell]
PUT .kibana
{
"index.mapper.dynamic": true
}
+
. Start Kibana and navigate to the web UI and verify that there are no error messages related to dynamic mapping.
IMPORTANT: If your Elasticsearch installation is protected by http://www.elastic.co/overview/shield/[Shield]
see {shield}/kibana.html#using-kibana4-with-shield[Using Kibana with Shield] for additional setup
instructions.
[float]
[[connect]]
=== Connect Kibana with Elasticsearch
Before you can start using Kibana, you need to tell it which Elasticsearch indices you want to explore. The first time
you access Kibana, you are prompted to define an _index pattern_ that matches the name of one or more of your indices.
That's it. That's all you need to configure to start using Kibana. You can add index patterns at any time from the
<<settings-create-pattern,Settings tab>>.
Before you can start using Kibana, you need to tell it which Elasticsearch indices you want to explore.
The first time you access Kibana, you are prompted to define an _index pattern_ that matches the name of
one or more of your indices. That's it. That's all you need to configure to start using Kibana. You can
add index patterns at any time from the <<settings-create-pattern,Settings tab>>.
TIP: By default, Kibana connects to the Elasticsearch instance running on `localhost`. To connect to a different
Elasticsearch instance, modify the Elasticsearch URL in the `kibana.yml` configuration file and restart Kibana. For
information about using Kibana with your production nodes, see <<production>>.
TIP: By default, Kibana connects to the Elasticsearch instance running on `localhost`. To connect to a
different Elasticsearch instance, modify the Elasticsearch URL in the `kibana.yml` configuration file and
restart Kibana. Forninformation about using Kibana with your production nodes, see <<production>>.
To configure the Elasticsearch indices you want to access with Kibana:
. Point your browser at port 5601 to access the Kibana UI. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`.
. Point your browser at port 5601 to access the Kibana UI. For example, `localhost:5601` or
`http://YOURDOMAIN.com:5601`.
+
image:images/Start-Page.png[Kibana start page]
+
. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. By default, Kibana
guesses that you're working with data being fed into Elasticsearch by Logstash. If that's the case, you can use the
default `logstash-*` as your index pattern. The asterisk (*) matches zero or more characters in an index's name. If
your Elasticsearch indices follow some other naming convention, enter an appropriate pattern. The "pattern" can also
simply be the name of a single index.
. Select the index field that contains the timestamp that you want to use to perform time-based comparisons. Kibana
reads the index mapping to list all of the fields that contain a timestamp. If your index doesn't have time-based data,
disable the *Index contains time-based events* option.
. Specify an index pattern that matches the name of one or more of your Elasticsearch indices. By default,
Kibana guesses that you're working with data being fed into Elasticsearch by Logstash. If that's the case,
you can use the default `logstash-*` as your index pattern. The asterisk (*) matches zero or more
characters in an index's name. If your Elasticsearch indices follow some other naming convention, enter
an appropriate pattern. The "pattern" can also simply be the name of a single index.
. Select the index field that contains the timestamp that you want to use to perform time-based
comparisons. Kibana reads the index mapping to list all of the fields that contain a timestamp. If your
index doesn't have time-based data, disable the *Index contains time-based events* option.
+
WARNING: Using event times to create index names is *deprecated* in this release of Kibana. Starting in the 2.1
release, Elasticsearch includes sophisticated date parsing APIs that Kibana uses to determine date information,
removing the need to specify dates in the index pattern name.
WARNING: Using event times to create index names is *deprecated* in this release of Kibana. Support for
this functionality will be removed entirely in the next major Kibana release. Elasticsearch 2.1 includes
sophisticated date parsing APIs that Kibana uses to determine date information, removing the need to
specify dates in the index pattern name.
+
. Click *Create* to add the index pattern. This first pattern is automatically configured as the default.
When you have more than one index pattern, you can designate which one to use as the default from *Settings > Indices*.
When you have more than one index pattern, you can designate which one to use as the default from
*Settings > Indices*.
All done! Kibana is now connected to your Elasticsearch data. Kibana displays a read-only list of fields configured for
the matching index.
All done! Kibana is now connected to your Elasticsearch data. Kibana displays a read-only list of fields
configured for the matching index.
NOTE: Kibana relies on dynamic mapping to use fields in visualizations and manage the
`.kibana` index. If you have disabled dynamic mapping, you need to manually provide
mappings for the fields that Kibana uses to create visualizations. For more information, see
<<kibana-dynamic-mapping, Kibana and Elasticsearch Dynamic Mapping>>.
[float]
[[explore]]
@ -127,5 +83,60 @@ You're ready to dive in to your data:
* Chart and map your data from the <<visualize, Visualize>> page.
* Create and view custom dashboards from the <<dashboard, Dashboard>> page.
For a tutorial that explores these core Kibana concepts, take a look at the <<getting-started, Getting
Started>> page.
For a step-by-step introduction to these core Kibana concepts, see the <<getting-started,
Getting Started>> tutorial.
[float]
[[kibana-dynamic-mapping]]
=== Kibana and Elasticsearch Dynamic Mapping
By default, Elasticsearch enables {ref}dynamic-mapping.html[dynamic mapping] for fields. Kibana needs
dynamic mapping to use fields in visualizations correctly, as well as to manage the `.kibana` index
where saved searches, visualizations, and dashboards are stored.
If your Elasticsearch use case requires you to disable dynamic mapping, you need to manually provide
mappings for fields that Kibana uses to create visualizations. You also need to manually enable dynamic
mapping for the `.kibana` index.
The following procedure assumes that the `.kibana` index does not already exist in Elasticsearch and
that the `index.mapper.dynamic` setting in `elasticsearch.yml` is set to `false`:
. Start Elasticsearch.
. Create the `.kibana` index with dynamic mapping enabled just for that index:
+
[source,shell]
PUT .kibana
{
"index.mapper.dynamic": true
}
+
. Start Kibana and navigate to the web UI and verify that there are no error messages related to dynamic
mapping.
include::kibana-repositories.asciidoc[]
[[upgrading-kibana]]
=== Upgrading Kibana
Your existing Kibana version is generally compatible with the next minor version release of Elasticsearch.
This means you should upgrade your Elasticsearch cluster(s) before or at the same time as Kibana.
We cannot guarantee compatibility between major version releases so in those cases both Elasticsearch and
Kibana must be upgraded together.
To upgrade Kibana:
. Create a https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html[snapshot]
of the existing `.kibana` index.
. Back up the `kibana.yml` configuration file.
. Take note of the Kibana plugins that are installed:
* `bin/kibana plugin --list` on 4.x versions of Kibana.
* `bin/kibana-plugin list` on 5.x versions of Kibana.
. To upgrade from an Archive File:
.. Extract the new version of Kibana into a different directory. See steps below.
.. Migrate any custom configuration from your old kibana.yml to your new one
.. Follow other steps below to complete the new installation.
.. Once the new version is fully configured and working with required plugins, remove the previous version
of Kibana
. To upgrade using a Linux Package Manager:
.. Uninstall the existing Kibana package: `apt-get remove kibana` or `yum remove kibana`
.. Install the new Kibana package. There have been some installer issues between various version of
Kibana so the uninstall and install process is safer than an upgrade.

View file

@ -5,7 +5,6 @@ import exposeClient from './expose_client';
import migrateConfig from './migrate_config';
import createKibanaIndex from './create_kibana_index';
import checkEsVersion from './check_es_version';
import manageUuid from './manage_uuid';
const NoConnections = elasticsearch.errors.NoConnections;
import util from 'util';
const format = util.format;
@ -19,7 +18,6 @@ const REQUEST_DELAY = 2500;
module.exports = function (plugin, server) {
const config = server.config();
const client = server.plugins.elasticsearch.client;
const uuidManagement = manageUuid(server);
plugin.status.yellow('Waiting for Elasticsearch');
@ -89,7 +87,6 @@ module.exports = function (plugin, server) {
return waitForPong()
.then(_.partial(checkEsVersion, server))
.then(waitForShards)
.then(uuidManagement)
.then(setGreenStatus)
.then(_.partial(migrateConfig, server))
.catch(err => plugin.status.red(err));

View file

@ -1,81 +0,0 @@
import uuid from 'node-uuid';
import { hostname } from 'os';
const serverHostname = hostname();
/* Handle different scenarios:
* - config uuid exists, data uuid exists and matches
* - nothing to do
* - config uuid missing, data uuid exists
* - set uuid from data as config uuid
* - config uuid exists, data uuid exists but mismatches
* - update data uuid with config uuid
* - config uuid missing, data uuid missing
* - generate new uuid, set in config and insert in data
* ("config uuid" = uuid in server.config,
* "data uuid" = uuid in .kibana index)
*/
export default function manageUuid(server) {
const TYPE = 'server';
const config = server.config();
const serverPort = server.info.port;
const client = server.plugins.elasticsearch.client;
return function uuidManagement() {
const fieldId = `${serverHostname}-${serverPort}`;
const kibanaIndex = config.get('kibana.index');
let kibanaUuid = config.get('uuid');
function logToServer(msg) {
server.log(['server', 'uuid', fieldId], msg);
}
return client.get({
index: kibanaIndex,
ignore: [404],
type: TYPE,
id: fieldId
}).then(result => {
if (result.found) {
if (kibanaUuid === result._source.uuid) {
// config uuid exists, data uuid exists and matches
logToServer(`Kibana instance UUID: ${kibanaUuid}`);
return;
}
if (!kibanaUuid) {
// config uuid missing, data uuid exists
kibanaUuid = result._source.uuid;
logToServer(`Resuming persistent Kibana instance UUID: ${kibanaUuid}`);
config.set('uuid', kibanaUuid);
return;
}
if (kibanaUuid !== result._source.uuid) {
// config uuid exists, data uuid exists but mismatches
logToServer(`Updating Kibana instance UUID to: ${kibanaUuid} (was: ${result._source.uuid})`);
return client.update({
index: kibanaIndex,
type: TYPE,
id: fieldId,
body: { doc: { uuid: kibanaUuid } }
});
}
}
// data uuid missing
if (!kibanaUuid) {
// config uuid missing
kibanaUuid = uuid.v4();
config.set('uuid', kibanaUuid);
}
logToServer(`Setting new Kibana instance UUID: ${kibanaUuid}`);
return client.index({
index: kibanaIndex,
type: TYPE,
id: fieldId,
body: { uuid: kibanaUuid }
});
});
};
}

View file

@ -1,7 +1,9 @@
import manageUuid from './server/lib/manage_uuid';
import ingest from './server/routes/api/ingest';
import search from './server/routes/api/search';
import settings from './server/routes/api/settings';
import scripts from './server/routes/api/scripts';
import * as systemApi from './server/lib/system_api';
module.exports = function (kibana) {
const kbnBaseUrl = '/app/kibana';
@ -84,10 +86,15 @@ module.exports = function (kibana) {
},
init: function (server, options) {
// uuid
manageUuid(server);
// routes
ingest(server);
search(server);
settings(server);
scripts(server);
server.expose('systemApi', systemApi);
}
});

View file

@ -13,18 +13,13 @@ import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logsta
let $parentScope;
let $scope;
let config;
let hits;
let indexPattern;
let indexPatternList;
let shortDotsValue;
// Sets up the directive, take an element, and a list of properties to attach to the parent scope.
const init = function ($elem, props) {
ngMock.inject(function ($rootScope, $compile, $timeout, _config_) {
shortDotsValue = _config_.get('shortDots:enable');
config = _config_;
config.set('shortDots:enable', false);
ngMock.inject(function ($rootScope, $compile, $timeout) {
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
@ -39,7 +34,6 @@ const init = function ($elem, props) {
const destroy = function () {
$scope.$destroy();
$parentScope.$destroy();
config.set('shortDots:enable', shortDotsValue);
};
describe('discover field chooser directives', function () {
@ -56,7 +50,21 @@ describe('discover field chooser directives', function () {
'</disc-field-chooser>'
);
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.module('kibana', ($provide) => {
$provide.decorator('config', ($delegate) => {
// disable shortDots for these tests
$delegate.get = _.wrap($delegate.get, function (origGet, name) {
if (name === 'shortDots:enable') {
return false;
} else {
return origGet.call(this, name);
}
});
return $delegate;
});
}));
beforeEach(ngMock.inject(function (Private) {
hits = Private(FixturesHitsProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);

View file

@ -1,11 +1,11 @@
import expect from 'expect.js';
import sinon from 'sinon';
import Joi from 'joi';
import * as kbnTestServer from '../../../../../test/utils/kbn_server.js';
import fromRoot from '../../../../utils/from_root';
import * as kbnTestServer from '../../../../../../test/utils/kbn_server.js';
import fromRoot from '../../../../../utils/from_root';
import manageUuid from '../manage_uuid';
describe('plugins/elasticsearch', function () {
describe('core_plugins/kibana/server/lib', function () {
describe('manage_uuid', function () {
const testUuid = 'c4add484-0cba-4e05-86fe-4baa112d9e53';
let kbnServer;
@ -23,7 +23,6 @@ describe('plugins/elasticsearch', function () {
});
await kbnServer.ready();
await kbnServer.server.plugins.elasticsearch.waitUntilReady();
});
// clear uuid stuff from previous test runs
@ -38,53 +37,48 @@ describe('plugins/elasticsearch', function () {
});
it('ensure config uuid is validated as a guid', async function () {
config.set('uuid', testUuid);
expect(config.get('uuid')).to.be(testUuid);
config.set('server.uuid', testUuid);
expect(config.get('server.uuid')).to.be(testUuid);
expect(() => {
config.set('uuid', 'foouid');
config.set('server.uuid', 'foouid');
}).to.throwException((e) => {
expect(e.name).to.be('ValidationError');
});
});
it('finds the previously set uuid with config match', async function () {
const uuidManagement = manageUuid(kbnServer.server);
const msg = `Kibana instance UUID: ${testUuid}`;
config.set('uuid', testUuid);
config.set('server.uuid', testUuid);
await uuidManagement();
await uuidManagement();
await manageUuid(kbnServer.server);
await manageUuid(kbnServer.server);
expect(kbnServer.server.log.lastCall.args[1]).to.be.eql(msg);
});
it('updates the previously set uuid with config value', async function () {
const uuidManagement = manageUuid(kbnServer.server);
config.set('uuid', testUuid);
config.set('server.uuid', testUuid);
await uuidManagement();
await manageUuid(kbnServer.server);
const newUuid = '5b2de169-2785-441b-ae8c-186a1936b17d';
const msg = `Updating Kibana instance UUID to: ${newUuid} (was: ${testUuid})`;
config.set('uuid', newUuid);
await uuidManagement();
config.set('server.uuid', newUuid);
await manageUuid(kbnServer.server);
expect(kbnServer.server.log.lastCall.args[1]).to.be(msg);
});
it('resumes the uuid stored in data and sets it to the config', async function () {
const uuidManagement = manageUuid(kbnServer.server);
const partialMsg = 'Resuming persistent Kibana instance UUID';
config.set('uuid'); // set to undefined
config.set('server.uuid'); // set to undefined
await uuidManagement();
await manageUuid(kbnServer.server);
expect(config.get('uuid')).to.be.ok(); // not undefined any more
expect(config.get('server.uuid')).to.be.ok(); // not undefined any more
expect(kbnServer.server.log.lastCall.args[1]).to.match(new RegExp(`^${partialMsg}`));
});
});
});

View file

@ -0,0 +1,22 @@
import expect from 'expect.js';
import { isSystemApiRequest } from '../system_api';
describe('system_api', () => {
describe('#isSystemApiRequest', () => {
it ('returns true for a system API HTTP request', () => {
const mockHapiRequest = {
headers: {
'kbn-system-api': true
}
};
expect(isSystemApiRequest(mockHapiRequest)).to.be(true);
});
it ('returns false for a non-system API HTTP request', () => {
const mockHapiRequest = {
headers: {}
};
expect(isSystemApiRequest(mockHapiRequest)).to.be(false);
});
});
});

View file

@ -0,0 +1,72 @@
import uuid from 'node-uuid';
import Promise from 'bluebird';
import { join as pathJoin } from 'path';
import { readFile as readFileCallback, writeFile as writeFileCallback } from 'fs';
const FILE_ENCODING = 'utf8';
export default async function manageUuid(server) {
const config = server.config();
const serverPort = server.info.port;
const serverHostname = config.get('server.host');
const fileName = `${serverHostname}:${serverPort}`;
const uuidFile = pathJoin(config.get('path.data'), fileName);
async function detectUuid() {
const readFile = Promise.promisify(readFileCallback);
try {
const result = await readFile(uuidFile);
return result.toString(FILE_ENCODING);
} catch (e) {
return false;
}
}
async function writeUuid(uuid) {
const writeFile = Promise.promisify(writeFileCallback);
try {
return await writeFile(uuidFile, uuid, { encoding: FILE_ENCODING });
} catch (e) {
return false;
}
}
// detect if uuid exists already from before a restart
const logToServer = (msg) => server.log(['server', 'uuid', fileName], msg);
const dataFileUuid = await detectUuid();
let serverConfigUuid = config.get('server.uuid'); // check if already set in config
if (dataFileUuid) {
// data uuid found
if (serverConfigUuid === dataFileUuid) {
// config uuid exists, data uuid exists and matches
logToServer(`Kibana instance UUID: ${dataFileUuid}`);
return;
}
if (!serverConfigUuid) {
// config uuid missing, data uuid exists
serverConfigUuid = dataFileUuid;
logToServer(`Resuming persistent Kibana instance UUID: ${serverConfigUuid}`);
config.set('server.uuid', serverConfigUuid);
return;
}
if (serverConfigUuid !== dataFileUuid) {
// config uuid exists, data uuid exists but mismatches
logToServer(`Updating Kibana instance UUID to: ${serverConfigUuid} (was: ${dataFileUuid})`);
return writeUuid(serverConfigUuid);
}
}
// data uuid missing
if (!serverConfigUuid) {
// config uuid missing
serverConfigUuid = uuid.v4();
config.set('server.uuid', serverConfigUuid);
}
logToServer(`Setting new Kibana instance UUID: ${serverConfigUuid}`);
return writeUuid(serverConfigUuid);
}

View file

@ -0,0 +1,11 @@
const SYSTEM_API_HEADER_NAME = 'kbn-system-api';
/**
* Checks on the *server-side*, if an HTTP request is a system API request
*
* @param request HAPI request object
* @return true if request is a system API request; false, otherwise
*/
export function isSystemApiRequest(request) {
return !!request.headers[SYSTEM_API_HEADER_NAME];
}

View file

@ -30,9 +30,9 @@ module.exports = () => Joi.object({
exclusive: Joi.boolean().default(false)
}).default(),
uuid: Joi.string().guid().default(),
server: Joi.object({
uuid: Joi.string().guid().default(),
name: Joi.string().default(os.hostname()),
host: Joi.string().hostname().default('localhost'),
port: Joi.number().default(5601),

View file

@ -18,7 +18,7 @@ export default function (kbnServer, server, config) {
handler: function (request, reply) {
return reply({
name: config.get('server.name'),
uuid: config.get('uuid'),
uuid: config.get('server.uuid'),
status: kbnServer.status.toJSON(),
metrics: kbnServer.metrics
});

View file

@ -1,7 +1,9 @@
import $ from 'jquery';
import { remove } from 'lodash';
import './kbn_chrome.less';
import UiModules from 'ui/modules';
import { isSystemApiRequest } from 'ui/system_api';
export default function (chrome, internals) {
@ -41,6 +43,10 @@ export default function (chrome, internals) {
$rootScope.$on('$routeUpdate', onRouteChange);
onRouteChange();
const allPendingHttpRequests = () => $http.pendingRequests;
const removeSystemApiRequests = (pendingHttpRequests = []) => remove(pendingHttpRequests, isSystemApiRequest);
$scope.$watchCollection(allPendingHttpRequests, removeSystemApiRequests);
// and some local values
chrome.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs;

View file

@ -118,6 +118,15 @@ describe('Notifier', function () {
it('includes stack', function () {
expect(notify('error').stack).to.be.defined;
});
it('has css class helper functions', function () {
expect(notify('error').getIconClass()).to.equal('fa fa-warning');
expect(notify('error').getButtonClass()).to.equal('btn-danger');
expect(notify('error').getAlertClassStack()).to.equal('toast-stack alert alert-danger');
expect(notify('error').getAlertClass()).to.equal('toast alert alert-danger');
expect(notify('error').getButtonGroupClass()).to.equal('toast-controls');
expect(notify('error').getToastMessageClass()).to.equal('toast-message');
});
});
describe('#warning', function () {
@ -156,6 +165,15 @@ describe('Notifier', function () {
it('does not include stack', function () {
expect(notify('warning').stack).not.to.be.defined;
});
it('has css class helper functions', function () {
expect(notify('warning').getIconClass()).to.equal('fa fa-warning');
expect(notify('warning').getButtonClass()).to.equal('btn-warning');
expect(notify('warning').getAlertClassStack()).to.equal('toast-stack alert alert-warning');
expect(notify('warning').getAlertClass()).to.equal('toast alert alert-warning');
expect(notify('warning').getButtonGroupClass()).to.equal('toast-controls');
expect(notify('warning').getToastMessageClass()).to.equal('toast-message');
});
});
describe('#info', function () {
@ -194,6 +212,15 @@ describe('Notifier', function () {
it('does not include stack', function () {
expect(notify('info').stack).not.to.be.defined;
});
it('has css class helper functions', function () {
expect(notify('info').getIconClass()).to.equal('fa fa-info-circle');
expect(notify('info').getButtonClass()).to.equal('btn-info');
expect(notify('info').getAlertClassStack()).to.equal('toast-stack alert alert-info');
expect(notify('info').getAlertClass()).to.equal('toast alert alert-info');
expect(notify('info').getButtonGroupClass()).to.equal('toast-controls');
expect(notify('info').getToastMessageClass()).to.equal('toast-message');
});
});
describe('#custom', function () {
@ -251,6 +278,18 @@ describe('Notifier', function () {
expect(customNotification.customActions).to.have.length(customParams.actions.length);
});
it('custom actions have getButtonClass method', function () {
customNotification.customActions.forEach((action, idx) => {
expect(action).to.have.property('getButtonClass');
expect(action.getButtonClass).to.be.a('function');
if (idx === 0) {
expect(action.getButtonClass()).to.be('btn-primary btn-info');
} else {
expect(action.getButtonClass()).to.be('btn-default btn-info');
}
});
});
it('gives a default action if none are provided', function () {
// destroy the default custom notification, avoid duplicate handling
customNotification.clear();
@ -357,6 +396,15 @@ describe('Notifier', function () {
it('does not include stack', function () {
expect(notify('banner').stack).not.to.be.defined;
});
it('has css class helper functions', function () {
expect(notify('banner').getIconClass()).to.equal('');
expect(notify('banner').getButtonClass()).to.equal('btn-banner');
expect(notify('banner').getAlertClassStack()).to.equal('toast-stack alert alert-banner');
expect(notify('banner').getAlertClass()).to.equal('alert alert-banner');
expect(notify('banner').getButtonGroupClass()).to.equal('toast-controls-banner');
expect(notify('banner').getToastMessageClass()).to.equal('toast-message-banner');
});
});
function notify(fnName) {

View file

@ -85,6 +85,27 @@ function restartNotifTimer(notif, cb) {
startNotifTimer(notif, cb);
}
const typeToButtonClassMap = {
danger: 'btn-danger', // NOTE: `error` type is internally named as `danger`
warning: 'btn-warning',
info: 'btn-info',
banner: 'btn-banner'
};
const buttonHierarchyClass = (index) => {
if (index === 0) {
// first action: primary className
return 'btn-primary';
}
// subsequent actions: secondary/default className
return 'btn-default';
};
const typeToAlertClassMap = {
danger: `alert-danger`,
warning: `alert-warning`,
info: `alert-info`,
banner: `alert-banner`,
};
function add(notif, cb) {
_.set(notif, 'info.version', version);
_.set(notif, 'info.buildNum', buildNum);
@ -97,10 +118,14 @@ function add(notif, cb) {
});
} else if (notif.customActions) {
// wrap all of the custom functions in a close
notif.customActions = notif.customActions.map(action => {
notif.customActions = notif.customActions.map((action, index) => {
return {
key: action.text,
callback: closeNotif(notif, action.callback, action.text)
callback: closeNotif(notif, action.callback, action.text),
getButtonClass() {
const buttonTypeClass = typeToButtonClassMap[notif.type];
return `${buttonHierarchyClass(index)} ${buttonTypeClass}`;
}
};
});
}
@ -111,6 +136,18 @@ function add(notif, cb) {
return notif.timerId ? true : false;
};
// decorate the notification with helper functions for the template
notif.getButtonClass = () => typeToButtonClassMap[notif.type];
notif.getAlertClassStack = () => `toast-stack alert ${typeToAlertClassMap[notif.type]}`;
notif.getIconClass = () => (notif.type === 'banner') ? '' : `fa fa-${notif.icon}`;
notif.getToastMessageClass = () => (notif.type === 'banner') ? 'toast-message-banner' : 'toast-message';
notif.getAlertClass = () => (notif.type === 'banner') ?
`alert ${typeToAlertClassMap[notif.type]}` : // not including `.toast` class leaves out the flex properties for banner
`toast alert ${typeToAlertClassMap[notif.type]}`;
notif.getButtonGroupClass = () => (notif.type === 'banner') ?
'toast-controls-banner' :
'toast-controls';
let dup = null;
if (notif.content) {
dup = _.find(notifs, function (item) {

View file

@ -8,21 +8,21 @@
margin: 0;
padding: 0;
list-style: none;
}
&-countdown {
.toaster-countdown {
.badge {
color: @white;
}
&:hover {
.badge {
color: @white;
font-size: 0;
}
&:hover {
.badge {
font-size: 0;
}
.badge:after {
font-size: @font-size-small;
content: attr(hover-text);
}
.badge:after {
font-size: @font-size-small;
content: attr(hover-text);
}
}
}
@ -91,11 +91,29 @@
background: darken(@alert-danger-bg, 25%);
}
.alert-banner {
background-color: #c0c0c0;
background-color: white;
padding: 10px 15px;
a {
color: #328caa;
}
.toaster-countdown {
background-color: white;
}
.badge {
background-color: #328caa;
}
}
.toast-message {
white-space: normal;
}
}
// toast message class for banner needs to be display:block because banner does
// not have a flex layout, and the styled element could be any kind of element
.toast-message-banner {
display: block;
}

View file

@ -1,75 +1,76 @@
<div class="toaster-container">
<ul class="toaster">
<li ng-repeat="notif in list" kbn-toast notif="notif">
<div class="toast alert" ng-class="'alert-' + notif.type">
<div ng-class="notif.getAlertClass()">
<span ng-show="notif.count > 1" class="badge">{{ notif.count }}</span>
<i class="fa" ng-class="'fa-' + notif.icon" tooltip="{{notif.title}}"></i>
<i ng-class="notif.getIconClass()" tooltip="{{notif.title}}"></i>
<kbn-truncated
ng-if="notif.content"
source="{{notif.content | markdown}}"
is-html="true"
length="250"
class="toast-message"
ng-class="notif.getToastMessageClass()"
></kbn-truncated>
<render-directive
ng-if="notif.directive"
definition="notif.directive"
notif="notif"
class="toast-message"
ng-class="notif.getToastMessageClass()"
></render-directive>
<div class="btn-group pull-right toast-controls">
<button
type="button"
ng-if="notif.isTimed()"
class="btn toaster-countdown"
ng-class="'btn-' + notif.type"
ng-click="notif.cancelTimer()"
><span class="badge" hover-text="stop">{{notif.timeRemaining}}s</span></button>
<div class="btn-group" ng-class="notif.getButtonGroupClass()">
<button
type="button"
ng-if="notif.stack && !notif.showStack"
class="btn"
ng-class="'btn-' + notif.type"
ng-class="notif.getButtonClass()"
ng-click="notif.cancelTimer(); notif.showStack = true"
>More Info</button>
<button
type="button"
ng-if="notif.stack && notif.showStack"
class="btn"
ng-class="'btn-' + notif.type"
ng-class="notif.getButtonClass()"
ng-click="notif.showStack = false"
>Less Info</button>
<button
type="button"
ng-if="notif.accept"
class="btn"
ng-class="'btn-' + notif.type"
ng-class="notif.getButtonClass()"
ng-click="notif.accept()"
>OK</button>
<button
type="button"
ng-if="notif.address"
class="btn"
ng-class="'btn-' + notif.type"
ng-class="notif.getButtonClass()"
ng-click="notif.address()"
>Fix it</button>
<button
type="button"
class="btn"
ng-repeat="action in notif.customActions"
ng-class="'btn-' + notif.type"
ng-class="action.getButtonClass()"
ng-click="action.callback()"
ng-bind="action.key"
></button>
</div>
<button
type="button"
ng-if="notif.isTimed()"
class="toaster-countdown"
ng-class="notif.getButtonClass()"
ng-click="notif.cancelTimer()"
><span class="badge" hover-text="stop">{{notif.timeRemaining}}s</span></button>
</div>
<div ng-if="notif.stack && notif.showStack" class="toast-stack alert" ng-class="'alert-' + notif.type">
<div ng-if="notif.stack && notif.showStack" ng-class="notif.getAlertClassStack()">
<pre ng-repeat="stack in notif.stacks" ng-bind="stack"></pre>
</div>

View file

@ -0,0 +1,38 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { addSystemApiHeader, isSystemApiRequest } from '../system_api';
describe('system_api', () => {
describe('#addSystemApiHeader', () => {
it ('adds the correct system API header', () => {
const headers = {
'kbn-version': '4.6.0'
};
const newHeaders = addSystemApiHeader(headers);
expect(newHeaders).to.have.property('kbn-system-api');
expect(newHeaders['kbn-system-api']).to.be(true);
expect(newHeaders).to.have.property('kbn-version');
expect(newHeaders['kbn-version']).to.be('4.6.0');
});
});
describe('#isSystemApiRequest', () => {
it ('returns true for a system API HTTP request', () => {
const mockRequest = {
headers: {
'kbn-system-api': true
}
};
expect(isSystemApiRequest(mockRequest)).to.be(true);
});
it ('returns false for a non-system API HTTP request', () => {
const mockRequest = {
headers: {}
};
expect(isSystemApiRequest(mockRequest)).to.be(false);
});
});
});

View file

@ -0,0 +1,26 @@
const SYSTEM_API_HEADER_NAME = 'kbn-system-api';
/**
* Adds a custom header designating request as system API
* @param originalHeaders Object representing set of headers
* @return Object representing set of headers, with system API header added in
*/
export function addSystemApiHeader(originalHeaders) {
const systemApiHeaders = {
[SYSTEM_API_HEADER_NAME]: true
};
return {
...originalHeaders,
...systemApiHeaders
};
}
/**
* Returns true if request is a system API request; false otherwise
*
* @param request Object Request object created by $http service
* @return true if request is a system API request; false otherwise
*/
export function isSystemApiRequest(request) {
return !!request.headers[SYSTEM_API_HEADER_NAME];
}

View file

@ -41,7 +41,7 @@ bdd.describe('discover tab', function describeIndexTests() {
return PageObjects.discover.getSidebarWidth()
.then(function (width) {
PageObjects.common.debug('expanded sidebar width = ' + width);
expect(width > 180).to.be(true);
expect(width > 20).to.be(true);
});
});
@ -66,7 +66,7 @@ bdd.describe('discover tab', function describeIndexTests() {
})
.then(function (width) {
PageObjects.common.debug('expanded sidebar width = ' + width);
expect(width > 180).to.be(true);
expect(width > 20).to.be(true);
});
});
});

View file

@ -142,7 +142,7 @@ bdd.describe('visualize app', function describeIndexTests() {
});
});
bdd.it('"Fit data bounds" should zoom to level 3', function pageHeader() {
bdd.it('Fit data bounds should zoom to level 3', function pageHeader() {
var expectedPrecision2ZoomCircles = [ { color: '#750000', radius: 192 },
{ color: '#750000', radius: 191 },
{ color: '#750000', radius: 177 },

View file

@ -17,7 +17,7 @@ bdd.describe('visualize app', function () {
bdd.before(function () {
var self = this;
remote.setWindowSize(1200,800);
remote.setWindowSize(1280,800);
PageObjects.common.debug('Starting visualize before method');
var logstash = scenarioManager.loadIfEmpty('logstashFunctional');

View file

@ -15,17 +15,19 @@ import {
import util from 'util';
import getUrl from '../../utils/get_url';
import {
config,
defaultTryTimeout,
defaultFindTimeout,
remote,
shieldPage
shieldPage,
esClient
} from '../index';
import {
Log,
Try,
Try
} from '../utils';
const mkdirpAsync = promisify(mkdirp);
@ -83,8 +85,23 @@ export default class Common {
function navigateTo(url) {
return self.try(function () {
// since we're using hash URLs, always reload first to force re-render
self.debug('navigate to: ' + url);
return self.remote.get(url)
return esClient.getDefaultIndex()
.then(function (defaultIndex) {
if (appName === 'discover' || appName === 'visualize' || appName === 'dashboard') {
if (!defaultIndex) {
// https://github.com/elastic/kibana/issues/7496
// Even though most tests are using esClient to set the default index, sometimes Kibana clobbers
// that change. If we got here, fix it.
self.debug(' >>>>>>>> WARNING Navigating to [' + appName + '] with defaultIndex=' + defaultIndex);
self.debug(' >>>>>>>> Setting defaultIndex to "logstash-*""');
return esClient.updateConfigDoc({'dateFormat:tz':'UTC', 'defaultIndex':'logstash-*'});
}
}
})
.then(function () {
self.debug('navigate to: ' + url);
return self.remote.get(url);
})
.then(function () {
return self.sleep(700);
})

View file

@ -51,6 +51,9 @@ export default class DiscoverPage {
return this.clickLoadSavedSearchButton()
.then(() => {
this.findTimeout.findByLinkText(searchName).click();
})
.then(() => {
return PageObjects.header.getSpinnerDone();
});
}
@ -79,8 +82,11 @@ export default class DiscoverPage {
}
getBarChartData() {
return this.findTimeout
.findAllByCssSelector('rect[data-label="Count"]')
return PageObjects.header.getSpinnerDone()
.then(() => {
return this.findTimeout
.findAllByCssSelector('rect[data-label="Count"]');
})
.then(function (chartData) {
function getChartData(chart) {
@ -128,13 +134,19 @@ export default class DiscoverPage {
return this.findTimeout
.findByCssSelector('option[label="' + interval + '"]')
.click();
})
.then(() => {
return PageObjects.header.getSpinnerDone();
});
}
getHitCount() {
return this.findTimeout
.findByCssSelector('strong.discover-info-hits')
.getVisibleText();
return PageObjects.header.getSpinnerDone()
.then(() => {
return this.findTimeout
.findByCssSelector('strong.discover-info-hits')
.getVisibleText();
});
}
query(queryString) {
@ -146,6 +158,9 @@ export default class DiscoverPage {
return this.findTimeout
.findByCssSelector('button[aria-label="Search"]')
.click();
})
.then(() => {
return PageObjects.header.getSpinnerDone();
});
}

View file

@ -68,12 +68,39 @@ export default (function () {
);
} else {
configId = response.hits.hits[0]._id;
Log.debug('config._id =' + configId);
Log.debug('config._id = ' + configId);
return configId;
}
});
},
/*
** Gets defaultIndex from the config doc.
*/
getDefaultIndex: function () {
var defaultIndex;
return this.client.search({
index: '.kibana',
type: 'config'
})
.then(function (response) {
if (response.errors) {
throw new Error(
'get config failed\n' +
response.items
.map(i => i[Object.keys(i)[0]].error)
.filter(Boolean)
.map(err => ' ' + JSON.stringify(err))
.join('\n')
);
} else {
defaultIndex = response.hits.hits[0]._source.defaultIndex;
Log.debug('config.defaultIndex = ' + defaultIndex);
return defaultIndex;
}
});
},
/**
* Add fields to the config doc (like setting timezone and defaultIndex)