Migrate graph server and licensing handling (#52279)

This commit is contained in:
Joe Reuter 2019-12-13 11:33:15 +01:00 committed by GitHub
parent 28e81f2b49
commit c73f984e7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 419 additions and 503 deletions

View file

@ -69,6 +69,7 @@ export interface LegacyPluginOptions {
noParse: string[];
home: string[];
mappings: any;
migrations: any;
savedObjectSchemas: SavedObjectsSchemaDefinition;
savedObjectsManagement: SavedObjectsManagementDefinition;
visTypes: string[];

View file

@ -32,6 +32,7 @@ import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/publ
import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public';
import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public';
import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public';
import { LicensingPluginSetup } from '../../../../../x-pack/plugins/licensing/common/types';
export interface PluginsSetup {
data: ReturnType<DataPlugin['setup']>;
@ -43,6 +44,7 @@ export interface PluginsSetup {
dev_tools: DevToolsSetup;
kibana_legacy: KibanaLegacySetup;
share: SharePluginSetup;
licensing: LicensingPluginSetup;
}
export interface PluginsStart {

View file

@ -12,7 +12,7 @@
"xpack.endpoint": "plugins/endpoint",
"xpack.features": "plugins/features",
"xpack.fileUpload": "legacy/plugins/file_upload",
"xpack.graph": "legacy/plugins/graph",
"xpack.graph": ["legacy/plugins/graph", "plugins/graph"],
"xpack.grokDebugger": "legacy/plugins/grokdebugger",
"xpack.idxMgmt": "legacy/plugins/index_management",
"xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management",

View file

@ -7,11 +7,12 @@
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import migrations from './migrations';
import { initServer } from './server';
import mappings from './mappings.json';
import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types';
export function graph(kibana) {
export const graph: LegacyPluginInitializer = kibana => {
return new kibana.Plugin({
id: 'graph',
configPrefix: 'xpack.graph',
@ -26,17 +27,17 @@ export function graph(kibana) {
main: 'plugins/graph/index',
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'],
home: ['plugins/graph/register_feature'],
mappings,
migrations,
},
config(Joi) {
config(Joi: any) {
return Joi.object({
enabled: Joi.boolean().default(true),
canEditDrillDownUrls: Joi.boolean().default(true),
savePolicy: Joi.string().valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']).default('configAndData'),
savePolicy: Joi.string()
.valid(['config', 'configAndDataWithConsent', 'configAndData', 'none'])
.default('configAndData'),
}).default();
},
@ -45,7 +46,7 @@ export function graph(kibana) {
const config = server.config();
return {
graphSavePolicy: config.get('xpack.graph.savePolicy'),
canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls')
canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls'),
};
});
@ -72,11 +73,9 @@ export function graph(kibana) {
read: ['index-pattern', 'graph-workspace'],
},
ui: [],
}
}
},
},
});
initServer(server);
},
});
}
};

View file

@ -13,7 +13,6 @@ import { isColorDark, hexToRgb } from '@elastic/eui';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { addAppRedirectMessageToUrl } from 'ui/notify';
import appTemplate from './angular/templates/index.html';
import listingTemplate from './angular/templates/listing_ng_wrapper.html';
@ -39,10 +38,10 @@ import {
hasFieldsSelector
} from './state_management';
import { formatHttpError } from './helpers/format_http_error';
import { checkLicense } from '../../../../plugins/graph/common/check_license';
export function initGraphApp(angularModule, deps) {
const {
xpackInfo,
chrome,
savedGraphWorkspaces,
toastNotifications,
@ -63,17 +62,6 @@ export function initGraphApp(angularModule, deps) {
const app = angularModule;
function checkLicense(kbnBaseUrl) {
const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') &&
xpackInfo.get('features.graph.enableAppLink');
if (!licenseAllowsToShowThisPage) {
const message = xpackInfo.get('features.graph.message');
const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message);
window.location.href = newUrl;
throw new Error('Graph license error');
}
}
app.directive('vennDiagram', function (reactDirective) {
return reactDirective(VennDiagram);
});
@ -123,7 +111,6 @@ export function initGraphApp(angularModule, deps) {
template: listingTemplate,
badge: getReadonlyBadge,
controller($location, $scope) {
checkLicense(kbnBaseUrl);
const services = savedObjectRegistry.byLoaderPropertiesName;
const graphService = services['Graph workspace'];
@ -164,7 +151,6 @@ export function initGraphApp(angularModule, deps) {
) : savedGraphWorkspaces.get();
},
//Copied from example found in wizard.js ( Kibana TODO - can't
indexPatterns: function () {
return savedObjectsClient.find({
type: 'index-pattern',
@ -185,10 +171,8 @@ export function initGraphApp(angularModule, deps) {
//======== Controller for basic UI ==================
app.controller('graphuiPlugin', function ($scope, $route, $location, confirmModal) {
checkLicense(kbnBaseUrl);
function handleError(err) {
checkLicense(kbnBaseUrl);
const toastTitle = i18n.translate('xpack.graph.errorToastTitle', {
defaultMessage: 'Graph Error',
description: '"Graph" is a product name and should not be translated.',
@ -206,7 +190,6 @@ export function initGraphApp(angularModule, deps) {
}
async function handleHttpError(error) {
checkLicense(kbnBaseUrl);
toastNotifications.addDanger(formatHttpError(error));
}

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { npStart } from 'ui/new_platform';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
const navLinkUpdates = {};
navLinkUpdates.hidden = true;
const showAppLink = xpackInfo.get('features.graph.showAppLink', false);
navLinkUpdates.hidden = !showAppLink;
if (showAppLink) {
navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false);
navLinkUpdates.tooltip = xpackInfo.get('features.graph.message');
}
npStart.core.chrome.navLinks.update('graph', navLinkUpdates);

View file

@ -12,8 +12,6 @@ import 'ui/autoload/all';
import chrome from 'ui/chrome';
import { IPrivate } from 'ui/private';
// @ts-ignore
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
// @ts-ignore
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { npSetup, npStart } from 'ui/new_platform';
@ -45,9 +43,9 @@ async function getAngularInjectedDependencies(): Promise<LegacyAngularInjectedDe
const instance = new GraphPlugin();
instance.setup(npSetup.core, {
__LEGACY: {
xpackInfo,
Storage,
},
...npSetup.plugins,
});
instance.start(npStart.core, {
npData: npStart.plugins.data,

View file

@ -9,6 +9,7 @@ import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/co
import { Plugin as DataPlugin } from 'src/plugins/data/public';
import { LegacyAngularInjectedDependencies } from './render_app';
import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public';
import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types';
export interface GraphPluginStartDependencies {
npData: ReturnType<DataPlugin['start']>;
@ -18,8 +19,8 @@ export interface GraphPluginStartDependencies {
export interface GraphPluginSetupDependencies {
__LEGACY: {
Storage: any;
xpackInfo: any;
};
licensing: LicensingPluginSetup;
}
export interface GraphPluginStartDependencies {
@ -34,7 +35,7 @@ export class GraphPlugin implements Plugin {
private savedObjectsClient: SavedObjectsClientContract | null = null;
private angularDependencies: LegacyAngularInjectedDependencies | null = null;
setup(core: CoreSetup, { __LEGACY: { xpackInfo, Storage } }: GraphPluginSetupDependencies) {
setup(core: CoreSetup, { __LEGACY: { Storage }, licensing }: GraphPluginSetupDependencies) {
core.application.register({
id: 'graph',
title: 'Graph',
@ -42,10 +43,10 @@ export class GraphPlugin implements Plugin {
const { renderApp } = await import('./render_app');
return renderApp({
...params,
licensing,
navigation: this.navigationStart!,
npData: this.npDataStart!,
savedObjectsClient: this.savedObjectsClient!,
xpackInfo,
addBasePath: core.http.basePath.prepend,
getBasePath: core.http.basePath.get,
canEditDrillDownUrls: core.injectedMetadata.getInjectedVar(

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { i18n } from '@kbn/i18n';
FeatureCatalogueRegistryProvider.register(() => {
return {
id: 'graph',
title: 'Graph',
description: i18n.translate('xpack.graph.pluginDescription', {
defaultMessage: 'Surface and analyze relevant relationships in your Elasticsearch data.',
}),
icon: 'graphApp',
path: '/app/graph',
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA
};
});

View file

@ -19,6 +19,8 @@ import { configureAppAngularModule } from 'ui/legacy_compat';
import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav';
// @ts-ignore
import { confirmModalFactory } from 'ui/modals/confirm_modal';
// @ts-ignore
import { addAppRedirectMessageToUrl } from 'ui/notify';
// type imports
import {
@ -36,6 +38,8 @@ import {
IndexPatternsContract,
} from '../../../../../src/plugins/data/public';
import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public';
import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types';
import { checkLicense } from '../../../../plugins/graph/common/check_license';
/**
* These are dependencies of the Graph app besides the base dependencies
@ -49,13 +53,13 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies {
capabilities: Record<string, boolean | Record<string, boolean>>;
coreStart: AppMountContext['core'];
navigation: NavigationStart;
licensing: LicensingPluginSetup;
chrome: ChromeStart;
config: IUiSettingsClient;
toastNotifications: ToastsStart;
indexPatterns: IndexPatternsContract;
npData: ReturnType<DataPlugin['start']>;
savedObjectsClient: SavedObjectsClientContract;
xpackInfo: { get(path: string): unknown };
addBasePath: (url: string) => string;
getBasePath: () => string;
Storage: any;
@ -82,9 +86,23 @@ export interface LegacyAngularInjectedDependencies {
export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => {
const graphAngularModule = createLocalAngularModule(deps.navigation);
configureAppAngularModule(graphAngularModule, deps.coreStart as LegacyCoreStart, true);
const licenseSubscription = deps.licensing.license$.subscribe(license => {
const info = checkLicense(license);
const licenseAllowsToShowThisPage = info.showAppLink && info.enableAppLink;
if (!licenseAllowsToShowThisPage) {
const newUrl = addAppRedirectMessageToUrl(deps.addBasePath(deps.kbnBaseUrl), info.message);
window.location.href = newUrl;
}
});
initGraphApp(graphAngularModule, deps);
const $injector = mountGraphApp(appBasePath, element);
return () => $injector.get('$rootScope').$destroy();
return () => {
licenseSubscription.unsubscribe();
$injector.get('$rootScope').$destroy();
};
};
const mainTemplate = (basePath: string) => `<div style="height: 100%">

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status';
import { checkLicense } from './lib';
import { graphExploreRoute, searchProxyRoute } from './routes';
export function initServer(server) {
const graphPlugin = server.plugins.graph;
const xpackMainPlugin = server.plugins.xpack_main;
mirrorPluginStatus(xpackMainPlugin, graphPlugin);
xpackMainPlugin.status.once('green', () => {
// Register a function that is called whenever the xpack info changes,
// to re-compute the license check results
xpackMainPlugin.info.feature('graph').registerLicenseCheckResultsGenerator(checkLicense);
});
server.route(graphExploreRoute);
server.route(searchProxyRoute);
}

View file

@ -1,120 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { set } from 'lodash';
import sinon from 'sinon';
import { checkLicense } from '../check_license';
describe('check_license: ', function () {
let mockLicenseInfo;
let licenseCheckResult;
beforeEach(() => {
mockLicenseInfo = {
isAvailable: () => true
};
});
describe('mockLicenseInfo is not set', () => {
beforeEach(() => {
mockLicenseInfo = null;
licenseCheckResult = checkLicense(mockLicenseInfo);
});
it ('should set showAppLink to true', () => {
expect(licenseCheckResult.showAppLink).to.be(true);
});
it ('should set enableAppLink to false', () => {
expect(licenseCheckResult.enableAppLink).to.be(false);
});
});
describe('mockLicenseInfo is set but not available', () => {
beforeEach(() => {
mockLicenseInfo = { isAvailable: () => false };
licenseCheckResult = checkLicense(mockLicenseInfo);
});
it ('should set showAppLink to true', () => {
expect(licenseCheckResult.showAppLink).to.be(true);
});
it ('should set enableAppLink to false', () => {
expect(licenseCheckResult.enableAppLink).to.be(false);
});
});
describe('graph is disabled in Elasticsearch', () => {
beforeEach(() => {
set(mockLicenseInfo, 'feature', sinon.stub().withArgs('graph').returns({ isEnabled: () => false }));
licenseCheckResult = checkLicense(mockLicenseInfo);
});
it ('should set showAppLink to false', () => {
expect(licenseCheckResult.showAppLink).to.be(false);
});
});
describe('graph is enabled in Elasticsearch', () => {
beforeEach(() => {
set(mockLicenseInfo, 'feature', sinon.stub().withArgs('graph').returns({ isEnabled: () => true }));
});
describe('& license is trial or platinum', () => {
beforeEach(() => {
set(mockLicenseInfo, 'license.isOneOf', sinon.stub().withArgs([ 'trial', 'platinum' ]).returns(true));
set(mockLicenseInfo, 'license.getType', () => 'trial');
});
describe('& license is active', () => {
beforeEach(() => {
set(mockLicenseInfo, 'license.isActive', () => true);
licenseCheckResult = checkLicense(mockLicenseInfo);
});
it ('should set showAppLink to true', () => {
expect(licenseCheckResult.showAppLink).to.be(true);
});
it ('should set enableAppLink to true', () => {
expect(licenseCheckResult.enableAppLink).to.be(true);
});
});
describe('& license is expired', () => {
beforeEach(() => {
set(mockLicenseInfo, 'license.isActive', () => false);
licenseCheckResult = checkLicense(mockLicenseInfo);
});
it ('should set showAppLink to true', () => {
expect(licenseCheckResult.showAppLink).to.be(true);
});
it ('should set enableAppLink to false', () => {
expect(licenseCheckResult.enableAppLink).to.be(false);
});
});
});
describe('& license is neither trial nor platinum', () => {
beforeEach(() => {
set(mockLicenseInfo, 'license.isOneOf', () => false);
set(mockLicenseInfo, 'license.getType', () => 'basic');
set(mockLicenseInfo, 'license.isActive', () => true);
licenseCheckResult = checkLicense(mockLicenseInfo);
});
it ('should set showAppLink to false', () => {
expect(licenseCheckResult.showAppLink).to.be(false);
});
});
});
});

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export function checkLicense(xpackLicenseInfo) {
if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) {
return {
showAppLink: true,
enableAppLink: false,
message: i18n.translate('xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage', {
defaultMessage: 'Graph is unavailable - license information is not available at this time.',
})
};
}
const graphFeature = xpackLicenseInfo.feature('graph');
if (!graphFeature.isEnabled()) {
return {
showAppLink: false,
enableAppLink: false,
message: i18n.translate('xpack.graph.serverSideErrors.unavailableGraphErrorMessage', {
defaultMessage: 'Graph is unavailable',
})
};
}
const isLicenseActive = xpackLicenseInfo.license.isActive();
let message;
if (!isLicenseActive) {
message = i18n.translate('xpack.graph.serverSideErrors.expiredLicenseErrorMessage', {
defaultMessage: 'Graph is unavailable - license has expired.',
});
}
if (xpackLicenseInfo.license.isOneOf([ 'trial', 'platinum' ])) {
return {
showAppLink: true,
enableAppLink: isLicenseActive,
message
};
}
message = i18n.translate('xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage', {
defaultMessage: 'Graph is unavailable for the current {licenseType} license. Please upgrade your license.',
values: {
licenseType: xpackLicenseInfo.license.getType(),
},
});
return {
showAppLink: false,
enableAppLink: false,
message
};
}

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { get } from 'lodash';
export async function callEsGraphExploreApi({ callCluster, index, query }) {
try {
return {
ok: true,
resp: await callCluster('transport.request', {
'path': '/' + encodeURIComponent(index) + '/_graph/explore',
body: query,
method: 'POST',
query: {}
})
};
} catch (error) {
// Extract known reasons for bad choice of field
const relevantCause = [].concat(get(error, 'body.error.root_cause', []) || [])
.find(cause => {
return (
cause.reason.includes('Fielddata is disabled on text fields') ||
cause.reason.includes('No support for examining floating point') ||
cause.reason.includes('Sample diversifying key must be a single valued-field') ||
cause.reason.includes('Failed to parse query') ||
cause.type == 'parsing_exception'
);
});
if (relevantCause) {
throw Boom.badRequest(relevantCause.reason);
}
throw Boom.boomify(error);
}
}

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
export async function callEsSearchApi({ callCluster, index, body, queryParams }) {
try {
return {
ok: true,
resp: await callCluster('search', {
...queryParams,
index,
body
})
};
} catch (error) {
throw Boom.boomify(error, { statusCode: error.statusCode || 500 });
}
}

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { callEsSearchApi } from './call_es_search_api';
export { callEsGraphExploreApi } from './call_es_graph_explore_api';

View file

@ -1,17 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { checkLicense } from './check_license';
export {
callEsGraphExploreApi,
callEsSearchApi,
} from './es';
export {
getCallClusterPre,
verifyApiAccessPre,
} from './pre';

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const getCallClusterPre = {
assign: 'callCluster',
method(request) {
const cluster = request.server.plugins.elasticsearch.getCluster('data');
return (...args) => cluster.callWithRequest(request, ...args);
}
};

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { getCallClusterPre } from './get_call_cluster_pre';
export { verifyApiAccessPre } from './verify_api_access_pre';

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
export function verifyApiAccessPre(request, h) {
const xpackInfo = request.server.plugins.xpack_main.info;
const graph = xpackInfo.feature('graph');
const licenseCheckResults = graph.getLicenseCheckResults();
if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) {
return null;
} else {
throw Boom.forbidden(licenseCheckResults.message);
}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import {
verifyApiAccessPre,
getCallClusterPre,
callEsGraphExploreApi,
} from '../lib';
export const graphExploreRoute = {
path: '/api/graph/graphExplore',
method: 'POST',
config: {
pre: [
verifyApiAccessPre,
getCallClusterPre,
],
validate: {
payload: Joi.object().keys({
index: Joi.string().required(),
query: Joi.object().required().unknown(true)
}).default()
},
handler(request) {
return callEsGraphExploreApi({
callCluster: request.pre.callCluster,
index: request.payload.index,
query: request.payload.query,
});
}
}
};

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import Boom from 'boom';
import {
verifyApiAccessPre,
getCallClusterPre,
callEsSearchApi,
} from '../lib';
export const searchProxyRoute = {
path: '/api/graph/searchProxy',
method: 'POST',
config: {
pre: [
getCallClusterPre,
verifyApiAccessPre,
],
validate: {
payload: Joi.object().keys({
index: Joi.string().required(),
body: Joi.object().unknown(true).default()
}).default()
},
async handler(request) {
const includeFrozen = await request.getUiSettingsService().get('search:includeFrozen');
return await callEsSearchApi({
callCluster: request.pre.callCluster,
index: request.payload.index,
body: request.payload.body,
queryParams: {
rest_total_hits_as_int: true,
ignore_throttled: !includeFrozen,
}
});
}
}
};

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { ILicense, LICENSE_CHECK_STATE } from '../../licensing/common/types';
import { assertNever } from '../../../../src/core/utils';
export interface GraphLicenseInformation {
showAppLink: boolean;
enableAppLink: boolean;
message: string;
}
export function checkLicense(license: ILicense | undefined): GraphLicenseInformation {
if (!license || !license.isAvailable) {
return {
showAppLink: true,
enableAppLink: false,
message: i18n.translate(
'xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage',
{
defaultMessage:
'Graph is unavailable - license information is not available at this time.',
}
),
};
}
const graphFeature = license.getFeature('graph');
if (!graphFeature.isEnabled) {
return {
showAppLink: false,
enableAppLink: false,
message: i18n.translate('xpack.graph.serverSideErrors.unavailableGraphErrorMessage', {
defaultMessage: 'Graph is unavailable',
}),
};
}
const check = license.check('graph', 'platinum');
switch (check.state) {
case LICENSE_CHECK_STATE.Expired:
return {
showAppLink: true,
enableAppLink: false,
message: check.message || '',
};
case LICENSE_CHECK_STATE.Invalid:
case LICENSE_CHECK_STATE.Unavailable:
return {
showAppLink: false,
enableAppLink: false,
message: check.message || '',
};
case LICENSE_CHECK_STATE.Valid:
return {
showAppLink: true,
enableAppLink: true,
message: '',
};
default:
return assertNever(check.state);
}
}

View file

@ -0,0 +1,9 @@
{
"id": "graph",
"version": "8.0.0",
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["licensing"],
"optionalPlugins": ["home"]
}

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { initServer } from './init_server';
import { GraphPlugin } from './plugin';
export const plugin = () => new GraphPlugin();

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart } from 'kibana/public';
import { Plugin } from 'src/core/public';
import { toggleNavLink } from './services/toggle_nav_link';
import { LicensingPluginSetup } from '../../licensing/common/types';
import { checkLicense } from '../common/check_license';
import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
} from '../../../../src/plugins/home/public';
export interface GraphPluginSetupDependencies {
licensing: LicensingPluginSetup;
home?: HomePublicPluginSetup;
}
export class GraphPlugin implements Plugin {
private licensing: LicensingPluginSetup | null = null;
setup(core: CoreSetup, { licensing, home }: GraphPluginSetupDependencies) {
this.licensing = licensing;
if (home) {
home.featureCatalogue.register({
id: 'graph',
title: 'Graph',
description: i18n.translate('xpack.graph.pluginDescription', {
defaultMessage: 'Surface and analyze relevant relationships in your Elasticsearch data.',
}),
icon: 'graphApp',
path: '/app/graph',
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA,
});
}
}
start(core: CoreStart) {
if (this.licensing === null) {
throw new Error('Start called before setup');
}
this.licensing.license$.subscribe(license => {
toggleNavLink(checkLicense(license), core.chrome.navLinks);
});
}
stop() {}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ChromeNavLink, ChromeNavLinks } from 'kibana/public';
import { GraphLicenseInformation } from '../../common/check_license';
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
type ChromeNavLinkUpdate = Mutable<Partial<ChromeNavLink>>;
export function toggleNavLink(
licenseInformation: GraphLicenseInformation,
navLinks: ChromeNavLinks
) {
const navLinkUpdates: ChromeNavLinkUpdate = {
hidden: !licenseInformation.showAppLink,
};
if (licenseInformation.showAppLink) {
navLinkUpdates.disabled = !licenseInformation.enableAppLink;
navLinkUpdates.tooltip = licenseInformation.message;
}
navLinks.update('graph', navLinkUpdates);
}

View file

@ -4,5 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { graphExploreRoute } from './graph_explore';
export { searchProxyRoute } from './search_proxy';
import { GraphPlugin } from './plugin';
export const plugin = () => new GraphPlugin();

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { Observable, Subscription } from 'rxjs';
import { ILicense } from '../../../licensing/common/types';
import { checkLicense, GraphLicenseInformation } from '../../common/check_license';
export class LicenseState {
private licenseInformation: GraphLicenseInformation = checkLicense(undefined);
private subscription: Subscription | null = null;
private updateInformation(license: ILicense | undefined) {
this.licenseInformation = checkLicense(license);
}
public start(license$: Observable<ILicense>) {
this.subscription = license$.subscribe(this.updateInformation.bind(this));
}
public stop() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
public getLicenseInformation() {
return this.licenseInformation;
}
}
export function verifyApiAccess(licenseState: LicenseState) {
const licenseCheckResults = licenseState.getLicenseInformation();
if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) {
return;
}
throw Boom.forbidden(licenseCheckResults.message);
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin, CoreSetup } from 'src/core/server';
import { LicensingPluginSetup } from '../../licensing/common/types';
import { LicenseState } from './lib/license_state';
import { registerSearchRoute } from './routes/search';
import { registerExploreRoute } from './routes/explore';
export class GraphPlugin implements Plugin {
private licenseState: LicenseState | null = null;
public async setup(core: CoreSetup, { licensing }: { licensing: LicensingPluginSetup }) {
const licenseState = new LicenseState();
licenseState.start(licensing.license$);
this.licenseState = licenseState;
const router = core.http.createRouter();
registerSearchRoute({ licenseState, router });
registerExploreRoute({ licenseState, router });
}
public start() {}
public stop() {
if (this.licenseState) {
this.licenseState.stop();
}
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import Boom from 'boom';
import { get } from 'lodash';
import { LicenseState, verifyApiAccess } from '../lib/license_state';
export function registerExploreRoute({
router,
licenseState,
}: {
router: IRouter;
licenseState: LicenseState;
}) {
router.post(
{
path: '/api/graph/graphExplore',
validate: {
body: schema.object({
index: schema.string(),
query: schema.object({}, { allowUnknowns: true }),
}),
},
},
router.handleLegacyErrors(
async (
{
core: {
elasticsearch: {
dataClient: { callAsCurrentUser: callCluster },
},
},
},
request,
response
) => {
verifyApiAccess(licenseState);
try {
return response.ok({
body: {
resp: await callCluster('transport.request', {
path: '/' + encodeURIComponent(request.body.index) + '/_graph/explore',
body: request.body.query,
method: 'POST',
query: {},
}),
},
});
} catch (error) {
// Extract known reasons for bad choice of field
const relevantCause = get(
error,
'body.error.root_cause',
[] as Array<{ type: string; reason: string }>
).find(cause => {
return (
cause.reason.includes('Fielddata is disabled on text fields') ||
cause.reason.includes('No support for examining floating point') ||
cause.reason.includes('Sample diversifying key must be a single valued-field') ||
cause.reason.includes('Failed to parse query') ||
cause.type === 'parsing_exception'
);
});
if (relevantCause) {
throw Boom.badRequest(relevantCause.reason);
}
throw Boom.boomify(error);
}
}
)
);
}

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import Boom from 'boom';
import { LicenseState, verifyApiAccess } from '../lib/license_state';
export function registerSearchRoute({
router,
licenseState,
}: {
router: IRouter;
licenseState: LicenseState;
}) {
router.post(
{
path: '/api/graph/searchProxy',
validate: {
body: schema.object({
index: schema.string(),
body: schema.object({}, { allowUnknowns: true }),
}),
},
},
router.handleLegacyErrors(
async (
{
core: {
uiSettings: { client: uiSettings },
elasticsearch: {
dataClient: { callAsCurrentUser: callCluster },
},
},
},
request,
response
) => {
verifyApiAccess(licenseState);
const includeFrozen = await uiSettings.get<boolean>('search:includeFrozen');
try {
return response.ok({
body: {
resp: await callCluster('search', {
index: request.body.index,
body: request.body.body,
rest_total_hits_as_int: true,
ignore_throttled: !includeFrozen,
}),
},
});
} catch (error) {
throw Boom.boomify(error, { statusCode: error.statusCode || 500 });
}
}
)
);
}

View file

@ -5170,10 +5170,8 @@
"xpack.graph.savedWorkspaces.graphWorkspacesLabel": "グラフワークスペース",
"xpack.graph.saveWorkspace.successNotification.noDataSavedText": "構成が保存されましたが、データは保存されませんでした",
"xpack.graph.saveWorkspace.successNotificationTitle": "「{workspaceTitle}」が保存されました",
"xpack.graph.serverSideErrors.expiredLicenseErrorMessage": "グラフを利用できません。ライセンスが期限切れです。",
"xpack.graph.serverSideErrors.unavailableGraphErrorMessage": "グラフを利用できません",
"xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。",
"xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage": "現在の {licenseType} ライセンスではグラフを利用できません。ライセンスをアップグレードしてください。",
"xpack.graph.settings.advancedSettings.certaintyInputHelpText": "関連用語が登録される前に証拠として必要なドキュメントの最低数です",
"xpack.graph.settings.advancedSettings.certaintyInputLabel": "確実性",
"xpack.graph.settings.advancedSettings.diversityFieldInputHelpText1": "ドキュメントのサンプルが 1 種類に偏らないように、バイアスの原因の認識に役立つフィールドを選択してください。",

View file

@ -5172,10 +5172,8 @@
"xpack.graph.savedWorkspaces.graphWorkspacesLabel": "Graph 工作空间",
"xpack.graph.saveWorkspace.successNotification.noDataSavedText": "配置会被保存,但不保存数据",
"xpack.graph.saveWorkspace.successNotificationTitle": "已保存“{workspaceTitle}”",
"xpack.graph.serverSideErrors.expiredLicenseErrorMessage": "Graph 不可用 - 许可已过期。",
"xpack.graph.serverSideErrors.unavailableGraphErrorMessage": "Graph 不可用",
"xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage": "Graph 不可用 - 许可信息当前不可用。",
"xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage": "当前{licenseType}许可的 Graph 不可用。请升级您的许可。",
"xpack.graph.settings.advancedSettings.certaintyInputHelpText": "在引入相关字词之前作为证据所需的最小文档数量。",
"xpack.graph.settings.advancedSettings.certaintyInputLabel": "确定性",
"xpack.graph.settings.advancedSettings.diversityFieldInputHelpText1": "为避免文档示例过于雷同,请选取有助于识别偏差来源的字段。",