Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Stack monitoring] Convert Angular views to React #111309

Closed
19 tasks done
Tracked by #115024
matschaffer opened this issue Sep 7, 2021 · 5 comments
Closed
19 tasks done
Tracked by #115024

[Stack monitoring] Convert Angular views to React #111309

matschaffer opened this issue Sep 7, 2021 · 5 comments
Labels
Epic: Stack Monitoring de-angularization Feature:Stack Monitoring Team:Infra Monitoring UI - DEPRECATED DEPRECATED - Label for the Infra Monitoring UI team. Use Team:obs-ux-infra_services

Comments

@matschaffer
Copy link
Contributor

matschaffer commented Sep 7, 2021

Initial pre-requisite work, to be completed by Sep 10

Parallel work, stage 1 (complete by Sep 16)

Parallel work, stage 2 (complete by Oct 1)

Parallel work, stage 3 (complete by Oct 7)

Parallel work, stage 4 (completed by Oct 15)

NOTE: "Internal collection detected" warning doesn't have to be migrated #111629
Tech debt #112512

@botelastic botelastic bot added the needs-team Issues missing a team label label Sep 7, 2021
@matschaffer matschaffer added Feature:Stack Monitoring Team:Infra Monitoring UI - DEPRECATED DEPRECATED - Label for the Infra Monitoring UI team. Use Team:obs-ux-infra_services labels Sep 7, 2021
@elasticmachine
Copy link
Contributor

Pinging @elastic/logs-metrics-ui (Team:logs-metrics-ui)

@phillipb
Copy link
Contributor

phillipb commented Sep 23, 2021

For anyone picking up new views to migrate, here's a how-to:

How to see the react app

The react app is toggled on by flipping a config flag here:

render_react_app: schema.boolean({ defaultValue: false }),
. You can hard code the default_value for testing if that's easier, but don't check it in obviously.

Folder Structure

All the angular views/routes are nested under: https://github.com/elastic/kibana/tree/4584a8b570402aa07832cf3e5b520e5d2cfa7166/x-pack/plugins/monitoring/public/views

The new react pages are nested under: https://github.com/elastic/kibana/tree/a949cc8121c37a799ed3655a8612ac6052b263a6/x-pack/plugins/monitoring/public/application/pages

When picking up a new group of views to port over, you'll start by creating a new directory under monitoring/public/application/pages if it doesn't exist.

Create a new PageTemplate if it doesn't exist

Screen Shot 2021-09-23 at 9 22 05 AM

The page template is where all the navigation for the group of pages is defined. The angular equivalent is defined in main monitoring directive. This directive is a master template for all of stack monitoring and it turns on/off tabs in the navigation based on what module the user is viewing.

Angular

<div ng-if="monitoringMain.inElasticsearch" class="euiTabs" role="navigation">
<a
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')"
kbn-href="#/elasticsearch"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.esNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}"
i18n-id="xpack.monitoring.esNavigation.overviewLinkText"
i18n-default-message="Overview"
></a>
<a
ng-if="!monitoringMain.instance"
kbn-href="#/elasticsearch/nodes"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('nodes')}"
i18n-id="xpack.monitoring.esNavigation.nodesLinkText"
i18n-default-message="Nodes"
></a>
<a
ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')"
kbn-href="#/elasticsearch/indices"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}"
i18n-id="xpack.monitoring.esNavigation.indicesLinkText"
i18n-default-message="Indices"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')"
kbn-href=""
class="euiTab euiTab-isDisabled"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}"
i18n-id="xpack.monitoring.esNavigation.indicesLinkText"
i18n-default-message="Indices"
></a>
<a
ng-if="!monitoringMain.instance && monitoringMain.isMlSupported()"
kbn-href="#/elasticsearch/ml_jobs"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('ml')}"
i18n-id="xpack.monitoring.esNavigation.jobsLinkText"
i18n-default-message="Machine learning jobs"
></a>
<a
ng-if="(monitoringMain.isCcrEnabled || monitoringMain.isActiveTab('ccr')) && !monitoringMain.instance"
kbn-href="#/elasticsearch/ccr"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('ccr')}"
i18n-id="xpack.monitoring.esNavigation.ccrLinkText"
i18n-default-message="CCR"
></a>
<a
ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')"
kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}"
>
<span
ng-if="monitoringMain.tabIconClass"
class="fa {{ monitoringMain.tabIconClass }}"
title="{{ monitoringMain.tabIconLabel }}"
></span>
<span
i18n-id="xpack.monitoring.esNavigation.instance.overviewLinkText"
i18n-default-message="Overview"
></span>
</a>
<a
ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')"
data-test-subj="esNodeDetailAdvancedLink"
kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}/advanced"
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.page === 'advanced'}"
i18n-id="xpack.monitoring.esNavigation.instance.advancedLinkText"
i18n-default-message="Advanced"
>
</a>
</div>

React

export const ElasticsearchTemplate: React.FC<ElasticsearchTemplateProps> = ({
cluster,
...props
}) => {
const tabs: TabMenuItem[] = [
{
id: 'overview',
label: i18n.translate('xpack.monitoring.esNavigation.overviewLinkText', {
defaultMessage: 'Overview',
}),
route: '/elasticsearch',
},
{
id: 'nodes',
label: i18n.translate('xpack.monitoring.esNavigation.nodesLinkText', {
defaultMessage: 'Nodes',
}),
route: '/elasticsearch/nodes',
},
{
id: 'indices',
label: i18n.translate('xpack.monitoring.esNavigation.indicesLinkText', {
defaultMessage: 'Indices',
}),
route: '/elasticsearch/indices',
},
];
if (mlIsSupported(cluster.license)) {
tabs.push({
id: 'ml',
label: i18n.translate('xpack.monitoring.esNavigation.jobsLinkText', {
defaultMessage: 'Machine learning jobs',
}),
route: '/elasticsearch/ml_jobs',
});
}
if (cluster.isCcrEnabled) {
tabs.push({
id: 'ccr',
label: i18n.translate('xpack.monitoring.esNavigation.ccrLinkText', {
defaultMessage: 'CCR',
}),
route: '/elasticsearch/ccr',
});
}
return <PageTemplate {...props} tabs={tabs} product="elasticsearch" />;
};

Create a route for the page

Add a RouteInit component that points to the new react component.

Angular

uiRoutes.when('/elasticsearch', {
template,
resolve: {
clusters(Private) {
const routeInit = Private(routeInitProvider);
return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] });
},
},
controllerAs: 'elasticsearchOverview',
controller: ElasticsearchOverviewController,
});

React

<RouteInit
path="/elasticsearch"
component={ElasticsearchOverviewPage}
codePaths={[CODE_PATH_ELASTICSEARCH]}
fetchAllClusters={false}
/>

Create a react component that is the equivalent to the angular controller you're trying to replace

In stack monitoring, the angular controllers are responsible for fetching data and passing it into the react components. We need to replace those controllers with react components. The key function to look for in angular is this.renderReact. That will tell you all the data your prop needs.

Angular:

controller: class ElasticsearchNodesController extends MonitoringViewBaseEuiTableController {
constructor($injector, $scope) {
const $route = $injector.get('$route');
const globalState = $injector.get('globalState');
const showCgroupMetricsElasticsearch = $injector.get('showCgroupMetricsElasticsearch');
$scope.cluster =
find($route.current.locals.clusters, {
cluster_uuid: globalState.cluster_uuid,
}) || {};
const getPageData = ($injector, _api = undefined, routeOptions = {}) => {
_api; // to fix eslint
const $http = $injector.get('$http');
const globalState = $injector.get('globalState');
const timeBounds = Legacy.shims.timefilter.getBounds();
const getNodes = (clusterUuid = globalState.cluster_uuid) =>
$http.post(`../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`, {
ccs: globalState.ccs,
timeRange: {
min: timeBounds.min.toISOString(),
max: timeBounds.max.toISOString(),
},
...routeOptions,
});
const promise = globalState.cluster_uuid
? getNodes()
: new Promise((resolve) => resolve({ data: {} }));
return promise
.then((response) => response.data)
.catch((err) => {
const Private = $injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
});
};
super({
title: i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', {
defaultMessage: 'Elasticsearch - Nodes',
}),
pageTitle: i18n.translate('xpack.monitoring.elasticsearch.nodes.pageTitle', {
defaultMessage: 'Elasticsearch nodes',
}),
storageKey: 'elasticsearch.nodes',
reactNodeId: 'elasticsearchNodesReact',
defaultData: {},
getPageData,
$scope,
$injector,
fetchDataImmediately: false, // We want to apply pagination before sending the first request,
alerts: {
shouldFetch: true,
options: {
alertTypeIds: [
RULE_CPU_USAGE,
RULE_DISK_USAGE,
RULE_THREAD_POOL_SEARCH_REJECTIONS,
RULE_THREAD_POOL_WRITE_REJECTIONS,
RULE_MEMORY_USAGE,
RULE_MISSING_MONITORING_DATA,
],
},
},
});
this.isCcrEnabled = $scope.cluster.isCcrEnabled;
$scope.$watch(
() => this.data,
(data) => {
if (!data) {
return;
}
const { clusterStatus, nodes, totalNodeCount } = data;
const pagination = {
...this.pagination,
totalItemCount: totalNodeCount,
};
this.renderReact(
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName={ELASTICSEARCH_SYSTEM_ID}
render={({ setupMode, flyoutComponent, bottomBarComponent }) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<ElasticsearchNodes
clusterStatus={clusterStatus}
clusterUuid={globalState.cluster_uuid}
setupMode={setupMode}
nodes={nodes}
alerts={this.alerts}
showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch}
{...this.getPaginationTableProps(pagination)}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
);
}
);
}

React:

export const ElasticsearchNodesPage: React.FC<ComponentProps> = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { showCgroupMetricsElasticsearch } = useContext(ExternalConfigContext);
const { services } = useKibana<{ data: any }>();
const { getPaginationRouteOptions, updateTotalItemCount, getPaginationTableProps } =
useTable('elasticsearch.nodes');
const clusterUuid = globalState.cluster_uuid;
const ccs = globalState.ccs;
const cluster = find(clusters, {
cluster_uuid: clusterUuid,
});
const [data, setData] = useState({} as any);
const title = i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', {
defaultMessage: 'Elasticsearch - Nodes',
});
const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.pageTitle', {
defaultMessage: 'Elasticsearch nodes',
});
const getPageData = useCallback(async () => {
const bounds = services.data?.query.timefilter.timefilter.getBounds();
const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`;
const response = await services.http?.fetch(url, {
method: 'POST',
body: JSON.stringify({
ccs,
timeRange: {
min: bounds.min.toISOString(),
max: bounds.max.toISOString(),
},
...getPaginationRouteOptions(),
}),
});
setData(response);
updateTotalItemCount(response.totalNodeCount);
}, [
ccs,
clusterUuid,
services.data?.query.timefilter.timefilter,
services.http,
getPaginationRouteOptions,
updateTotalItemCount,
]);
return (
<ElasticsearchTemplate
title={title}
pageTitle={pageTitle}
getPageData={getPageData}
data-test-subj="elasticsearchOverviewPage"
cluster={cluster}
>
<div data-test-subj="elasticsearchNodesListingPage">
<SetupModeRenderer
render={({ setupMode, flyoutComponent, bottomBarComponent }: SetupModeProps) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<ElasticsearchNodes
clusterStatus={data.clusterStatus}
clusterUuid={globalState.cluster_uuid}
setupMode={setupMode}
nodes={data.nodes}
alerts={{}}
showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch}
{...getPaginationTableProps()}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
</div>
</ElasticsearchTemplate>
);
};

@estermv
Copy link
Contributor

estermv commented Oct 4, 2021

How to see the react app

The react app is toggled on by flipping a config flag here:

render_react_app: schema.boolean({ defaultValue: false }),

. You can hard code the default_value for testing if that's easier, but don't check it in obviously.

Probably I'm not saying anything new here, but I find it easier to have this line monitoring.ui.render_react_app: true in my kibana.dev.yml. This file is ignored so no risk to commit changes accidentally.

@matschaffer
Copy link
Contributor Author

I think we're all set here folks. See #109791 for next steps.

@simianhacker
Copy link
Member

.--. .- .-. - -.--    - .. -- . .-.-.- 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Epic: Stack Monitoring de-angularization Feature:Stack Monitoring Team:Infra Monitoring UI - DEPRECATED DEPRECATED - Label for the Infra Monitoring UI team. Use Team:obs-ux-infra_services
Projects
None yet
Development

No branches or pull requests

9 participants