[Uptime] Shim UI exports for new platform (#44722) (#47218)

* Remove depdency on legacy interface.

* Remove custom interface, use common breadcrumb type.

* Add HTML template for app react entry point.

* Update app props.

* Add constant for react app entry point.

* Remove dependency on legacy capabilities provider.

* Delete legacy kibana framework adapter.

* Add New Platform adapter, reference in startup code.

* Remove dependency on legacy capabilities function.

* Delete reference to obsolete interface.

* Fix busted types in new adapter.

* Add new plugin class, delete old bootstrap code.

* Provide default for potentially-undefined value.

* Delete obsolete file.

* Update plugin constructor and start interfaces.

* Add @ts-ignore for unused constructor parameter.

* Import autocomplete provider from new platform.
This commit is contained in:
Justin Kambic 2019-10-03 18:57:34 -04:00 committed by GitHub
parent fa2d0c7f86
commit fa16d789fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 190 additions and 214 deletions

View file

@ -5,6 +5,7 @@
*/
export const PLUGIN = {
APP_ROOT_ID: 'react-uptime-root',
ID: 'uptime',
ROUTER_BASE_NAME: '/app/uptime#/',
LOCAL_STORAGE_KEY: 'xpack.uptime',

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
import './apps/kibana_app';
import './apps/index';

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { compose } from '../lib/compose/kibana_compose';
import { startApp } from './start_app';
import chrome from 'ui/chrome';
import { npStart } from 'ui/new_platform';
import { Plugin } from './plugin';
startApp(compose());
new Plugin({ opaqueId: Symbol('uptime') }, chrome).start(npStart);

View file

@ -0,0 +1,58 @@
/*
* 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 { LegacyCoreStart, PluginInitializerContext } from 'src/core/public';
import { PluginsStart } from 'ui/new_platform/new_platform';
import { Chrome } from 'ui/chrome';
import { UMFrontendLibs } from '../lib/lib';
import { PLUGIN } from '../../common/constants';
import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter';
import template from './template.html';
import { UptimeApp } from '../uptime_app';
import { createApolloClient } from '../lib/adapters/framework/apollo_client_adapter';
export interface StartObject {
core: LegacyCoreStart;
plugins: PluginsStart;
}
export class Plugin {
constructor(
// @ts-ignore this is added to satisfy the New Platform typing constraint,
// but we're not leveraging any of its functionality yet.
private readonly initializerContext: PluginInitializerContext,
private readonly chrome: Chrome
) {
this.chrome = chrome;
}
public start(start: StartObject): void {
const {
core,
plugins: {
data: { autocomplete },
},
} = start;
const libs: UMFrontendLibs = {
framework: getKibanaFrameworkAdapter(core, autocomplete),
};
// @ts-ignore improper type description
this.chrome.setRootTemplate(template);
const checkForRoot = () => {
return new Promise(resolve => {
const ready = !!document.getElementById(PLUGIN.APP_ROOT_ID);
if (ready) {
resolve();
} else {
setTimeout(() => resolve(checkForRoot()), 10);
}
});
};
checkForRoot().then(() => {
libs.framework.render(UptimeApp, createApolloClient);
});
}
}

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 'react-vis/dist/style.css';
import 'ui/angular-bootstrap';
import 'ui/autoload/all';
import 'ui/autoload/styles';
import 'ui/courier';
import 'ui/persisted_log';
import { createApolloClient } from '../lib/adapters/framework/apollo_client_adapter';
import { UMFrontendLibs } from '../lib/lib';
import { UptimeApp } from '../uptime_app';
export async function startApp(libs: UMFrontendLibs) {
libs.framework.render(UptimeApp, createApolloClient);
}

View file

@ -0,0 +1 @@
<div id="react-uptime-root"></div>

View file

@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Badge } from 'ui/chrome/api/badge';
export type UMBadge = Badge | undefined;
import { ChromeBadge } from 'src/core/public';
export type UMBadge = ChromeBadge | undefined;

View file

@ -5,24 +5,20 @@
*/
import { i18n } from '@kbn/i18n';
import { ChromeBreadcrumb } from 'src/core/public';
export interface UMBreadcrumb {
text: string;
href?: string;
}
const makeOverviewBreadcrumb = (search?: string): UMBreadcrumb => ({
const makeOverviewBreadcrumb = (search?: string): ChromeBreadcrumb => ({
text: i18n.translate('xpack.uptime.breadcrumbs.overviewBreadcrumbText', {
defaultMessage: 'Uptime',
}),
href: `#/${search ? search : ''}`,
});
export const getOverviewPageBreadcrumbs = (search?: string): UMBreadcrumb[] => [
export const getOverviewPageBreadcrumbs = (search?: string): ChromeBreadcrumb[] => [
makeOverviewBreadcrumb(search),
];
export const getMonitorPageBreadcrumb = (name: string, search?: string): UMBreadcrumb[] => [
export const getMonitorPageBreadcrumb = (name: string, search?: string): ChromeBreadcrumb[] => [
makeOverviewBreadcrumb(search),
{ text: name },
];

View file

@ -6,18 +6,17 @@
import React, { useState, useEffect, useContext } from 'react';
import { uniqueId, startsWith } from 'lodash';
import { npStart } from 'ui/new_platform';
import { EuiCallOut } from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { AutocompleteProviderRegister, AutocompleteSuggestion } from 'src/plugins/data/public';
import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public/index_patterns/index_patterns';
import { Typeahead } from './typeahead';
import { getIndexPattern } from '../../../lib/adapters/index_pattern';
import { UptimeSettingsContext } from '../../../contexts';
import { useUrlParams } from '../../../hooks';
import { toStaticIndexPattern } from '../../../lib/helper';
import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public';
const Container = styled.div`
margin-bottom: 10px;
@ -28,10 +27,7 @@ interface State {
isLoadingIndexPattern: boolean;
}
const getAutocompleteProvider = (language: string) =>
npStart.plugins.data.autocomplete.getProvider(language);
function convertKueryToEsQuery(kuery: string, indexPattern: StaticIndexPattern) {
function convertKueryToEsQuery(kuery: string, indexPattern: unknown) {
const ast = fromKueryExpression(kuery);
return toElasticsearchQuery(ast, indexPattern);
}
@ -39,9 +35,10 @@ function convertKueryToEsQuery(kuery: string, indexPattern: StaticIndexPattern)
function getSuggestions(
query: string,
selectionStart: number,
apmIndexPattern: StaticIndexPattern
apmIndexPattern: StaticIndexPattern,
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>
) {
const autocompleteProvider = getAutocompleteProvider('kuery');
const autocompleteProvider = autocomplete.getProvider('kuery');
if (!autocompleteProvider) {
return [];
}
@ -62,7 +59,11 @@ function getSuggestions(
return suggestions;
}
export function KueryBar() {
interface Props {
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>;
}
export function KueryBar({ autocomplete }: Props) {
const [state, setState] = useState<State>({
suggestions: [],
isLoadingIndexPattern: true,
@ -94,7 +95,12 @@ export function KueryBar() {
currentRequestCheck = currentRequest;
try {
let suggestions = await getSuggestions(inputValue, selectionStart, indexPattern);
let suggestions = await getSuggestions(
inputValue,
selectionStart,
indexPattern,
autocomplete
);
suggestions = suggestions
.filter((suggestion: AutocompleteSuggestion) => !startsWith(suggestion.text, 'span.'))
.slice(0, 15);

View file

@ -4,16 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { capabilities as uiCapabilities } from 'ui/capabilities';
interface IntegratedAppsAvailability {
[key: string]: boolean;
}
export const getIntegratedAppAvailability = (
capabilities: any,
integratedApps: string[]
): IntegratedAppsAvailability => {
const capabilities = uiCapabilities.get();
return integratedApps.reduce((supportedSolutions: IntegratedAppsAvailability, solutionName) => {
supportedSolutions[solutionName] =
capabilities[solutionName] && capabilities[solutionName].show === true;

View file

@ -1,140 +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 ReactDOM from 'react-dom';
import { unmountComponentAtNode } from 'react-dom';
import chrome from 'ui/chrome';
import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
import { PLUGIN, INTEGRATED_SOLUTIONS } from '../../../../common/constants';
import { BootstrapUptimeApp, UMFrameworkAdapter } from '../../lib';
import { CreateGraphQLClient } from './framework_adapter_types';
import { renderUptimeKibanaGlobalHelp } from './kibana_global_help';
import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry';
import { getIntegratedAppAvailability } from './capabilities_adapter';
export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter {
private uiRoutes: any;
private xsrfHeader: string;
private uriPath: string;
constructor(uiRoutes: any) {
this.uiRoutes = uiRoutes;
this.xsrfHeader = chrome.getXsrfToken();
this.uriPath = `${chrome.getBasePath()}/api/uptime/graphql`;
}
/**
* This function will acquire all the existing data from Kibana
* services and persisted state expected by the plugin's props
* interface. It then renders the plugin.
*/
public render = (
renderComponent: BootstrapUptimeApp,
createGraphQLClient: CreateGraphQLClient
) => {
const route = {
controllerAs: 'uptime',
// @ts-ignore angular
controller: ($scope, $route, config, $location, $window) => {
const graphQLClient = createGraphQLClient(this.uriPath, this.xsrfHeader);
$scope.$$postDigest(() => {
const elem = document.getElementById('uptimeReactRoot');
// set up route with current base path
const basePath = chrome.getBasePath();
const routerBasename = basePath.endsWith('/')
? `${basePath}/${PLUGIN.ROUTER_BASE_NAME}`
: basePath + PLUGIN.ROUTER_BASE_NAME;
/**
* TODO: this is a redirect hack to deal with a problem that largely
* in testing but rarely occurs in the real world, where the specified
* URL contains `.../app/uptime{SOME_URL_PARAM_TEXT}#` instead of
* a path like `.../app/uptime#{SOME_URL_PARAM_TEXT}`.
*
* This redirect will almost never be triggered in practice, but it makes more
* sense to include it here rather than altering the existing testing
* infrastructure underlying the rest of Kibana.
*
* We welcome a more permanent solution that will result in the deletion of the
* block below.
*/
if ($location.absUrl().indexOf(PLUGIN.ROUTER_BASE_NAME) === -1) {
$window.location.replace(routerBasename);
}
// determine whether dark mode is enabled
const darkMode = config.get('theme:darkMode', false) || false;
/**
* We pass this global help setup as a prop to the app, because for
* localization it's necessary to have the provider mounted before
* we can render our help links, as they rely on i18n.
*/
const renderGlobalHelpControls = () =>
// render Uptime feedback link in global help menu
chrome.helpExtension.set((element: HTMLDivElement) => {
ReactDOM.render(
renderUptimeKibanaGlobalHelp(ELASTIC_WEBSITE_URL, DOC_LINK_VERSION),
element
);
return () => ReactDOM.unmountComponentAtNode(element);
});
/**
* These values will let Uptime know if the integrated solutions
* are available. If any/all of them are unavaialble, we should not show
* links/integrations to those apps.
*/
const {
apm: isApmAvailable,
infrastructure: isInfraAvailable,
logs: isLogsAvailable,
} = getIntegratedAppAvailability(INTEGRATED_SOLUTIONS);
ReactDOM.render(
renderComponent({
basePath,
client: graphQLClient,
darkMode,
isApmAvailable,
isInfraAvailable,
isLogsAvailable,
logMonitorPageLoad: getTelemetryMonitorPageLogger(this.xsrfHeader, basePath),
logOverviewPageLoad: getTelemetryOverviewPageLogger(this.xsrfHeader, basePath),
renderGlobalHelpControls,
routerBasename,
setBadge: chrome.badge.set,
setBreadcrumbs: chrome.breadcrumbs.set,
}),
elem
);
this.manageAngularLifecycle($scope, $route, elem);
});
},
template:
'<uptime-app section="kibana" id="uptimeReactRoot" class="app-wrapper-panel"></uptime-app>',
};
this.uiRoutes.enable();
// TODO: hack to refer all routes to same endpoint, use a more proper way of achieving this
this.uiRoutes.otherwise(route);
};
// @ts-ignore angular params
private manageAngularLifecycle = ($scope, $route, elem) => {
const lastRoute = $route.current;
const deregister = $scope.$on('$locationChangeSuccess', () => {
const currentRoute = $route.current;
if (lastRoute.$$route && lastRoute.$$route.template === currentRoute.$$route.template) {
$route.current = lastRoute;
}
});
$scope.$on('$destroy', () => {
deregister();
unmountComponentAtNode(elem);
});
};
}

View file

@ -0,0 +1,76 @@
/*
* 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 { ChromeBreadcrumb, CoreStart } from 'src/core/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { get } from 'lodash';
import { AutocompleteProviderRegister } from 'src/plugins/data/public';
import { CreateGraphQLClient } from './framework_adapter_types';
import { UptimeApp, UptimeAppProps } from '../../../uptime_app';
import { getIntegratedAppAvailability } from './capabilities_adapter';
import { INTEGRATED_SOLUTIONS, PLUGIN } from '../../../../common/constants';
import { getTelemetryMonitorPageLogger, getTelemetryOverviewPageLogger } from '../telemetry';
import { renderUptimeKibanaGlobalHelp } from './kibana_global_help';
import { UMFrameworkAdapter, BootstrapUptimeApp } from '../../lib';
import { createApolloClient } from './apollo_client_adapter';
export const getKibanaFrameworkAdapter = (
core: CoreStart,
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>
): UMFrameworkAdapter => {
const {
application: { capabilities },
chrome: { setBadge, setHelpExtension },
docLinks: { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL },
http: { basePath },
i18n,
} = core;
let breadcrumbs: ChromeBreadcrumb[] = [];
core.chrome.getBreadcrumbs$().subscribe((nextBreadcrumbs?: ChromeBreadcrumb[]) => {
breadcrumbs = nextBreadcrumbs || [];
});
const { apm, infrastructure, logs } = getIntegratedAppAvailability(
capabilities,
INTEGRATED_SOLUTIONS
);
const canSave = get(capabilities, 'uptime.save', false);
const props: UptimeAppProps = {
basePath: basePath.get(),
canSave,
client: createApolloClient(`${basePath.get()}/api/uptime/graphql`, 'true'),
darkMode: core.uiSettings.get('theme:darkMode'),
autocomplete,
i18n,
isApmAvailable: apm,
isInfraAvailable: infrastructure,
isLogsAvailable: logs,
kibanaBreadcrumbs: breadcrumbs,
logMonitorPageLoad: getTelemetryMonitorPageLogger('true', basePath.get()),
logOverviewPageLoad: getTelemetryOverviewPageLogger('true', basePath.get()),
renderGlobalHelpControls: () =>
setHelpExtension((element: HTMLElement) => {
ReactDOM.render(
renderUptimeKibanaGlobalHelp(ELASTIC_WEBSITE_URL, DOC_LINK_VERSION),
element
);
return () => ReactDOM.unmountComponentAtNode(element);
}),
routerBasename: basePath.prepend(PLUGIN.ROUTER_BASE_NAME),
setBadge,
setBreadcrumbs: core.chrome.setBreadcrumbs,
};
return {
// TODO: these parameters satisfy the interface but are no longer needed
render: async (createComponent: BootstrapUptimeApp, cgc: CreateGraphQLClient) => {
const node = await document.getElementById('react-uptime-root');
if (node) {
ReactDOM.render(<UptimeApp {...props} />, node);
}
},
};
};

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.
*/
import uiRoutes from 'ui/routes';
import { UMKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { UMFrontendLibs } from '../lib';
export function compose(): UMFrontendLibs {
const libs: UMFrontendLibs = {
framework: new UMKibanaFrameworkAdapter(uiRoutes),
};
return libs;
}

View file

@ -7,8 +7,8 @@
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import React from 'react';
import { ChromeBreadcrumb } from 'src/core/public';
import { UMBadge } from '../badge';
import { UMBreadcrumb } from '../breadcrumbs';
import { UptimeAppProps } from '../uptime_app';
import { CreateGraphQLClient } from './adapters/framework/framework_adapter_types';
@ -16,7 +16,7 @@ export interface UMFrontendLibs {
framework: UMFrameworkAdapter;
}
export type UMUpdateBreadcrumbs = (breadcrumbs: UMBreadcrumb[]) => void;
export type UMUpdateBreadcrumbs = (breadcrumbs: ChromeBreadcrumb[]) => void;
export type UMUpdateBadge = (badge: UMBadge) => void;

View file

@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import React, { Fragment, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { AutocompleteProviderRegister } from 'src/plugins/data/public';
import { getOverviewPageBreadcrumbs } from '../breadcrumbs';
import {
EmptyState,
@ -29,12 +30,13 @@ import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } f
interface OverviewPageProps {
basePath: string;
logOverviewPageLoad: () => void;
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>;
history: any;
location: {
pathname: string;
search: string;
};
logOverviewPageLoad: () => void;
setBreadcrumbs: UMUpdateBreadcrumbs;
}
@ -54,7 +56,12 @@ const EuiFlexItemStyled = styled(EuiFlexItem)`
}
`;
export const OverviewPage = ({ basePath, logOverviewPageLoad, setBreadcrumbs }: Props) => {
export const OverviewPage = ({
basePath,
autocomplete,
logOverviewPageLoad,
setBreadcrumbs,
}: Props) => {
const { colors, setHeadingText } = useContext(UptimeSettingsContext);
const [getUrlParams, updateUrl] = useUrlParams();
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = getUrlParams();
@ -130,7 +137,7 @@ export const OverviewPage = ({ basePath, logOverviewPageLoad, setBreadcrumbs }:
<EmptyState basePath={basePath} implementsCustomErrorState={true} variables={{}}>
<EuiFlexGroup gutterSize="xs" wrap responsive>
<EuiFlexItem grow={1} style={{ flexBasis: 500 }}>
<KueryBar />
<KueryBar autocomplete={autocomplete} />
</EuiFlexItem>
<EuiFlexItemStyled grow={true}>
<FilterGroup

View file

@ -13,8 +13,8 @@ import React, { useEffect, useState } from 'react';
import { ApolloProvider } from 'react-apollo';
import { Provider as ReduxProvider } from 'react-redux';
import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { capabilities } from 'ui/capabilities';
import { I18nContext } from 'ui/i18n';
import { I18nStart, ChromeBreadcrumb } from 'src/core/public';
import { AutocompleteProviderRegister } from 'src/plugins/data/public';
import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib';
import { MonitorPage, OverviewPage, NotFoundPage } from './pages';
import { UptimeRefreshContext, UptimeSettingsContext, UMSettingsContextValues } from './contexts';
@ -32,11 +32,15 @@ export interface UptimeAppColors {
export interface UptimeAppProps {
basePath: string;
canSave: boolean;
client: UMGraphQLClient;
darkMode: boolean;
autocomplete: Pick<AutocompleteProviderRegister, 'getProvider'>;
i18n: I18nStart;
isApmAvailable: boolean;
isInfraAvailable: boolean;
isLogsAvailable: boolean;
kibanaBreadcrumbs: ChromeBreadcrumb[];
logMonitorPageLoad: () => void;
logOverviewPageLoad: () => void;
routerBasename: string;
@ -48,8 +52,11 @@ export interface UptimeAppProps {
const Application = (props: UptimeAppProps) => {
const {
basePath,
canSave,
client,
darkMode,
autocomplete,
i18n: i18nCore,
isApmAvailable,
isInfraAvailable,
isLogsAvailable,
@ -86,7 +93,7 @@ const Application = (props: UptimeAppProps) => {
useEffect(() => {
renderGlobalHelpControls();
setBadge(
!capabilities.get().uptime.save
!canSave
? {
text: i18n.translate('xpack.uptime.badge.readOnly.text', {
defaultMessage: 'Read only',
@ -133,7 +140,7 @@ const Application = (props: UptimeAppProps) => {
};
return (
<I18nContext>
<i18nCore.Context>
<ReduxProvider store={store}>
<Router basename={routerBasename}>
<Route
@ -167,6 +174,7 @@ const Application = (props: UptimeAppProps) => {
render={routerProps => (
<OverviewPage
basePath={basePath}
autocomplete={autocomplete}
logOverviewPageLoad={logOverviewPageLoad}
setBreadcrumbs={setBreadcrumbs}
{...routerProps}
@ -196,7 +204,7 @@ const Application = (props: UptimeAppProps) => {
/>
</Router>
</ReduxProvider>
</I18nContext>
</i18nCore.Context>
);
};