in x-pack/solutions/observability/plugins/observability/public/components/rule_condition_chart/rule_condition_chart.tsx [94:504]
export function RuleConditionChart({
metricExpression,
searchConfiguration,
dataView,
groupBy,
error,
annotations,
timeRange,
chartOptions: { seriesType, interval } = {},
additionalFilters = [],
}: RuleConditionChartProps) {
const {
services: { lens },
} = useKibana();
const { euiTheme } = useEuiTheme();
const {
metrics,
timeSize,
timeUnit,
threshold,
comparator,
equation,
label,
warningComparator,
warningThreshold,
} = metricExpression;
const [attributes, setAttributes] = useState<LensAttributes>();
const [aggMap, setAggMap] = useState<AggMap>();
const [formula, setFormula] = useState<string>('');
const [thresholdReferenceLine, setThresholdReferenceLine] = useState<XYReferenceLinesLayer[]>();
const [warningThresholdReferenceLine, setWarningThresholdReferenceLine] =
useState<XYReferenceLinesLayer[]>();
const [alertAnnotation, setAlertAnnotation] = useState<XYByValueAnnotationsLayer>();
const [chartLoading, setChartLoading] = useState<boolean>(false);
const filters = [...(searchConfiguration.filter || []), ...additionalFilters];
const formulaAsync = useAsync(() => {
return lens.stateHelperApi();
}, [lens]);
// Handle Lens error
useEffect(() => {
// Lens does not expose or provide a way to check if there is an error in the chart, yet.
// To work around this, we check if the element with class 'lnsEmbeddedError' is found in the DOM.
setTimeout(function () {
const errorDiv = document.querySelector('.lnsEmbeddedError');
if (errorDiv) {
const paragraphElements = errorDiv.querySelectorAll('p');
if (!paragraphElements) return;
paragraphElements[0].innerText = i18n.translate(
'xpack.observability.ruleCondition.chart.error_equation.title',
{
defaultMessage: 'An error occurred while rendering the chart',
}
);
if (paragraphElements.length > 1) {
paragraphElements[1].innerText = i18n.translate(
'xpack.observability.ruleCondition.chart.error_equation.description',
{
defaultMessage: 'Check the rule equation.',
}
);
}
}
});
}, [chartLoading, attributes]);
// Build the warning threshold reference line
useEffect(() => {
if (!warningThreshold) {
if (warningThresholdReferenceLine?.length) {
setWarningThresholdReferenceLine([]);
}
return;
}
const refLayers = [];
if (
warningComparator === COMPARATORS.NOT_BETWEEN ||
(warningComparator === COMPARATORS.BETWEEN && warningThreshold.length === 2)
) {
const refLineStart = new XYReferenceLinesLayer({
data: [
{
value: (warningThreshold[0] || 0).toString(),
color: euiTheme.colors.warning,
fill: warningComparator === COMPARATORS.NOT_BETWEEN ? 'below' : 'none',
},
],
});
const refLineEnd = new XYReferenceLinesLayer({
data: [
{
value: (warningThreshold[1] || 0).toString(),
color: euiTheme.colors.warning,
fill: warningComparator === COMPARATORS.NOT_BETWEEN ? 'above' : 'none',
},
],
});
refLayers.push(refLineStart, refLineEnd);
} else {
let fill: FillStyle = 'above';
if (
warningComparator === COMPARATORS.LESS_THAN ||
warningComparator === COMPARATORS.LESS_THAN_OR_EQUALS
) {
fill = 'below';
}
const warningThresholdRefLine = new XYReferenceLinesLayer({
data: [
{
value: (warningThreshold[0] || 0).toString(),
color: euiTheme.colors.warning,
fill,
},
],
});
// A transparent line to add extra buffer at the top of threshold
const bufferRefLine = new XYReferenceLinesLayer({
data: [
{
value: getBufferThreshold(warningThreshold[0]),
color: 'transparent',
fill,
},
],
});
refLayers.push(warningThresholdRefLine, bufferRefLine);
}
setWarningThresholdReferenceLine(refLayers);
}, [
warningThreshold,
warningComparator,
euiTheme.colors.warning,
metrics,
warningThresholdReferenceLine?.length,
]);
// Build the threshold reference line
useEffect(() => {
if (!threshold) return;
const refLayers = [];
if (
comparator === COMPARATORS.NOT_BETWEEN ||
(comparator === COMPARATORS.BETWEEN && threshold.length === 2)
) {
const refLineStart = new XYReferenceLinesLayer({
data: [
{
value: (threshold[0] || 0).toString(),
color: euiTheme.colors.danger,
fill: comparator === COMPARATORS.NOT_BETWEEN ? 'below' : 'none',
},
],
});
const refLineEnd = new XYReferenceLinesLayer({
data: [
{
value: (threshold[1] || 0).toString(),
color: euiTheme.colors.danger,
fill: comparator === COMPARATORS.NOT_BETWEEN ? 'above' : 'none',
},
],
});
refLayers.push(refLineStart, refLineEnd);
} else {
let fill: FillStyle = 'above';
if (comparator === COMPARATORS.LESS_THAN || comparator === COMPARATORS.LESS_THAN_OR_EQUALS) {
fill = 'below';
}
const thresholdRefLine = new XYReferenceLinesLayer({
data: [
{
value: (threshold[0] || 0).toString(),
color: euiTheme.colors.danger,
fill,
},
],
});
// A transparent line to add extra buffer at the top of threshold
const bufferRefLine = new XYReferenceLinesLayer({
data: [
{
value: getBufferThreshold(threshold[0]),
color: 'transparent',
fill,
},
],
});
refLayers.push(thresholdRefLine, bufferRefLine);
}
setThresholdReferenceLine(refLayers);
}, [threshold, comparator, euiTheme.colors.danger, metrics]);
// Build alert annotation
useEffect(() => {
if (!annotations) return;
const alertAnnotationLayer = new XYByValueAnnotationsLayer({
annotations,
ignoreGlobalFilters: true,
dataView,
});
setAlertAnnotation(alertAnnotationLayer);
}, [euiTheme.colors.danger, dataView, annotations]);
// Build the aggregation map from the metrics
useEffect(() => {
if (!metrics || metrics.length === 0) {
return;
}
const aggMapFromMetrics = metrics.reduce((acc, metric) => {
const { operation, operationWithField, sourceField } = getLensOperationFromRuleMetric(metric);
return {
...acc,
[metric.name]: { operation, operationWithField, sourceField },
};
}, {} as AggMap);
setAggMap(aggMapFromMetrics);
}, [metrics]);
// Parse the equation
useEffect(() => {
try {
if (!aggMap) return;
const parser = new PainlessTinyMathParser({
aggMap,
equation: equation || Object.keys(aggMap || {}).join(' + '),
});
setFormula(parser.parse());
} catch (e) {
// The error will appear on Lens chart.
setAttributes(undefined);
return;
}
}, [aggMap, equation]);
useEffect(() => {
if (!formulaAsync.value || !dataView || !formula) {
return;
}
const formatId = lensFieldFormatter(metrics);
const baseLayer = {
type: 'formula',
value: formula,
label: label ?? formula,
groupBy,
format: {
id: formatId,
params: {
decimals: formatId === LensFieldFormat.PERCENT ? 0 : 2,
suffix: isRate(metrics) && formatId === LensFieldFormat.NUMBER ? EventsAsUnit : undefined,
},
},
};
const xYDataLayerOptions: XYLayerOptions = {
buckets: {
type: 'date_histogram',
params: {
interval: interval || `${timeSize}${timeUnit}`,
},
},
seriesType: seriesType ? seriesType : 'bar',
};
const firstMetricAggMap = aggMap && metrics.length > 0 ? aggMap[metrics[0].name] : undefined;
const convertToMaxOperation = ['counter_rate', 'last_value', 'percentile'];
const orderParams: TopValuesOrderParams = firstMetricAggMap
? {
orderDirection: 'desc',
orderBy: { type: 'custom' },
orderAgg: {
label: firstMetricAggMap.operationWithField,
dataType: 'number',
operationType: convertToMaxOperation.includes(firstMetricAggMap.operation)
? 'max'
: firstMetricAggMap.operation,
sourceField: firstMetricAggMap.sourceField,
isBucketed: false,
scale: 'ratio',
},
}
: undefined;
if (groupBy && groupBy?.length) {
xYDataLayerOptions.breakdown = {
type: 'top_values',
field: groupBy[0],
params: {
size: 3,
secondaryFields: (groupBy as string[]).slice(1),
accuracyMode: false,
...orderParams,
},
};
}
const xyDataLayer = new XYDataLayer({
data: [baseLayer].map((layer) => ({
type: layer.type,
value: layer.value,
label: layer.label,
format: layer.format,
// We always scale the chart with seconds with RATE Agg.
timeScale: isRate(metrics) ? 's' : undefined,
})),
options: xYDataLayerOptions,
});
const layers: Array<XYDataLayer | XYReferenceLinesLayer | XYByValueAnnotationsLayer> = [
xyDataLayer,
];
if (warningThresholdReferenceLine) {
layers.push(...warningThresholdReferenceLine);
}
if (thresholdReferenceLine) {
layers.push(...thresholdReferenceLine);
}
if (alertAnnotation) {
layers.push(alertAnnotation);
}
const attributesLens = new LensAttributesBuilder({
visualization: new XYChart({
visualOptions: {
valueLabels: 'hide',
axisTitlesVisibilitySettings: {
x: true,
yLeft: false,
yRight: true,
},
},
layers,
formulaAPI: formulaAsync.value.formula,
dataView,
}),
}).build();
const lensBuilderAtt = { ...attributesLens, type: 'lens' };
setAttributes(lensBuilderAtt);
}, [
comparator,
dataView,
equation,
label,
searchConfiguration,
formula,
formulaAsync.value,
groupBy,
interval,
metrics,
threshold,
thresholdReferenceLine,
alertAnnotation,
timeSize,
timeUnit,
seriesType,
warningThresholdReferenceLine,
aggMap,
]);
if (
!dataView ||
!attributes ||
error?.equation ||
Object.keys(error?.metrics || error?.metric || {}).length !== 0 ||
!timeSize ||
!timeRange
) {
return (
<div style={{ maxHeight: 180, minHeight: 180 }}>
<EuiEmptyPrompt
iconType="visArea"
titleSize="xxs"
data-test-subj="thresholdRuleNoChartData"
body={
<FormattedMessage
id="xpack.observability.customThreshold.rule.charts.noData.title"
defaultMessage="No chart data available, check the rule {errorSourceField}"
values={{
errorSourceField:
Object.keys(error?.metrics || {}).length !== 0
? 'aggregation fields'
: error?.equation
? 'equation'
: 'conditions',
}}
/>
}
/>
</div>
);
}
return (
<div>
<lens.EmbeddableComponent
onLoad={setChartLoading}
id="ruleConditionChart"
style={{ height: 180 }}
timeRange={timeRange}
attributes={attributes}
disableTriggers={true}
query={(searchConfiguration.query as Query) || defaultQuery}
filters={filters}
/>
</div>
);
}