[Exploratory View] Embedddable component (#113108)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2021-10-05 17:23:44 +02:00 committed by GitHub
parent 93d1a7fbcc
commit b8cdc6fd1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 501 additions and 43 deletions

View file

@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/consistent-type-definitions": 0
}
}

View file

@ -0,0 +1,8 @@
# Embedded Observability exploratory view example
To run this example plugin, use the command `yarn start --run-examples`.
This example shows how to embed Exploratory view into other observability solution applications. Using the exploratory view `EmbeddableComponent` of the `observability` start plugin,
you can pass in a valid Exploratory view series attributes which will get rendered the same way exploratory view works using Lens Embeddable. Updating the
configuration will reload the embedded visualization.

View file

@ -0,0 +1,20 @@
{
"id": "exploratoryViewExample",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["exploratory_view_example"],
"server": false,
"ui": true,
"requiredPlugins": [
"observability",
"data",
"embeddable",
"developerExamples"
],
"optionalPlugins": [],
"requiredBundles": [],
"owner": {
"name": "`Synthetics team`",
"githubTeam": "uptime"
}
}

View file

@ -0,0 +1,14 @@
{
"name": "exploratory_view_example",
"version": "1.0.0",
"main": "target/examples/exploratory_view_example",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Elastic License 2.0",
"scripts": {
"kbn": "node ../../../scripts/kbn.js",
"build": "rm -rf './target' && ../../../node_modules/.bin/tsc"
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
import { IndexPattern } from 'src/plugins/data/public';
import { CoreStart } from 'kibana/public';
import { StartDependencies } from './plugin';
import { AllSeries } from '../../../plugins/observability/public';
export const App = (props: {
core: CoreStart;
plugins: StartDependencies;
defaultIndexPattern: IndexPattern | null;
}) => {
const ExploratoryViewComponent = props.plugins.observability.ExploratoryViewEmbeddable;
const seriesList: AllSeries = [
{
name: 'Monitors response duration',
time: {
from: 'now-5d',
to: 'now',
},
reportDefinitions: {
'monitor.id': ['ALL_VALUES'],
},
breakdown: 'observer.geo.name',
operationType: 'average',
dataType: 'synthetics',
seriesType: 'line',
selectedMetricField: 'monitor.duration.us',
},
];
const hrefLink = props.plugins.observability.createExploratoryViewUrl(
{ reportType: 'kpi-over-time', allSeries: seriesList },
props.core.http.basePath.get()
);
return (
<EuiPage>
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Observability Exploratory View Example</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody style={{ maxWidth: 800, margin: '0 auto', height: '70vh' }}>
<p>
This app embeds an Observability Exploratory view as embeddable component. Make sure
you have data in heartbeat-* index within last 5 days for this demo to work.
</p>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton aria-label="Open in exploratory view" href={hrefLink} target="_blank">
Edit in exploratory view (new tab)
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<ExploratoryViewComponent
attributes={seriesList}
reportType="kpi-over-time"
title={'Monitor response duration'}
/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ExploratoryViewExamplePlugin } from './plugin';
export const plugin = () => new ExploratoryViewExamplePlugin();

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { CoreSetup, AppMountParameters } from 'kibana/public';
import { StartDependencies } from './plugin';
export const mount =
(coreSetup: CoreSetup<StartDependencies>) =>
async ({ element }: AppMountParameters) => {
const [core, plugins] = await coreSetup.getStartServices();
const { App } = await import('./app');
const deps = {
core,
plugins,
};
const defaultIndexPattern = await plugins.data.indexPatterns.getDefault();
const i18nCore = core.i18n;
const reactElement = (
<i18nCore.Context>
<App {...deps} defaultIndexPattern={defaultIndexPattern} />
</i18nCore.Context>
);
render(reactElement, element);
return () => unmountComponentAtNode(element);
};

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Plugin, CoreSetup, AppNavLinkStatus } from '../../../../src/core/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { ObservabilityPublicStart } from '../../../plugins/observability/public';
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
import { mount } from './mount';
export interface SetupDependencies {
developerExamples: DeveloperExamplesSetup;
}
export interface StartDependencies {
data: DataPublicPluginStart;
observability: ObservabilityPublicStart;
}
export class ExploratoryViewExamplePlugin
implements Plugin<void, void, SetupDependencies, StartDependencies>
{
public setup(core: CoreSetup<StartDependencies>, { developerExamples }: SetupDependencies) {
core.application.register({
id: 'exploratory_view_example',
title: 'Observability Exploratory View example',
navLinkStatus: AppNavLinkStatus.hidden,
mount: mount(core),
order: 1000,
});
developerExamples.register({
appId: 'exploratory_view_example',
title: 'Observability Exploratory View',
description:
'Embed Observability exploratory view in your observability solution app to render common visualizations',
});
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types"
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
{ "path": "../../plugins/observability/tsconfig.json" },
{ "path": "../../../examples/developer_examples/tsconfig.json" },
]
}

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import styled from 'styled-components';
import { AllSeries, useTheme } from '../../../..';
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
import { ReportViewType } from '../types';
import { getLayerConfigs } from '../hooks/use_lens_attributes';
import { LensPublicStart } from '../../../../../../lens/public';
import { OperationTypeComponent } from '../series_editor/columns/operation_type_select';
import { IndexPatternState } from '../hooks/use_app_index_pattern';
export interface ExploratoryEmbeddableProps {
reportType: ReportViewType;
attributes: AllSeries;
appendTitle?: JSX.Element;
title: string | JSX.Element;
showCalculationMethod?: boolean;
}
export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps {
lens: LensPublicStart;
indexPatterns: IndexPatternState;
}
// eslint-disable-next-line import/no-default-export
export default function Embeddable({
reportType,
attributes,
title,
appendTitle,
indexPatterns,
lens,
showCalculationMethod = false,
}: ExploratoryEmbeddableComponentProps) {
const LensComponent = lens?.EmbeddableComponent;
const series = Object.entries(attributes)[0][1];
const [operationType, setOperationType] = useState(series?.operationType);
const theme = useTheme();
const layerConfigs: LayerConfig[] = getLayerConfigs(attributes, reportType, theme, indexPatterns);
if (layerConfigs.length < 1) {
return null;
}
const lensAttributes = new LensAttributes(layerConfigs);
if (!LensComponent) {
return <EuiText>No lens component</EuiText>;
}
return (
<Wrapper>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="s">
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
{showCalculationMethod && (
<EuiFlexItem grow={false} style={{ minWidth: 150 }}>
<OperationTypeComponent
operationType={operationType}
onChange={(val) => {
setOperationType(val);
}}
/>
</EuiFlexItem>
)}
{appendTitle}
</EuiFlexGroup>
<LensComponent
id="exploratoryView"
style={{ height: '100%' }}
timeRange={series?.time}
attributes={lensAttributes.getJSON()}
onBrushEnd={({ range }) => {}}
/>
</Wrapper>
);
}
const Wrapper = styled.div`
height: 100%;
&&& {
> :nth-child(2) {
height: calc(100% - 56px);
}
}
`;

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { CoreStart } from 'kibana/public';
import type { ExploratoryEmbeddableProps, ExploratoryEmbeddableComponentProps } from './embeddable';
import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns';
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
import type { IndexPatternState } from '../hooks/use_app_index_pattern';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
const Embeddable = React.lazy(() => import('./embeddable'));
function ExploratoryViewEmbeddable(props: ExploratoryEmbeddableComponentProps) {
return (
<React.Suspense fallback={<EuiLoadingSpinner />}>
<Embeddable {...props} />
</React.Suspense>
);
}
export function getExploratoryViewEmbeddable(
core: CoreStart,
plugins: ObservabilityPublicPluginsStart
) {
return (props: ExploratoryEmbeddableProps) => {
const [indexPatterns, setIndexPatterns] = useState<IndexPatternState>({} as IndexPatternState);
const [loading, setLoading] = useState(false);
const series = props.attributes[0];
const isDarkMode = core.uiSettings.get('theme:darkMode');
const loadIndexPattern = useCallback(async ({ dataType }) => {
setLoading(true);
try {
const obsvIndexP = new ObservabilityIndexPatterns(plugins.data);
const indPattern = await obsvIndexP.getIndexPattern(dataType, 'heartbeat-*');
setIndexPatterns((prevState) => ({ ...(prevState ?? {}), [dataType]: indPattern }));
setLoading(false);
} catch (e) {
setLoading(false);
}
}, []);
useEffect(() => {
loadIndexPattern({ dataType: series.dataType });
}, [series.dataType, loadIndexPattern]);
if (Object.keys(indexPatterns).length === 0 || loading) {
return <EuiLoadingSpinner />;
}
return (
<EuiThemeProvider darkMode={isDarkMode}>
<ExploratoryViewEmbeddable {...props} indexPatterns={indexPatterns} lens={plugins.lens} />
</EuiThemeProvider>
);
};
}

View file

@ -17,10 +17,11 @@ import {
} from './use_series_storage';
import { getDefaultConfigs } from '../configurations/default_configs';
import { SeriesUrl, UrlFilter } from '../types';
import { useAppIndexPatternContext } from './use_app_index_pattern';
import { ReportViewType, SeriesUrl, UrlFilter } from '../types';
import { IndexPatternState, useAppIndexPatternContext } from './use_app_index_pattern';
import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox';
import { useTheme } from '../../../../hooks/use_theme';
import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common';
export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => {
return Object.entries(reportDefinitions ?? {})
@ -33,6 +34,54 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio
.filter(({ values }) => !values.includes(ALL_VALUES_SELECTED)) as UrlFilter[];
};
export function getLayerConfigs(
allSeries: AllSeries,
reportType: ReportViewType,
theme: EuiTheme,
indexPatterns: IndexPatternState
) {
const layerConfigs: LayerConfig[] = [];
allSeries.forEach((series, seriesIndex) => {
const indexPattern = indexPatterns?.[series?.dataType];
if (
indexPattern &&
!isEmpty(series.reportDefinitions) &&
!series.hidden &&
series.selectedMetricField
) {
const seriesConfig = getDefaultConfigs({
reportType,
indexPattern,
dataType: series.dataType,
});
const filters: UrlFilter[] = (series.filters ?? []).concat(
getFiltersFromDefs(series.reportDefinitions)
);
const color = `euiColorVis${seriesIndex}`;
layerConfigs.push({
filters,
indexPattern,
seriesConfig,
time: series.time,
name: series.name,
breakdown: series.breakdown,
seriesType: series.seriesType,
operationType: series.operationType,
reportDefinitions: series.reportDefinitions ?? {},
selectedMetricField: series.selectedMetricField,
color: series.color ?? (theme.eui as unknown as Record<string, string>)[color],
});
}
});
return layerConfigs;
}
export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => {
const { storage, allSeries, lastRefresh, reportType } = useSeriesStorage();
@ -47,44 +96,7 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null
const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []);
const layerConfigs: LayerConfig[] = [];
allSeriesT.forEach((series, seriesIndex) => {
const indexPattern = indexPatterns?.[series?.dataType];
if (
indexPattern &&
!isEmpty(series.reportDefinitions) &&
!series.hidden &&
series.selectedMetricField
) {
const seriesConfig = getDefaultConfigs({
reportType,
indexPattern,
dataType: series.dataType,
});
const filters: UrlFilter[] = (series.filters ?? []).concat(
getFiltersFromDefs(series.reportDefinitions)
);
const color = `euiColorVis${seriesIndex}`;
layerConfigs.push({
filters,
indexPattern,
seriesConfig,
time: series.time,
name: series.name,
breakdown: series.breakdown,
seriesType: series.seriesType,
operationType: series.operationType,
reportDefinitions: series.reportDefinitions ?? {},
selectedMetricField: series.selectedMetricField,
color: series.color ?? (theme.eui as unknown as Record<string, string>)[color],
});
}
});
const layerConfigs = getLayerConfigs(allSeriesT, reportType, theme, indexPatterns);
if (layerConfigs.length < 1) {
return null;

View file

@ -36,6 +36,24 @@ export function OperationTypeSelect({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultOperationType]);
return (
<OperationTypeComponent
onChange={onChange}
showLabel={true}
operationType={operationType || defaultOperationType}
/>
);
}
export function OperationTypeComponent({
operationType,
onChange,
showLabel = false,
}: {
operationType?: OperationType;
onChange: (value: OperationType) => void;
showLabel?: boolean;
}) {
const options = [
{
value: 'average' as OperationType,
@ -83,9 +101,17 @@ export function OperationTypeSelect({
return (
<EuiSuperSelect
compressed
fullWidth
prepend={
showLabel
? i18n.translate('xpack.observability.expView.operationType.label', {
defaultMessage: 'Calculation',
})
: undefined
}
data-test-subj="operationTypeSelect"
valueOfSelected={operationType || defaultOperationType}
valueOfSelected={operationType}
options={options}
onChange={onChange}
/>

View file

@ -71,6 +71,7 @@ export { getApmTraceUrl } from './utils/get_apm_trace_url';
export { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils';
export { ALL_VALUES_SELECTED } from './components/shared/field_value_suggestions/field_value_combobox';
export { FilterValueLabel } from './components/shared/filter_value_label/filter_value_label';
export type { AllSeries } from './components/shared/exploratory_view/hooks/use_series_storage';
export type { SeriesUrl } from './components/shared/exploratory_view/types';
export type {
@ -79,3 +80,4 @@ export type {
ObservabilityRuleTypeRegistry,
} from './rules/create_observability_rule_type_registry';
export { createObservabilityRuleTypeRegistryMock } from './rules/observability_rule_type_registry_mock';
export type { ExploratoryEmbeddableProps } from './components/shared/exploratory_view/embeddable/embeddable';

View file

@ -43,6 +43,8 @@ import { createObservabilityRuleTypeRegistry } from './rules/create_observabilit
import { createCallObservabilityApi } from './services/call_observability_api';
import { createNavigationRegistry, NavigationEntry } from './services/navigation_registry';
import { updateGlobalNavigation } from './update_global_navigation';
import { getExploratoryViewEmbeddable } from './components/shared/exploratory_view/embeddable';
import { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils';
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;
@ -233,7 +235,9 @@ export class Plugin
};
}
public start({ application }: CoreStart) {
public start(coreStart: CoreStart, pluginsStart: ObservabilityPublicPluginsStart) {
const { application } = coreStart;
const config = this.initializerContext.config.get();
updateGlobalNavigation({
@ -254,6 +258,8 @@ export class Plugin
navigation: {
PageTemplate,
},
createExploratoryViewUrl,
ExploratoryViewEmbeddable: getExploratoryViewEmbeddable(coreStart, pluginsStart),
};
}
}

View file

@ -38,4 +38,4 @@
"githubTeam": "uptime"
},
"description": "This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions."
}
}