[Monitoring] Migrate Elasticsearch ML Jobs View from Angular (#113974)

* [Monitoring] Migrate Elasticsearch ML Jobs View from Angular

* Add types

* Fix broken node links
This commit is contained in:
Zacqary Adam Xeper 2021-10-06 14:17:35 -05:00 committed by GitHub
parent f030960c98
commit cc84798f10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 317 additions and 4 deletions

View file

@ -39,6 +39,7 @@ import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page';
import { ElasticsearchIndexPage } from './pages/elasticsearch/index_page';
import { ElasticsearchIndexAdvancedPage } from './pages/elasticsearch/index_advanced_page';
import { ElasticsearchNodePage } from './pages/elasticsearch/node_page';
import { ElasticsearchMLJobsPage } from './pages/elasticsearch/ml_jobs_page';
import { ElasticsearchNodeAdvancedPage } from './pages/elasticsearch/node_advanced_page';
import { ElasticsearchCcrPage } from './pages/elasticsearch/ccr_page';
import { ElasticsearchCcrShardPage } from './pages/elasticsearch/ccr_shard_page';
@ -108,6 +109,13 @@ const MonitoringApp: React.FC<{
/>
{/* ElasticSearch Views */}
<RouteInit
path="/elasticsearch/ml_jobs"
component={ElasticsearchMLJobsPage}
codePaths={[CODE_PATH_ELASTICSEARCH]}
fetchAllClusters={false}
/>
<RouteInit
path="/elasticsearch/ccr/:index/shard/:shardId"
component={ElasticsearchCcrShardPage}

View file

@ -22,6 +22,7 @@ export const ElasticsearchIndicesPage: React.FC<ComponentProps> = ({ clusters })
const { services } = useKibana<{ data: any }>();
const { getPaginationTableProps } = useTable('elasticsearch.indices');
const clusterUuid = globalState.cluster_uuid;
const ccs = globalState.ccs;
const cluster = find(clusters, {
cluster_uuid: clusterUuid,
});
@ -53,6 +54,7 @@ export const ElasticsearchIndicesPage: React.FC<ComponentProps> = ({ clusters })
show_system_indices: showSystemIndices,
},
body: JSON.stringify({
ccs,
timeRange: {
min: bounds.min.toISOString(),
max: bounds.max.toISOString(),
@ -60,7 +62,13 @@ export const ElasticsearchIndicesPage: React.FC<ComponentProps> = ({ clusters })
}),
});
setData(response);
}, [showSystemIndices, clusterUuid, services.data?.query.timefilter.timefilter, services.http]);
}, [
ccs,
showSystemIndices,
clusterUuid,
services.data?.query.timefilter.timefilter,
services.http,
]);
return (
<ElasticsearchTemplate
@ -70,7 +78,7 @@ export const ElasticsearchIndicesPage: React.FC<ComponentProps> = ({ clusters })
data-test-subj="elasticsearchOverviewPage"
cluster={cluster}
>
<div data-test-subj="elasticsearchNodesListingPage">
<div data-test-subj="elasticsearchIndicesListingPage">
<SetupModeRenderer
render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>

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, { useContext, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { find } from 'lodash';
import { ElasticsearchTemplate } from './elasticsearch_template';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { GlobalStateContext } from '../../global_state_context';
import { ElasticsearchMLJobs } from '../../../components/elasticsearch';
import { ComponentProps } from '../../route_init';
import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer';
import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context';
import { useTable } from '../../hooks/use_table';
import type { MLJobs } from '../../../types';
interface SetupModeProps {
setupMode: any;
flyoutComponent: any;
bottomBarComponent: any;
}
export const ElasticsearchMLJobsPage: React.FC<ComponentProps> = ({ clusters }) => {
const globalState = useContext(GlobalStateContext);
const { services } = useKibana<{ data: any }>();
const { getPaginationTableProps } = useTable('elasticsearch.mlJobs');
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.mlJobs.routeTitle', {
defaultMessage: 'Elasticsearch - Machine Learning Jobs',
});
const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.mlJobs.pageTitle', {
defaultMessage: 'Elasticsearch machine learning jobs',
});
const getPageData = useCallback(async () => {
const bounds = services.data?.query.timefilter.timefilter.getBounds();
const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/ml_jobs`;
const response = await services.http?.fetch(url, {
method: 'POST',
body: JSON.stringify({
ccs,
timeRange: {
min: bounds.min.toISOString(),
max: bounds.max.toISOString(),
},
}),
});
setData({
clusterStatus: response.clusterStatus,
jobs: (response.rows as MLJobs).map((job) => {
if ('ml' in job && job.ml?.job) {
return {
...job.ml.job,
node: job.node,
job_id: job.ml.job.id,
};
}
return job;
}),
});
}, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]);
return (
<ElasticsearchTemplate
title={title}
pageTitle={pageTitle}
getPageData={getPageData}
data-test-subj="elasticsearchOverviewPage"
cluster={cluster}
>
<div data-test-subj="elasticsearchMLJobsListingPage">
<SetupModeRenderer
render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<ElasticsearchMLJobs
clusterStatus={data.clusterStatus}
jobs={data.jobs}
{...getPaginationTableProps()}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
</div>
</ElasticsearchTemplate>
);
};

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 ClusterStatus: FunctionComponent<Props>;

View file

@ -8,4 +8,5 @@
export const ElasticsearchOverview: FunctionComponent<Props>;
export const ElasticsearchNodes: FunctionComponent<Props>;
export const ElasticsearchIndices: FunctionComponent<Props>;
export const ElasticsearchMLJobs: FunctionComponent<Props>;
export const NodeReact: FunctionComponent<Props>;

View file

@ -9,3 +9,4 @@ export { ElasticsearchOverview } from './overview';
export { ElasticsearchNodes } from './nodes';
export { NodeReact } from './node';
export { ElasticsearchIndices } from './indices';
export { ElasticsearchMLJobs } from './ml_jobs';

View file

@ -6,10 +6,10 @@
*/
import React from 'react';
import { StatusIcon } from '../../status_icon';
import { i18n } from '@kbn/i18n';
import { StatusIcon } from '../../status_icon';
export function MachineLearningJobStatusIcon({ status }) {
export function MachineLearningJobStatusIcon({ status }: { status: string }) {
const type = (() => {
const statusKey = status.toUpperCase();

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 { ElasticsearchMLJobs } from './ml_jobs';

View file

@ -0,0 +1,168 @@
/*
* 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 { capitalize } from 'lodash';
import numeral from '@elastic/numeral';
import React from 'react';
import {
EuiLink,
EuiPage,
EuiPageContent,
EuiPageBody,
EuiPanel,
EuiSpacer,
Pagination,
EuiTableSortingType,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { LARGE_ABBREVIATED, LARGE_BYTES } from '../../../../common/formatting';
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
import type { MLJobs } from '../../../types';
import { EuiMonitoringTable } from '../../table';
import { MachineLearningJobStatusIcon } from '../ml_job_listing/status_icon';
import { ClusterStatus } from '../cluster_status';
interface Props {
clusterStatus: boolean;
jobs: MLJobs;
onTableChange: () => void;
sorting: EuiTableSortingType<string>;
pagination: Pagination;
}
type MLJob = MLJobs[0];
export const ElasticsearchMLJobs = ({
clusterStatus,
jobs,
sorting,
pagination,
onTableChange,
}: Props) => {
return (
<EuiPage>
<EuiPageBody>
<EuiPanel>
<ClusterStatus stats={clusterStatus} />
</EuiPanel>
<EuiSpacer size="m" />
<EuiPageContent>
<EuiMonitoringTable
className="mlJobsTable"
rows={jobs}
columns={columns}
sorting={{
...sorting,
sort: {
...sorting.sort,
field: 'job_id',
},
}}
pagination={pagination}
message={i18n.translate(
'xpack.monitoring.elasticsearch.mlJobListing.noJobsDescription',
{
defaultMessage:
'There are no Machine Learning Jobs that match your query. Try changing the time range selection.',
}
)}
search={{
box: {
incremental: true,
placeholder: i18n.translate(
'xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder',
{
defaultMessage: 'Filter Jobs…',
}
),
},
}}
onTableChange={onTableChange}
executeQueryOptions={{
defaultFields: ['job_id'],
}}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};
const columns = [
{
name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.jobIdTitle', {
defaultMessage: 'Job ID',
}),
field: 'job_id',
sortable: true,
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.stateTitle', {
defaultMessage: 'State',
}),
field: 'state',
sortable: true,
render: (state: string) => (
<div>
<MachineLearningJobStatusIcon status={state} />
&nbsp;
{capitalize(state)}
</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle', {
defaultMessage: 'Processed Records',
}),
field: 'data_counts.processed_record_count',
sortable: true,
render: (value: unknown) => <span>{numeral(value).format(LARGE_ABBREVIATED)}</span>,
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.modelSizeTitle', {
defaultMessage: 'Model Size',
}),
field: 'model_size_stats.model_bytes',
sortable: true,
render: (value: unknown) => <span>{numeral(value).format(LARGE_BYTES)}</span>,
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle', {
defaultMessage: 'Forecasts',
}),
field: 'forecasts_stats.total',
sortable: true,
render: (value: unknown) => <span>{numeral(value).format(LARGE_ABBREVIATED)}</span>,
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.nodeTitle', {
defaultMessage: 'Node',
}),
field: 'node.name',
sortable: true,
render: (name: string, job: MLJob) => {
if (job.node) {
if ('id' in job.node) {
return (
<EuiLink href={getSafeForExternalLink(`#/elasticsearch/nodes/${job.node.id}`)}>
{name}
</EuiLink>
);
} else return <span>{name}</span>;
}
return (
<FormattedMessage
id="xpack.monitoring.elasticsearch.mlJobListing.noDataLabel"
defaultMessage="N/A"
/>
);
},
},
];

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 StatusIcon: FunctionComponent<Props>;

View file

@ -7,3 +7,4 @@
export const euiTableStorageGetter: (string) => any;
export const euiTableStorageSetter: (string) => any;
export const EuiMonitoringTable: FunctionComponent<Props>;

View file

@ -14,6 +14,8 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { MonitoringConfig } from '../server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { MLJobs } from '../server/lib/elasticsearch/get_ml_jobs';
export interface MonitoringStartPluginDependencies {
navigation: NavigationStart;

View file

@ -35,6 +35,8 @@ export function handleResponse(response: ElasticsearchResponse) {
);
}
export type MLJobs = ReturnType<typeof handleResponse>;
export function getMlJobs(req: LegacyRequest, esIndexPattern: string) {
checkParam(esIndexPattern, 'esIndexPattern in getMlJobs');