[Stack Monitoring][Angular removal] No-data page (#110432)

* Basic no-data page

* Rename to NoDataPage

* Add getData property to pass into EuiSuperDatePicker from pages

* Wire getData and redirect for no data page

* Draft port of isLoading & model updating

* Add todo on handling checkers

* Switch to model as state object

* Add checkers

* Porting enabler

* Fix build checks

* Attempting to smooth out enablement

* Clean up CI errors

* Fix breadcrumbs

* Fix linter warning

* Fix checkers dependency (I hope)

* Hook up catchReason

* Add a stub for react setup mode

* Clean warnings

* Fix toggleSetupMode by calling initSetupModeState first

* Translating checker strings

* typo on "xpack"

* Move isCollection/reason check in NoData

This replicates how the angular app did selective re-rendering of the react component, but while still being able to render the component.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Mat Schaffer 2021-09-22 01:31:02 +09:00 committed by GitHub
parent fd0f42386f
commit d37ad90e53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 311 additions and 7 deletions

View file

@ -18,6 +18,7 @@ import { GlobalStateProvider } from './global_state_context';
import { ExternalConfigContext, ExternalConfig } from './external_config_context';
import { createPreserveQueryHistory } from './preserve_query_history';
import { RouteInit } from './route_init';
import { NoDataPage } from './pages/no_data';
import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview';
import { CODE_PATH_ELASTICSEARCH } from '../../common/constants';
import { MonitoringTimeContainer } from './hooks/use_monitoring_time';
@ -54,7 +55,7 @@ const MonitoringApp: React.FC<{
<BreadcrumbContainer.Provider history={history}>
<Router history={history}>
<Switch>
<Route path="/no-data" component={NoData} />
<Route path="/no-data" component={NoDataPage} />
<Route path="/loading" component={LoadingPage} />
<RouteInit
path="/license"
@ -98,10 +99,6 @@ const MonitoringApp: React.FC<{
);
};
const NoData: React.FC<{}> = () => {
return <div>No data page</div>;
};
const Home: React.FC<{}> = () => {
return <div>Home page (Cluster listing)</div>;
};

View file

@ -0,0 +1,57 @@
/*
* 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.
*/
// From x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js
export class Enabler {
http: any;
updateModel: any;
constructor(http: any, updateModel: (properties: any) => void) {
this.http = http;
this.updateModel = updateModel;
}
async enableCollectionInterval() {
try {
this.updateModel({ isCollectionIntervalUpdating: true });
await this.http.fetch('../api/monitoring/v1/elasticsearch_settings/set/collection_interval', {
method: 'PUT',
});
this.updateModel({
isCollectionIntervalUpdated: true,
isCollectionIntervalUpdating: false,
});
} catch (err) {
this.updateModel({
errors: (err as any).data,
isCollectionIntervalUpdated: false,
isCollectionIntervalUpdating: false,
});
}
}
async enableCollectionEnabled() {
try {
this.updateModel({ isCollectionEnabledUpdating: true });
await this.http.fetch('../api/monitoring/v1/elasticsearch_settings/set/collection_enabled', {
method: 'PUT',
});
this.updateModel({
isCollectionEnabledUpdated: true,
isCollectionEnabledUpdating: false,
});
} catch (err) {
this.updateModel({
errors: (err as any).data,
isCollectionEnabledUpdated: false,
isCollectionEnabledUpdating: false,
});
}
}
}

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 { NoDataPage } from './no_data_page';

View file

@ -0,0 +1,240 @@
/*
* 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, useContext, useState } from 'react';
import { Redirect } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { NoData } from '../../../components/no_data';
import { PageTemplate } from '../page_template';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { CODE_PATH_LICENSE, STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants';
import { Legacy } from '../../../legacy_shims';
import { Enabler } from './enabler';
import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs';
import { initSetupModeState } from '../../setup_mode/setup_mode';
import { GlobalStateContext } from '../../global_state_context';
const CODE_PATHS = [CODE_PATH_LICENSE];
interface NoDataPageSetupDeps {
http: any;
data: any;
}
interface SettingsChecker {
message: string;
api: string;
next?: SettingsChecker;
}
const clusterCheckers: SettingsChecker[] = [
{
message: i18n.translate('xpack.monitoring.noData.checker.clusterSettings', {
defaultMessage: 'Checking cluster settings API on production cluster',
}),
api: '../api/monitoring/v1/elasticsearch_settings/check/cluster',
},
{
message: i18n.translate('xpack.monitoring.noData.checker.nodesSettings', {
defaultMessage: 'Checking nodes settings API on production cluster',
}),
api: '../api/monitoring/v1/elasticsearch_settings/check/nodes',
},
];
export const NoDataPage = () => {
const title = i18n.translate('xpack.monitoring.noData.routeTitle', {
defaultMessage: 'Setup Monitoring',
});
const { services } = useKibana<NoDataPageSetupDeps>();
const [shouldRedirect, setShouldRedirect] = useState(false);
const [model, setModel] = useState({
errors: [], // errors can happen from trying to check or set ES settings
checkMessage: null, // message to show while waiting for api response
isLoading: true, // flag for in-progress state of checking for no data reason
isCollectionEnabledUpdating: false, // flags to indicate whether to show a spinner while waiting for ajax
isCollectionEnabledUpdated: false,
isCollectionIntervalUpdating: false,
isCollectionIntervalUpdated: false,
} as any);
const { update: updateBreadcrumbs } = useContext(BreadcrumbContainer.Context);
updateBreadcrumbs([
{
'data-test-subj': 'breadcrumbClusters',
text: 'Clusters',
href: '#/home',
ignoreGlobalState: true,
},
]);
const globalState = useContext(GlobalStateContext);
initSetupModeState(globalState, services.http);
// From x-pack/plugins/monitoring/public/views/no_data/model_updater.js
const updateModel = useCallback(
(properties: any) => {
setModel((previousModel: any) => {
const updated = { ...previousModel };
const keys = Object.keys(properties);
keys.forEach((key) => {
if (Array.isArray(updated[key])) {
updated[key].push(properties[key]);
} else {
updated[key] = properties[key];
}
});
return updated;
});
},
[setModel]
);
const getPageData = useCallback(async () => {
let catchReason;
try {
const clusters = await getClusters(services);
if (clusters && clusters.length) {
setShouldRedirect(true);
return;
}
} catch (err) {
if (err && err.status === 503) {
catchReason = {
property: 'custom',
message: err.data.message,
};
}
}
if (catchReason) {
updateModel({ reason: catchReason });
} else {
await startChecks(clusterCheckers, services.http, updateModel);
}
}, [services, updateModel]);
const enabler = new Enabler(services.http, updateModel);
return (
<PageTemplate title={title} getPageData={getPageData}>
{shouldRedirect ? (
<Redirect to="/home" />
) : (
<NoData {...model} enabler={enabler} isCloudEnabled={Legacy.shims.isCloud} />
)}
</PageTemplate>
);
};
async function getClusters(services: NoDataPageSetupDeps): Promise<any[]> {
const url = '../api/monitoring/v1/clusters';
const bounds = services.data?.query.timefilter.timefilter.getBounds();
const min = bounds.min.toISOString();
const max = bounds.max.toISOString();
const response = await services.http?.fetch(url, {
method: 'POST',
body: JSON.stringify({
css: undefined,
timeRange: {
min,
max,
},
codePaths: CODE_PATHS,
}),
});
return formatClusters(response);
}
// From x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js
const mapCheckers = (_checkers: SettingsChecker[]) => {
return _checkers.map((current, checkerIndex) => {
const next = _checkers[checkerIndex + 1];
if (next !== undefined) {
current.next = next;
}
return current;
});
};
// From x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js
function startChecks(
checkers: SettingsChecker[],
http: { fetch: any },
updateModel: (properties: any) => void
) {
const runCheck = async (currentChecker: SettingsChecker): Promise<any> => {
updateModel({ checkMessage: currentChecker.message });
const { found, reason, error, errorReason } = await executeCheck(currentChecker, http);
if (error) {
updateModel({ errors: errorReason });
if (currentChecker.next) {
return runCheck(currentChecker.next);
}
} else if (found) {
return updateModel({
reason,
isLoading: false,
checkMessage: null,
});
} else if (currentChecker.next) {
return runCheck(currentChecker.next);
}
// dead end
updateModel({
reason: null,
isLoading: false,
checkMessage: null,
});
};
const _checkers = mapCheckers(checkers);
return runCheck(_checkers[0]);
}
async function executeCheck(checker: SettingsChecker, http: { fetch: any }): Promise<any> {
try {
const response = await http.fetch(checker.api, {
method: 'GET',
});
const { found, reason } = response;
return { found, reason };
} catch (err: any) {
const { data } = err;
return {
error: true,
found: false,
errorReason: data,
};
}
}
function formatClusters(clusters: any): any[] {
return clusters.map(formatCluster);
}
function formatCluster(cluster: any) {
if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) {
cluster.cluster_name = 'Standalone Cluster';
}
return cluster;
}

View file

@ -32,9 +32,9 @@ import { CloudDeployment } from './blurbs';
import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link';
function NoDataMessage(props) {
const { isLoading, reason, checkMessage } = props;
const { isLoading, reason, checkMessage, isCollectionEnabledUpdated } = props;
if (isLoading) {
if ((isCollectionEnabledUpdated && !reason) || isLoading) {
return <CheckingSettings checkMessage={checkMessage} />;
}

View file

@ -159,6 +159,8 @@ export const disableElasticsearchInternalCollection = async () => {
};
export const toggleSetupMode = (inSetupMode: boolean) => {
if (isReactMigrationEnabled()) return setupModeReact.toggleSetupMode(inSetupMode);
checkAngularState();
const globalState = angularState.injector.get('globalState');