-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Cloud Posture] Dashboard Redesign - data counter cards #144565
Changes from all commits
4892f6f
17aafaf
cd404f4
2ea6c1a
1be7828
ed8605f
322a336
cbe7151
80787e7
8f05630
e23bca1
99177fa
602cf74
8737c28
8649448
5d86a1f
b277484
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* 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 { css } from '@emotion/react'; | ||
import { EuiCard, EuiIcon, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui'; | ||
import type { EuiTextProps, EuiCardProps } from '@elastic/eui'; | ||
|
||
export type CspCounterCardProps = Pick<EuiCardProps, 'onClick' | 'id' | 'title' | 'description'> & { | ||
descriptionColor?: EuiTextProps['color']; | ||
}; | ||
|
||
export const CspCounterCard = (counter: CspCounterCardProps) => { | ||
const { euiTheme } = useEuiTheme(); | ||
|
||
return ( | ||
<EuiCard | ||
title={ | ||
<EuiTitle size="xxxs"> | ||
<h6>{counter.title}</h6> | ||
</EuiTitle> | ||
} | ||
hasBorder | ||
onClick={counter.onClick} | ||
paddingSize="m" | ||
textAlign="left" | ||
layout="vertical" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the default and can be omitted as we don't really care about the layout since we don't use an icon, we only care about the text alignment |
||
css={css` | ||
position: relative; | ||
|
||
:hover .euiIcon { | ||
color: ${euiTheme.colors.primary}; | ||
transition: ${euiTheme.animation.normal}; | ||
} | ||
`} | ||
data-test-subj={counter.id} | ||
> | ||
<EuiText color={counter.descriptionColor}> | ||
<EuiTitle size="xs"> | ||
<h3>{counter.description}</h3> | ||
</EuiTitle> | ||
</EuiText> | ||
{counter.onClick && ( | ||
<EuiIcon | ||
type="link" | ||
css={css` | ||
position: absolute; | ||
top: ${euiTheme.size.m}; | ||
right: ${euiTheme.size.m}; | ||
`} | ||
/> | ||
)} | ||
</EuiCard> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* | ||
* 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 { render } from '@testing-library/react'; | ||
import { expectIdsInDoc } from '../../../test/utils'; | ||
import { DASHBOARD_COUNTER_CARDS } from '../test_subjects'; | ||
import { CloudSummarySection } from './cloud_summary_section'; | ||
import { mockDashboardData } from '../compliance_dashboard.test'; | ||
import { TestProvider } from '../../../test/test_provider'; | ||
import { screen } from '@testing-library/react'; | ||
|
||
describe('<CloudSummarySection />', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd expect tests here to verify the rest of the functionality of the section (risks table, score chart, etc.) If you consider this out of scope please open a task There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah i'll add those tests here on every new component task, its already listed in the tasks |
||
const renderCloudSummarySection = (alterMockData = {}) => { | ||
render( | ||
<TestProvider> | ||
<CloudSummarySection complianceData={{ ...mockDashboardData, ...alterMockData }} /> | ||
</TestProvider> | ||
); | ||
}; | ||
|
||
it('renders all counter cards', () => { | ||
renderCloudSummarySection(); | ||
|
||
expectIdsInDoc({ | ||
be: [ | ||
DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED, | ||
DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED, | ||
DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS, | ||
], | ||
}); | ||
}); | ||
|
||
it('renders counters content according to mock', async () => { | ||
renderCloudSummarySection(); | ||
|
||
expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED)).toHaveTextContent('1'); | ||
expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED)).toHaveTextContent( | ||
'162' | ||
); | ||
expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS)).toHaveTextContent('17'); | ||
}); | ||
|
||
it('renders counters value in compact abbreviation if its above one million', () => { | ||
renderCloudSummarySection({ stats: { resourcesEvaluated: 999999, totalFailed: 1000000 } }); | ||
|
||
expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED)).toHaveTextContent( | ||
'999,999' | ||
); | ||
expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS)).toHaveTextContent('1M'); | ||
}); | ||
|
||
it('renders 0 as empty state', () => { | ||
renderCloudSummarySection({ stats: { totalFailed: undefined } }); | ||
|
||
expect(screen.getByTestId(DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS)).toHaveTextContent('0'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,16 +5,22 @@ | |
* 2.0. | ||
*/ | ||
|
||
import React from 'react'; | ||
import React, { useMemo } from 'react'; | ||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; | ||
import { PartitionElementEvent } from '@elastic/charts'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { FlexItemGrowSize } from '@elastic/eui/src/components/flex/flex_item'; | ||
import { DASHBOARD_COUNTER_CARDS } from '../test_subjects'; | ||
import { CspCounterCard, CspCounterCardProps } from '../../../components/csp_counter_card'; | ||
import { CompactFormattedNumber } from '../../../components/compact_formatted_number'; | ||
import { ChartPanel } from '../../../components/chart_panel'; | ||
import { CloudPostureScoreChart } from '../compliance_charts/cloud_posture_score_chart'; | ||
import type { ComplianceDashboardData, Evaluation } from '../../../../common/types'; | ||
import { RisksTable } from '../compliance_charts/risks_table'; | ||
import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; | ||
import { | ||
useNavigateFindings, | ||
useNavigateFindingsByResource, | ||
} from '../../../common/hooks/use_navigate_findings'; | ||
import { RULE_FAILED } from '../../../../common/constants'; | ||
|
||
const defaultHeight = 360; | ||
|
@@ -36,6 +42,7 @@ export const CloudSummarySection = ({ | |
complianceData: ComplianceDashboardData; | ||
}) => { | ||
const navToFindings = useNavigateFindings(); | ||
const navToFindingsByResource = useNavigateFindingsByResource(); | ||
|
||
const handleElementClick = (elements: PartitionElementEvent[]) => { | ||
const [element] = elements; | ||
|
@@ -56,9 +63,62 @@ export const CloudSummarySection = ({ | |
navToFindings({ 'result.evaluation': RULE_FAILED }); | ||
}; | ||
|
||
const counters: CspCounterCardProps[] = useMemo( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider just writing this in the declarative JSX instead of creating a variable and then using it: <EuiFlexItem key={DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED}>
<CspCounterCard
counter={{
id: DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED,
title: <CompactFormattedNumber number={complianceData.clusters.length} />,
description: i18n.translate(
'xpack.csp.dashboard.summarySection.counterCard.clustersEvaluatedDescription',
{ defaultMessage: 'Clusters Evaluated' }
),
}}
/>
</EuiFlexItem>
/* Rest of the cards */ I prefer it this way since you immediately "see" the layout when you look at the JSX, instead of jumping back and forth between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess its subjective, I prefer to have the JSX as clean as possible. "seeing" the layout is very important to me as well, I find it easier when things are clean and compact, I'm looking at it as a "list of counter cards" and keep the properties mess outside |
||
() => [ | ||
{ | ||
id: DASHBOARD_COUNTER_CARDS.CLUSTERS_EVALUATED, | ||
title: i18n.translate( | ||
'xpack.csp.dashboard.summarySection.counterCard.clustersEvaluatedDescription', | ||
{ defaultMessage: 'Clusters Evaluated' } | ||
), | ||
description: <CompactFormattedNumber number={complianceData.clusters.length} />, | ||
}, | ||
{ | ||
id: DASHBOARD_COUNTER_CARDS.RESOURCES_EVALUATED, | ||
title: i18n.translate( | ||
'xpack.csp.dashboard.summarySection.counterCard.resourcesEvaluatedDescription', | ||
{ defaultMessage: 'Resources Evaluated' } | ||
), | ||
description: ( | ||
<CompactFormattedNumber number={complianceData.stats.resourcesEvaluated || 0} /> | ||
), | ||
onClick: () => { | ||
navToFindingsByResource(); | ||
}, | ||
}, | ||
{ | ||
id: DASHBOARD_COUNTER_CARDS.FAILING_FINDINGS, | ||
title: i18n.translate( | ||
'xpack.csp.dashboard.summarySection.counterCard.failingFindingsDescription', | ||
{ defaultMessage: 'Failing Findings' } | ||
), | ||
description: <CompactFormattedNumber number={complianceData.stats.totalFailed} />, | ||
descriptionColor: complianceData.stats.totalFailed > 0 ? 'danger' : 'text', | ||
onClick: () => { | ||
navToFindings({ 'result.evaluation': RULE_FAILED }); | ||
}, | ||
}, | ||
], | ||
[ | ||
complianceData.clusters.length, | ||
complianceData.stats.resourcesEvaluated, | ||
complianceData.stats.totalFailed, | ||
navToFindings, | ||
navToFindingsByResource, | ||
] | ||
); | ||
|
||
return ( | ||
<EuiFlexGroup gutterSize="l" style={summarySectionWrapperStyle}> | ||
<EuiFlexItem grow={dashboardColumnsGrow.first} /> | ||
<EuiFlexItem grow={dashboardColumnsGrow.first}> | ||
<EuiFlexGroup direction="column"> | ||
{counters.map((counter) => ( | ||
<EuiFlexItem key={counter.id}> | ||
<CspCounterCard {...counter} /> | ||
</EuiFlexItem> | ||
))} | ||
</EuiFlexGroup> | ||
</EuiFlexItem> | ||
<EuiFlexItem grow={dashboardColumnsGrow.second}> | ||
<ChartPanel | ||
title={i18n.translate('xpack.csp.dashboard.summarySection.cloudPostureScorePanelTitle', { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider de-structuring props in the arguments:
({ title, onClick, id, description, descriptionColor })