[Stack Monitoring] Add setup mode to react app (#110670)

* Show setup mode button and setup bottom bar

* Adapt setup mode in react components to work without angular

* Add setup mode data update to react app

* Add missing functions from setup mode

* Revert setup mode changes from react components

* remove some empty lines

* Add setup button to  monitoring toolbar

* Fix types

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ester Martí Vilaseca 2021-09-03 15:15:53 +02:00 committed by GitHub
parent 6f357e0433
commit 75486ecd12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 514 additions and 48 deletions

View file

@ -13,9 +13,11 @@ interface GlobalStateProviderProps {
toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'];
}
interface State {
export interface State {
cluster_uuid?: string;
ccs?: any;
inSetupMode?: boolean;
save?: () => void;
}
export const GlobalStateContext = createContext({} as State);

View file

@ -15,8 +15,15 @@ import { TabMenuItem } from '../page_template';
import { PageLoading } from '../../../components';
import { Overview } from '../../../components/cluster/overview';
import { ExternalConfigContext } from '../../external_config_context';
import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
const CODE_PATHS = [CODE_PATH_ALL];
interface SetupModeProps {
setupMode: any;
flyoutComponent: any;
bottomBarComponent: any;
}
export const ClusterOverview: React.FC<{}> = () => {
// TODO: check how many requests with useClusters
@ -49,11 +56,20 @@ export const ClusterOverview: React.FC<{}> = () => {
return (
<PageTemplate title={title} pageTitle={pageTitle} tabs={tabs}>
{loaded ? (
<Overview
cluster={clusters[0]}
alerts={[]}
setupMode={{}}
showLicenseExpiration={externalConfig.showLicenseExpiration}
<SetupModeRenderer
render={({ setupMode, flyoutComponent, bottomBarComponent }: SetupModeProps) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<Overview
cluster={clusters[0]}
alerts={[]}
setupMode={setupMode}
showLicenseExpiration={externalConfig.showLicenseExpiration}
/>
{/* <EnableAlertsModal alerts={this.alerts} /> */}
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
) : (
<PageLoading />

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
import { EuiTab, EuiTabs } from '@elastic/eui';
import React from 'react';
import { useTitle } from '../hooks/use_title';
import { MonitoringToolbar } from '../../components/shared/toolbar';
@ -29,34 +29,7 @@ export const PageTemplate: React.FC<PageTemplateProps> = ({ title, pageTitle, ta
return (
<div className="app-container">
<EuiFlexGroup gutterSize="l" justifyContent="spaceBetween" responsive>
<EuiFlexItem>
<EuiFlexGroup
gutterSize="none"
justifyContent="spaceEvenly"
direction="column"
responsive
>
<EuiFlexItem>
<div id="setupModeNav">{/* HERE GOES THE SETUP BUTTON */}</div>
</EuiFlexItem>
<EuiFlexItem className="monTopNavSecondItem">
{pageTitle && (
<div data-test-subj="monitoringPageTitle">
<EuiTitle size="xs">
<h1>{pageTitle}</h1>
</EuiTitle>
</div>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<MonitoringToolbar />
</EuiFlexItem>
</EuiFlexGroup>
<MonitoringToolbar pageTitle={pageTitle} />
{tabs && (
<EuiTabs>
{tabs.map((item, idx) => {

View file

@ -0,0 +1,200 @@
/*
* 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 { render } from 'react-dom';
import { get, includes } from 'lodash';
import { i18n } from '@kbn/i18n';
import { HttpStart } from 'kibana/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { Legacy } from '../../legacy_shims';
import { SetupModeEnterButton } from '../../components/setup_mode/enter_button';
import { SetupModeFeature } from '../../../common/enums';
import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context';
import { State as GlobalState } from '../../application/global_state_context';
function isOnPage(hash: string) {
return includes(window.location.hash, hash);
}
let globalState: GlobalState;
let httpService: HttpStart;
interface ISetupModeState {
enabled: boolean;
data: any;
callback?: (() => void) | null;
hideBottomBar: boolean;
}
const setupModeState: ISetupModeState = {
enabled: false,
data: null,
callback: null,
hideBottomBar: false,
};
export const getSetupModeState = () => setupModeState;
export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => {
globalState.cluster_uuid = clusterUuid;
globalState.save?.();
};
export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => {
const clusterUuid = globalState.cluster_uuid;
const ccs = globalState.ccs;
let url = '../api/monitoring/v1/setup/collection';
if (uuid) {
url += `/node/${uuid}`;
} else if (!fetchWithoutClusterUuid && clusterUuid) {
url += `/cluster/${clusterUuid}`;
} else {
url += '/cluster';
}
try {
const response = await httpService.post(url, {
body: JSON.stringify({
ccs,
}),
});
return response;
} catch (err) {
// TODO: handle errors
throw new Error(err);
}
};
const notifySetupModeDataChange = () => setupModeState.callback && setupModeState.callback();
export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => {
const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid);
setupModeState.data = data;
const hasPermissions = get(data, '_meta.hasPermissions', false);
if (!hasPermissions) {
let text: string = '';
if (!hasPermissions) {
text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', {
defaultMessage: 'You do not have the necessary permissions to do this.',
});
}
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', {
defaultMessage: 'Setup mode is not available',
}),
text,
});
return toggleSetupMode(false);
}
notifySetupModeDataChange();
const clusterUuid = globalState.cluster_uuid;
if (!clusterUuid) {
const liveClusterUuid: string = get(data, '_meta.liveClusterUuid');
const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter(
(node: any) => node.isPartiallyMigrated || node.isFullyMigrated
);
if (liveClusterUuid && migratedEsNodes.length > 0) {
setNewlyDiscoveredClusterUuid(liveClusterUuid);
}
}
};
export const hideBottomBar = () => {
setupModeState.hideBottomBar = true;
notifySetupModeDataChange();
};
export const showBottomBar = () => {
setupModeState.hideBottomBar = false;
notifySetupModeDataChange();
};
export const disableElasticsearchInternalCollection = async () => {
const clusterUuid = globalState.cluster_uuid;
const url = `../api/monitoring/v1/setup/collection/${clusterUuid}/disable_internal_collection`;
try {
const response = await httpService.post(url);
return response;
} catch (err) {
// TODO: handle errors
throw new Error(err);
}
};
export const toggleSetupMode = (inSetupMode: boolean) => {
setupModeState.enabled = inSetupMode;
globalState.inSetupMode = inSetupMode;
globalState.save?.();
setSetupModeMenuItem();
notifySetupModeDataChange();
if (inSetupMode) {
// Intentionally do not await this so we don't block UI operations
updateSetupModeData();
}
};
export const setSetupModeMenuItem = () => {
if (isOnPage('no-data')) {
return;
}
const enabled = !globalState.inSetupMode;
const I18nContext = Legacy.shims.I18nContext;
render(
<KibanaContextProvider services={Legacy.shims.kibanaServices}>
<I18nContext>
<SetupModeEnterButton enabled={enabled} toggleSetupMode={toggleSetupMode} />
</I18nContext>
</KibanaContextProvider>,
document.getElementById('setupModeNav')
);
};
export const initSetupModeState = async (
state: GlobalState,
http: HttpStart,
callback?: () => void
) => {
globalState = state;
httpService = http;
if (callback) {
setupModeState.callback = callback;
}
if (globalState.inSetupMode) {
toggleSetupMode(true);
}
};
export const isInSetupMode = (context?: ISetupModeContext) => {
if (context?.setupModeSupported === false) {
return false;
}
if (setupModeState.enabled) {
return true;
}
return globalState.inSetupMode;
};
export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => {
if (!setupModeState.enabled) {
return false;
}
if (feature === SetupModeFeature.MetricbeatMigration) {
if (Legacy.shims.isCloud) {
return false;
}
}
return true;
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export const SetupModeRenderer: FunctionComponent<Props>;

View file

@ -0,0 +1,217 @@
/*
* 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, { Fragment } from 'react';
import {
getSetupModeState,
initSetupModeState,
updateSetupModeData,
disableElasticsearchInternalCollection,
toggleSetupMode,
setSetupModeMenuItem,
} from './setup_mode';
import { Flyout } from '../../components/metricbeat_migration/flyout';
import {
EuiBottomBar,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiTextColor,
EuiIcon,
EuiSpacer,
} from '@elastic/eui';
import { findNewUuid } from '../../components/renderers/lib/find_new_uuid';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { GlobalStateContext } from '../../application/global_state_context';
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
class WrappedSetupModeRenderer extends React.Component {
globalState;
state = {
renderState: false,
isFlyoutOpen: false,
instance: null,
newProduct: null,
isSettingUpNew: false,
};
UNSAFE_componentWillMount() {
this.globalState = this.context;
const { kibana } = this.props;
initSetupModeState(this.globalState, kibana.services.http, (_oldData) => {
const newState = { renderState: true };
const { productName } = this.props;
if (!productName) {
this.setState(newState);
return;
}
const setupModeState = getSetupModeState();
if (!setupModeState.enabled || !setupModeState.data) {
this.setState(newState);
return;
}
const data = setupModeState.data[productName];
const oldData = _oldData ? _oldData[productName] : null;
if (data && oldData) {
const newUuid = findNewUuid(Object.keys(oldData.byUuid), Object.keys(data.byUuid));
if (newUuid) {
newState.newProduct = data.byUuid[newUuid];
}
}
this.setState(newState);
});
setSetupModeMenuItem();
}
reset() {
this.setState({
renderState: false,
isFlyoutOpen: false,
instance: null,
newProduct: null,
isSettingUpNew: false,
});
}
getFlyout(data, meta) {
const { productName } = this.props;
const { isFlyoutOpen, instance, isSettingUpNew, newProduct } = this.state;
if (!data || !isFlyoutOpen) {
return null;
}
let product = null;
if (newProduct) {
product = newProduct;
}
// For new instance discovery flow, we pass in empty instance object
else if (instance && Object.keys(instance).length) {
product = data.byUuid[instance.uuid];
}
if (!product) {
const uuids = Object.values(data.byUuid);
if (uuids.length && !isSettingUpNew) {
product = uuids[0];
} else {
product = {
isNetNewUser: true,
};
}
}
return (
<Flyout
onClose={() => this.reset()}
productName={productName}
product={product}
meta={meta}
instance={instance}
updateProduct={updateSetupModeData}
isSettingUpNew={isSettingUpNew}
/>
);
}
getBottomBar(setupModeState) {
if (!setupModeState.enabled || setupModeState.hideBottomBar) {
return null;
}
return (
<Fragment>
<EuiSpacer size="xxl" />
<EuiBottomBar>
<EuiFlexGroup
justifyContent="spaceBetween"
alignItems="center"
data-test-subj="monitoringSetupModeBottomBar"
>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTextColor color="ghost">
<FormattedMessage
id="xpack.monitoring.setupMode.description"
defaultMessage="You are in setup mode. The ({flagIcon}) icon indicates configuration options."
values={{
flagIcon: <EuiIcon type="flag" />,
}}
/>
</EuiTextColor>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
color="danger"
fill
iconType="flag"
iconSide="right"
size="s"
onClick={() => toggleSetupMode(false)}
>
{i18n.translate('xpack.monitoring.setupMode.exit', {
defaultMessage: `Exit setup mode`,
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
</Fragment>
);
}
async shortcutToFinishMigration() {
await disableElasticsearchInternalCollection();
await updateSetupModeData();
}
render() {
const { render, productName } = this.props;
const setupModeState = getSetupModeState();
let data = { byUuid: {} };
if (setupModeState.data) {
if (productName && setupModeState.data[productName]) {
data = setupModeState.data[productName];
} else if (setupModeState.data) {
data = setupModeState.data;
}
}
const meta = setupModeState.data ? setupModeState.data._meta : null;
return render({
setupMode: {
data,
meta,
enabled: setupModeState.enabled,
productName,
updateSetupModeData,
shortcutToFinishMigration: () => this.shortcutToFinishMigration(),
openFlyout: (instance, isSettingUpNew) =>
this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }),
closeFlyout: () => this.setState({ isFlyoutOpen: false }),
},
flyoutComponent: this.getFlyout(data, meta),
bottomBarComponent: this.getBottomBar(setupModeState),
});
}
}
WrappedSetupModeRenderer.contextType = GlobalStateContext;
export const SetupModeRenderer = withKibana(WrappedSetupModeRenderer);

View file

@ -5,11 +5,21 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, OnRefreshChangeProps } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
EuiTitle,
OnRefreshChangeProps,
} from '@elastic/eui';
import React, { useContext, useCallback } from 'react';
import { MonitoringTimeContainer } from '../../application/pages/use_monitoring_time';
export const MonitoringToolbar = () => {
interface MonitoringToolbarProps {
pageTitle?: string;
}
export const MonitoringToolbar: React.FC<MonitoringToolbarProps> = ({ pageTitle }) => {
const {
currentTimerange,
handleTimeChange,
@ -38,18 +48,36 @@ export const MonitoringToolbar = () => {
);
return (
<EuiFlexGroup gutterSize={'xl'} justifyContent={'spaceBetween'}>
<EuiFlexItem>Setup Button</EuiFlexItem>
<EuiFlexGroup gutterSize="l" justifyContent="spaceBetween" responsive>
<EuiFlexItem>
<EuiFlexGroup gutterSize="none" justifyContent="spaceEvenly" direction="column" responsive>
<EuiFlexItem>
<div id="setupModeNav">{/* HERE GOES THE SETUP BUTTON */}</div>
</EuiFlexItem>
<EuiFlexItem className="monTopNavSecondItem">
{pageTitle && (
<div data-test-subj="monitoringPageTitle">
<EuiTitle size="xs">
<h1>{pageTitle}</h1>
</EuiTitle>
</div>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={currentTimerange.from}
end={currentTimerange.to}
onTimeChange={onTimeChange}
onRefresh={() => {}}
isPaused={isPaused}
refreshInterval={refreshInterval}
onRefreshChange={onRefreshChange}
/>
<div style={{ padding: 8 }}>
<EuiSuperDatePicker
start={currentTimerange.from}
end={currentTimerange.to}
onTimeChange={onTimeChange}
onRefresh={() => {}}
isPaused={isPaused}
refreshInterval={refreshInterval}
onRefreshChange={onRefreshChange}
/>
</div>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
let config: { [key: string]: unknown } = {};
export const setConfig = (externalConfig: { [key: string]: unknown }) => {
config = externalConfig;
};
export const isReactMigrationEnabled = () => {
return config.renderReactApp;
};

View file

@ -15,6 +15,8 @@ import { ajaxErrorHandlersProvider } from './ajax_error_handler';
import { SetupModeEnterButton } from '../components/setup_mode/enter_button';
import { SetupModeFeature } from '../../common/enums';
import { ISetupModeContext } from '../components/setup_mode/setup_mode_context';
import * as setupModeReact from '../application/setup_mode/setup_mode';
import { isReactMigrationEnabled } from '../external_config';
function isOnPage(hash: string) {
return includes(window.location.hash, hash);
@ -209,6 +211,7 @@ export const initSetupModeState = async ($scope: any, $injector: any, callback?:
};
export const isInSetupMode = (context?: ISetupModeContext) => {
if (isReactMigrationEnabled()) return setupModeReact.isInSetupMode(context);
if (context?.setupModeSupported === false) {
return false;
}
@ -222,6 +225,7 @@ export const isInSetupMode = (context?: ISetupModeContext) => {
};
export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => {
if (isReactMigrationEnabled()) return setupModeReact.isSetupModeFeatureEnabled(feature);
if (!setupModeState.enabled) {
return false;
}

View file

@ -36,6 +36,7 @@ import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_reject
import { createMemoryUsageAlertType } from './alerts/memory_usage_alert';
import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert';
import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert';
import { setConfig } from './external_config';
interface MonitoringSetupPluginDependencies {
home?: HomePublicPluginSetup;
@ -125,6 +126,7 @@ export class MonitoringPlugin
});
const config = Object.fromEntries(externalConfig);
setConfig(config);
if (config.renderReactApp) {
const { renderApp } = await import('./application');
return renderApp(coreStart, pluginsStart, params, config);