mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Fix Single Metric Viewer and Anomaly Explorer charts still loading even after failure (#98490) (#98790)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Quynh Nguyen <43350163+qn895@users.noreply.github.com>
This commit is contained in:
parent
d16d17b955
commit
e695a5bd04
8 changed files with 205 additions and 72 deletions
|
@ -207,7 +207,7 @@ export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string |
|
|||
return i18n.translate(
|
||||
'xpack.ml.timeSeriesJob.jobWithUnsupportedCompositeAggregationMessage',
|
||||
{
|
||||
defaultMessage: 'Disabled because the datafeed contains unsupported composite sources.',
|
||||
defaultMessage: 'the datafeed contains unsupported composite sources',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ export function getSingleMetricViewerJobErrorMessage(job: CombinedJob): string |
|
|||
|
||||
if (isChartableTimeSeriesViewJob === false) {
|
||||
return i18n.translate('xpack.ml.timeSeriesJob.notViewableTimeSeriesJobMessage', {
|
||||
defaultMessage: 'Disabled because not a viewable time series job.',
|
||||
defaultMessage: 'not a viewable time series job',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,15 @@ export function ResultLinks({ jobs }) {
|
|||
const singleMetricDisabledMessage =
|
||||
jobs.length === 1 && jobs[0].isNotSingleMetricViewerJobMessage;
|
||||
|
||||
const singleMetricDisabledMessageText =
|
||||
singleMetricDisabledMessage !== undefined
|
||||
? i18n.translate('xpack.ml.jobsList.resultActions.singleMetricDisabledMessageText', {
|
||||
defaultMessage: 'Disabled because {reason}.',
|
||||
values: {
|
||||
reason: singleMetricDisabledMessage,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;
|
||||
const { createLinkWithUserDefaults } = useCreateADLinks();
|
||||
const timeSeriesExplorerLink = useMemo(
|
||||
|
@ -60,7 +69,7 @@ export function ResultLinks({ jobs }) {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={singleMetricDisabledMessage ?? openJobsInSingleMetricViewerText}
|
||||
content={singleMetricDisabledMessageText ?? openJobsInSingleMetricViewerText}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
href={timeSeriesExplorerLink}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { each, find, get, map, reduce, sortBy } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map as mapObservable } from 'rxjs/operators';
|
||||
import { catchError, map as mapObservable } from 'rxjs/operators';
|
||||
import { RecordForInfluencer } from './results_service/results_service';
|
||||
import {
|
||||
isMappableJob,
|
||||
|
@ -29,7 +29,11 @@ import { CriteriaField, MlResultsService } from './results_service';
|
|||
import { TimefilterContract, TimeRange } from '../../../../../../src/plugins/data/public';
|
||||
import { CHART_TYPE, ChartType } from '../explorer/explorer_constants';
|
||||
import type { ChartRecord } from '../explorer/explorer_utils';
|
||||
import { RecordsForCriteria, ScheduledEventsByBucket } from './results_service/result_service_rx';
|
||||
import {
|
||||
RecordsForCriteria,
|
||||
ResultResponse,
|
||||
ScheduledEventsByBucket,
|
||||
} from './results_service/result_service_rx';
|
||||
import { isPopulatedObject } from '../../../common/util/object_utils';
|
||||
import { AnomalyRecordDoc } from '../../../common/types/anomalies';
|
||||
import {
|
||||
|
@ -60,9 +64,8 @@ interface ChartPoint {
|
|||
numberOfCauses?: number;
|
||||
scheduledEvents?: any[];
|
||||
}
|
||||
interface MetricData {
|
||||
interface MetricData extends ResultResponse {
|
||||
results: Record<string, number>;
|
||||
success: boolean;
|
||||
}
|
||||
interface SeriesConfig {
|
||||
jobId: JobId;
|
||||
|
@ -91,6 +94,8 @@ export interface SeriesConfigWithMetadata extends SeriesConfig {
|
|||
loading?: boolean;
|
||||
chartData?: ChartPoint[] | null;
|
||||
mapData?: Array<ChartRecord | undefined>;
|
||||
plotEarliest?: number;
|
||||
plotLatest?: number;
|
||||
}
|
||||
|
||||
export const isSeriesConfigWithMetadata = (arg: unknown): arg is SeriesConfigWithMetadata => {
|
||||
|
@ -545,6 +550,19 @@ export class AnomalyExplorerChartsService {
|
|||
return data;
|
||||
}
|
||||
|
||||
function handleError(errorMsg: string, jobId: string): void {
|
||||
// Group the jobIds by the type of error message
|
||||
if (!data.errorMessages) {
|
||||
data.errorMessages = {};
|
||||
}
|
||||
|
||||
if (data.errorMessages[errorMsg]) {
|
||||
data.errorMessages[errorMsg].add(jobId);
|
||||
} else {
|
||||
data.errorMessages[errorMsg] = new Set([jobId]);
|
||||
}
|
||||
}
|
||||
|
||||
// Query 1 - load the raw metric data.
|
||||
function getMetricData(
|
||||
mlResultsService: MlResultsService,
|
||||
|
@ -577,6 +595,17 @@ export class AnomalyExplorerChartsService {
|
|||
bucketSpanSeconds * 1000,
|
||||
config.datafeedConfig
|
||||
)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
handleError(
|
||||
i18n.translate('xpack.ml.timeSeriesJob.metricDataErrorMessage', {
|
||||
defaultMessage: 'an error occurred while retrieving metric data',
|
||||
}),
|
||||
job.job_id
|
||||
);
|
||||
return of({ success: false, results: {}, error });
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
} else {
|
||||
// Extract the partition, by, over fields on which to filter.
|
||||
|
@ -638,8 +667,15 @@ export class AnomalyExplorerChartsService {
|
|||
});
|
||||
resolve(obj);
|
||||
})
|
||||
.catch((resp) => {
|
||||
reject(resp);
|
||||
.catch((error) => {
|
||||
handleError(
|
||||
i18n.translate('xpack.ml.timeSeriesJob.modelPlotDataErrorMessage', {
|
||||
defaultMessage: 'an error occurred while retrieving model plot data',
|
||||
}),
|
||||
job.job_id
|
||||
);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -665,6 +701,17 @@ export class AnomalyExplorerChartsService {
|
|||
range.max,
|
||||
ANOMALIES_MAX_RESULTS
|
||||
)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
handleError(
|
||||
i18n.translate('xpack.ml.timeSeriesJob.recordsForCriteriaErrorMessage', {
|
||||
defaultMessage: 'an error occurred while retrieving anomaly records',
|
||||
}),
|
||||
config.jobId
|
||||
);
|
||||
return of({ success: false, records: [], error });
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
|
@ -683,6 +730,17 @@ export class AnomalyExplorerChartsService {
|
|||
1,
|
||||
MAX_SCHEDULED_EVENTS
|
||||
)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
handleError(
|
||||
i18n.translate('xpack.ml.timeSeriesJob.scheduledEventsByBucketErrorMessage', {
|
||||
defaultMessage: 'an error occurred while retrieving scheduled events',
|
||||
}),
|
||||
config.jobId
|
||||
);
|
||||
return of({ success: false, events: {}, error });
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
|
@ -707,20 +765,30 @@ export class AnomalyExplorerChartsService {
|
|||
}
|
||||
|
||||
const datafeedQuery = get(config, 'datafeedConfig.query', null);
|
||||
return mlResultsService.getEventDistributionData(
|
||||
Array.isArray(config.datafeedConfig.indices)
|
||||
? config.datafeedConfig.indices[0]
|
||||
: config.datafeedConfig.indices,
|
||||
splitField,
|
||||
filterField,
|
||||
datafeedQuery,
|
||||
config.metricFunction,
|
||||
config.metricFieldName,
|
||||
config.timeField,
|
||||
range.min,
|
||||
range.max,
|
||||
config.bucketSpanSeconds * 1000
|
||||
);
|
||||
|
||||
return mlResultsService
|
||||
.getEventDistributionData(
|
||||
Array.isArray(config.datafeedConfig.indices)
|
||||
? config.datafeedConfig.indices[0]
|
||||
: config.datafeedConfig.indices,
|
||||
splitField,
|
||||
filterField,
|
||||
datafeedQuery,
|
||||
config.metricFunction,
|
||||
config.metricFieldName,
|
||||
config.timeField,
|
||||
range.min,
|
||||
range.max,
|
||||
config.bucketSpanSeconds * 1000
|
||||
)
|
||||
.catch((err) => {
|
||||
handleError(
|
||||
i18n.translate('xpack.ml.timeSeriesJob.eventDistributionDataErrorMessage', {
|
||||
defaultMessage: 'an error occurred while retrieving data',
|
||||
}),
|
||||
config.jobId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// first load and wait for required data,
|
||||
|
@ -883,20 +951,23 @@ export class AnomalyExplorerChartsService {
|
|||
);
|
||||
const overallChartLimits = chartLimits(allDataPoints);
|
||||
|
||||
data.seriesToPlot = response.map((d, i) => {
|
||||
return {
|
||||
...seriesConfigsForPromises[i],
|
||||
loading: false,
|
||||
chartData: processedData[i],
|
||||
plotEarliest: chartRange.min,
|
||||
plotLatest: chartRange.max,
|
||||
selectedEarliest: selectedEarliestMs,
|
||||
selectedLatest: selectedLatestMs,
|
||||
chartLimits: USE_OVERALL_CHART_LIMITS
|
||||
? overallChartLimits
|
||||
: chartLimits(processedData[i]),
|
||||
};
|
||||
});
|
||||
data.seriesToPlot = response
|
||||
// Don't show the charts if there was an issue retrieving metric or anomaly data
|
||||
.filter((r) => r[0]?.success === true && r[1]?.success === true)
|
||||
.map((d, i) => {
|
||||
return {
|
||||
...seriesConfigsForPromises[i],
|
||||
loading: false,
|
||||
chartData: processedData[i],
|
||||
plotEarliest: chartRange.min,
|
||||
plotLatest: chartRange.max,
|
||||
selectedEarliest: selectedEarliestMs,
|
||||
selectedLatest: selectedLatestMs,
|
||||
chartLimits: USE_OVERALL_CHART_LIMITS
|
||||
? overallChartLimits
|
||||
: chartLimits(processedData[i]),
|
||||
};
|
||||
});
|
||||
|
||||
if (mapData.length) {
|
||||
// push map data in if it's available
|
||||
|
|
|
@ -28,9 +28,11 @@ import { isPopulatedObject } from '../../../../common/util/object_utils';
|
|||
import { InfluencersFilterQuery } from '../../../../common/types/es_client';
|
||||
import { RecordForInfluencer } from './results_service';
|
||||
import { isRuntimeMappings } from '../../../../common';
|
||||
import { ErrorType } from '../../../../common/util/errors';
|
||||
|
||||
interface ResultResponse {
|
||||
export interface ResultResponse {
|
||||
success: boolean;
|
||||
error?: ErrorType;
|
||||
}
|
||||
|
||||
export interface MetricData extends ResultResponse {
|
||||
|
|
|
@ -31,7 +31,7 @@ export function toastNotificationServiceProvider(toastNotifications: ToastsStart
|
|||
toastNotifications.addSuccess(toastOrTitle, options);
|
||||
}
|
||||
|
||||
function displayErrorToast(error: ErrorType, title?: string) {
|
||||
function displayErrorToast(error: ErrorType, title?: string, toastLifeTimeMs?: number) {
|
||||
const errorObj = extractErrorProperties(error);
|
||||
toastNotifications.addError(new MLRequestFailure(errorObj, error), {
|
||||
title:
|
||||
|
@ -39,6 +39,7 @@ export function toastNotificationServiceProvider(toastNotifications: ToastsStart
|
|||
i18n.translate('xpack.ml.toastNotificationService.errorTitle', {
|
||||
defaultMessage: 'An error has occurred',
|
||||
}),
|
||||
...(toastLifeTimeMs ? { toastLifeTimeMs } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 { TimeseriesexplorerChartDataError } from './timeseriesexplorer_chart_data_error';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const TimeseriesexplorerChartDataError = ({ errorMsg }: { errorMsg: string }) => {
|
||||
return <EuiEmptyPrompt iconType="alert" title={<h2>{errorMsg}</h2>} />;
|
||||
};
|
|
@ -34,8 +34,6 @@ import {
|
|||
EuiAccordion,
|
||||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { getToastNotifications } from '../util/dependency_cache';
|
||||
import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search';
|
||||
|
@ -87,6 +85,7 @@ import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/times
|
|||
import { aggregationTypeTransform } from '../../../common/util/anomaly_utils';
|
||||
import { isMetricDetector } from './get_function_description';
|
||||
import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors';
|
||||
import { TimeseriesexplorerChartDataError } from './components/timeseriesexplorer_chart_data_error';
|
||||
|
||||
// Used to indicate the chart is being plotted across
|
||||
// all partition field values, where the cardinality of the field cannot be
|
||||
|
@ -131,6 +130,7 @@ function getTimeseriesexplorerDefaultState() {
|
|||
zoomTo: undefined,
|
||||
zoomFromFocusLoaded: undefined,
|
||||
zoomToFocusLoaded: undefined,
|
||||
chartDataError: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -151,6 +151,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
tableInterval: PropTypes.string,
|
||||
tableSeverity: PropTypes.number,
|
||||
zoom: PropTypes.object,
|
||||
toastNotificationService: PropTypes.object,
|
||||
};
|
||||
|
||||
state = getTimeseriesexplorerDefaultState();
|
||||
|
@ -390,6 +391,13 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId);
|
||||
};
|
||||
|
||||
displayErrorToastMessages = (error, errorMsg) => {
|
||||
if (this.props.toastNotificationService) {
|
||||
this.props.toastNotificationService.displayErrorToast(error, errorMsg, 2000);
|
||||
}
|
||||
this.setState({ loading: false, chartDataError: errorMsg });
|
||||
};
|
||||
|
||||
loadSingleMetricData = (fullRefresh = true) => {
|
||||
const {
|
||||
autoZoomDuration,
|
||||
|
@ -426,6 +434,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
fullRefresh,
|
||||
loadCounter: currentLoadCounter + 1,
|
||||
loading: true,
|
||||
chartDataError: undefined,
|
||||
...(fullRefresh
|
||||
? {
|
||||
chartDetails: undefined,
|
||||
|
@ -558,11 +567,11 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
stateUpdate.contextChartData = fullRangeChartData;
|
||||
finish(counter);
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log(
|
||||
'Time series explorer - error getting metric data from elasticsearch:',
|
||||
resp
|
||||
);
|
||||
.catch((err) => {
|
||||
const errorMsg = i18n.translate('xpack.ml.timeSeriesExplorer.metricDataErrorMessage', {
|
||||
defaultMessage: 'Error getting metric data',
|
||||
});
|
||||
this.displayErrorToastMessages(err, errorMsg);
|
||||
});
|
||||
|
||||
// Query 2 - load max record score at same granularity as context chart
|
||||
|
@ -581,11 +590,15 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
stateUpdate.swimlaneData = fullRangeRecordScoreData;
|
||||
finish(counter);
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log(
|
||||
'Time series explorer - error getting bucket anomaly scores from elasticsearch:',
|
||||
resp
|
||||
.catch((err) => {
|
||||
const errorMsg = i18n.translate(
|
||||
'xpack.ml.timeSeriesExplorer.bucketAnomalyScoresErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Error getting bucket anomaly scores',
|
||||
}
|
||||
);
|
||||
|
||||
this.displayErrorToastMessages(err, errorMsg);
|
||||
});
|
||||
|
||||
// Query 3 - load details on the chart used in the chart title (charting function and entity(s)).
|
||||
|
@ -601,10 +614,12 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
stateUpdate.chartDetails = resp.results;
|
||||
finish(counter);
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log(
|
||||
'Time series explorer - error getting entity counts from elasticsearch:',
|
||||
resp
|
||||
.catch((err) => {
|
||||
this.displayErrorToastMessages(
|
||||
err,
|
||||
i18n.translate('xpack.ml.timeSeriesExplorer.entityCountsErrorMessage', {
|
||||
defaultMessage: 'Error getting entity counts',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -633,10 +648,13 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
stateUpdate.contextForecastData = processForecastResults(resp.results);
|
||||
finish(counter);
|
||||
})
|
||||
.catch((resp) => {
|
||||
console.log(
|
||||
`Time series explorer - error loading data for forecast ID ${selectedForecastId}`,
|
||||
resp
|
||||
.catch((err) => {
|
||||
this.displayErrorToastMessages(
|
||||
err,
|
||||
i18n.translate('xpack.ml.timeSeriesExplorer.forecastDataErrorMessage', {
|
||||
defaultMessage: 'Error loading forecast data for forecast ID {forecastId}',
|
||||
values: { forecastId: selectedForecastId },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -695,8 +713,10 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
},
|
||||
}
|
||||
);
|
||||
const toastNotifications = getToastNotifications();
|
||||
toastNotifications.addWarning(warningText);
|
||||
if (this.props.toastNotificationService) {
|
||||
this.props.toastNotificationService.displayWarningToast(warningText);
|
||||
}
|
||||
|
||||
detectorIndex = detectors[0].index;
|
||||
}
|
||||
|
||||
|
@ -716,16 +736,17 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
// perhaps due to user's advanced setting using incorrect date-maths
|
||||
const { invalidTimeRangeError } = this.props;
|
||||
if (invalidTimeRangeError) {
|
||||
const toastNotifications = getToastNotifications();
|
||||
toastNotifications.addWarning(
|
||||
i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', {
|
||||
defaultMessage:
|
||||
'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.',
|
||||
values: {
|
||||
field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
|
||||
},
|
||||
})
|
||||
);
|
||||
if (this.props.toastNotificationService) {
|
||||
this.props.toastNotificationService.displayWarningToast(
|
||||
i18n.translate('xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout', {
|
||||
defaultMessage:
|
||||
'The time filter was changed to the full range for this job due to an invalid default time filter. Check the advanced settings for {field}.',
|
||||
values: {
|
||||
field: ANOMALY_DETECTION_DEFAULT_TIME_RANGE,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Required to redraw the time series chart when the container is resized.
|
||||
|
@ -853,7 +874,8 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
if (
|
||||
previousProps === undefined ||
|
||||
!isEqual(previousProps.bounds, this.props.bounds) ||
|
||||
!isEqual(previousProps.lastRefresh, this.props.lastRefresh) ||
|
||||
(!isEqual(previousProps.lastRefresh, this.props.lastRefresh) &&
|
||||
previousProps.lastRefresh !== 0) ||
|
||||
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
|
||||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
|
||||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
|
||||
|
@ -938,6 +960,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
zoomTo,
|
||||
zoomFromFocusLoaded,
|
||||
zoomToFocusLoaded,
|
||||
chartDataError,
|
||||
} = this.state;
|
||||
const chartProps = {
|
||||
modelPlotEnabled,
|
||||
|
@ -1041,10 +1064,15 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
/>
|
||||
)}
|
||||
|
||||
{loading === false && chartDataError !== undefined && (
|
||||
<TimeseriesexplorerChartDataError errorMsg={chartDataError} />
|
||||
)}
|
||||
|
||||
{arePartitioningFieldsProvided &&
|
||||
jobs.length > 0 &&
|
||||
(fullRefresh === false || loading === false) &&
|
||||
hasResults === false && (
|
||||
hasResults === false &&
|
||||
chartDataError === undefined && (
|
||||
<TimeseriesexplorerNoChartData
|
||||
dataNotChartable={dataNotChartable}
|
||||
entities={entityControls}
|
||||
|
@ -1149,6 +1177,7 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<TimeSeriesChartWithTooltips
|
||||
chartProps={chartProps}
|
||||
contextAggregationInterval={contextAggregationInterval}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue