[7.x] [APM] License prompt for service map (#52668) (#54503)

Display a link to platinum license upgrade on the service map.

Also add `useKibanaUrl` and `useLicense` hooks.

Make the `LicenseContext` (which is used in a couple class components and on page load) use the license observable from the NP plugin.

Add missing export of `useObservable` to kibana_react.
This commit is contained in:
Nathan L Smith 2020-01-13 09:59:17 -06:00 committed by GitHub
parent 94391c15fe
commit cbc3f24f69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 87 deletions

View file

@ -148,7 +148,9 @@ export class ServiceIntegrations extends React.Component<Props, State> {
panels={[
{
id: 0,
items: this.getPanelItems(license.features.ml?.is_available)
items: this.getPanelItems(
license?.getFeature('ml').isAvailable
)
}
]}
/>

View file

@ -0,0 +1,66 @@
/*
* 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 React from 'react';
import {
EuiEmptyPrompt,
EuiButton,
EuiPanel,
EuiFlexGroup,
EuiFlexItem
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useKibanaUrl } from '../../../hooks/useKibanaUrl';
export function PlatinumLicensePrompt() {
// Set the height to give it some top margin
const style = { height: '60vh' };
const licensePageUrl = useKibanaUrl(
'/app/kibana',
'/management/elasticsearch/license_management/home'
);
return (
<EuiFlexGroup
alignItems="center"
justifyContent="spaceAround"
style={style}
>
<EuiFlexItem grow={false}>
<EuiPanel grow={false} hasShadow={true}>
<EuiEmptyPrompt
actions={[
<EuiButton fill={true} href={licensePageUrl}>
{i18n.translate(
'xpack.apm.serviceMap.licensePromptButtonText',
{
defaultMessage: 'Start 30-day Platinum trial'
}
)}
</EuiButton>
]}
body={
<p>
{i18n.translate('xpack.apm.serviceMap.licensePromptBody', {
defaultMessage:
"In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data."
})}
</p>
}
title={
<h2>
{i18n.translate('xpack.apm.serviceMap.licensePromptTitle', {
defaultMessage: 'Service maps is available in Platinum.'
})}
</h2>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { useUrlParams } from '../../../hooks/useUrlParams';
import React from 'react';
import { useFetcher } from '../../../hooks/useFetcher';
import { Cytoscape } from './Cytoscape';
import { useLicense } from '../../../hooks/useLicense';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { Controls } from './Controls';
import { Cytoscape } from './Cytoscape';
import { PlatinumLicensePrompt } from './PlatinumLicensePrompt';
interface ServiceMapProps {
serviceName?: string;
@ -53,8 +55,11 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
);
const elements = Array.isArray(data) ? data : [];
const license = useLicense();
const isValidPlatinumLicense =
license?.isActive && license?.type === 'platinum';
return (
return isValidPlatinumLicense ? (
<Cytoscape
elements={elements}
serviceName={serviceName}
@ -62,5 +67,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
>
<Controls />
</Cytoscape>
) : (
<PlatinumLicensePrompt />
);
}

View file

@ -170,7 +170,7 @@ export class TransactionCharts extends Component<TransactionChartProps> {
</EuiFlexItem>
<LicenseContext.Consumer>
{license =>
this.renderMLHeader(license.features.ml?.is_available)
this.renderMLHeader(license?.getFeature('ml').isAvailable)
}
</LicenseContext.Consumer>
</EuiFlexGroup>

View file

@ -3,33 +3,27 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher';
import { loadLicense, LicenseApiResponse } from '../../services/rest/xpack';
import { InvalidLicenseNotification } from './InvalidLicenseNotification';
import { useObservable } from 'react-use';
import { ILicense } from '../../../../../../plugins/licensing/public';
import { useApmPluginContext } from '../../hooks/useApmPluginContext';
import { InvalidLicenseNotification } from './InvalidLicenseNotification';
const initialLicense: LicenseApiResponse = {
features: {},
license: {
is_active: false
}
};
export const LicenseContext = React.createContext(initialLicense);
export const LicenseContext = React.createContext<ILicense | undefined>(
undefined
);
export const LicenseProvider: React.FC = ({ children }) => {
const { http } = useApmPluginContext().core;
const { data = initialLicense, status } = useFetcher(
() => loadLicense(http),
[http]
);
const hasValidLicense = data.license.is_active;
export function LicenseProvider({ children }: { children: React.ReactChild }) {
const { license$ } = useApmPluginContext().plugins.licensing;
const license = useObservable(license$);
const hasInvalidLicense = !license?.isActive;
// if license is invalid show an error message
if (status === FETCH_STATUS.SUCCESS && !hasValidLicense) {
if (hasInvalidLicense) {
return <InvalidLicenseNotification />;
}
// render rest of application and pass down license via context
return <LicenseContext.Provider value={data} children={children} />;
};
return <LicenseContext.Provider value={license} children={children} />;
}

View file

@ -0,0 +1,19 @@
/*
* 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 url from 'url';
import { useApmPluginContext } from './useApmPluginContext';
export function useKibanaUrl(
/** The path to the plugin */ path: string,
/** The hash path */ hash: string
) {
const { core } = useApmPluginContext();
return url.format({
pathname: core.http.basePath.prepend(path),
hash
});
}

View file

@ -0,0 +1,12 @@
/*
* 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 { useContext } from 'react';
import { LicenseContext } from '../context/LicenseContext';
export function useLicense() {
return useContext(LicenseContext);
}

View file

@ -6,35 +6,36 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router-dom';
import { Route, Router, Switch } from 'react-router-dom';
import styled from 'styled-components';
import { metadata } from 'ui/metadata';
import { HomePublicPluginSetup } from '../../../../../../src/plugins/home/public';
import {
CoreStart,
Plugin,
CoreSetup,
PluginInitializerContext,
PackageInfo
CoreStart,
PackageInfo,
Plugin,
PluginInitializerContext
} from '../../../../../../src/core/public';
import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public';
import { history } from '../utils/history';
import { LocationProvider } from '../context/LocationContext';
import { UrlParamsProvider } from '../context/UrlParamsContext';
import { px, unit, units } from '../style/variables';
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
import { LicenseProvider } from '../context/LicenseContext';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { HomePublicPluginSetup } from '../../../../../../src/plugins/home/public';
import { LicensingPluginSetup } from '../../../../../plugins/licensing/public';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { ApmPluginContext } from '../context/ApmPluginContext';
import { LicenseProvider } from '../context/LicenseContext';
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
import { LocationProvider } from '../context/LocationContext';
import { MatchedRouteProvider } from '../context/MatchedRouteContext';
import { UrlParamsProvider } from '../context/UrlParamsContext';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
import { setHelpExtension } from './setHelpExtension';
import { setReadonlyBadge } from './updateBadge';
import { px, unit, units } from '../style/variables';
import { history } from '../utils/history';
import { featureCatalogueEntry } from './featureCatalogueEntry';
import { getConfigFromInjectedMetadata } from './getConfigFromInjectedMetadata';
import { setHelpExtension } from './setHelpExtension';
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
import { ApmPluginContext } from '../context/ApmPluginContext';
import { setReadonlyBadge } from './updateBadge';
export const REACT_APP_ROOT_ID = 'react-apm-root';
@ -64,6 +65,7 @@ export type ApmPluginStart = void;
export interface ApmPluginSetupDeps {
data: DataPublicPluginSetup;
home: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
}
export interface ConfigSchema {

View file

@ -1,45 +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 { HttpStart } from 'kibana/public';
import { callApi } from './callApi';
export interface LicenseApiResponse {
license: {
is_active: boolean;
};
features: {
beats_management?: Record<string, unknown>;
graph?: Record<string, unknown>;
grokdebugger?: Record<string, unknown>;
index_management?: Record<string, unknown>;
logstash?: Record<string, unknown>;
ml?: {
is_available: boolean;
license_type: number;
has_expired: boolean;
enable_links: boolean;
show_links: boolean;
};
reporting?: Record<string, unknown>;
rollup?: Record<string, unknown>;
searchprofiler?: Record<string, unknown>;
security?: Record<string, unknown>;
spaces?: Record<string, unknown>;
tilemap?: Record<string, unknown>;
watcher?: {
is_available: boolean;
enable_links: boolean;
show_links: boolean;
};
};
}
export async function loadLicense(http: HttpStart) {
return callApi<LicenseApiResponse>(http, {
pathname: `/api/xpack/v1/info`
});
}