Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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 { renderRuleStats } from './rule_stats';
export type { RuleStatsState } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* 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 { renderRuleStats } from './rule_stats';
import { render, screen } from '@testing-library/react';

const RULES_PAGE_LINK = '/app/observability/alerts/rules';
const STAT_CLASS = 'euiStat';
const STAT_TITLE_PRIMARY_CLASS = 'euiStat__title--primary';
const STAT_BUTTON_CLASS = 'euiButtonEmpty';

describe('Rule stats', () => {
test('renders all rule stats', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 0,
error: 0,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
expect(stats.length).toEqual(6);
});
test('disabled stat is not clickable, when there are no disabled rules', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 0,
error: 0,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { findByText, container } = render(stats[4]);
const disabledElement = await findByText('Disabled');
expect(disabledElement).toBeInTheDocument();
expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1);
expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0);
expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0);
});

test('disabled stat is clickable, when there are disabled rules', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 1,
muted: 0,
error: 0,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { container } = render(stats[4]);
expect(screen.getByText('Disabled').closest('a')).toHaveAttribute(
'href',
`${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(disabled))`
);

expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1);
});

test('disabled stat count is link-colored, when there are disabled rules', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 1,
muted: 0,
error: 0,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { container } = render(stats[4]);
expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1);
});

test('snoozed stat is not clickable, when there are no snoozed rules', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 0,
error: 0,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { findByText, container } = render(stats[3]);
const snoozedElement = await findByText('Snoozed');
expect(snoozedElement).toBeInTheDocument();
expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1);
expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0);
expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0);
});

test('snoozed stat is clickable, when there are snoozed rules', () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 1,
error: 0,
snoozed: 1,
},
RULES_PAGE_LINK,
false
);
const { container } = render(stats[3]);
expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1);
expect(screen.getByText('Snoozed').closest('a')).toHaveAttribute(
'href',
`${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(snoozed))`
);
});

test('snoozed stat count is link-colored, when there are snoozed rules', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 1,
error: 0,
snoozed: 1,
},
RULES_PAGE_LINK,
false
);
const { container } = render(stats[3]);
expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1);
});

test('errors stat is not clickable, when there are no error rules', async () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 0,
error: 0,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { findByText, container } = render(stats[2]);
const errorsElement = await findByText('Errors');
expect(errorsElement).toBeInTheDocument();
expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1);
expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0);
expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0);
});

test('errors stat is clickable, when there are error rules', () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 0,
error: 2,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { container } = render(stats[2]);
expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1);
expect(screen.getByText('Errors').closest('a')).toHaveAttribute(
'href',
`${RULES_PAGE_LINK}?_a=(lastResponse:!(error),status:!())`
);
});

test('errors stat count is link-colored, when there are error rules', () => {
const stats = renderRuleStats(
{
total: 11,
disabled: 0,
muted: 0,
error: 2,
snoozed: 0,
},
RULES_PAGE_LINK,
false
);
const { container } = render(stats[2]);
expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 from 'react';
import { EuiButtonEmpty, EuiStat } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { euiStyled } from '@kbn/kibana-react-plugin/common';

interface RuleStatsState {
total: number;
disabled: number;
muted: number;
error: number;
snoozed: number;
}
type StatType = 'disabled' | 'snoozed' | 'error';

const Divider = euiStyled.div`
border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
height: 100%;
`;

const StyledStat = euiStyled(EuiStat)`
.euiText {
line-height: 1;
}
`;

const ConditionalWrap = ({
condition,
wrap,
children,
}: {
condition: boolean;
wrap: (wrappedChildren: React.ReactNode) => JSX.Element;
children: JSX.Element;
}): JSX.Element => (condition ? wrap(children) : children);

export const renderRuleStats = (
ruleStats: RuleStatsState,
manageRulesHref: string,
ruleStatsLoading: boolean
) => {
const createRuleStatsLink = (stats: RuleStatsState, statType: StatType) => {
const count = stats[statType];
let statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!())`;
if (count > 0) {
switch (statType) {
case 'error':
statsLink = `${manageRulesHref}?_a=(lastResponse:!(error),status:!())`;
break;
case 'snoozed':
case 'disabled':
statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!(${statType}))`;
break;
default:
break;
}
}
return statsLink;
};

const disabledStatsComponent = (
<ConditionalWrap
condition={ruleStats.disabled > 0}
wrap={(wrappedChildren) => (
<EuiButtonEmpty href={createRuleStatsLink(ruleStats, 'disabled')}>
{wrappedChildren}
</EuiButtonEmpty>
)}
>
<StyledStat
title={ruleStats.disabled}
description={i18n.translate('xpack.observability.alerts.ruleStats.disabled', {
defaultMessage: 'Disabled',
})}
color="primary"
titleColor={ruleStats.disabled > 0 ? 'primary' : ''}
titleSize="xs"
isLoading={ruleStatsLoading}
data-test-subj="statDisabled"
/>
</ConditionalWrap>
);

const snoozedStatsComponent = (
<ConditionalWrap
condition={ruleStats.muted + ruleStats.snoozed > 0}
wrap={(wrappedChildren) => (
<EuiButtonEmpty href={createRuleStatsLink(ruleStats, 'snoozed')}>
{wrappedChildren}
</EuiButtonEmpty>
)}
>
<StyledStat
title={ruleStats.muted + ruleStats.snoozed}
description={i18n.translate('xpack.observability.alerts.ruleStats.muted', {
defaultMessage: 'Snoozed',
})}
color="primary"
titleColor={ruleStats.muted + ruleStats.snoozed > 0 ? 'primary' : ''}
titleSize="xs"
isLoading={ruleStatsLoading}
data-test-subj="statMuted"
/>
</ConditionalWrap>
);

const errorStatsComponent = (
<ConditionalWrap
condition={ruleStats.error > 0}
wrap={(wrappedChildren) => (
<EuiButtonEmpty href={createRuleStatsLink(ruleStats, 'error')}>
{wrappedChildren}
</EuiButtonEmpty>
)}
>
<StyledStat
title={ruleStats.error}
description={i18n.translate('xpack.observability.alerts.ruleStats.errors', {
defaultMessage: 'Errors',
})}
color="primary"
titleColor={ruleStats.error > 0 ? 'primary' : ''}
titleSize="xs"
isLoading={ruleStatsLoading}
data-test-subj="statErrors"
/>
</ConditionalWrap>
);
return [
<StyledStat
title={ruleStats.total}
description={i18n.translate('xpack.observability.alerts.ruleStats.ruleCount', {
defaultMessage: 'Rule count',
})}
color="primary"
titleSize="xs"
isLoading={ruleStatsLoading}
data-test-subj="statRuleCount"
/>,
disabledStatsComponent,
snoozedStatsComponent,
errorStatsComponent,
<Divider />,
<EuiButtonEmpty href={manageRulesHref}>
{i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', {
defaultMessage: 'Manage Rules',
})}
</EuiButtonEmpty>,
].reverse();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 interface RuleStatsState {
total: number;
disabled: number;
muted: number;
error: number;
snoozed: number;
}

export type StatType = 'disabled' | 'snoozed' | 'error';
Loading