[Logs + Metrics UI] Clean up async plugin initialization (#67654)

This refactors the browser-side plugin bootstrap code such that the eagerly loaded bundle `infra.plugin.js` is minimal and the rest of the logs and metrics app bundles are loaded only when the apps are visited.
This commit is contained in:
Felix Stürmer 2020-06-09 23:37:26 +02:00 committed by GitHub
parent 2e3578602f
commit 938771a537
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 528 additions and 780 deletions

View file

@ -384,3 +384,7 @@ export const Expressions: React.FC<Props> = (props) => {
</>
);
};
// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Expressions;

View file

@ -5,7 +5,6 @@
*/
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import {
MetricExpressionParams,
Comparator,
@ -106,3 +105,5 @@ export function validateMetricThreshold({
return validationResult;
}
const isNumber = (value: unknown): value is number => typeof value === 'number';

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
import { Expressions } from './components/expression';
import { validateMetricThreshold } from './components/validation';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types';
@ -18,7 +18,7 @@ export function createMetricThresholdAlertType(): AlertTypeModel {
defaultMessage: 'Metric threshold',
}),
iconClass: 'bell',
alertParamsExpression: Expressions,
alertParamsExpression: React.lazy(() => import('./components/expression')),
validate: validateMetricThreshold,
defaultActionMessage: i18n.translate(
'xpack.infra.metrics.alerting.threshold.defaultActionMessage',

View file

@ -0,0 +1,47 @@
/*
* 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 { CoreStart } from 'kibana/public';
import { ApolloClient } from 'apollo-client';
import {
useUiSetting$,
KibanaContextProvider,
} from '../../../../../src/plugins/kibana_react/public';
import { TriggersActionsProvider } from '../utils/triggers_actions_context';
import { ClientPluginDeps } from '../types';
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
import { ApolloClientContext } from '../utils/apollo_context';
import { EuiThemeProvider } from '../../../observability/public';
import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt';
export const CommonInfraProviders: React.FC<{
apolloClient: ApolloClient<{}>;
triggersActionsUI: TriggersAndActionsUIPublicPluginStart;
}> = ({ apolloClient, children, triggersActionsUI }) => {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
return (
<TriggersActionsProvider triggersActionsUI={triggersActionsUI}>
<ApolloClientContext.Provider value={apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<NavigationWarningPromptProvider>{children}</NavigationWarningPromptProvider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</TriggersActionsProvider>
);
};
export const CoreProviders: React.FC<{
core: CoreStart;
plugins: ClientPluginDeps;
}> = ({ children, core, plugins }) => {
return (
<KibanaContextProvider services={{ ...core, ...plugins }}>
<core.i18n.Context>{children}</core.i18n.Context>
</KibanaContextProvider>
);
};

View file

@ -0,0 +1,13 @@
/*
* 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 CONTAINER_CLASSNAME = 'infra-container-element';
export const prepareMountElement = (element: HTMLElement) => {
// Ensure the element we're handed from application mounting is assigned a class
// for our index.scss styles to apply to.
element.classList.add(CONTAINER_CLASSNAME);
};

View file

@ -0,0 +1,98 @@
/*
* 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 { EuiErrorBoundary } from '@elastic/eui';
import { createBrowserHistory, History } from 'history';
import { AppMountParameters } from 'kibana/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, RouteProps, Router, Switch } from 'react-router-dom';
import url from 'url';
// This exists purely to facilitate legacy app/infra URL redirects.
// It will be removed in 8.0.0.
export async function renderApp({ element }: AppMountParameters) {
const history = createBrowserHistory();
ReactDOM.render(<LegacyApp history={history} />, element);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
}
const LegacyApp: React.FunctionComponent<{ history: History<unknown> }> = ({ history }) => {
return (
<EuiErrorBoundary>
<Router history={history}>
<Switch>
<Route
path={'/'}
render={({ location }: RouteProps) => {
if (!location) {
return null;
}
let nextPath = '';
let nextBasePath = '';
let nextSearch;
if (
location.hash.indexOf('#infrastructure') > -1 ||
location.hash.indexOf('#/infrastructure') > -1
) {
nextPath = location.hash.replace(
new RegExp(
'#infrastructure/|#/infrastructure/|#/infrastructure|#infrastructure',
'g'
),
''
);
nextBasePath = location.pathname.replace('app/infra', 'app/metrics');
} else if (
location.hash.indexOf('#logs') > -1 ||
location.hash.indexOf('#/logs') > -1
) {
nextPath = location.hash.replace(
new RegExp('#logs/|#/logs/|#/logs|#logs', 'g'),
''
);
nextBasePath = location.pathname.replace('app/infra', 'app/logs');
} else {
// This covers /app/infra and /app/infra/home (both of which used to render
// the metrics inventory page)
nextPath = 'inventory';
nextBasePath = location.pathname.replace('app/infra', 'app/metrics');
nextSearch = undefined;
}
// app/infra#infrastructure/metrics/:type/:node was changed to app/metrics/detail/:type/:node, this
// accounts for that edge case
nextPath = nextPath.replace('metrics/', 'detail/');
// Query parameters (location.search) will arrive as part of location.hash and not location.search
const nextPathParts = nextPath.split('?');
nextPath = nextPathParts[0];
nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined;
let nextUrl = url.format({
pathname: `${nextBasePath}/${nextPath}`,
hash: undefined,
search: nextSearch,
});
nextUrl = nextUrl.replace('//', '/');
window.location.href = nextUrl;
return null;
}}
/>
</Switch>
</Router>
</EuiErrorBoundary>
);
};

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 { ApolloClient } from 'apollo-client';
import { History } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import { AppMountParameters } from '../../../../../src/core/public';
import '../index.scss';
import { NotFoundPage } from '../pages/404';
import { LinkToLogsPage } from '../pages/link_to/link_to_logs';
import { LogsPage } from '../pages/logs';
import { ClientPluginDeps } from '../types';
import { createApolloClient } from '../utils/apollo_client';
import { CommonInfraProviders, CoreProviders } from './common_providers';
import { prepareMountElement } from './common_styles';
export const renderApp = (
core: CoreStart,
plugins: ClientPluginDeps,
{ element, history }: AppMountParameters
) => {
const apolloClient = createApolloClient(core.http.fetch);
prepareMountElement(element);
ReactDOM.render(
<LogsApp apolloClient={apolloClient} core={core} history={history} plugins={plugins} />,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
const LogsApp: React.FC<{
apolloClient: ApolloClient<{}>;
core: CoreStart;
history: History<unknown>;
plugins: ClientPluginDeps;
}> = ({ apolloClient, core, history, plugins }) => {
const uiCapabilities = core.application.capabilities;
return (
<CoreProviders core={core} plugins={plugins}>
<CommonInfraProviders
apolloClient={apolloClient}
triggersActionsUI={plugins.triggers_actions_ui}
>
<Router history={history}>
<Switch>
<Route path="/link-to" component={LinkToLogsPage} />
{uiCapabilities?.logs?.show && <Route path="/" component={LogsPage} />}
<Route component={NotFoundPage} />
</Switch>
</Router>
</CommonInfraProviders>
</CoreProviders>
);
};

View file

@ -0,0 +1,82 @@
/*
* 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 { ApolloClient } from 'apollo-client';
import { History } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import { AppMountParameters } from '../../../../../src/core/public';
import '../index.scss';
import { NotFoundPage } from '../pages/404';
import { LinkToMetricsPage } from '../pages/link_to/link_to_metrics';
import { InfrastructurePage } from '../pages/metrics';
import { MetricDetail } from '../pages/metrics/metric_detail';
import { ClientPluginDeps } from '../types';
import { createApolloClient } from '../utils/apollo_client';
import { RedirectWithQueryParams } from '../utils/redirect_with_query_params';
import { CommonInfraProviders, CoreProviders } from './common_providers';
import { prepareMountElement } from './common_styles';
export const renderApp = (
core: CoreStart,
plugins: ClientPluginDeps,
{ element, history }: AppMountParameters
) => {
const apolloClient = createApolloClient(core.http.fetch);
prepareMountElement(element);
ReactDOM.render(
<MetricsApp apolloClient={apolloClient} core={core} history={history} plugins={plugins} />,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
const MetricsApp: React.FC<{
apolloClient: ApolloClient<{}>;
core: CoreStart;
history: History<unknown>;
plugins: ClientPluginDeps;
}> = ({ apolloClient, core, history, plugins }) => {
const uiCapabilities = core.application.capabilities;
return (
<CoreProviders core={core} plugins={plugins}>
<CommonInfraProviders
apolloClient={apolloClient}
triggersActionsUI={plugins.triggers_actions_ui}
>
<Router history={history}>
<Switch>
<Route path="/link-to" component={LinkToMetricsPage} />
{uiCapabilities?.infrastructure?.show && (
<RedirectWithQueryParams from="/" exact={true} to="/inventory" />
)}
{uiCapabilities?.infrastructure?.show && (
<RedirectWithQueryParams from="/snapshot" exact={true} to="/inventory" />
)}
{uiCapabilities?.infrastructure?.show && (
<RedirectWithQueryParams from="/metrics-explorer" exact={true} to="/explorer" />
)}
{uiCapabilities?.infrastructure?.show && (
<Route path="/detail/:type/:node" component={MetricDetail} />
)}
{uiCapabilities?.infrastructure?.show && (
<Route path="/" component={InfrastructurePage} />
)}
<Route component={NotFoundPage} />
</Switch>
</Router>
</CommonInfraProviders>
</CoreProviders>
);
};

View file

@ -1,80 +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 from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from 'react-apollo';
import { CoreStart, AppMountParameters } from 'kibana/public';
// TODO use theme provided from parentApp when kibana supports it
import { EuiErrorBoundary } from '@elastic/eui';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { EuiThemeProvider } from '../../../observability/public/typings/eui_styled_components';
import { InfraFrontendLibs } from '../lib/lib';
import { ApolloClientContext } from '../utils/apollo_context';
import { HistoryContext } from '../utils/history_context';
import {
useUiSetting$,
KibanaContextProvider,
} from '../../../../../src/plugins/kibana_react/public';
import { AppRouter } from '../routers';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
import { TriggersActionsProvider } from '../utils/triggers_actions_context';
import '../index.scss';
import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt';
export const CONTAINER_CLASSNAME = 'infra-container-element';
export async function startApp(
libs: InfraFrontendLibs,
core: CoreStart,
plugins: object,
params: AppMountParameters,
Router: AppRouter,
triggersActionsUI: TriggersAndActionsUIPublicPluginSetup
) {
const { element, history } = params;
const InfraPluginRoot: React.FunctionComponent = () => {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
return (
<core.i18n.Context>
<EuiErrorBoundary>
<TriggersActionsProvider triggersActionsUI={triggersActionsUI}>
<ApolloProvider client={libs.apolloClient}>
<ApolloClientContext.Provider value={libs.apolloClient}>
<EuiThemeProvider darkMode={darkMode}>
<HistoryContext.Provider value={history}>
<NavigationWarningPromptProvider>
<Router history={history} />
</NavigationWarningPromptProvider>
</HistoryContext.Provider>
</EuiThemeProvider>
</ApolloClientContext.Provider>
</ApolloProvider>
</TriggersActionsProvider>
</EuiErrorBoundary>
</core.i18n.Context>
);
};
const App: React.FunctionComponent = () => (
<KibanaContextProvider services={{ ...core, ...plugins }}>
<InfraPluginRoot />
</KibanaContextProvider>
);
// Ensure the element we're handed from application mounting is assigned a class
// for our index.scss styles to apply to.
element.className += ` ${CONTAINER_CLASSNAME}`;
ReactDOM.render(<App />, element);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
}

View file

@ -1,100 +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 { createBrowserHistory } from 'history';
import React from 'react';
import url from 'url';
import ReactDOM from 'react-dom';
import { AppMountParameters } from 'kibana/public';
import { Route, Router, Switch, RouteProps } from 'react-router-dom';
// TODO use theme provided from parentApp when kibana supports it
import { EuiErrorBoundary } from '@elastic/eui';
// This exists purely to facilitate legacy app/infra URL redirects.
// It will be removed in 8.0.0.
export async function startLegacyApp(params: AppMountParameters) {
const { element } = params;
const history = createBrowserHistory();
const App: React.FunctionComponent = () => {
return (
<EuiErrorBoundary>
<Router history={history}>
<Switch>
<Route
path={'/'}
render={({ location }: RouteProps) => {
if (!location) {
return null;
}
let nextPath = '';
let nextBasePath = '';
let nextSearch;
if (
location.hash.indexOf('#infrastructure') > -1 ||
location.hash.indexOf('#/infrastructure') > -1
) {
nextPath = location.hash.replace(
new RegExp(
'#infrastructure/|#/infrastructure/|#/infrastructure|#infrastructure',
'g'
),
''
);
nextBasePath = location.pathname.replace('app/infra', 'app/metrics');
} else if (
location.hash.indexOf('#logs') > -1 ||
location.hash.indexOf('#/logs') > -1
) {
nextPath = location.hash.replace(
new RegExp('#logs/|#/logs/|#/logs|#logs', 'g'),
''
);
nextBasePath = location.pathname.replace('app/infra', 'app/logs');
} else {
// This covers /app/infra and /app/infra/home (both of which used to render
// the metrics inventory page)
nextPath = 'inventory';
nextBasePath = location.pathname.replace('app/infra', 'app/metrics');
nextSearch = undefined;
}
// app/inra#infrastructure/metrics/:type/:node was changed to app/metrics/detail/:type/:node, this
// accounts for that edge case
nextPath = nextPath.replace('metrics/', 'detail/');
// Query parameters (location.search) will arrive as part of location.hash and not location.search
const nextPathParts = nextPath.split('?');
nextPath = nextPathParts[0];
nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined;
let nextUrl = url.format({
pathname: `${nextBasePath}/${nextPath}`,
hash: undefined,
search: nextSearch,
});
nextUrl = nextUrl.replace('//', '/');
window.location.href = nextUrl;
return null;
}}
/>
</Switch>
</Router>
</EuiErrorBoundary>
);
};
ReactDOM.render(<App />, element);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
}

View file

@ -336,6 +336,10 @@ export const Expressions: React.FC<Props> = (props) => {
);
};
// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Expressions;
interface ExpressionRowProps {
nodeType: InventoryItemType;
expressionId: number;

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types';
import { Expressions } from './expression';
import { validateMetricThreshold } from './validation';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
@ -18,7 +18,7 @@ export function getInventoryMetricAlertType(): AlertTypeModel {
defaultMessage: 'Inventory',
}),
iconClass: 'bell',
alertParamsExpression: Expressions,
alertParamsExpression: React.lazy(() => import('./expression')),
validate: validateMetricThreshold,
defaultActionMessage: i18n.translate(
'xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage',

View file

@ -6,8 +6,6 @@
import { i18n } from '@kbn/i18n';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { isNumber } from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
@ -95,3 +93,5 @@ export function validateMetricThreshold({
return validationResult;
}
const isNumber = (value: unknown): value is number => typeof value === 'number';

View file

@ -236,3 +236,7 @@ export const Editor: React.FC<Props> = (props) => {
</>
);
};
// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Editor;

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types';
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types';
import { ExpressionEditor } from './expression_editor';
import { validateExpression } from './validation';
export function getAlertType(): AlertTypeModel {
@ -17,7 +17,7 @@ export function getAlertType(): AlertTypeModel {
defaultMessage: 'Log threshold',
}),
iconClass: 'bell',
alertParamsExpression: ExpressionEditor,
alertParamsExpression: React.lazy(() => import('./expression_editor/editor')),
validate: validateExpression,
defaultActionMessage: i18n.translate(
'xpack.infra.logs.alerting.threshold.defaultActionMessage',

View file

@ -1,99 +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 { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
import { withClientState } from 'apollo-link-state';
import { CoreStart, HttpFetchOptions } from 'src/core/public';
import { InfraFrontendLibs } from './lib/lib';
import introspectionQueryResultData from './graphql/introspection.json';
import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api';
export function composeLibs(core: CoreStart) {
const cache = new InMemoryCache({
addTypename: false,
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),
});
const observableApi = new InfraKibanaObservableApiAdapter({
basePath: core.http.basePath.get(),
});
const wrappedFetch = (path: string, options: HttpFetchOptions) => {
return new Promise<Response>(async (resolve, reject) => {
// core.http.fetch isn't 100% compatible with the Fetch API and will
// throw Errors on 401s. This top level try / catch handles those scenarios.
try {
core.http
.fetch(path, {
...options,
// Set headers to undefined due to this bug: https://github.com/apollographql/apollo-link/issues/249,
// Apollo will try to set a "content-type" header which will conflict with the "Content-Type" header that
// core.http.fetch correctly sets.
headers: undefined,
asResponse: true,
})
.then((res) => {
if (!res.response) {
return reject();
}
// core.http.fetch will parse the Response and set a body before handing it back. As such .text() / .json()
// will have already been called on the Response instance. However, Apollo will also want to call
// .text() / .json() on the instance, as it expects the raw Response instance, rather than core's wrapper.
// .text() / .json() can only be called once, and an Error will be thrown if those methods are accessed again.
// This hacks around that by setting up a new .text() method that will restringify the JSON response we already have.
// This does result in an extra stringify / parse cycle, which isn't ideal, but as we only have a few endpoints left using
// GraphQL this shouldn't create excessive overhead.
// Ref: https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http/src/httpLink.ts#L134
// and
// https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L125
return resolve({
...res.response,
text: () => {
return new Promise(async (resolveText, rejectText) => {
if (res.body) {
return resolveText(JSON.stringify(res.body));
} else {
return rejectText();
}
});
},
});
});
} catch (error) {
reject(error);
}
});
};
const HttpLink = createHttpLink({
fetch: wrappedFetch,
uri: `/api/infra/graphql`,
});
const graphQLOptions = {
cache,
link: ApolloLink.from([
withClientState({
cache,
resolvers: {},
}),
HttpLink,
]),
};
const apolloClient = new ApolloClient(graphQLOptions);
const libs: InfraFrontendLibs = {
apolloClient,
observableApi,
};
return libs;
}

View file

@ -1,131 +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 { parse, stringify } from 'query-string';
import { Location } from 'history';
import { omit } from 'lodash';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
// eslint-disable-next-line @typescript-eslint/camelcase
import { decode_object, encode_object } from 'rison-node';
import { Omit } from '../lib/lib';
interface AnyObject {
[key: string]: any;
}
interface WithStateFromLocationOptions<StateInLocation> {
mapLocationToState: (location: Location) => StateInLocation;
mapStateToLocation: (state: StateInLocation, location: Location) => Location;
}
type InjectedPropsFromLocation<StateInLocation> = Partial<StateInLocation> & {
pushStateInLocation?: (state: StateInLocation) => void;
replaceStateInLocation?: (state: StateInLocation) => void;
};
export const withStateFromLocation = <StateInLocation extends {}>({
mapLocationToState,
mapStateToLocation,
}: WithStateFromLocationOptions<StateInLocation>) => <
WrappedComponentProps extends InjectedPropsFromLocation<StateInLocation>
>(
WrappedComponent: React.ComponentType<WrappedComponentProps>
) => {
const wrappedName = WrappedComponent.displayName || WrappedComponent.name;
return withRouter(
class WithStateFromLocation extends React.PureComponent<
RouteComponentProps<{}> &
Omit<WrappedComponentProps, InjectedPropsFromLocation<StateInLocation>>
> {
public static displayName = `WithStateFromLocation(${wrappedName})`;
public render() {
const { location } = this.props;
const otherProps = omit(this.props, ['location', 'history', 'match', 'staticContext']);
const stateFromLocation = mapLocationToState(location);
return (
// @ts-ignore
<WrappedComponent
{...otherProps}
{...stateFromLocation}
pushStateInLocation={this.pushStateInLocation}
replaceStateInLocation={this.replaceStateInLocation}
/>
);
}
private pushStateInLocation = (state: StateInLocation) => {
const { history, location } = this.props;
const newLocation = mapStateToLocation(state, this.props.location);
if (newLocation !== location) {
history.push(newLocation);
}
};
private replaceStateInLocation = (state: StateInLocation) => {
const { history, location } = this.props;
const newLocation = mapStateToLocation(state, this.props.location);
if (newLocation !== location) {
history.replace(newLocation);
}
};
}
);
};
const decodeRisonAppState = (queryValues: { _a?: string }): AnyObject => {
try {
return queryValues && queryValues._a ? decode_object(queryValues._a) : {};
} catch (error) {
if (error instanceof Error && error.message.startsWith('rison decoder error')) {
return {};
}
throw error;
}
};
const encodeRisonAppState = (state: AnyObject) => ({
_a: encode_object(state),
});
export const mapRisonAppLocationToState = <State extends {}>(
mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => state as State
) => (location: Location): State => {
const queryValues = parse(location.search.substring(1), { sort: false });
const decodedState = decodeRisonAppState(queryValues);
return mapState(decodedState);
};
export const mapStateToRisonAppLocation = <State extends {}>(
mapState: (state: State) => AnyObject = (state: State) => state
) => (state: State, location: Location): Location => {
const previousQueryValues = parse(location.search.substring(1), { sort: false });
const previousState = decodeRisonAppState(previousQueryValues);
const encodedState = encodeRisonAppState({
...previousState,
...mapState(state),
});
const newQueryValues = stringify(
{
...previousQueryValues,
...encodedState,
},
{ sort: false }
);
return {
...location,
search: `?${newQueryValues}`,
};
};

View file

@ -1,44 +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 gql from 'graphql-tag';
import { sharedFragments } from '../../common/graphql/shared';
export const logEntriesQuery = gql`
query LogEntries(
$sourceId: ID = "default"
$timeKey: InfraTimeKeyInput!
$countBefore: Int = 0
$countAfter: Int = 0
$filterQuery: String
) {
source(id: $sourceId) {
id
logEntriesAround(
key: $timeKey
countBefore: $countBefore
countAfter: $countAfter
filterQuery: $filterQuery
) {
start {
...InfraTimeKeyFields
}
end {
...InfraTimeKeyFields
}
hasMoreBefore
hasMoreAfter
entries {
...InfraLogEntryFields
}
}
}
}
${sharedFragments.InfraTimeKey}
${sharedFragments.InfraLogEntryFields}
`;

View file

@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { encode } from 'rison-node';
import { createMemoryHistory } from 'history';
import { renderHook } from '@testing-library/react-hooks';
import { createMemoryHistory } from 'history';
import React from 'react';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { HistoryContext } from '../utils/history_context';
import { Router } from 'react-router-dom';
import { encode } from 'rison-node';
import { coreMock } from 'src/core/public/mocks';
import { useLinkProps, LinkDescriptor } from './use_link_props';
import { ScopedHistory } from '../../../../../src/core/public';
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { LinkDescriptor, useLinkProps } from './use_link_props';
const PREFIX = '/test-basepath/s/test-space/app/';
@ -30,9 +30,9 @@ const scopedHistory = new ScopedHistory(history, `${PREFIX}${INTERNAL_APP}`);
const ProviderWrapper: React.FC = ({ children }) => {
return (
<HistoryContext.Provider value={scopedHistory}>
<Router history={scopedHistory}>
<KibanaContextProvider services={{ ...coreStartMock }}>{children}</KibanaContextProvider>;
</HistoryContext.Provider>
</Router>
);
};

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext, PluginInitializer } from 'kibana/public';
import { Plugin, ClientSetup, ClientStart, ClientPluginsSetup, ClientPluginsStart } from './plugin';
import { PluginInitializer, PluginInitializerContext } from 'kibana/public';
import { ClientSetup, ClientStart, Plugin } from './plugin';
import { ClientPluginsSetup, ClientPluginsStart } from './types';
export const plugin: PluginInitializer<
ClientSetup,

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 { ajax } from 'rxjs/ajax';
import { map } from 'rxjs/operators';
import {
InfraObservableApi,
InfraObservableApiPostParams,
InfraObservableApiResponse,
} from '../../lib';
export class InfraKibanaObservableApiAdapter implements InfraObservableApi {
private basePath: string;
private defaultHeaders: {
[headerName: string]: boolean | string;
};
constructor({ basePath }: { basePath: string }) {
this.basePath = basePath;
this.defaultHeaders = {
'kbn-xsrf': true,
};
}
public post = <RequestBody extends {} = {}, ResponseBody extends {} = {}>({
url,
body,
}: InfraObservableApiPostParams<RequestBody>): InfraObservableApiResponse<ResponseBody> =>
ajax({
body: body ? JSON.stringify(body) : undefined,
headers: {
...this.defaultHeaders,
'Content-Type': 'application/json',
},
method: 'POST',
responseType: 'json',
timeout: 30000,
url: `${this.basePath}/api/${url}`,
withCredentials: true,
}).pipe(map(({ response, status }) => ({ response, status })));
}

View file

@ -4,102 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { IModule, IScope } from 'angular';
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import { AxiosRequestConfig } from 'axios';
import React from 'react';
import { Observable } from 'rxjs';
import * as rt from 'io-ts';
import { i18n } from '@kbn/i18n';
import { SourceQuery } from '../graphql/types';
import * as rt from 'io-ts';
import {
SnapshotMetricInput,
SnapshotGroupBy,
InfraTimerangeInput,
SnapshotGroupBy,
SnapshotMetricInput,
SnapshotNodeMetric,
SnapshotNodePath,
} from '../../common/http_api/snapshot_api';
import { SourceQuery } from '../graphql/types';
import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options';
export interface InfraFrontendLibs {
apolloClient: InfraApolloClient;
observableApi: InfraObservableApi;
}
export type InfraTimezoneProvider = () => string;
export type InfraApolloClient = ApolloClient<NormalizedCacheObject>;
export interface InfraFrameworkAdapter {
// Insstance vars
appState?: object;
kbnVersion?: string;
timezone?: string;
// Methods
setUISettings(key: string, value: any): void;
render(component: React.ReactElement<any>): void;
renderBreadcrumbs(component: React.ReactElement<any>): void;
}
export type InfraFramworkAdapterConstructable = new (
uiModule: IModule,
timezoneProvider: InfraTimezoneProvider
) => InfraFrameworkAdapter;
// TODO: replace AxiosRequestConfig with something more defined
export type InfraRequestConfig = AxiosRequestConfig;
export interface InfraApiAdapter {
get<T>(url: string, config?: InfraRequestConfig | undefined): Promise<T>;
post(url: string, data?: any, config?: AxiosRequestConfig | undefined): Promise<object>;
delete(url: string, config?: InfraRequestConfig | undefined): Promise<object>;
put(url: string, data?: any, config?: InfraRequestConfig | undefined): Promise<object>;
}
export interface InfraObservableApiPostParams<RequestBody extends {} = {}> {
url: string;
body?: RequestBody;
}
export type InfraObservableApiResponse<BodyType extends {} = {}> = Observable<{
status: number;
response: BodyType;
}>;
export interface InfraObservableApi {
post<RequestBody extends {} = {}, ResponseBody extends {} = {}>(
params: InfraObservableApiPostParams<RequestBody>
): InfraObservableApiResponse<ResponseBody>;
}
export interface InfraUiKibanaAdapterScope extends IScope {
breadcrumbs: any[];
topNavMenu: any[];
}
export interface InfraKibanaUIConfig {
get(key: string): any;
set(key: string, value: any): Promise<boolean>;
}
export interface InfraKibanaAdapterServiceRefs {
config: InfraKibanaUIConfig;
rootScope: IScope;
}
export type InfraBufferedKibanaServiceCall<ServiceRefs> = (serviceRefs: ServiceRefs) => void;
export interface InfraField {
name: string;
type: string;
searchable: boolean;
aggregatable: boolean;
}
export type InfraWaffleData = InfraWaffleMapGroup[];
export interface InfraWaffleMapNode {
pathId: string;
id: string;
@ -221,8 +137,6 @@ export interface InfraOptions {
wafflemap: InfraWaffleMapOptions;
}
export type Omit<T1, T2> = Pick<T1, Exclude<keyof T1, keyof T2>>;
export interface InfraWaffleMapBounds {
min: number;
max: number;

View file

@ -35,7 +35,7 @@ describe('RedirectToNodeLogs component', () => {
expect(component).toMatchInlineSnapshot(`
<Redirect
to="/?sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
to="/stream?sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
/>
`);
});
@ -47,7 +47,7 @@ describe('RedirectToNodeLogs component', () => {
expect(component).toMatchInlineSnapshot(`
<Redirect
to="/?sourceId=default&logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)"
to="/stream?sourceId=default&logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)"
/>
`);
});
@ -59,7 +59,7 @@ describe('RedirectToNodeLogs component', () => {
expect(component).toMatchInlineSnapshot(`
<Redirect
to="/?sourceId=default&logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)"
to="/stream?sourceId=default&logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)"
/>
`);
});
@ -73,7 +73,7 @@ describe('RedirectToNodeLogs component', () => {
expect(component).toMatchInlineSnapshot(`
<Redirect
to="/?logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
to="/stream?logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
/>
`);
});
@ -89,7 +89,7 @@ describe('RedirectToNodeLogs component', () => {
expect(component).toMatchInlineSnapshot(`
<Redirect
to="/?logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&sourceId=default&logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)"
to="/stream?logPosition=(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)&sourceId=default&logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)"
/>
`);
});
@ -103,7 +103,7 @@ describe('RedirectToNodeLogs component', () => {
expect(component).toMatchInlineSnapshot(`
<Redirect
to="/?sourceId=SOME-OTHER-SOURCE&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
to="/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)"
/>
`);
});

View file

@ -71,7 +71,7 @@ export const RedirectToNodeLogs = ({
replaceSourceIdInQueryString(sourceId)
)('');
return <Redirect to={`/?${searchString}`} />;
return <Redirect to={`/stream?${searchString}`} />;
};
export const getNodeLogsUrl = ({

View file

@ -96,6 +96,7 @@ export const LogsPageContent: React.FunctionComponent = () => {
<Route path={logCategoriesTab.pathname} component={LogEntryCategoriesPage} />
<Route path={settingsTab.pathname} component={LogsSettingsPage} />
<RedirectWithQueryParams from={'/analysis'} to={logRateTab.pathname} exact />
<RedirectWithQueryParams from={'/'} to={streamTab.pathname} exact />
</Switch>
</ColumnarPage>
);

View file

@ -15,7 +15,7 @@ import { fieldToName } from '../lib/field_to_display_name';
import { NodeContextMenu } from './waffle/node_context_menu';
import { InventoryItemType } from '../../../../../common/inventory_models/types';
import { SnapshotNode, SnapshotNodePath } from '../../../../../common/http_api/snapshot_api';
import { CONTAINER_CLASSNAME } from '../../../../apps/start_app';
import { CONTAINER_CLASSNAME } from '../../../../apps/common_styles';
interface Props {
nodes: SnapshotNode[];

View file

@ -26,7 +26,9 @@ export const useWaffleTime = () => {
const [state, setState] = useState<WaffleTimeState>(urlState);
useEffect(() => setUrlState(state), [setUrlState, state]);
useEffect(() => {
setUrlState(state);
}, [setUrlState, state]);
const { currentTime, isAutoReloading } = urlState;

View file

@ -4,14 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createMemoryHistory } from 'history';
import React from 'react';
import { Router } from 'react-router-dom';
import { mountHook } from 'test_utils/enzyme_helpers';
import { ScopedHistory } from '../../../../../../../../src/core/public';
import { useMetricsTime } from './use_metrics_time';
describe('useMetricsTime hook', () => {
describe('timeRange state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useMetricsTime().timeRange);
const { getLastHookValue } = mountHook(
() => useMetricsTime().timeRange,
createProviderWrapper()
);
const hookValue = getLastHookValue();
expect(hookValue).toHaveProperty('from');
expect(hookValue).toHaveProperty('to');
@ -19,7 +25,7 @@ describe('useMetricsTime hook', () => {
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useMetricsTime());
const { act, getLastHookValue } = mountHook(() => useMetricsTime(), createProviderWrapper());
const timeRange = {
from: 'now-15m',
@ -37,12 +43,15 @@ describe('useMetricsTime hook', () => {
describe('AutoReloading state', () => {
it('has a default value', () => {
const { getLastHookValue } = mountHook(() => useMetricsTime().isAutoReloading);
const { getLastHookValue } = mountHook(
() => useMetricsTime().isAutoReloading,
createProviderWrapper()
);
expect(getLastHookValue()).toBe(false);
});
it('can be updated', () => {
const { act, getLastHookValue } = mountHook(() => useMetricsTime());
const { act, getLastHookValue } = mountHook(() => useMetricsTime(), createProviderWrapper());
act(({ setAutoReload }) => {
setAutoReload(true);
@ -52,3 +61,17 @@ describe('useMetricsTime hook', () => {
});
});
});
const createProviderWrapper = () => {
const INITIAL_URL = '/test-basepath/s/test-space/app/metrics';
const history = createMemoryHistory();
history.push(INITIAL_URL);
const scopedHistory = new ScopedHistory(history, INITIAL_URL);
const ProviderWrapper: React.FC = ({ children }) => {
return <Router history={scopedHistory}>{children}</Router>;
};
return ProviderWrapper;
};

View file

@ -4,54 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { merge } from 'lodash';
import {
Plugin as PluginClass,
AppMountParameters,
CoreSetup,
CoreStart,
Plugin as PluginClass,
PluginInitializerContext,
AppMountParameters,
} from 'kibana/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type';
import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type';
import { registerStartSingleton } from './legacy_singletons';
import { registerFeatures } from './register_feature';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public';
import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type';
import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type';
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
import { ClientPluginsSetup, ClientPluginsStart } from './types';
export type ClientSetup = void;
export type ClientStart = void;
export interface ClientPluginsSetup {
home: HomePublicPluginSetup;
data: DataPublicPluginSetup;
usageCollection: UsageCollectionSetup;
dataEnhanced: DataEnhancedSetup;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
}
export interface ClientPluginsStart {
data: DataPublicPluginStart;
dataEnhanced: DataEnhancedStart;
}
export type InfraPlugins = ClientPluginsSetup & ClientPluginsStart;
const getMergedPlugins = (setup: ClientPluginsSetup, start: ClientPluginsStart): InfraPlugins => {
return merge({}, setup, start);
};
export class Plugin
implements PluginClass<ClientSetup, ClientStart, ClientPluginsSetup, ClientPluginsStart> {
constructor(context: PluginInitializerContext) {}
constructor(_context: PluginInitializerContext) {}
setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) {
setup(core: CoreSetup<ClientPluginsStart, ClientStart>, pluginsSetup: ClientPluginsSetup) {
registerFeatures(pluginsSetup.home);
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getInventoryMetricAlertType());
@ -69,16 +44,18 @@ export class Plugin
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters) => {
const [coreStart, pluginsStart] = await core.getStartServices();
const plugins = getMergedPlugins(pluginsSetup, pluginsStart as ClientPluginsStart);
const { startApp, composeLibs, LogsRouter } = await this.downloadAssets();
const { renderApp } = await import('./apps/logs_app');
return startApp(
composeLibs(coreStart),
return renderApp(
coreStart,
plugins,
params,
LogsRouter,
pluginsSetup.triggers_actions_ui
{
data: pluginsStart.data,
dataEnhanced: pluginsSetup.dataEnhanced,
home: pluginsSetup.home,
triggers_actions_ui: pluginsStart.triggers_actions_ui,
usageCollection: pluginsSetup.usageCollection,
},
params
);
},
});
@ -94,16 +71,18 @@ export class Plugin
category: DEFAULT_APP_CATEGORIES.observability,
mount: async (params: AppMountParameters) => {
const [coreStart, pluginsStart] = await core.getStartServices();
const plugins = getMergedPlugins(pluginsSetup, pluginsStart as ClientPluginsStart);
const { startApp, composeLibs, MetricsRouter } = await this.downloadAssets();
const { renderApp } = await import('./apps/metrics_app');
return startApp(
composeLibs(coreStart),
return renderApp(
coreStart,
plugins,
params,
MetricsRouter,
pluginsSetup.triggers_actions_ui
{
data: pluginsStart.data,
dataEnhanced: pluginsSetup.dataEnhanced,
home: pluginsSetup.home,
triggers_actions_ui: pluginsStart.triggers_actions_ui,
usageCollection: pluginsSetup.usageCollection,
},
params
);
},
});
@ -116,28 +95,14 @@ export class Plugin
title: 'infra',
navLinkStatus: 3,
mount: async (params: AppMountParameters) => {
const { startLegacyApp } = await import('./apps/start_legacy_app');
return startLegacyApp(params);
const { renderApp } = await import('./apps/legacy_app');
return renderApp(params);
},
});
}
start(core: CoreStart, plugins: ClientPluginsStart) {
start(core: CoreStart, _plugins: ClientPluginsStart) {
registerStartSingleton(core);
}
private async downloadAssets() {
const [{ startApp }, { composeLibs }, { LogsRouter, MetricsRouter }] = await Promise.all([
import('./apps/start_app'),
import('./compose_libs'),
import('./routers'),
]);
return {
startApp,
composeLibs,
LogsRouter,
MetricsRouter,
};
}
}

View file

@ -1,15 +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 { History } from 'history';
export * from './logs_router';
export * from './metrics_router';
interface RouterProps {
history: History;
}
export type AppRouter = React.FC<RouterProps>;

View file

@ -1,31 +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 from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { NotFoundPage } from '../pages/404';
import { LinkToLogsPage } from '../pages/link_to';
import { LogsPage } from '../pages/logs';
import { RedirectWithQueryParams } from '../utils/redirect_with_query_params';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { AppRouter } from './index';
export const LogsRouter: AppRouter = ({ history }) => {
const uiCapabilities = useKibana().services.application?.capabilities;
return (
<Router history={history}>
<Switch>
<Route path="/link-to" component={LinkToLogsPage} />
{uiCapabilities?.logs?.show && (
<RedirectWithQueryParams from="/" exact={true} to="/stream" />
)}
{uiCapabilities?.logs?.show && <Route path="/" component={LogsPage} />}
<Route component={NotFoundPage} />
</Switch>
</Router>
);
};

View file

@ -1,41 +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 from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { NotFoundPage } from '../pages/404';
import { InfrastructurePage } from '../pages/metrics';
import { MetricDetail } from '../pages/metrics/metric_detail';
import { RedirectWithQueryParams } from '../utils/redirect_with_query_params';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { AppRouter } from './index';
import { LinkToMetricsPage } from '../pages/link_to';
export const MetricsRouter: AppRouter = ({ history }) => {
const uiCapabilities = useKibana().services.application?.capabilities;
return (
<Router history={history}>
<Switch>
<Route path="/link-to" component={LinkToMetricsPage} />
{uiCapabilities?.infrastructure?.show && (
<RedirectWithQueryParams from="/" exact={true} to="/inventory" />
)}
{uiCapabilities?.infrastructure?.show && (
<RedirectWithQueryParams from="/snapshot" exact={true} to="/inventory" />
)}
{uiCapabilities?.infrastructure?.show && (
<RedirectWithQueryParams from="/metrics-explorer" exact={true} to="/explorer" />
)}
{uiCapabilities?.infrastructure?.show && (
<Route path="/detail/:type/:node" component={MetricDetail} />
)}
{uiCapabilities?.infrastructure?.show && <Route path="/" component={InfrastructurePage} />}
<Route component={NotFoundPage} />
</Switch>
</Router>
);
};

View file

@ -0,0 +1,25 @@
/*
* 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 { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public';
import { DataEnhancedSetup } from '../../data_enhanced/public';
export interface ClientPluginsSetup {
dataEnhanced: DataEnhancedSetup;
home: HomePublicPluginSetup;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
usageCollection: UsageCollectionSetup;
}
export interface ClientPluginsStart {
data: DataPublicPluginStart;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
}
export type ClientPluginDeps = ClientPluginsSetup & ClientPluginsStart;

View file

@ -0,0 +1,85 @@
/*
* 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 { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import ApolloClient from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
import { withClientState } from 'apollo-link-state';
import { HttpFetchOptions, HttpHandler } from 'src/core/public';
import introspectionQueryResultData from '../graphql/introspection.json';
export const createApolloClient = (fetch: HttpHandler) => {
const cache = new InMemoryCache({
addTypename: false,
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),
});
const wrappedFetch = (path: string, options: HttpFetchOptions) => {
return new Promise<Response>(async (resolve, reject) => {
// core.http.fetch isn't 100% compatible with the Fetch API and will
// throw Errors on 401s. This top level try / catch handles those scenarios.
try {
fetch(path, {
...options,
// Set headers to undefined due to this bug: https://github.com/apollographql/apollo-link/issues/249,
// Apollo will try to set a "content-type" header which will conflict with the "Content-Type" header that
// core.http.fetch correctly sets.
headers: undefined,
asResponse: true,
}).then((res) => {
if (!res.response) {
return reject();
}
// core.http.fetch will parse the Response and set a body before handing it back. As such .text() / .json()
// will have already been called on the Response instance. However, Apollo will also want to call
// .text() / .json() on the instance, as it expects the raw Response instance, rather than core's wrapper.
// .text() / .json() can only be called once, and an Error will be thrown if those methods are accessed again.
// This hacks around that by setting up a new .text() method that will restringify the JSON response we already have.
// This does result in an extra stringify / parse cycle, which isn't ideal, but as we only have a few endpoints left using
// GraphQL this shouldn't create excessive overhead.
// Ref: https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http/src/httpLink.ts#L134
// and
// https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-http-common/src/index.ts#L125
return resolve({
...res.response,
text: () => {
return new Promise(async (resolveText, rejectText) => {
if (res.body) {
return resolveText(JSON.stringify(res.body));
} else {
return rejectText();
}
});
},
});
});
} catch (error) {
reject(error);
}
});
};
const HttpLink = createHttpLink({
fetch: wrappedFetch,
uri: `/api/infra/graphql`,
});
const graphQLOptions = {
cache,
link: ApolloLink.from([
withClientState({
cache,
resolvers: {},
}),
HttpLink,
]),
};
return new ApolloClient(graphQLOptions);
};

View file

@ -5,10 +5,10 @@
*/
import * as React from 'react';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public';
interface ContextProps {
triggersActionsUI: TriggersAndActionsUIPublicPluginSetup | null;
triggersActionsUI: TriggersAndActionsUIPublicPluginStart | null;
}
export const TriggerActionsContext = React.createContext<ContextProps>({
@ -16,7 +16,7 @@ export const TriggerActionsContext = React.createContext<ContextProps>({
});
interface Props {
triggersActionsUI: TriggersAndActionsUIPublicPluginSetup;
triggersActionsUI: TriggersAndActionsUIPublicPluginStart;
}
export const TriggersActionsProvider: React.FC<Props> = (props) => {

View file

@ -8,10 +8,9 @@ import { parse, stringify } from 'query-string';
import { Location } from 'history';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { decode, encode, RisonValue } from 'rison-node';
import { useHistory } from 'react-router-dom';
import { url } from '../../../../../src/plugins/kibana_utils/public';
import { useHistory } from './history_context';
export const useUrlState = <State>({
defaultState,
decodeUrlState,