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

This commit is contained in:
Fuyao Zhao 2019-03-18 17:52:37 -07:00
commit 3a1a49f354
192 changed files with 2923 additions and 2556 deletions

View file

@ -68,8 +68,8 @@ security is enabled, `xpack.security.encryptionKey`.
============
`xpack.reporting.queue.pollInterval`::
Specifies the number of milliseconds that idle workers wait between polling the
index for pending jobs. Defaults to `3000` (3 seconds).
Specifies the number of milliseconds that the reporting poller waits between polling the
index for any pending Reporting jobs. Defaults to `3000` (3 seconds).
[[xpack-reporting-q-timeout]]`xpack.reporting.queue.timeout`::
How long each worker has to produce a report. If your machine is slow or under

View file

@ -122,7 +122,8 @@ which is proxied through the Kibana server.
`kibana.index:`:: *Default: ".kibana"* Kibana uses an index in Elasticsearch to
store saved searches, visualizations and dashboards. Kibana creates a new index
if the index doesnt already exist.
if the index doesnt already exist. If you configure a custom index, the name must
be lowercase, and conform to {es} {ref}/indices-create-index.html[index name limitations].
`logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana
stores log output.

View file

@ -48,6 +48,7 @@
"test:ui:runner": "node scripts/functional_test_runner",
"test:server": "grunt test:server",
"test:coverage": "grunt test:coverage",
"typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts",
"checkLicenses": "grunt licenses --dev",
"build": "node scripts/build --all-platforms",
"start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ",
@ -411,6 +412,7 @@
"tslint-microsoft-contrib": "^6.0.0",
"tslint-plugin-prettier": "^2.0.0",
"typescript": "^3.3.3333",
"typings-tester": "^0.3.2",
"vinyl-fs": "^3.0.2",
"xml2js": "^0.4.19",
"xmlbuilder": "9.0.4",

View file

@ -0,0 +1,45 @@
/*
* 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 { BasePathService, BasePathStart } from './base_path_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<BasePathStart> = {
get: jest.fn(),
addToPath: jest.fn(),
removeFromPath: jest.fn(),
};
startContract.get.mockReturnValue('get');
startContract.addToPath.mockReturnValue('addToPath');
startContract.removeFromPath.mockReturnValue('removeFromPath');
return startContract;
};
type BasePathServiceContract = PublicMethodsOf<BasePathService>;
const createMock = () => {
const mocked: jest.Mocked<BasePathServiceContract> = {
start: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const basePathServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { BasePathService } from './base_path_service';
function setup(options: any = {}) {
@ -25,9 +26,8 @@ function setup(options: any = {}) {
const service = new BasePathService();
const injectedMetadata = {
getBasePath: jest.fn().mockReturnValue(injectedBasePath),
} as any;
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getBasePath.mockReturnValue(injectedBasePath);
const start = service.start({
injectedMetadata,

View file

@ -0,0 +1,60 @@
/*
* 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 { BehaviorSubject } from 'rxjs';
import { Brand, Breadcrumb, ChromeService, ChromeStart } from './chrome_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<ChromeStart> = {
setBrand: jest.fn(),
getBrand$: jest.fn(),
setIsVisible: jest.fn(),
getIsVisible$: jest.fn(),
setIsCollapsed: jest.fn(),
getIsCollapsed$: jest.fn(),
addApplicationClass: jest.fn(),
removeApplicationClass: jest.fn(),
getApplicationClasses$: jest.fn(),
getBreadcrumbs$: jest.fn(),
setBreadcrumbs: jest.fn(),
getHelpExtension$: jest.fn(),
setHelpExtension: jest.fn(),
};
startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as Brand));
startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false));
startContract.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));
startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name']));
startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as Breadcrumb]));
startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined));
return startContract;
};
type ChromeServiceContract = PublicMethodsOf<ChromeService>;
const createMock = () => {
const mocked: jest.Mocked<ChromeServiceContract> = {
start: jest.fn(),
stop: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const chromeServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -20,6 +20,9 @@
import * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
const store = new Map();
(window as any).localStorage = {
setItem: (key: string, value: string) => store.set(String(key), String(value)),
@ -31,22 +34,8 @@ import { ChromeService } from './chrome_service';
function defaultStartDeps(): any {
return {
notifications: {
toasts: {
addWarning: jest.fn(),
},
},
injectedMetadata: {
injectedMetadataStart: true,
getCspConfig: jest.fn().mockReturnValue({ warnLegacyBrowsers: true }),
getKibanaVersion: jest.fn().mockReturnValue('kibanaVersion'),
getLegacyMetadata: jest.fn().mockReturnValue({
uiSettings: {
defaults: { legacyInjectedUiSettingDefaults: true },
user: { legacyInjectedUiSettingUserValues: true },
},
}),
},
notifications: notificationServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
};
}

View file

@ -17,118 +17,72 @@
* under the License.
*/
import { BasePathService } from './base_path';
import { ChromeService } from './chrome';
import { FatalErrorsService } from './fatal_errors';
import { HttpService } from './http';
import { I18nService } from './i18n';
import { InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformService } from './legacy';
import { NotificationsService } from './notifications';
import { UiSettingsService } from './ui_settings';
const MockLegacyPlatformService = jest.fn<LegacyPlatformService, any>(
function _MockLegacyPlatformService(this: any) {
this.start = jest.fn();
this.stop = jest.fn();
return this;
}
);
import { basePathServiceMock } from './base_path/base_path_service.mock';
import { chromeServiceMock } from './chrome/chrome_service.mock';
import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from './http/http_service.mock';
import { i18nServiceMock } from './i18n/i18n_service.mock';
import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock';
import { legacyPlatformServiceMock } from './legacy/legacy_service.mock';
import { notificationServiceMock } from './notifications/notifications_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
const MockLegacyPlatformService = legacyPlatformServiceMock.create();
const LegacyPlatformServiceConstructor = jest
.fn()
.mockImplementation(() => MockLegacyPlatformService);
jest.mock('./legacy', () => ({
LegacyPlatformService: MockLegacyPlatformService,
LegacyPlatformService: LegacyPlatformServiceConstructor,
}));
const mockInjectedMetadataStart = {};
const MockInjectedMetadataService = jest.fn<InjectedMetadataService, any>(
function _MockInjectedMetadataService(this: any) {
this.start = jest.fn().mockReturnValue(mockInjectedMetadataStart);
return this;
}
);
const MockInjectedMetadataService = injectedMetadataServiceMock.create();
const InjectedMetadataServiceConstructor = jest
.fn()
.mockImplementation(() => MockInjectedMetadataService);
jest.mock('./injected_metadata', () => ({
InjectedMetadataService: MockInjectedMetadataService,
InjectedMetadataService: InjectedMetadataServiceConstructor,
}));
const mockFatalErrorsStart = {};
const MockFatalErrorsService = jest.fn<FatalErrorsService, any>(function _MockFatalErrorsService(
this: any
) {
this.start = jest.fn().mockReturnValue(mockFatalErrorsStart);
this.add = jest.fn();
return this;
});
const MockFatalErrorsService = fatalErrorsServiceMock.create();
const FatalErrorsServiceConstructor = jest.fn().mockImplementation(() => MockFatalErrorsService);
jest.mock('./fatal_errors', () => ({
FatalErrorsService: MockFatalErrorsService,
FatalErrorsService: FatalErrorsServiceConstructor,
}));
const mockI18nStart = {};
const MockI18nService = jest.fn<I18nService, any>(function _MockI18nService(this: any) {
this.start = jest.fn().mockReturnValue(mockI18nStart);
this.stop = jest.fn();
return this;
});
const MockI18nService = i18nServiceMock.create();
const I18nServiceConstructor = jest.fn().mockImplementation(() => MockI18nService);
jest.mock('./i18n', () => ({
I18nService: MockI18nService,
I18nService: I18nServiceConstructor,
}));
const mockNotificationStart = {};
const MockNotificationsService = jest.fn<NotificationsService, any>(
function _MockNotificationsService(this: any) {
this.start = jest.fn().mockReturnValue(mockNotificationStart);
this.add = jest.fn();
this.stop = jest.fn();
return this;
}
);
const MockNotificationsService = notificationServiceMock.create();
const NotificationServiceConstructor = jest.fn().mockImplementation(() => MockNotificationsService);
jest.mock('./notifications', () => ({
NotificationsService: MockNotificationsService,
NotificationsService: NotificationServiceConstructor,
}));
const mockHttp = {};
const MockHttpService = jest.fn<HttpService, any>(function _MockNotificationsService(this: any) {
this.start = jest.fn().mockReturnValue(mockHttp);
this.stop = jest.fn();
return this;
});
const MockHttpService = httpServiceMock.create();
const HttpServiceConstructor = jest.fn().mockImplementation(() => MockHttpService);
jest.mock('./http', () => ({
HttpService: MockHttpService,
HttpService: HttpServiceConstructor,
}));
const mockBasePathStart = {};
const MockBasePathService = jest.fn<BasePathService, any>(function _MockNotificationsService(
this: any
) {
this.start = jest.fn().mockReturnValue(mockBasePathStart);
return this;
});
const MockBasePathService = basePathServiceMock.create();
const BasePathServiceConstructor = jest.fn().mockImplementation(() => MockBasePathService);
jest.mock('./base_path', () => ({
BasePathService: MockBasePathService,
BasePathService: BasePathServiceConstructor,
}));
const mockUiSettings = {};
const MockUiSettingsService = jest.fn<UiSettingsService, any>(function _MockNotificationsService(
this: any
) {
this.start = jest.fn().mockReturnValue(mockUiSettings);
this.stop = jest.fn();
return this;
});
const MockUiSettingsService = uiSettingsServiceMock.create();
const UiSettingsServiceConstructor = jest.fn().mockImplementation(() => MockUiSettingsService);
jest.mock('./ui_settings', () => ({
UiSettingsService: MockUiSettingsService,
UiSettingsService: UiSettingsServiceConstructor,
}));
const mockChromeStart = {};
const MockChromeService = jest.fn<ChromeService, any>(function _MockNotificationsService(
this: any
) {
this.start = jest.fn().mockReturnValue(mockChromeStart);
this.stop = jest.fn();
return this;
});
const MockChromeService = chromeServiceMock.create();
const ChromeServiceConstructor = jest.fn().mockImplementation(() => MockChromeService);
jest.mock('./chrome', () => ({
ChromeService: MockChromeService,
ChromeService: ChromeServiceConstructor,
}));
import { CoreSystem } from './core_system';
@ -149,52 +103,52 @@ beforeEach(() => {
jest.clearAllMocks();
});
function createCoreSystem(params = {}) {
return new CoreSystem({
...defaultCoreSystemParams,
...params,
});
}
describe('constructor', () => {
it('creates instances of services', () => {
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
});
createCoreSystem();
expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
expect(MockI18nService).toHaveBeenCalledTimes(1);
expect(MockFatalErrorsService).toHaveBeenCalledTimes(1);
expect(MockNotificationsService).toHaveBeenCalledTimes(1);
expect(MockHttpService).toHaveBeenCalledTimes(1);
expect(MockBasePathService).toHaveBeenCalledTimes(1);
expect(MockUiSettingsService).toHaveBeenCalledTimes(1);
expect(MockChromeService).toHaveBeenCalledTimes(1);
expect(InjectedMetadataServiceConstructor).toHaveBeenCalledTimes(1);
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1);
expect(I18nServiceConstructor).toHaveBeenCalledTimes(1);
expect(FatalErrorsServiceConstructor).toHaveBeenCalledTimes(1);
expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1);
expect(HttpServiceConstructor).toHaveBeenCalledTimes(1);
expect(BasePathServiceConstructor).toHaveBeenCalledTimes(1);
expect(UiSettingsServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
});
it('passes injectedMetadata param to InjectedMetadataService', () => {
const injectedMetadata = { injectedMetadata: true } as any;
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
createCoreSystem({
injectedMetadata,
});
expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1);
expect(MockInjectedMetadataService).toHaveBeenCalledWith({
expect(InjectedMetadataServiceConstructor).toHaveBeenCalledTimes(1);
expect(InjectedMetadataServiceConstructor).toHaveBeenCalledWith({
injectedMetadata,
});
});
it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => {
const requireLegacyFiles = { requireLegacyFiles: true } as any;
const useLegacyTestHarness = { useLegacyTestHarness: true } as any;
const requireLegacyFiles = { requireLegacyFiles: true };
const useLegacyTestHarness = { useLegacyTestHarness: true };
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
createCoreSystem({
requireLegacyFiles,
useLegacyTestHarness,
});
expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1);
expect(MockLegacyPlatformService).toHaveBeenCalledWith({
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1);
expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({
targetDomElement: expect.any(HTMLElement),
requireLegacyFiles,
useLegacyTestHarness,
@ -202,46 +156,39 @@ describe('constructor', () => {
});
it('passes a dom element to NotificationsService', () => {
// tslint:disable no-unused-expression
new CoreSystem({
...defaultCoreSystemParams,
});
createCoreSystem();
expect(MockNotificationsService).toHaveBeenCalledTimes(1);
expect(MockNotificationsService).toHaveBeenCalledWith({
expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1);
expect(NotificationServiceConstructor).toHaveBeenCalledWith({
targetDomElement: expect.any(HTMLElement),
});
});
it('passes browserSupportsCsp to ChromeService', () => {
new CoreSystem({
...defaultCoreSystemParams,
});
createCoreSystem();
expect(MockChromeService).toHaveBeenCalledTimes(1);
expect(MockChromeService).toHaveBeenCalledWith({
expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1);
expect(ChromeServiceConstructor).toHaveBeenCalledWith({
browserSupportsCsp: expect.any(Boolean),
});
});
it('passes injectedMetadata, rootDomElement, and a stopCoreSystem function to FatalErrorsService', () => {
const rootDomElement = document.createElement('div');
const injectedMetadata = { injectedMetadata: true } as any;
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
const coreSystem = createCoreSystem({
rootDomElement,
injectedMetadata,
});
expect(MockFatalErrorsService).toHaveBeenCalledTimes(1);
expect(MockFatalErrorsService).toHaveBeenLastCalledWith({
expect(FatalErrorsServiceConstructor).toHaveBeenCalledTimes(1);
expect(FatalErrorsServiceConstructor).toHaveBeenLastCalledWith({
rootDomElement,
injectedMetadata: expect.any(MockInjectedMetadataService),
injectedMetadata: MockInjectedMetadataService,
stopCoreSystem: expect.any(Function),
});
const [{ stopCoreSystem }] = MockFatalErrorsService.mock.calls[0];
const [{ stopCoreSystem }] = FatalErrorsServiceConstructor.mock.calls[0];
expect(coreSystem.stop).not.toHaveBeenCalled();
stopCoreSystem();
@ -251,75 +198,55 @@ describe('constructor', () => {
describe('#stop', () => {
it('calls legacyPlatform.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const coreSystem = createCoreSystem();
const [legacyPlatformService] = MockLegacyPlatformService.mock.instances;
expect(legacyPlatformService.stop).not.toHaveBeenCalled();
expect(MockLegacyPlatformService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(legacyPlatformService.stop).toHaveBeenCalled();
expect(MockLegacyPlatformService.stop).toHaveBeenCalled();
});
it('calls notifications.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const coreSystem = createCoreSystem();
const [notificationsService] = MockNotificationsService.mock.instances;
expect(notificationsService.stop).not.toHaveBeenCalled();
expect(MockNotificationsService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(notificationsService.stop).toHaveBeenCalled();
expect(MockNotificationsService.stop).toHaveBeenCalled();
});
it('calls http.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const [httpService] = MockHttpService.mock.instances;
expect(httpService.stop).not.toHaveBeenCalled();
const coreSystem = createCoreSystem();
expect(MockHttpService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(httpService.stop).toHaveBeenCalled();
expect(MockHttpService.stop).toHaveBeenCalled();
});
it('calls chrome.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const coreSystem = createCoreSystem();
const [chromeService] = MockChromeService.mock.instances;
expect(chromeService.stop).not.toHaveBeenCalled();
expect(MockChromeService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(chromeService.stop).toHaveBeenCalled();
expect(MockChromeService.stop).toHaveBeenCalled();
});
it('calls uiSettings.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const coreSystem = createCoreSystem();
const [uiSettings] = MockUiSettingsService.mock.instances;
expect(uiSettings.stop).not.toHaveBeenCalled();
expect(MockUiSettingsService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(uiSettings.stop).toHaveBeenCalled();
expect(MockUiSettingsService.stop).toHaveBeenCalled();
});
it('calls i18n.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const coreSystem = createCoreSystem();
const [i18n] = MockI18nService.mock.instances;
expect(i18n.stop).not.toHaveBeenCalled();
expect(MockI18nService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(i18n.stop).toHaveBeenCalled();
expect(MockI18nService.stop).toHaveBeenCalled();
});
it('clears the rootDomElement', () => {
const rootDomElement = document.createElement('div');
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
const coreSystem = createCoreSystem({
rootDomElement,
});
@ -332,8 +259,7 @@ describe('#stop', () => {
describe('#start()', () => {
function startCore(rootDomElement = defaultCoreSystemParams.rootDomElement) {
const core = new CoreSystem({
...defaultCoreSystemParams,
const core = createCoreSystem({
rootDomElement,
});
@ -349,94 +275,60 @@ describe('#start()', () => {
it('calls injectedMetadata#start()', () => {
startCore();
const [mockInstance] = MockInjectedMetadataService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
expect(MockInjectedMetadataService.start).toHaveBeenCalledTimes(1);
});
it('calls http#start()', () => {
startCore();
const [mockInstance] = MockHttpService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({
fatalErrors: mockFatalErrorsStart,
});
expect(MockHttpService.start).toHaveBeenCalledTimes(1);
});
it('calls basePath#start()', () => {
startCore();
const [mockInstance] = MockBasePathService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({
injectedMetadata: mockInjectedMetadataStart,
});
expect(MockBasePathService.start).toHaveBeenCalledTimes(1);
});
it('calls uiSettings#start()', () => {
startCore();
const [mockInstance] = MockUiSettingsService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({
notifications: mockNotificationStart,
http: mockHttp,
injectedMetadata: mockInjectedMetadataStart,
basePath: mockBasePathStart,
});
expect(MockUiSettingsService.start).toHaveBeenCalledTimes(1);
});
it('calls i18n#start()', () => {
startCore();
const [mockInstance] = MockI18nService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
expect(MockI18nService.start).toHaveBeenCalledTimes(1);
});
it('calls fatalErrors#start()', () => {
startCore();
const [mockInstance] = MockFatalErrorsService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({ i18n: mockI18nStart });
expect(MockFatalErrorsService.start).toHaveBeenCalledTimes(1);
});
it('calls notifications#start()', () => {
startCore();
const [mockInstance] = MockNotificationsService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({ i18n: mockI18nStart });
expect(MockNotificationsService.start).toHaveBeenCalledTimes(1);
});
it('calls chrome#start()', () => {
startCore();
const [mockInstance] = MockChromeService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith({
notifications: mockNotificationStart,
injectedMetadata: mockInjectedMetadataStart,
});
});
it('returns start contract', () => {
expect(startCore()).toEqual({ fatalErrors: mockFatalErrorsStart });
expect(MockChromeService.start).toHaveBeenCalledTimes(1);
});
});
describe('LegacyPlatform targetDomElement', () => {
it('only mounts the element when started, before starting the legacyPlatformService', () => {
const rootDomElement = document.createElement('div');
const core = new CoreSystem({
...defaultCoreSystemParams,
const core = createCoreSystem({
rootDomElement,
});
const [legacyPlatform] = MockLegacyPlatformService.mock.instances;
let targetDomElementParentInStart: HTMLElement;
(legacyPlatform as any).start.mockImplementation(() => {
MockLegacyPlatformService.start.mockImplementation(() => {
targetDomElementParentInStart = targetDomElement.parentElement;
});
// targetDomElement should not have a parent element when the LegacyPlatformService is constructed
const [[{ targetDomElement }]] = MockLegacyPlatformService.mock.calls;
const [[{ targetDomElement }]] = LegacyPlatformServiceConstructor.mock.calls;
expect(targetDomElement).toHaveProperty('parentElement', null);
// starting the core system should mount the targetDomElement as a child of the rootDomElement
@ -448,20 +340,20 @@ describe('LegacyPlatform targetDomElement', () => {
describe('Notifications targetDomElement', () => {
it('only mounts the element when started, before starting the notificationsService', () => {
const rootDomElement = document.createElement('div');
const core = new CoreSystem({
...defaultCoreSystemParams,
const core = createCoreSystem({
rootDomElement,
});
const [notifications] = MockNotificationsService.mock.instances;
let targetDomElementParentInStart: HTMLElement;
(notifications as any).start.mockImplementation(() => {
targetDomElementParentInStart = targetDomElement.parentElement;
});
MockNotificationsService.start.mockImplementation(
(): any => {
targetDomElementParentInStart = targetDomElement.parentElement;
}
);
// targetDomElement should not have a parent element when the LegacyPlatformService is constructed
const [[{ targetDomElement }]] = MockNotificationsService.mock.calls;
const [[{ targetDomElement }]] = NotificationServiceConstructor.mock.calls;
expect(targetDomElement).toHaveProperty('parentElement', null);
// starting the core system should mount the targetDomElement as a child of the rootDomElement

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 { FatalErrorsService, FatalErrorsStart } from './fatal_errors_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<FatalErrorsStart> = {
add: jest.fn<never, any>(() => undefined as never),
get$: jest.fn(),
};
return startContract;
};
type FatalErrorsServiceContract = PublicMethodsOf<FatalErrorsService>;
const createMock = () => {
const mocked: jest.Mocked<FatalErrorsServiceContract> = {
start: jest.fn(),
add: jest.fn<never, any>(() => undefined as never),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const fatalErrorsServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,42 @@
/*
* 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 { HttpService, HttpStart } from './http_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<HttpStart> = {
addLoadingCount: jest.fn(),
getLoadingCount$: jest.fn(),
};
return startContract;
};
type HttpServiceContract = PublicMethodsOf<HttpService>;
const createMock = () => {
const mocked: jest.Mocked<HttpServiceContract> = {
start: jest.fn(),
stop: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const httpServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -20,13 +20,12 @@
import * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock';
import { HttpService } from './http_service';
function setup() {
const service = new HttpService();
const fatalErrors: any = {
add: jest.fn(),
};
const fatalErrors = fatalErrorsServiceMock.createStartContract();
const start = service.start({ fatalErrors });
return { service, fatalErrors, start };

View file

@ -16,29 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
import { I18nService, I18nStart } from './i18n_service';
import _ from 'lodash';
import { uiModules } from './modules';
const NL_RE = /\n/g;
const events = 'keydown keypress keyup change';
const createStartContractMock = () => {
const startContract: jest.Mocked<I18nStart> = {
Context: jest.fn(),
};
return startContract;
};
uiModules.get('kibana')
.directive('elasticTextarea', function () {
return {
restrict: 'A',
link: function ($scope, $el) {
type I18nServiceContract = PublicMethodsOf<I18nService>;
const createMock = () => {
const mocked: jest.Mocked<I18nServiceContract> = {
start: jest.fn(),
stop: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
function resize() {
$el.attr('rows', _.size($el.val().match(NL_RE)) + 1);
}
$el.on(events, resize);
$scope.$evalAsync(resize);
$scope.$on('$destroy', function () {
$el.off(events, resize);
});
}
};
});
export const i18nServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { InjectedMetadataService, InjectedMetadataStart } from './injected_metadata_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<InjectedMetadataStart> = {
getBasePath: jest.fn(),
getKibanaVersion: jest.fn(),
getCspConfig: jest.fn(),
getLegacyMetadata: jest.fn(),
getInjectedVar: jest.fn(),
getInjectedVars: jest.fn(),
};
startContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
startContract.getKibanaVersion.mockReturnValue('kibanaVersion');
startContract.getLegacyMetadata.mockReturnValue({
uiSettings: {
defaults: { legacyInjectedUiSettingDefaults: true },
user: { legacyInjectedUiSettingUserValues: true },
},
} as any);
return startContract;
};
type InjectedMetadataServiceContract = PublicMethodsOf<InjectedMetadataService>;
const createMock = () => {
const mocked: jest.Mocked<InjectedMetadataServiceContract> = {
start: jest.fn(),
getKibanaVersion: jest.fn(),
getKibanaBuildNumber: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const injectedMetadataServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -40,7 +40,7 @@ Array [
]
`;
exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstraped to targetDomElement 1`] = `
exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement 1`] = `
<div
class="ng-scope"
/>

View file

@ -16,19 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { LegacyPlatformService } from './legacy_service';
import { uiModules } from '../modules';
const module = uiModules.get('kibana');
type LegacyPlatformServiceContract = PublicMethodsOf<LegacyPlatformService>;
const createMock = () => {
const mocked: jest.Mocked<LegacyPlatformServiceContract> = {
start: jest.fn(),
stop: jest.fn(),
};
return mocked;
};
module.directive('focusOn', ($timeout) => ({
restrict: 'A',
link: function (scope, elem, attrs) {
scope.$on(attrs.focusOn, () => {
$timeout(() => {
return elem.find('input,select')
.addBack('input,select')
.focus();
});
});
}
}));
export const legacyPlatformServiceMock = {
create: createMock,
};

View file

@ -18,7 +18,6 @@
*/
import angular from 'angular';
import * as Rx from 'rxjs';
const mockLoadOrder: string[] = [];
@ -142,34 +141,24 @@ jest.mock('ui/chrome/services/global_nav_state', () => {
};
});
import { basePathServiceMock } from '../base_path/base_path_service.mock';
import { chromeServiceMock } from '../chrome/chrome_service.mock';
import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { i18nServiceMock } from '../i18n/i18n_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { LegacyPlatformService } from './legacy_service';
const fatalErrorsStart = {} as any;
const notificationsStart = {
toasts: {},
} as any;
const injectedMetadataStart: any = {
getBasePath: jest.fn(),
getLegacyMetadata: jest.fn(),
};
const httpStart = {
addLoadingCount: jest.fn(),
getLoadingCount$: jest
.fn()
.mockImplementation(() => new Rx.Observable(observer => observer.next(0))),
};
const basePathStart = {
get: jest.fn(),
addToPath: jest.fn(),
removeFromPath: jest.fn(),
};
const uiSettingsStart: any = {};
const chromeStart: any = {};
const i18nStart: any = { Context: () => '' };
const basePathStart = basePathServiceMock.createStartContract();
const chromeStart = chromeServiceMock.createStartContract();
const fatalErrorsStart = fatalErrorsServiceMock.createStartContract();
const httpStart = httpServiceMock.createStartContract();
const i18nStart = i18nServiceMock.createStartContract();
const injectedMetadataStart = injectedMetadataServiceMock.createStartContract();
const notificationsStart = notificationServiceMock.createStartContract();
const uiSettingsStart = uiSettingsServiceMock.createStartContract();
const defaultParams = {
targetDomElement: document.createElement('div'),
@ -200,7 +189,7 @@ describe('#start()', () => {
describe('default', () => {
it('passes legacy metadata from injectedVars to ui/metadata', () => {
const legacyMetadata = { isLegacyMetadata: true };
injectedMetadataStart.getLegacyMetadata.mockReturnValue(legacyMetadata);
injectedMetadataStart.getLegacyMetadata.mockReturnValue(legacyMetadata as any);
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
@ -422,7 +411,7 @@ describe('#stop()', () => {
expect(targetDomElement).toMatchSnapshot();
});
it('destroys the angular scope and empties the targetDomElement if angular is bootstraped to targetDomElement', () => {
it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', () => {
const targetDomElement = document.createElement('div');
const scopeDestroySpy = jest.fn();
@ -431,7 +420,7 @@ describe('#stop()', () => {
targetDomElement,
});
// simulate bootstraping with a module "foo"
// simulate bootstrapping with a module "foo"
angular.module('foo', []).directive('bar', () => ({
restrict: 'E',
link($scope) {

28
src/core/public/mocks.ts Normal file
View file

@ -0,0 +1,28 @@
/*
* 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 { basePathServiceMock } from './base_path/base_path_service.mock';
export { chromeServiceMock } from './chrome/chrome_service.mock';
export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
export { httpServiceMock } from './http/http_service.mock';
export { i18nServiceMock } from './i18n/i18n_service.mock';
export { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock';
export { legacyPlatformServiceMock } from './legacy/legacy_service.mock';
export { notificationServiceMock } from './notifications/notifications_service.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';

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 { NotificationsService, NotificationsStart } from './notifications_service';
import { toastsServiceMock } from './toasts/toasts_service.mock';
import { ToastsStart } from './toasts/toasts_start';
const createStartContractMock = () => {
const startContract: jest.Mocked<NotificationsStart> = {
// we have to suppress type errors until decide how to mock es6 class
toasts: (toastsServiceMock.createStartContract() as unknown) as ToastsStart,
};
return startContract;
};
type NotificationsServiceContract = PublicMethodsOf<NotificationsService>;
const createMock = () => {
const mocked: jest.Mocked<NotificationsServiceContract> = {
start: jest.fn(),
stop: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const notificationServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,35 @@
/*
* 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 { ToastsStart } from './toasts_start';
const createStartContractMock = () => {
const startContract: jest.Mocked<PublicMethodsOf<ToastsStart>> = {
get$: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
addDanger: jest.fn(),
};
return startContract;
};
export const toastsServiceMock = {
createStartContract: createStartContractMock,
};

View file

@ -5,7 +5,9 @@ exports[`#start constructs UiSettingsClient and UiSettingsApi: UiSettingsApi arg
"calls": Array [
Array [
Object {
"basePathStart": true,
"addToPath": [MockFunction],
"get": [MockFunction],
"removeFromPath": [MockFunction],
},
"kibanaVersion",
],

View file

@ -22,12 +22,12 @@ import fetchMock from 'fetch-mock/es5/client';
import * as Rx from 'rxjs';
import { takeUntil, toArray } from 'rxjs/operators';
import { basePathServiceMock } from '../base_path/base_path_service.mock';
import { UiSettingsApi } from './ui_settings_api';
function setup() {
const basePath: any = {
addToPath: jest.fn(path => `/foo/bar${path}`),
};
const basePath = basePathServiceMock.createStartContract();
basePath.addToPath.mockImplementation(path => `/foo/bar${path}`);
const uiSettingsApi = new UiSettingsApi(basePath, 'v9.9.9');

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { UiSettingsService, UiSettingsStart } from './ui_settings_service';
const createStartContractMock = () => {
const startContract: jest.Mocked<PublicMethodsOf<UiSettingsStart>> = {
getAll: jest.fn(),
get: jest.fn(),
get$: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
isDeclared: jest.fn(),
isDefault: jest.fn(),
isCustom: jest.fn(),
isOverridden: jest.fn(),
overrideLocalDefault: jest.fn(),
getUpdate$: jest.fn(),
getSaved$: jest.fn(),
stop: jest.fn(),
};
// we have to suppress type errors until decide how to mock es6 class
return (startContract as unknown) as UiSettingsStart;
};
type UiSettingsServiceContract = PublicMethodsOf<UiSettingsService>;
const createMock = () => {
const mocked: jest.Mocked<UiSettingsServiceContract> = {
start: jest.fn(),
stop: jest.fn(),
};
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const uiSettingsServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -56,31 +56,19 @@ const MockUiSettingsClient = mockClass('./ui_settings_client', UiSettingsClient,
inst.stop = jest.fn();
});
// Load the service
import { basePathServiceMock } from '../base_path/base_path_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { UiSettingsService } from './ui_settings_service';
const httpStart = {
addLoadingCount: jest.fn(),
};
const httpStart = httpServiceMock.createStartContract();
const defaultDeps: any = {
notifications: {
notificationsStart: true,
},
const defaultDeps = {
notifications: notificationServiceMock.createStartContract(),
http: httpStart,
injectedMetadata: {
injectedMetadataStart: true,
getKibanaVersion: jest.fn().mockReturnValue('kibanaVersion'),
getLegacyMetadata: jest.fn().mockReturnValue({
uiSettings: {
defaults: { legacyInjectedUiSettingDefaults: true },
user: { legacyInjectedUiSettingUserValues: true },
},
}),
},
basePath: {
basePathStart: true,
},
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
basePath: basePathServiceMock.createStartContract(),
};
afterEach(() => {

View file

@ -21,12 +21,6 @@ import { ObjectToConfigAdapter } from './object_to_config_adapter';
import { ConfigService } from './config_service';
type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];
type PublicMethodsOf<T> = Pick<T, MethodKeysOf<T>>;
type ConfigSericeContract = PublicMethodsOf<ConfigService>;
const createConfigServiceMock = () => {
const mocked: jest.Mocked<ConfigSericeContract> = {

View file

@ -34,12 +34,6 @@ const createStartContractMock = () => {
return startContract;
};
type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];
type PublicMethodsOf<T> = Pick<T, MethodKeysOf<T>>;
type ElasticsearchServiceContract = PublicMethodsOf<ElasticsearchService>;
const createMock = () => {
const mocked: jest.Mocked<ElasticsearchServiceContract> = {

View file

@ -28,12 +28,6 @@ const createStartContractMock = () => {
return startContract;
};
type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];
type PublicMethodsOf<T> = Pick<T, MethodKeysOf<T>>;
type HttpSericeContract = PublicMethodsOf<HttpService>;
const createHttpServiceMock = () => {
const mocked: jest.Mocked<HttpSericeContract> = {

View file

@ -21,12 +21,6 @@
import { Logger } from './logger';
import { LoggingService } from './logging_service';
type MethodKeysOf<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
}[keyof T];
type PublicMethodsOf<T> = Pick<T, MethodKeysOf<T>>;
type LoggingServiceContract = PublicMethodsOf<LoggingService>;
type MockedLogger = jest.Mocked<Logger>;

23
src/core/server/mocks.ts Normal file
View file

@ -0,0 +1,23 @@
/*
* 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 { configServiceMock } from './config/config_service.mock';
export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
export { httpServiceMock } from './http/http_service.mock';
export { loggingServiceMock } from './logging/logging_service.mock';

View file

@ -120,7 +120,6 @@ export const TEMPORARILY_IGNORED_PATHS = [
'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/tlConfig.js',
'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json',
'src/fixtures/vislib/mock_data/terms/_seriesMultiple.js',
'src/legacy/ui/public/angular-bootstrap/accordion/accordion-group.html',
'src/legacy/ui/public/angular-bootstrap/bindHtml/bindHtml.js',
'src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-html-unsafe-popup.html',
'src/legacy/ui/public/angular-bootstrap/tooltip/tooltip-popup.html',
@ -138,7 +137,6 @@ export const TEMPORARILY_IGNORED_PATHS = [
'src/legacy/ui/public/assets/favicons/mstile-310x150.png',
'src/legacy/ui/public/assets/favicons/mstile-310x310.png',
'src/legacy/ui/public/assets/favicons/safari-pinned-tab.svg',
'src/legacy/ui/public/directives/__tests__/confirm-click.js',
'src/legacy/ui/public/styles/bootstrap/component-animations.less',
'src/legacy/ui/public/styles/bootstrap/input-groups.less',
'src/legacy/ui/public/styles/bootstrap/list-group.less',

View file

@ -132,6 +132,9 @@ export default function (kibana) {
'kql-telemetry': {
isNamespaceAgnostic: true,
},
'sample-data-telemetry': {
isNamespaceAgnostic: true,
},
},
injectDefaultVars(server, options) {

View file

@ -183,5 +183,15 @@
"type": "long"
}
}
},
"sample-data-telemetry": {
"properties": {
"installCount": {
"type": "long"
},
"unInstallCount": {
"type": "long"
}
}
}
}

View file

@ -19,7 +19,7 @@
import { createAction } from 'redux-actions';
import { KibanaAction } from '../../selectors/types';
import { PanelId, PanelsMap, PanelState } from '../selectors';
import { PanelId, PanelState, PanelStateMap } from '../selectors';
export enum PanelActionTypeKeys {
DELETE_PANEL = 'DELETE_PANEL',
@ -37,7 +37,7 @@ export interface UpdatePanelAction
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANEL, PanelState> {}
export interface UpdatePanelsAction
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANELS, PanelsMap> {}
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANELS, PanelStateMap> {}
export interface ResetPanelTitleAction
extends KibanaAction<PanelActionTypeKeys.RESET_PANEl_TITLE, PanelId> {}
@ -50,7 +50,8 @@ export interface SetPanelTitleActionPayload {
export interface SetPanelTitleAction
extends KibanaAction<PanelActionTypeKeys.SET_PANEl_TITLE, SetPanelTitleActionPayload> {}
export interface SetPanelsAction extends KibanaAction<PanelActionTypeKeys.SET_PANELS, PanelsMap> {}
export interface SetPanelsAction
extends KibanaAction<PanelActionTypeKeys.SET_PANELS, PanelStateMap> {}
export type PanelActions =
| DeletePanelAction
@ -66,5 +67,5 @@ export const resetPanelTitle = createAction<PanelId>(PanelActionTypeKeys.RESET_P
export const setPanelTitle = createAction<SetPanelTitleActionPayload>(
PanelActionTypeKeys.SET_PANEl_TITLE
);
export const updatePanels = createAction<PanelsMap>(PanelActionTypeKeys.UPDATE_PANELS);
export const setPanels = createAction<PanelsMap>(PanelActionTypeKeys.SET_PANELS);
export const updatePanels = createAction<PanelStateMap>(PanelActionTypeKeys.UPDATE_PANELS);
export const setPanels = createAction<PanelStateMap>(PanelActionTypeKeys.SET_PANELS);

View file

@ -20,20 +20,20 @@
import _ from 'lodash';
import { Reducer } from 'redux';
import { PanelActions, PanelActionTypeKeys, SetPanelTitleActionPayload } from '../actions';
import { PanelId, PanelsMap, PanelState } from '../selectors';
import { PanelId, PanelState, PanelStateMap } from '../selectors';
const deletePanel = (panels: PanelsMap, panelId: PanelId): PanelsMap => {
const deletePanel = (panels: PanelStateMap, panelId: PanelId): PanelStateMap => {
const panelsCopy = { ...panels };
delete panelsCopy[panelId];
return panelsCopy;
};
const updatePanel = (panels: PanelsMap, panelState: PanelState): PanelsMap => ({
const updatePanel = (panels: PanelStateMap, panelState: PanelState): PanelStateMap => ({
...panels,
[panelState.panelIndex]: panelState,
});
const updatePanels = (panels: PanelsMap, updatedPanels: PanelsMap): PanelsMap => {
const updatePanels = (panels: PanelStateMap, updatedPanels: PanelStateMap): PanelStateMap => {
const panelsCopy = { ...panels };
Object.values(updatedPanels).forEach(panel => {
panelsCopy[panel.panelIndex] = panel;
@ -41,7 +41,7 @@ const updatePanels = (panels: PanelsMap, updatedPanels: PanelsMap): PanelsMap =>
return panelsCopy;
};
const resetPanelTitle = (panels: PanelsMap, panelId: PanelId) => ({
const resetPanelTitle = (panels: PanelStateMap, panelId: PanelId) => ({
...panels,
[panelId]: {
...panels[panelId],
@ -49,7 +49,7 @@ const resetPanelTitle = (panels: PanelsMap, panelId: PanelId) => ({
},
});
const setPanelTitle = (panels: PanelsMap, payload: SetPanelTitleActionPayload) => ({
const setPanelTitle = (panels: PanelStateMap, payload: SetPanelTitleActionPayload) => ({
...panels,
[payload.panelId]: {
...panels[payload.panelId],
@ -57,9 +57,9 @@ const setPanelTitle = (panels: PanelsMap, payload: SetPanelTitleActionPayload) =
},
});
const setPanels = (panels: PanelsMap, newPanels: PanelsMap) => _.cloneDeep(newPanels);
const setPanels = (panels: PanelStateMap, newPanels: PanelStateMap) => _.cloneDeep(newPanels);
export const panelsReducer: Reducer<PanelsMap> = (panels = {}, action): PanelsMap => {
export const panelsReducer: Reducer<PanelStateMap> = (panels = {}, action): PanelStateMap => {
switch ((action as PanelActions).type) {
case PanelActionTypeKeys.DELETE_PANEL:
return deletePanel(panels, action.payload);

View file

@ -34,11 +34,11 @@ import {
EmbeddableReduxState,
EmbeddablesMap,
PanelId,
PanelsMap,
PanelState,
PanelStateMap,
} from './types';
export const getPanels = (dashboard: DashboardState): PanelsMap => dashboard.panels;
export const getPanels = (dashboard: DashboardState): PanelStateMap => dashboard.panels;
export const getPanel = (dashboard: DashboardState, panelId: PanelId): PanelState =>
getPanels(dashboard)[panelId];

View file

@ -65,7 +65,7 @@ export interface EmbeddableReduxState {
readonly lastReloadRequestTime: number;
}
export interface PanelsMap {
export interface PanelStateMap {
readonly [panelId: string]: PanelState;
}
@ -80,7 +80,7 @@ export interface DashboardMetadata {
export interface DashboardState {
readonly view: ViewState;
readonly panels: PanelsMap;
readonly panels: PanelStateMap;
readonly embeddables: EmbeddablesMap;
readonly metadata: DashboardMetadata;
}

View file

@ -22,7 +22,6 @@ import { management } from 'ui/management';
import './_view';
import './_objects';
import 'ace';
import 'ui/directives/confirm_click';
import { uiModules } from 'ui/modules';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';

View file

@ -12,7 +12,7 @@
// While we are on a small screen the visualization is below the
// editor. In this cases it needs a minimum height, since it would otherwise
// maybe end up with 0 height since it just gets the flexbox rest of the screen.
min-height: 400px;
min-height: $euiSizeL * 10;
}
}

View file

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { EuiToolTip, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isBoolean } from 'lodash';
function AddDeleteButtons(props) {
const { testSubj } = props;
@ -59,43 +60,85 @@ function AddDeleteButtons(props) {
</EuiFlexItem>
);
};
const createClone = () => {
let cloneBtn = null;
if (props.onClone && !props.disableAdd) {
cloneBtn = (
<EuiFlexItem grow={false}>
<EuiToolTip content={props.cloneTooltip}>
<EuiButtonIcon
data-test-subj={`${testSubj}CloneBtn`}
aria-label={props.cloneTooltip}
iconType="copy"
onClick={props.onClone}
/>
</EuiToolTip>
</EuiFlexItem>
);
}
return cloneBtn;
};
const createActivatePanel = () => {
let activatePanelBtn = null;
if (isBoolean(props.isPanelActive)) {
const tooltip = props.isPanelActive ? props.deactivatePanelTooltip : props.activatePanelTooltip;
const iconType = props.isPanelActive ? 'eye' : 'eyeClosed';
activatePanelBtn = (
<EuiFlexItem grow={false}>
<EuiToolTip content={tooltip} >
<EuiButtonIcon
data-test-subj={`${testSubj}ActivatePanelBtn`}
aria-label={tooltip}
iconType={iconType}
onClick={props.togglePanelActivation}
/>
</EuiToolTip>
</EuiFlexItem>
);
}
return activatePanelBtn;
};
const deleteBtn = createDelete();
const addBtn = createAdd();
let clone;
if (props.onClone && !props.disableAdd) {
clone = (
<EuiFlexItem grow={false}>
<EuiToolTip content={props.cloneTooltip}>
<EuiButtonIcon
data-test-subj={`${testSubj}CloneBtn`}
aria-label={props.cloneTooltip}
iconType="copy"
onClick={props.onClone}
/>
</EuiToolTip>
</EuiFlexItem>
);
}
const cloneBtn = createClone();
const activatePanelBtn = createActivatePanel();
return (
<EuiFlexGroup gutterSize="s" responsive={props.responsive} justifyContent="flexEnd">
{ clone }
{ addBtn }
{ deleteBtn }
{activatePanelBtn}
{cloneBtn}
{addBtn}
{deleteBtn}
</EuiFlexGroup>
);
}
AddDeleteButtons.defaultProps = {
testSubj: 'Add',
activeTooltip: i18n.translate('tsvb.addDeleteButtons.addButtonDefaultTooltip', { defaultMessage: 'Add' }),
addTooltip: i18n.translate('tsvb.addDeleteButtons.addButtonDefaultTooltip', { defaultMessage: 'Add' }),
deleteTooltip: i18n.translate('tsvb.addDeleteButtons.deleteButtonDefaultTooltip', { defaultMessage: 'Delete' }),
cloneTooltip: i18n.translate('tsvb.addDeleteButtons.cloneButtonDefaultTooltip', { defaultMessage: 'Clone' })
cloneTooltip: i18n.translate('tsvb.addDeleteButtons.cloneButtonDefaultTooltip', { defaultMessage: 'Clone' }),
activatePanelTooltip: i18n.translate('tsvb.addDeleteButtons.reEnableTooltip', { defaultMessage: 'Re-enable' }),
deactivatePanelTooltip: i18n.translate('tsvb.addDeleteButtons.temporarilyDisableTooltip', { defaultMessage: 'Temporarily Disable' }),
};
AddDeleteButtons.propTypes = {
addTooltip: PropTypes.string,
deleteTooltip: PropTypes.string,
cloneTooltip: PropTypes.string,
activatePanelTooltip: PropTypes.string,
deactivatePanelTooltip: PropTypes.string,
togglePanelActivation: PropTypes.func,
isPanelActive: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
onClone: PropTypes.func,

View file

@ -78,6 +78,11 @@ class AnnotationsEditor extends Component {
const fn = collectionActions.handleChange.bind(null, this.props);
fn(_.assign({}, model, part));
};
const togglePanelActivation = () => {
handleChange({
hidden: !model.hidden,
});
};
const htmlId = htmlIdGenerator(model.id);
const handleAdd = collectionActions.handleAdd
.bind(null, this.props, newAnnotation);
@ -249,6 +254,8 @@ class AnnotationsEditor extends Component {
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
togglePanelActivation={togglePanelActivation}
isPanelActive={!model.hidden}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -36,38 +36,43 @@ const lookup = {
metric,
timeseries,
gauge,
markdown
markdown,
};
class Series extends Component {
constructor(props) {
super(props);
this.state = {
visible: true,
selectedTab: 'metrics'
selectedTab: 'metrics',
};
this.handleChange = this.handleChange.bind(this);
this.switchTab = this.switchTab.bind(this);
this.toggleVisible = this.toggleVisible.bind(this);
}
switchTab(selectedTab) {
switchTab = (selectedTab) => {
this.setState({ selectedTab });
}
};
handleChange(part) {
handleChange = (part) => {
if (this.props.onChange) {
const { model } = this.props;
const doc = _.assign({}, model, part);
this.props.onChange(doc);
}
}
};
toggleVisible(e) {
togglePanelActivation = () => {
const { model } = this.props;
this.handleChange({
hidden: !model.hidden,
});
};
toggleVisible = (e) => {
e.preventDefault();
this.setState({ visible: !this.state.visible });
}
};
render() {
const { panel } = this.props;
@ -95,7 +100,8 @@ class Series extends Component {
style: this.props.style,
switchTab: this.switchTab,
toggleVisible: this.toggleVisible,
visible: this.state.visible
togglePanelActivation: this.togglePanelActivation,
visible: this.state.visible,
};
return (<Component {...params}/>);
}
@ -113,7 +119,7 @@ class Series extends Component {
}
Series.defaultProps = {
name: 'metrics'
name: 'metrics',
};
Series.propTypes = {

View file

@ -226,7 +226,7 @@ GaugeSeriesUi.propTypes = {
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
};
const GaugeSeries = injectI18n(GaugeSeriesUi);

View file

@ -48,6 +48,7 @@ function getColors(props) {
function GaugeVisualization(props) {
const { backgroundColor, model, visData } = props;
const colors = getColors(props);
const series = _.get(visData, `${model.id}.series`, [])
.filter(row => row)
.map((row, i) => {

View file

@ -196,7 +196,7 @@ MarkdownSeriesUi.propTypes = {
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
};
const MarkdownSeries = injectI18n(MarkdownSeriesUi);

View file

@ -193,6 +193,8 @@ function MetricSeriesUi(props) {
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
togglePanelActivation={props.togglePanelActivation}
isPanelActive={!model.hidden}
disableDelete={disableDelete}
disableAdd={disableAdd}
responsive={false}
@ -228,7 +230,8 @@ MetricSeriesUi.propTypes = {
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
};
const MetricSeries = injectI18n(MetricSeriesUi);

View file

@ -165,6 +165,8 @@ function TableSeries(props) {
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
togglePanelActivation={props.togglePanelActivation}
isPanelActive={!model.hidden}
disableDelete={disableDelete}
disableAdd={disableAdd}
responsive={false}
@ -198,7 +200,8 @@ TableSeries.propTypes = {
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
};
export default injectI18n(TableSeries);

View file

@ -57,6 +57,12 @@ class TableVis extends Component {
this.dateFormatter = new DateFormat({}, this.props.getConfig);
}
get visibleSeries() {
return _
.get(this.props, 'model.series', [])
.filter(series => !series.hidden);
}
renderRow = row => {
const { model } = this.props;
let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key;
@ -65,7 +71,7 @@ class TableVis extends Component {
rowDisplay = (<a href={url}>{rowDisplay}</a>);
}
const columns = row.series.filter(item => item).map(item => {
const column = model.series.find(c => c.id === item.id);
const column = this.visibleSeries.find(c => c.id === item.id);
if (!column) return null;
const formatter = tickFormatter(column.formatter, column.value_template, this.props.getConfig);
const value = formatter(item.last);
@ -74,14 +80,14 @@ class TableVis extends Component {
const trendIcon = item.slope > 0 ? 'sortUp' : 'sortDown';
trend = (
<span>
&nbsp; <EuiIcon type={trendIcon} color="subdued" />
&nbsp; <EuiIcon type={trendIcon} color="subdued"/>
</span>
);
}
const style = { color: getColor(column.color_rules, 'text', item.last) };
return (
<td key={`${row.key}-${item.id}`} data-test-subj="tvbTableVis__value" className="eui-textRight" style={style}>
<span>{ value }</span>
<span>{value}</span>
{trend}
</td>
);
@ -92,16 +98,17 @@ class TableVis extends Component {
{columns}
</tr>
);
}
};
renderHeader() {
const { model, uiState, onUiState } = this.props;
const stateKey = `${model.type}.sort`;
const sort = uiState.get(stateKey, {
column: '_default_',
order: 'asc'
order: 'asc',
});
const columns = model.series.map(item => {
const columns = this.visibleSeries.map(item => {
const metric = _.last(item.metrics);
const label = metric.type === 'percentile' ?
getPercentileLabel(metric, item) :
@ -125,7 +132,7 @@ class TableVis extends Component {
sortIcon = 'empty';
}
sortComponent = (
<EuiIcon type={sortIcon} />
<EuiIcon type={sortIcon}/>
);
}
let headerContent = (
@ -162,7 +169,7 @@ class TableVis extends Component {
sortIcon = 'empty';
}
const sortComponent = (
<EuiIcon type={sortIcon} />
<EuiIcon type={sortIcon}/>
);
const handleSortClick = () => {
let order;
@ -176,7 +183,7 @@ class TableVis extends Component {
return (
<tr>
<th className="eui-textLeft" scope="col" onClick={handleSortClick}>{label} {sortComponent}</th>
{ columns }
{columns}
</tr>
);
}
@ -201,14 +208,14 @@ class TableVis extends Component {
rows = (
<tr>
<td
colSpan={model.series.length + 1}
colSpan={this.visibleSeries.length + 1}
>
{message}
</td>
</tr>
);
}
return(
return (
<div className="tvbVis" data-test-subj="tableView">
<table className="table">
<thead>
@ -225,7 +232,7 @@ class TableVis extends Component {
}
TableVis.defaultProps = {
sort: {}
sort: {},
};
TableVis.propTypes = {
@ -236,7 +243,7 @@ TableVis.propTypes = {
onUiState: PropTypes.func,
uiState: PropTypes.object,
pageNumber: PropTypes.number,
getConfig: PropTypes.func
getConfig: PropTypes.func,
};
export default TableVis;

View file

@ -195,6 +195,8 @@ const TimeseriesSeries = injectI18n(function (props) {
cloneTooltip={intl.formatMessage({ id: 'tsvb.timeSeries.cloneSeriesTooltip', defaultMessage: 'Clone Series' })}
onDelete={onDelete}
onClone={props.onClone}
togglePanelActivation={props.togglePanelActivation}
isPanelActive={!model.hidden}
onAdd={onAdd}
disableDelete={disableDelete}
disableAdd={disableAdd}
@ -232,7 +234,8 @@ TimeseriesSeries.propTypes = {
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
};
export default injectI18n(TimeseriesSeries);

View file

@ -188,6 +188,8 @@ const TopNSeries = injectI18n(function (props) {
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
togglePanelActivation={props.togglePanelActivation}
isPanelActive={!model.hidden}
disableDelete={disableDelete}
disableAdd={disableAdd}
responsive={false}
@ -222,7 +224,8 @@ TopNSeries.propTypes = {
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
};
export default TopNSeries;

View file

@ -24,7 +24,8 @@ function validAnnotation(annotation) {
annotation.time_field &&
annotation.fields &&
annotation.icon &&
annotation.template;
annotation.template &&
!annotation.hidden;
}
export async function getAnnotations(req, panel, esQueryConfig, searchStrategy, capabilities) {

View file

@ -22,6 +22,7 @@ import handleErrorResponse from './handle_error_response';
import { getAnnotations } from './get_annotations';
import { SearchStrategiesRegister } from '../search_strategies/search_strategies_register';
import { getEsQueryConfig } from './helpers/get_es_query_uisettings';
import { getActiveSeries } from './helpers/get_active_series';
export async function getSeriesData(req, panel) {
const panelIndexPattern = panel.index_pattern;
@ -29,7 +30,9 @@ export async function getSeriesData(req, panel) {
const searchRequest = searchStrategy.getSearchRequest(req, panelIndexPattern);
const esQueryConfig = await getEsQueryConfig(req);
const bodiesPromises = panel.series.map(series => getSeriesRequestParams(req, panel, series, esQueryConfig, capabilities));
const bodiesPromises = getActiveSeries(panel)
.map(series => getSeriesRequestParams(req, panel, series, esQueryConfig, capabilities));
const body = (await Promise.all(bodiesPromises))
.reduce((acc, items) => acc.concat(items), []);

View file

@ -16,17 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
export const getActiveSeries = (panel) => {
const shouldNotApplyFilter = ['gauge', 'markdown'].includes(panel.type);
import { uiModules } from '../modules';
const module = uiModules.get('kibana');
return (panel.series || [])
.filter(series => !series.hidden || shouldNotApplyFilter);
};
module.directive('stringToNumber', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$formatters.push((value) => {
return parseFloat(value);
});
}
};
});

View file

@ -22,29 +22,32 @@ import processors from '../response_processors/table';
import getLastValue from '../../../../common/get_last_value';
import regression from 'regression';
import { first, get, set } from 'lodash';
import { getActiveSeries } from '../helpers/get_active_series';
export default function processBucket(panel) {
return bucket => {
const series = panel.series.map(series => {
const timeseries = get(bucket, `${series.id}.timeseries`);
const buckets = get(bucket, `${series.id}.buckets`);
const series = getActiveSeries(panel)
.map(series => {
const timeseries = get(bucket, `${series.id}.timeseries`);
const buckets = get(bucket, `${series.id}.buckets`);
if (!timeseries && buckets) {
const meta = get(bucket, `${series.id}.meta`);
const timeseries = {
buckets: get(bucket, `${series.id}.buckets`)
};
set(bucket, series.id, { meta, timeseries });
}
if (!timeseries && buckets) {
const meta = get(bucket, `${series.id}.meta`);
const timeseries = {
buckets: get(bucket, `${series.id}.buckets`),
};
set(bucket, series.id, { meta, timeseries });
}
const processor = buildProcessorFunction(processors, bucket, panel, series);
const result = first(processor([]));
if (!result) return null;
const data = get(result, 'data', []);
const linearRegression = regression.linear(data);
result.last = getLastValue(data);
result.slope = linearRegression.equation[0];
return result;
});
const processor = buildProcessorFunction(processors, bucket, panel, series);
const result = first(processor([]));
if (!result) return null;
const data = get(result, 'data', []);
const linearRegression = regression.linear(data);
result.last = getLastValue(data);
result.slope = linearRegression.equation[0];
return result;
});
return { key: bucket.key, series };
};
}

View file

@ -19,6 +19,7 @@
import Boom from 'boom';
import Joi from 'joi';
import { usage } from '../usage';
import { loadData } from './lib/load_data';
import { createIndexName } from './lib/create_index_name';
import {
@ -171,6 +172,9 @@ export const createInstallRoute = () => ({
.code(403);
}
// track the usage operation in a non-blocking way
usage(request).addInstall(params.id);
return h.response({
elasticsearchIndicesCreated: counts,
kibanaSavedObjectsLoaded: sampleDataset.savedObjects.length,

View file

@ -19,6 +19,7 @@
import _ from 'lodash';
import Joi from 'joi';
import { usage } from '../usage';
import { createIndexName } from './lib/create_index_name';
export const createUninstallRoute = () => ({
@ -70,6 +71,9 @@ export const createUninstallRoute = () => ({
}
}
// track the usage operation in a non-blocking way
usage(request).addUninstall(params.id);
return {};
},
},

View file

@ -29,6 +29,9 @@ import {
logsSpecProvider,
ecommerceSpecProvider
} from './data_sets';
import {
makeSampleDataUsageCollector
} from './usage';
export function sampleDataMixin(kbnServer, server) {
server.route(createListRoute());
@ -94,4 +97,6 @@ export function sampleDataMixin(kbnServer, server) {
server.registerSampleDataset(flightsSpecProvider);
server.registerSampleDataset(logsSpecProvider);
server.registerSampleDataset(ecommerceSpecProvider);
makeSampleDataUsageCollector(server);
}

View file

@ -17,24 +17,25 @@
* under the License.
*/
import { uiModules } from '../modules';
import * as Hapi from 'hapi';
import { fetchProvider } from './collector_fetch';
uiModules
.get('kibana')
.directive('validateLowercase', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, elem, attr, ctrl) {
ctrl.$validators.lowercase = function (modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid per lowercase rules
return true;
}
interface KbnServer extends Hapi.Server {
usage: any;
}
return viewValue.toLowerCase() === viewValue;
};
}
};
});
export function makeSampleDataUsageCollector(server: KbnServer) {
let index: string;
try {
index = server.config().get('kibana.index');
} catch (err) {
return; // kibana plugin is not enabled (test environment)
}
server.usage.collectorSet.register(
server.usage.collectorSet.makeUsageCollector({
type: 'sample-data',
fetch: fetchProvider(index),
})
);
}

View file

@ -0,0 +1,180 @@
/*
* 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 sinon from 'sinon';
import { fetchProvider } from './collector_fetch';
describe('Sample Data Fetch', () => {
let callClusterMock: sinon.SinonStub;
beforeEach(() => {
callClusterMock = sinon.stub();
});
test('uninitialized .kibana', async () => {
const fetch = fetchProvider('index');
const telemetry = await fetch(callClusterMock);
expect(telemetry).toMatchInlineSnapshot(`undefined`);
});
test('installed data set', async () => {
const fetch = fetchProvider('index');
callClusterMock.returns({
hits: {
hits: [
{
_id: 'sample-data-telemetry:test1',
_source: {
updated_at: '2019-03-13T22:02:09Z',
'sample-data-telemetry': { installCount: 1 },
},
},
],
},
});
const telemetry = await fetch(callClusterMock);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"installed": Array [
"test1",
],
"last_install_date": "2019-03-13T22:02:09.000Z",
"last_install_set": "test1",
"last_uninstall_date": null,
"last_uninstall_set": null,
"uninstalled": Array [],
}
`);
});
test('multiple installed data sets', async () => {
const fetch = fetchProvider('index');
callClusterMock.returns({
hits: {
hits: [
{
_id: 'sample-data-telemetry:test1',
_source: {
updated_at: '2019-03-13T22:02:09Z',
'sample-data-telemetry': { installCount: 1 },
},
},
{
_id: 'sample-data-telemetry:test2',
_source: {
updated_at: '2019-03-13T22:13:17Z',
'sample-data-telemetry': { installCount: 1 },
},
},
],
},
});
const telemetry = await fetch(callClusterMock);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"installed": Array [
"test1",
"test2",
],
"last_install_date": "2019-03-13T22:13:17.000Z",
"last_install_set": "test2",
"last_uninstall_date": null,
"last_uninstall_set": null,
"uninstalled": Array [],
}
`);
});
test('installed data set, missing counts', async () => {
const fetch = fetchProvider('index');
callClusterMock.returns({
hits: {
hits: [
{
_id: 'sample-data-telemetry:test1',
_source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} },
},
],
},
});
const telemetry = await fetch(callClusterMock);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"installed": Array [],
"last_install_date": null,
"last_install_set": null,
"last_uninstall_date": null,
"last_uninstall_set": null,
"uninstalled": Array [],
}
`);
});
test('installed and uninstalled data sets', async () => {
const fetch = fetchProvider('index');
callClusterMock.returns({
hits: {
hits: [
{
_id: 'sample-data-telemetry:test0',
_source: {
updated_at: '2019-03-13T22:29:32Z',
'sample-data-telemetry': { installCount: 4, unInstallCount: 4 },
},
},
{
_id: 'sample-data-telemetry:test1',
_source: {
updated_at: '2019-03-13T22:02:09Z',
'sample-data-telemetry': { installCount: 1 },
},
},
{
_id: 'sample-data-telemetry:test2',
_source: {
updated_at: '2019-03-13T22:13:17Z',
'sample-data-telemetry': { installCount: 1 },
},
},
],
},
});
const telemetry = await fetch(callClusterMock);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"installed": Array [
"test1",
"test2",
],
"last_install_date": "2019-03-13T22:13:17.000Z",
"last_install_set": "test2",
"last_uninstall_date": "2019-03-13T22:29:32.000Z",
"last_uninstall_set": "test0",
"uninstalled": Array [
"test0",
],
}
`);
});
});

View file

@ -0,0 +1,132 @@
/*
* 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 { get } from 'lodash';
import moment from 'moment';
interface SearchHit {
_id: string;
_source: {
'sample-data-telemetry': {
installCount?: number;
unInstallCount?: number;
};
updated_at: Date;
};
}
interface TelemetryResponse {
installed: string[];
uninstalled: string[];
last_install_date: moment.Moment | null;
last_install_set: string | null;
last_uninstall_date: moment.Moment | null;
last_uninstall_set: string | null;
}
export function fetchProvider(index: string) {
return async (callCluster: any) => {
const response = await callCluster('search', {
index,
body: {
query: { term: { type: { value: 'sample-data-telemetry' } } },
_source: { includes: ['sample-data-telemetry', 'type', 'updated_at'] },
},
filter_path: 'hits.hits._id,hits.hits._source',
ignore: [404],
});
const getLast = (
dataSet: string,
dataDate: moment.Moment,
accumSet: string | null,
accumDate: moment.Moment | null
) => {
let lastDate = accumDate;
let lastSet = accumSet;
if (!accumDate || (accumDate && dataDate > accumDate)) {
// if a max date has not been accumulated yet, or if the current date is the new max
lastDate = dataDate;
lastSet = dataSet;
}
return { lastDate, lastSet };
};
const initial: TelemetryResponse = {
installed: [],
uninstalled: [],
last_install_date: null,
last_install_set: null,
last_uninstall_date: null,
last_uninstall_set: null,
};
const hits: any[] = get(response, 'hits.hits', []);
if (hits == null || hits.length === 0) {
return;
}
return hits.reduce((telemetry: TelemetryResponse, hit: SearchHit) => {
const { installCount = 0, unInstallCount = 0 } = hit._source['sample-data-telemetry'] || {
installCount: 0,
unInstallCount: 0,
};
if (installCount === 0 && unInstallCount === 0) {
return telemetry;
}
const isSampleDataSetInstalled = installCount - unInstallCount > 0;
const dataSet = hit._id.replace('sample-data-telemetry:', ''); // sample-data-telemetry:ecommerce => ecommerce
const dataDate = moment.utc(hit._source.updated_at);
if (isSampleDataSetInstalled) {
const { lastDate, lastSet } = getLast(
dataSet,
dataDate,
telemetry.last_install_set,
telemetry.last_install_date
);
return {
...telemetry,
installed: telemetry.installed.concat(dataSet),
last_install_date: lastDate,
last_install_set: lastSet,
};
} else {
const { lastDate, lastSet } = getLast(
dataSet,
dataDate,
telemetry.last_uninstall_set,
telemetry.last_uninstall_date
);
return {
...telemetry,
uninstalled: telemetry.uninstalled.concat(dataSet),
last_uninstall_date: lastDate,
last_uninstall_set: lastSet,
};
}
}, initial);
};
}

View file

@ -0,0 +1,21 @@
/*
* 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 { makeSampleDataUsageCollector } from './collector';
export { usage } from './usage';

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 * as Hapi from 'hapi';
const SAVED_OBJECT_ID = 'sample-data-telemetry';
export function usage(request: Hapi.Request) {
const { server } = request;
const handleIncrementError = (err: Error) => {
if (err != null) {
server.log(['debug', 'sample_data', 'telemetry'], err.stack);
}
server.log(
['warning', 'sample_data', 'telemetry'],
`saved objects repository incrementCounter encountered an error: ${err}`
);
};
const {
savedObjects: { getSavedObjectsRepository },
} = server;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
return {
addInstall: async (dataSet: string) => {
try {
internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `installCount`);
} catch (err) {
handleIncrementError(err);
}
},
addUninstall: async (dataSet: string) => {
try {
internalRepository.incrementCounter(SAVED_OBJECT_ID, dataSet, `unInstallCount`);
} catch (err) {
handleIncrementError(err);
}
},
};
}

View file

@ -17,15 +17,10 @@
* under the License.
*/
import { uiModules } from '../modules';
const module = uiModules.get('kibana');
interface AggParam {
type: string;
name: string;
displayName?: string;
}
module.directive('uiSelectFocusOn', ($timeout) => ({
restrict: 'A',
require: 'uiSelect',
link: function (scope, elem, attrs, uiSelect) {
scope.$on(attrs.uiSelectFocusOn, () => {
$timeout(() => uiSelect.activate());
});
}
}));
export { AggParam };

View file

@ -20,6 +20,7 @@
import _ from 'lodash';
import { AggParams } from './agg_params';
import { fieldFormats } from '../registry/field_formats';
import { i18n } from '@kbn/i18n';
/**
* Generic AggType Constructor
@ -118,6 +119,7 @@ function AggType(config) {
if (config.customLabels !== false) {
this.params.push({
name: 'customLabel',
displayName: i18n.translate('common.ui.aggTypes.string.customLabel', { defaultMessage: 'Custom label' }),
type: 'string',
write: _.noop
});

View file

@ -311,6 +311,7 @@ export const termsBucketAgg = new BucketAggType({
},
{
name: 'exclude',
displayName: i18n.translate('common.ui.aggTypes.buckets.terms.excludeLabel', { defaultMessage: 'Exclude' }),
type: 'string',
advanced: true,
disabled: isNotType('string'),
@ -318,6 +319,7 @@ export const termsBucketAgg = new BucketAggType({
},
{
name: 'include',
displayName: i18n.translate('common.ui.aggTypes.buckets.terms.includeLabel', { defaultMessage: 'Include' }),
type: 'string',
advanced: true,
disabled: isNotType('string'),

View file

@ -1,13 +0,0 @@
<div class="form-group">
<div class="euiSpacer euiSpacer--m"></div>
<label for="visEditorStringInput{{agg.id}}{{aggParam.name}}">{{ aggParam.name | label }}</label>
<div>
<input
type="text"
id="visEditorStringInput{{agg.id}}{{aggParam.name}}"
ng-model="agg.params[aggParam.name]"
class="form-control"
>
</div>
</div>

View file

@ -0,0 +1,42 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { AggParamEditorProps } from '../../vis/editors/default';
function StringParamEditor({ agg, aggParam, value, setValue }: AggParamEditorProps<string>) {
return (
<EuiFormRow
label={aggParam.displayName || aggParam.name}
className="form-group"
fullWidth={true}
>
<EuiFieldText
value={value || ''}
data-test-subj={`visEditorStringInput${agg.id}${aggParam.name}`}
onChange={ev => setValue(ev.target.value)}
fullWidth={true}
/>
</EuiFormRow>
);
}
export { StringParamEditor };

View file

@ -17,4 +17,5 @@
* under the License.
*/
export { AggParam } from './agg_param';
export { AggType } from './agg_type';

View file

@ -17,16 +17,16 @@
* under the License.
*/
import editorHtml from '../controls/string.html';
import { BaseParamType } from './base';
import { createLegacyClass } from '../../utils/legacy_class';
import { StringParamEditor } from '../controls/string';
import { BaseParamType } from './base';
createLegacyClass(StringParamType).inherits(BaseParamType);
function StringParamType(config) {
StringParamType.Super.call(this, config);
}
StringParamType.prototype.editor = editorHtml;
StringParamType.prototype.editorComponent = StringParamEditor;
/**
* Write the aggregation parameter.
@ -45,3 +45,4 @@ StringParamType.prototype.write = function (aggConfig, output) {
};
export { StringParamType };

View file

@ -1,10 +0,0 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a href class="accordion-toggle" ng-click="toggleOpen()" accordion-transclude="heading"><span ng-class="{'text-muted': isDisabled}">{{heading}}</span></a>
</h4>
</div>
<div class="panel-collapse" collapse="!isOpen">
<div class="panel-body" ng-transclude></div>
</div>
</div>

View file

@ -1 +0,0 @@
<div class="panel-group" ng-transclude></div>

View file

@ -1,130 +0,0 @@
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
.constant('accordionConfig', {
closeOthers: true
})
.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
// This array keeps track of the accordion groups
this.groups = [];
// Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
this.closeOthers = function(openGroup) {
var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
if ( closeOthers ) {
angular.forEach(this.groups, function (group) {
if ( group !== openGroup ) {
group.isOpen = false;
}
});
}
};
// This is called from the accordion-group directive to add itself to the accordion
this.addGroup = function(groupScope) {
var that = this;
this.groups.push(groupScope);
groupScope.$on('$destroy', function (event) {
that.removeGroup(groupScope);
});
};
// This is called from the accordion-group directive when to remove itself
this.removeGroup = function(group) {
var index = this.groups.indexOf(group);
if ( index !== -1 ) {
this.groups.splice(index, 1);
}
};
}])
// The accordion directive simply sets up the directive controller
// and adds an accordion CSS class to itself element.
.directive('accordion', function () {
return {
restrict:'EA',
controller:'AccordionController',
transclude: true,
replace: false,
templateUrl: 'template/accordion/accordion.html'
};
})
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
.directive('accordionGroup', function() {
return {
require:'^accordion', // We need this directive to be inside an accordion
restrict:'EA',
transclude:true, // It transcludes the contents of the directive into the template
replace: true, // The element containing the directive will be replaced with the template
templateUrl:'template/accordion/accordion-group.html',
scope: {
heading: '@', // Interpolate the heading attribute onto this scope
isOpen: '=?',
isDisabled: '=?'
},
controller: function() {
this.setHeading = function(element) {
this.heading = element;
};
},
link: function(scope, element, attrs, accordionCtrl) {
accordionCtrl.addGroup(scope);
scope.$watch('isOpen', function(value) {
if ( value ) {
accordionCtrl.closeOthers(scope);
}
});
scope.toggleOpen = function() {
if ( !scope.isDisabled ) {
scope.isOpen = !scope.isOpen;
}
};
}
};
})
// Use accordion-heading below an accordion-group to provide a heading containing HTML
// <accordion-group>
// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
// </accordion-group>
.directive('accordionHeading', function() {
return {
restrict: 'EA',
transclude: true, // Grab the contents to be used as the heading
template: '', // In effect remove this element!
replace: true,
require: '^accordionGroup',
link: function(scope, element, attr, accordionGroupCtrl, transclude) {
// Pass the heading to the accordion-group controller
// so that it can be transcluded into the right place in the template
// [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
accordionGroupCtrl.setHeading(transclude(scope, function() {}));
}
};
})
// Use in the accordion-group template to indicate where you want the heading to be transcluded
// You must provide the property on the accordion-group controller that will hold the transcluded element
// <div class="accordion-group">
// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
// ...
// </div>
.directive('accordionTransclude', function() {
return {
require: '^accordionGroup',
link: function(scope, element, attr, controller) {
scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
if ( heading ) {
element.html('');
element.append(heading);
}
});
}
};
});

View file

@ -18,14 +18,3 @@ angular.module('ui.bootstrap.alert', [])
}
};
})
.directive('dismissOnTimeout', ['$timeout', function($timeout) {
return {
require: 'alert',
link: function(scope, element, attrs, alertCtrl) {
$timeout(function(){
alertCtrl.close();
}, parseInt(attrs.dismissOnTimeout, 10));
}
};
}]);

View file

@ -35,40 +35,4 @@ angular.module('ui.bootstrap.buttons', [])
});
}
};
})
.directive('btnCheckbox', function () {
return {
require: ['btnCheckbox', 'ngModel'],
controller: 'ButtonsController',
link: function (scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
function getTrueValue() {
return getCheckboxValue(attrs.btnCheckboxTrue, true);
}
function getFalseValue() {
return getCheckboxValue(attrs.btnCheckboxFalse, false);
}
function getCheckboxValue(attributeValue, defaultValue) {
var val = scope.$eval(attributeValue);
return angular.isDefined(val) ? val : defaultValue;
}
//model -> UI
ngModelCtrl.$render = function () {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
};
//ui->model
element.bind(buttonsCtrl.toggleEvent, function () {
scope.$apply(function () {
ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
ngModelCtrl.$render();
});
});
}
};
});

View file

@ -1,75 +0,0 @@
angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition'])
.directive('collapse', ['$transition', function ($transition) {
return {
link: function (scope, element, attrs) {
var initialAnimSkip = true;
var currentTransition;
function doTransition(change) {
var newTransition = $transition(element, change);
if (currentTransition) {
currentTransition.cancel();
}
currentTransition = newTransition;
newTransition.then(newTransitionDone, newTransitionDone);
return newTransition;
function newTransitionDone() {
// Make sure it's this transition, otherwise, leave it alone.
if (currentTransition === newTransition) {
currentTransition = undefined;
}
}
}
function expand() {
if (initialAnimSkip) {
initialAnimSkip = false;
expandDone();
} else {
element.removeClass('collapse').addClass('collapsing');
doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone);
}
}
function expandDone() {
element.removeClass('collapsing');
element.addClass('collapse in');
element.css({height: 'auto'});
}
function collapse() {
if (initialAnimSkip) {
initialAnimSkip = false;
collapseDone();
element.css({height: 0});
} else {
// CSS transitions don't work with height: auto, so we have to manually change the height to a specific value
element.css({ height: element[0].scrollHeight + 'px' });
//trigger reflow so a browser realizes that height was updated from auto to a specific value
var x = element[0].offsetWidth;
element.removeClass('collapse in').addClass('collapsing');
doTransition({ height: 0 }).then(collapseDone);
}
}
function collapseDone() {
element.removeClass('collapsing');
element.addClass('collapse');
}
scope.$watch(attrs.collapse, function (shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
expand();
}
});
}
};
}]);

View file

@ -1,126 +0,0 @@
angular.module('ui.bootstrap.dateparser', [])
.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
this.parsers = {};
var formatCodeToRegex = {
'yyyy': {
regex: '\\d{4}',
apply: function(value) { this.year = +value; }
},
'yy': {
regex: '\\d{2}',
apply: function(value) { this.year = +value + 2000; }
},
'y': {
regex: '\\d{1,4}',
apply: function(value) { this.year = +value; }
},
'MMMM': {
regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
},
'MMM': {
regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
},
'MM': {
regex: '0[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; }
},
'M': {
regex: '[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; }
},
'dd': {
regex: '[0-2][0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; }
},
'd': {
regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; }
},
'EEEE': {
regex: $locale.DATETIME_FORMATS.DAY.join('|')
},
'EEE': {
regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
}
};
function createParser(format) {
var map = [], regex = format.split('');
angular.forEach(formatCodeToRegex, function(data, code) {
var index = format.indexOf(code);
if (index > -1) {
format = format.split('');
regex[index] = '(' + data.regex + ')';
format[index] = '$'; // Custom symbol to define consumed part of format
for (var i = index + 1, n = index + code.length; i < n; i++) {
regex[i] = '';
format[i] = '$';
}
format = format.join('');
map.push({ index: index, apply: data.apply });
}
});
return {
regex: new RegExp('^' + regex.join('') + '$'),
map: orderByFilter(map, 'index')
};
}
this.parse = function(input, format) {
if ( !angular.isString(input) || !format ) {
return input;
}
format = $locale.DATETIME_FORMATS[format] || format;
if ( !this.parsers[format] ) {
this.parsers[format] = createParser(format);
}
var parser = this.parsers[format],
regex = parser.regex,
map = parser.map,
results = input.match(regex);
if ( results && results.length ) {
var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt;
for( var i = 1, n = results.length; i < n; i++ ) {
var mapper = map[i-1];
if ( mapper.apply ) {
mapper.apply.call(fields, results[i]);
}
}
if ( isValid(fields.year, fields.month, fields.date) ) {
dt = new Date( fields.year, fields.month, fields.date, fields.hours);
}
return dt;
}
};
// Check if date is valid for specific month (and year for February).
// Month: 0 = Jan, 1 = Feb, etc
function isValid(year, month, date) {
if ( month === 1 && date > 28) {
return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
}
if ( month === 3 || month === 5 || month === 8 || month === 10) {
return date < 31;
}
return true;
}
}]);

View file

@ -23,11 +23,9 @@ uiModules.get('kibana', [
angular.module('ui.bootstrap', [
'ui.bootstrap.tpls',
'ui.bootstrap.transition',
'ui.bootstrap.collapse',
'ui.bootstrap.alert',
'ui.bootstrap.bindHtml',
'ui.bootstrap.buttons',
'ui.bootstrap.dateparser',
'ui.bootstrap.position',
'ui.bootstrap.dropdown',
'ui.bootstrap.modal',
@ -35,7 +33,6 @@ angular.module('ui.bootstrap', [
'ui.bootstrap.tooltip',
'ui.bootstrap.popover',
'ui.bootstrap.progressbar',
'ui.bootstrap.rating',
'ui.bootstrap.tabs',
'ui.bootstrap.timepicker',
'ui.bootstrap.typeahead'
@ -53,7 +50,6 @@ angular.module('ui.bootstrap.tpls', [
'template/progressbar/bar.html',
'template/progressbar/progress.html',
'template/progressbar/progressbar.html',
'template/rating/rating.html',
'template/tabs/tab.html',
'template/tabs/tabset.html',
'template/timepicker/timepicker.html',
@ -61,19 +57,15 @@ angular.module('ui.bootstrap.tpls', [
'template/typeahead/typeahead-popup.html'
]);
import './accordion/accordion';
import './alert/alert';
import './bindHtml/bindHtml';
import './buttons/buttons';
import './collapse/collapse';
import './dateparser/dateparser';
import './dropdown/dropdown';
import './modal/modal';
import './pagination/pagination';
import './popover/popover';
import './position/position';
import './progressbar/progressbar';
import './rating/rating';
import './tabs/tabs';
import './timepicker/timepicker';
import './tooltip/tooltip';
@ -146,12 +138,6 @@ angular.module('template/progressbar/progressbar.html', []).run(['$templateCache
$templateCache.put('template/progressbar/progressbar.html', progressbar);
}]);
import rating from './rating/rating.html';
angular.module('template/rating/rating.html', []).run(['$templateCache', function($templateCache) {
$templateCache.put('template/rating/rating.html', rating);
}]);
import tab from './tabs/tab.html';
angular.module('template/tabs/tab.html', []).run(['$templateCache', function($templateCache) {

View file

@ -1,5 +0,0 @@
<span ng-mouseleave="reset()" ng-keydown="onKeydown($event)" tabindex="0" role="slider" aria-valuemin="0" aria-valuemax="{{range.length}}" aria-valuenow="{{value}}">
<i ng-repeat="r in range track by $index" ng-mouseenter="enter($index + 1)" ng-click="rate($index + 1)" class="fa" ng-class="$index < value && (r.stateOn || 'fa-star') || (r.stateOff || 'fa-star-o')">
<span class="euiScreenReaderOnly">({{ $index < value ? '*' : ' ' }})</span>
</i>
</span>

View file

@ -1,83 +0,0 @@
angular.module('ui.bootstrap.rating', [])
.constant('ratingConfig', {
max: 5,
stateOn: null,
stateOff: null
})
.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
var ngModelCtrl = { $setViewValue: angular.noop };
this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
$scope.range = this.buildTemplateObjects(ratingStates);
};
this.buildTemplateObjects = function(states) {
for (var i = 0, n = states.length; i < n; i++) {
states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
}
return states;
};
$scope.rate = function(value) {
if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
ngModelCtrl.$setViewValue(value);
ngModelCtrl.$render();
}
};
$scope.enter = function(value) {
if ( !$scope.readonly ) {
$scope.value = value;
}
$scope.onHover({value: value});
};
$scope.reset = function() {
$scope.value = ngModelCtrl.$viewValue;
$scope.onLeave();
};
$scope.onKeydown = function(evt) {
if (/(37|38|39|40)/.test(evt.which)) {
evt.preventDefault();
evt.stopPropagation();
$scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
}
};
this.render = function() {
$scope.value = ngModelCtrl.$viewValue;
};
}])
.directive('rating', function() {
return {
restrict: 'EA',
require: ['rating', 'ngModel'],
scope: {
readonly: '=?',
onHover: '&',
onLeave: '&'
},
controller: 'RatingController',
templateUrl: 'template/rating/rating.html',
replace: true,
link: function(scope, element, attrs, ctrls) {
var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if ( ngModelCtrl ) {
ratingCtrl.init( ngModelCtrl );
}
}
};
});

View file

@ -26,7 +26,6 @@ import '../config';
import '../courier';
import '../debounce';
import '../doc_title';
import '../elastic_textarea';
import '../es';
import '../events';
import '../fancy_forms';

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { basePathServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformInit__, initChromeBasePathApi } from './base_path';
function initChrome() {
@ -25,11 +26,7 @@ function initChrome() {
return chrome;
}
const newPlatformBasePath = {
get: jest.fn().mockReturnValue('get'),
addToPath: jest.fn().mockReturnValue('addToPath'),
removeFromPath: jest.fn().mockReturnValue('removeFromPath'),
};
const newPlatformBasePath = basePathServiceMock.createStartContract();
__newPlatformInit__(newPlatformBasePath);
beforeEach(() => {

View file

@ -19,14 +19,12 @@
import * as Rx from 'rxjs';
import { chromeServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformInit__, initChromeControlsApi } from './controls';
const newPlatformChrome = {
setIsVisible: jest.fn(),
getIsVisible$: jest.fn(),
};
const newPlatformChrome = chromeServiceMock.createStartContract();
__newPlatformInit__(newPlatformChrome as any);
__newPlatformInit__(newPlatformChrome);
function setup() {
const isVisible$ = new Rx.BehaviorSubject(true);

View file

@ -19,17 +19,12 @@
import * as Rx from 'rxjs';
import { chromeServiceMock } from '../../../../../core/public/mocks';
import { __newPlatformInit__, initChromeThemeApi } from './theme';
const newPlatformChrome = {
setBrand: jest.fn(),
getBrand$: jest.fn(),
addApplicationClass: jest.fn(),
removeApplicationClass: jest.fn(),
getApplicationClasses$: jest.fn(),
};
const newPlatformChrome = chromeServiceMock.createStartContract();
__newPlatformInit__(newPlatformChrome as any);
__newPlatformInit__(newPlatformChrome);
function setup() {
const brand$ = new Rx.BehaviorSubject({ logo: 'foo', smallLogo: 'foo' });

View file

@ -1,121 +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 angular from 'angular';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import $ from 'jquery';
import '../confirm_click';
import 'plugins/kibana/discover/index';
import sinon from 'sinon';
let $window;
let $parentScope;
let $scope;
let $elem;
const init = function (confirm) {
// Load the application
ngMock.module('kibana', function ($provide) {
$window = {
confirm: sinon.stub().returns(confirm)
};
$provide.value('$window', $window);
});
// Create the scope
ngMock.inject(function ($rootScope, $compile) {
// Give us a scope
$parentScope = $rootScope;
// Create the element
$elem = angular.element(
'<a confirm-click="runThis()">runThis</a>'
);
// And compile it
$compile($elem)($parentScope);
// Fire a digest cycle
$elem.scope().$digest();
// Grab the isolate scope so we can test it
$scope = $elem.scope();
// Add a function to check the run status of.
$scope.runThis = sinon.spy();
});
};
describe('confirmClick directive', function () {
describe('event handlers', function () {
let events;
beforeEach(function () {
init();
events = $._data($elem[0], 'events');
});
it('should get a click handler', function () {
expect(events).to.be.a(Object);
expect(events.click).to.be.a(Array);
});
it('should unbind click handlers when the scope is destroyed', function () {
$scope.$destroy();
expect(events.click).to.be(undefined);
});
});
describe('confirmed', function () {
beforeEach(() => init(true));
it('should trigger window.confirm when clicked', function () {
$elem.click();
expect($window.confirm.called).to.be(true);
});
it('should run the click function when positively confirmed', function () {
$elem.click();
expect($scope.runThis.called).to.be(true);
});
});
describe('not confirmed', function () {
beforeEach(() => init(false));
it('should not run the click function when canceled', function () {
$elem.click();
expect($scope.runThis.called).to.be(false);
});
});
});

View file

@ -1,120 +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 expect from 'expect.js';
import ngMock from 'ng_mock';
import '../validate_index_pattern';
// Load the kibana app dependencies.
describe('Validate index pattern directive', function () {
let $compile;
let $rootScope;
const noWildcardHtml = '<input type="text" ng-model="indexName" validate-index-pattern />';
const requiredHtml = '<input type="text" ng-model="indexName" validate-index-pattern required />';
const allowWildcardHtml = '<input type="text" ng-model="indexName" validate-index-pattern validate-index-pattern-allow-wildcard />';
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
function checkPattern(input, html) {
$rootScope.indexName = input;
const element = $compile(html)($rootScope);
$rootScope.$digest();
return element;
}
const emptyPatterns = [
undefined,
null,
''
];
const badPatterns = [
'.',
'..',
'foo\\bar',
'foo/bar',
'foo?bar',
'foo"bar',
'foo<bar',
'foo>bar',
'foo|bar',
'foo bar',
];
const goodPatterns = [
'...',
'foo',
'foo.bar',
'[foo-]YYYY-MM-DD',
'foo:bar',
'foo,bar',
];
const wildcardPatterns = [
'foo*',
'foo.bar*',
'foo.*'
];
badPatterns.forEach(function (pattern) {
it('should not accept index pattern: ' + pattern, function () {
const element = checkPattern(pattern, noWildcardHtml);
expect(element.hasClass('ng-invalid')).to.be(true);
expect(element.hasClass('ng-valid')).to.not.be(true);
});
});
goodPatterns.forEach(function (pattern) {
it('should accept index pattern: ' + pattern, function () {
const element = checkPattern(pattern, noWildcardHtml);
expect(element.hasClass('ng-invalid')).to.not.be(true);
expect(element.hasClass('ng-valid')).to.be(true);
});
});
emptyPatterns.forEach(function (pattern) {
it('should not accept index pattern: ' + pattern, function () {
const element = checkPattern(pattern, requiredHtml);
expect(element.hasClass('ng-invalid')).to.be(true);
expect(element.hasClass('ng-valid')).to.not.be(true);
});
});
it('should disallow wildcards by default', function () {
wildcardPatterns.forEach(function (pattern) {
const element = checkPattern(pattern, noWildcardHtml);
expect(element.hasClass('ng-invalid')).to.be(true);
expect(element.hasClass('ng-valid')).to.not.be(true);
});
});
it('should allow wildcards if the allow-wildcard attribute is present', function () {
wildcardPatterns.forEach(function (pattern) {
const element = checkPattern(pattern, allowWildcardHtml);
expect(element.hasClass('ng-invalid')).to.not.be(true);
expect(element.hasClass('ng-valid')).to.be(true);
});
});
});

View file

@ -1,44 +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 { uiModules } from '../modules';
uiModules
.get('kibana')
.directive('confirmClick', function ($window, i18n) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
$elem.bind('click', function () {
const message = attrs.confirmation || i18n('common.ui.directives.confirmClickButtonLabel', {
defaultMessage: 'Are you sure?'
});
if ($window.confirm(message)) { // eslint-disable-line no-alert
const action = attrs.confirmClick;
if (action) {
$scope.$apply($scope.$eval(action));
}
}
});
$scope.$on('$destroy', function () {
$elem.unbind('click');
});
},
};
});

View file

@ -1,102 +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 _ from 'lodash';
import $ from 'jquery';
import { uiModules } from '../modules';
const module = uiModules.get('kibana');
const html = '<span class="dropzone" ng-transclude></span>';
module.directive('fileUpload', function () {
return {
restrict: 'E',
transclude: true,
scope: {
onRead: '&',
onLocate: '&',
uploadSelector: '@'
},
template: html,
link: function ($scope, $elem, attrs) {
const $button = $elem.find($scope.uploadSelector);
const $dropzone = $elem.find('.dropzone');
const handleFile = (file) => {
if (_.isUndefined(file)) return;
if (_.has(attrs, 'onRead')) {
const reader = new FileReader();
reader.onload = function (e) {
$scope.$apply(function () {
$scope.onRead({ fileContents: e.target.result });
});
};
reader.readAsText(file);
}
if (_.has(attrs, 'onLocate')) {
$scope.$apply(function () {
$scope.onLocate({ file });
});
}
};
$dropzone.on('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
}
);
$dropzone.on('dragenter', function (e) {
e.preventDefault();
e.stopPropagation();
}
);
$dropzone.on('drop', function (e) {
e.stopPropagation();
e.preventDefault();
const file = _.get(e, 'originalEvent.dataTransfer.files[0]');
if (file) {
handleFile(file);
}
});
if ($button) {
const $fileInput = $('<input type="file" style="opacity: 0;' +
' display:none; position:absolute; right: -999999999px" id="testfile" />');
$elem.append($fileInput);
$fileInput.on('change', function (e) {
const target = e.srcElement || e.target;
if (_.get(target, 'files.length')) {
handleFile(target.files[0]);
}
});
$button.on('click', function () {
$fileInput.val(null);
$fileInput.trigger('click');
});
}
}
};
});

View file

@ -1,110 +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 { uiModules } from '../modules';
const module = uiModules.get('kibana');
import * as Rx from 'rxjs';
import { map, switchMap, share } from 'rxjs/operators';
const multipleUsageErrorMessage = 'Cannot use input-base-sixty-four directive on input with `multiple` attribute';
const createFileContent$ = (file) => {
return Rx.Observable.create(observer => {
const reader = new FileReader();
reader.onerror = (err) => {
observer.error(err);
};
reader.onload = () => {
observer.next(reader.result);
observer.complete();
};
reader.readAsDataURL(file);
return () => {
reader.abort();
};
});
};
module.directive('inputBaseSixtyFour', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: 'isolate',
link: function ($scope, $elem, attrs, ngModel) {
if ($elem.prop('multiple')) {
throw new Error(multipleUsageErrorMessage);
}
const maxSizeValidator = (dataUrl) => {
return {
errorKey: 'maxSize',
isValid: attrs.max === '' || dataUrl.length <= parseInt(attrs.max)
};
};
const validators = [ maxSizeValidator ];
// produce fileContent$ whenever the $element 'change' event is triggered.
const fileContent$ = Rx.fromEvent($elem, 'change', e => e).pipe(
map(e => e.target.files),
switchMap(files => {
if (files.length === 0) {
return [];
}
if (files.length > 1) {
throw new Error(multipleUsageErrorMessage);
}
return createFileContent$(files[0]);
}),
share()
);
// validate the content of the files after it is loaded
const validations$ = fileContent$.pipe(
map(fileContent => (
validators.map(validator => validator(fileContent))
))
);
// push results from input/validation to the ngModel
const unsubscribe = Rx
.combineLatest(fileContent$, validations$)
.subscribe(([ fileContent, validations ]) => {
$scope.$evalAsync(() => {
validations.forEach(validation => {
ngModel.$setValidity(validation.errorKey, validation.isValid);
});
if (validations.every(validation => validation.isValid)) {
ngModel.$setViewValue(fileContent);
}
});
}, (err) => {
throw err;
});
$scope.$on('destroy', unsubscribe);
}
};
});

View file

@ -1,50 +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 moment from 'moment';
import { uiModules } from '../modules';
const module = uiModules.get('kibana');
module.directive('inputDatetime', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $elem, attrs, ngModel) {
const format = 'YYYY-MM-DD HH:mm:ss.SSS';
$elem.after('<div class="input-datetime-format">' + format + '</div>');
// What should I make with the input from the user?
const fromUser = function (text) {
const parsed = moment(text, format);
return parsed.isValid() ? parsed : undefined;
};
// How should I present the data back to the user in the input field?
const toUser = function (datetime) {
return moment(datetime).format(format);
};
ngModel.$parsers.push(fromUser);
ngModel.$formatters.push(toUser);
}
};
});

View file

@ -1,57 +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 _ from 'lodash';
import { uiModules } from '../modules';
// See https://github.com/elastic/elasticsearch/issues/6736
uiModules
.get('kibana')
.directive('validateIndexPattern', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, elem, attr, ngModel) {
const illegalCharacters = ['\\', '/', '?', '"', '<', '>', '|', ' '];
const allowWildcard =
!_.isUndefined(attr.validateIndexPatternAllowWildcard)
&& attr.validateIndexPatternAllowWildcard !== 'false';
if (!allowWildcard) {
illegalCharacters.push('*');
}
const isValid = function (input) {
if (input == null || input === '') return !attr.required === true;
if (input === '.' || input === '..') return false;
const match = _.find(illegalCharacters, function (character) {
return input.indexOf(character) >= 0;
});
return !match;
};
ngModel.$validators.indexPattern = function (modelValue, viewValue) {
return isValid(viewValue);
};
}
};
});

View file

@ -20,7 +20,6 @@
import _ from 'lodash';
import html from './doc_table.html';
import { getSort } from './lib/get_sort';
import '../directives/truncated';
import '../directives/infinite_scroll';
import './components/table_header';
import './components/table_row';

View file

@ -19,8 +19,8 @@
import truncText from 'trunc-text';
import truncHTML from 'trunc-html';
import { uiModules } from '../modules';
import truncatedTemplate from './partials/truncated.html';
import { uiModules } from '../../modules';
import truncatedTemplate from '../partials/truncated.html';
import 'angular-sanitize';
const module = uiModules.get('kibana', ['ngSanitize']);

View file

@ -26,7 +26,7 @@ import { banners } from './banners';
import { Notifier } from './notifier';
import template from './partials/toaster.html';
import '../filters/markdown';
import '../directives/truncated';
import './directives/truncated';
import { FormattedMessage } from '@kbn/i18n/react';
import {

View file

@ -9,10 +9,12 @@
}
}
// IE specific fix for the datepicker to not collapse
@include euiBreakpoint('m', 'l', 'xl') {
.kbnQueryBar__datePickerWrapper {
max-width: 40vw;
flex-grow: 0;
flex-basis: auto;
// sass-lint:disable-block no-important
flex-grow: 0 !important;
flex-basis: auto !important;
}
}

View file

@ -26,7 +26,6 @@ import {
import {
EuiConfirmModal,
EuiIcon,
EuiColorPicker,
EuiIconTip,
EuiCallOut,
EuiSuperDatePicker,
@ -42,8 +41,6 @@ app.directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal))
app.directive('icon', reactDirective => reactDirective(EuiIcon));
app.directive('colorPicker', reactDirective => reactDirective(EuiColorPicker));
app.directive('iconTip', reactDirective => reactDirective(EuiIconTip, ['content', 'type', 'position', 'title', 'color']));
app.directive('callOut', reactDirective => reactDirective(EuiCallOut, ['title', 'color', 'size', 'iconType', 'children']));

View file

@ -104,7 +104,7 @@ describe('Vis-Editor-Agg-Params plugin directive', function () {
aggFilter: aggFilter
});
const customLabelElement = $elem.find('label:contains("Custom Label")');
const customLabelElement = $elem.find('label:contains("Custom label")');
expect(customLabelElement.length).to.be(1);
});
@ -117,7 +117,7 @@ describe('Vis-Editor-Agg-Params plugin directive', function () {
aggFilter: aggFilter
});
const customLabelElement = $elem.find('label:contains("Custom Label")');
const customLabelElement = $elem.find('label:contains("Custom label")');
expect(customLabelElement.length).to.be(0);
});
});

View file

@ -17,32 +17,82 @@
* under the License.
*/
import _ from 'lodash';
import { isFunction } from 'lodash';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from '../../../modules';
import { AggParamReactWrapper } from './agg_param_react_wrapper';
uiModules
.get('app/visualize')
.directive('visAggParamReactWrapper', reactDirective => reactDirective(wrapInI18nContext(AggParamReactWrapper), [
['agg', { watchDepth: 'collection' }],
['aggParam', { watchDepth: 'reference' }],
['paramEditor', { wrapApply: false }],
['onChange', { watchDepth: 'reference' }],
'value',
]))
.directive('visAggParamEditor', function (config) {
return {
restrict: 'E',
// We can't use scope binding here yet, since quiet a lot of child directives arbitrary access
// parent scope values right now. So we cannot easy change this, until we remove the whole directive.
scope: true,
template: function ($el) {
require: '?^ngModel',
template: function ($el, attrs) {
if (attrs.editorComponent) {
// Why do we need the `ng-if` here?
// Short answer: Preventing black magic
// Longer answer: The way this component is mounted in agg_params.js (by manually compiling)
// and adding to some array, once you switch an aggregation type, this component will once
// render once with a "broken state" (something like new aggParam, but still old template),
// before agg_params.js actually removes it from the DOM and create a correct version with
// the correct template. That ng-if check prevents us from crashing during that broken render.
return `<vis-agg-param-react-wrapper
ng-if="editorComponent"
param-editor="editorComponent"
agg="agg"
agg-param="aggParam"
on-change="onChange"
value="paramValue"
></vis-agg-param-react-wrapper>`;
}
return $el.html();
},
link: {
pre: function ($scope, $el, attr) {
$scope.$bind('aggParam', attr.aggParam);
$scope.$bind('agg', attr.agg);
$scope.$bind('editorComponent', attr.editorComponent);
},
post: function ($scope) {
post: function ($scope, $el, attr, ngModelCtrl) {
$scope.config = config;
$scope.optionEnabled = function (option) {
if (option && _.isFunction(option.enabled)) {
if (option && isFunction(option.enabled)) {
return option.enabled($scope.agg);
}
return true;
};
if (attr.editorComponent) {
$scope.$watch('agg.params[aggParam.name]', (value) => {
// Whenever the value of the parameter changed (e.g. by a reset or actually by calling)
// we store the new value in $scope.paramValue, which will be passed as a new value to the react component.
$scope.paramValue = value;
}, true);
}
$scope.onChange = (value) => {
// This is obviously not a good code quality, but without using scope binding (which we can't see above)
// to bind function values, this is right now the best temporary fix, until all of this will be gone.
$scope.$parent.onParamChange($scope.agg, $scope.aggParam.name, value);
if(ngModelCtrl) {
ngModelCtrl.$setDirty();
}
};
}
}
};

View file

@ -0,0 +1,30 @@
/*
* 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 { AggParam } from '../../../agg_types';
import { AggConfig } from '../../agg_config';
interface AggParamEditorProps<T> {
agg: AggConfig;
aggParam: AggParam;
value: T;
setValue(value: T): void;
}
export { AggParamEditorProps };

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { AggParam } from '../../../agg_types';
import { AggConfig } from '../../agg_config';
import { AggParamEditorProps } from './agg_param_editor_props';
interface AggParamReactWrapperProps<T> {
agg: AggConfig;
aggParam: AggParam;
paramEditor: React.FunctionComponent<AggParamEditorProps<T>>;
value: T;
onChange(value: T): void;
}
function AggParamReactWrapper<T>(props: AggParamReactWrapperProps<T>) {
const { agg, aggParam, paramEditor: ParamEditor, onChange, value } = props;
return <ParamEditor value={value} setValue={onChange} aggParam={aggParam} agg={agg} />;
}
export { AggParamReactWrapper };

View file

@ -18,18 +18,18 @@
*/
import $ from 'jquery';
import { has, get } from 'lodash';
import aggSelectHtml from './agg_select.html';
import advancedToggleHtml from './advanced_toggle.html';
import '../../../filters/match_any';
import './agg_param';
import { get, has } from 'lodash';
import { aggTypes } from '../../../agg_types';
import { uiModules } from '../../../modules';
import { documentationLinks } from '../../../documentation_links/documentation_links';
import aggParamsTemplate from './agg_params.html';
import { aggTypeFilters } from '../../../agg_types/filter';
import { editorConfigProviders } from '../config/editor_config_providers';
import { aggTypeFieldFilters } from '../../../agg_types/param_types/filter';
import { documentationLinks } from '../../../documentation_links/documentation_links';
import '../../../filters/match_any';
import { uiModules } from '../../../modules';
import { editorConfigProviders } from '../config/editor_config_providers';
import advancedToggleHtml from './advanced_toggle.html';
import './agg_param';
import aggParamsTemplate from './agg_params.html';
import aggSelectHtml from './agg_select.html';
uiModules
.get('app/visualize')
@ -56,6 +56,12 @@ uiModules
updateEditorConfig('default');
});
$scope.onParamChange = (agg, paramName, value) => {
if(agg.params[paramName] !== value) {
agg.params[paramName] = value;
}
};
function updateEditorConfig(property = 'fixedValue') {
$scope.editorConfig = editorConfigProviders.getConfigForAgg(
aggTypes.byType[$scope.groupName],
@ -185,23 +191,36 @@ uiModules
// build HTML editor given an aggParam and index
function getAggParamHTML(param, idx) {
// don't show params without an editor
if (!param.editor) {
if (!param.editor && !param.editorComponent) {
return;
}
const attrs = {
'agg-param': 'agg.type.params[' + idx + ']'
'agg-param': 'agg.type.params[' + idx + ']',
'agg': 'agg',
};
if (param.advanced) {
attrs['ng-show'] = 'advancedToggled';
}
if (param.editorComponent) {
attrs['editor-component'] = `agg.type.params[${idx}].editorComponent`;
// The form should interact with reactified components as well.
// So we set the ng-model (using a random ng-model variable) to have the method to set dirty
// inside the agg_param.js directive, which can get access to the ngModelController to manipulate it.
attrs['ng-model'] = normalizeModelName(`_internalNgModelState${$scope.agg.id}${param.name}`);
}
return $('<vis-agg-param-editor>')
.attr(attrs)
.append(param.editor)
.get(0);
}
function normalizeModelName(modelName = '') {
return modelName.replace('-', '_');
}
}
};
});

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 { AggParamEditorProps } from './agg_param_editor_props';

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