Skip to content

Commit b284567

Browse files
authored
feat: checks empty state (#1081)
* feat: add empty state component for empty checklist and empty home page - also changes usage text. * fix: adjust test Test is only failing in CI, not when ran locally
1 parent deaf047 commit b284567

9 files changed

Lines changed: 159 additions & 115 deletions

File tree

src/components/CheckUsage.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
import { FormProvider, useForm } from 'react-hook-form';
3+
import { screen, waitFor } from '@testing-library/react';
4+
import { DataTestIds } from 'test/dataTestIds';
5+
import { render } from 'test/render';
6+
7+
import { CheckFormValues, CheckType } from 'types';
8+
9+
import { toFormValues } from './CheckEditor/checkFormTransformations';
10+
import { CheckFormContextProvider } from './CheckForm/CheckFormContext/CheckFormContext';
11+
import { CheckUsage } from './CheckUsage';
12+
import { fallbackCheckMap } from './constants';
13+
14+
function RenderWrapper() {
15+
const mockedCheck = fallbackCheckMap[CheckType.HTTP];
16+
const defaultValues = toFormValues(mockedCheck, CheckType.HTTP);
17+
18+
const form = useForm<CheckFormValues>({ defaultValues });
19+
return (
20+
<FormProvider {...form}>
21+
<CheckFormContextProvider disabled={false}>
22+
<CheckUsage checkType={CheckType.HTTP} />
23+
</CheckFormContextProvider>
24+
</FormProvider>
25+
);
26+
}
27+
28+
async function renderComponent() {
29+
const result = render(<RenderWrapper />);
30+
await waitFor(() => screen.findByTestId(DataTestIds.CHECK_USAGE), { timeout: 3000 });
31+
32+
return result;
33+
}
34+
35+
describe('CheckUsage', () => {
36+
it('should render', async () => {
37+
const { container } = await renderComponent();
38+
expect(container).toBeInTheDocument();
39+
});
40+
41+
it('should render the correct label', async () => {
42+
await renderComponent();
43+
44+
expect(await screen.findByText('Estimated usage for this check', { selector: 'label > div' })).toBeInTheDocument();
45+
});
46+
});

src/components/CheckUsage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useFormContext } from 'react-hook-form';
33
import { GrafanaTheme2 } from '@grafana/data';
44
import { Icon, Label, useStyles2 } from '@grafana/ui';
55
import { css } from '@emotion/css';
6+
import { DataTestIds } from 'test/dataTestIds';
67

78
import { CheckFormValues, CheckType } from 'types';
89
import { checkFormValuesToUsageCalcValues } from 'utils';
@@ -46,7 +47,7 @@ export const CheckUsage = ({ checkType }: { checkType: CheckType }) => {
4647
}
4748

4849
return (
49-
<div className={styles.container}>
50+
<div data-testid={DataTestIds.CHECK_USAGE} className={styles.container}>
5051
<Label
5152
description={
5253
!hideTelemetry && (
@@ -61,7 +62,7 @@ export const CheckUsage = ({ checkType }: { checkType: CheckType }) => {
6162
)
6263
}
6364
>
64-
Approximate expected usage for this check
65+
Estimated usage for this check
6566
</Label>
6667
<div className={styles.calcList}>
6768
<div className={styles.section}>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { screen, waitFor } from '@testing-library/react';
3+
import { DataTestIds } from 'test/dataTestIds';
4+
import { render } from 'test/render';
5+
6+
import { ChecksEmptyState } from './ChecksEmptyState';
7+
8+
async function renderComponent() {
9+
const result = render(<ChecksEmptyState />);
10+
await waitFor(() => screen.getByTestId(DataTestIds.CHECKS_EMPTY_STATE), { timeout: 3000 });
11+
12+
return result;
13+
}
14+
15+
describe('ChecksEmptyState', () => {
16+
it('should render', async () => {
17+
const { container } = await renderComponent();
18+
expect(container).toBeInTheDocument();
19+
});
20+
21+
it('should render the correct message', async () => {
22+
await renderComponent();
23+
24+
expect(await screen.findByText("You haven't created any checks yet")).toBeInTheDocument();
25+
});
26+
27+
it('should render the correct button', async () => {
28+
await renderComponent();
29+
30+
expect(await screen.findByText('Create check')).toBeInTheDocument();
31+
});
32+
33+
it('should render the correct link', async () => {
34+
await renderComponent();
35+
36+
expect(await screen.findByText('Synthetic Monitoring docs')).toBeInTheDocument();
37+
});
38+
39+
it('should render the correct link href', async () => {
40+
await renderComponent();
41+
42+
expect(await screen.findByText('Synthetic Monitoring docs')).toHaveAttribute(
43+
'href',
44+
'https://grafana.com/docs/grafana-cloud/synthetic-monitoring/'
45+
);
46+
});
47+
48+
it('should render the correct link target', async () => {
49+
await renderComponent();
50+
51+
expect(await screen.findByText('Synthetic Monitoring docs')).toHaveAttribute('target', '_blank');
52+
});
53+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import { Button, EmptyState, TextLink } from '@grafana/ui';
3+
import { DataTestIds } from 'test/dataTestIds';
4+
5+
import { ROUTES } from 'routing/types';
6+
import { useNavigation } from 'hooks/useNavigation';
7+
8+
interface ChecksEmptyStatePageProps {
9+
className?: string;
10+
}
11+
12+
export function ChecksEmptyState({ className }: ChecksEmptyStatePageProps) {
13+
const navigate = useNavigation();
14+
const handleCallToAction = () => navigate(ROUTES.ChooseCheckGroup);
15+
16+
return (
17+
<div className={className} data-testid={DataTestIds.CHECKS_EMPTY_STATE}>
18+
<EmptyState
19+
variant="call-to-action"
20+
message="You haven't created any checks yet"
21+
button={
22+
<Button onClick={handleCallToAction} icon="plus">
23+
Create check
24+
</Button>
25+
}
26+
>
27+
<p>
28+
Create a check to start monitoring your services with Grafana Cloud, or check out the{' '}
29+
<TextLink external href="https://grafana.com/docs/grafana-cloud/synthetic-monitoring/">
30+
Synthetic Monitoring docs
31+
</TextLink>
32+
.
33+
</p>
34+
</EmptyState>
35+
</div>
36+
);
37+
}

src/page/CheckList/CheckList.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Check, FeatureName } from 'types';
1818
import { ROUTES } from 'routing/types';
1919
import { generateRoutePath } from 'routing/utils';
2020

21+
import { DataTestIds } from '../../test/dataTestIds';
2122
import { CheckList } from './CheckList';
2223

2324
jest.mock('hooks/useNavigation', () => {
@@ -64,9 +65,14 @@ test('renders empty state', async () => {
6465

6566
render(<CheckList />);
6667

67-
const emptyWarning = await screen.findByText('This account does not currently have any checks configured', {
68-
exact: false,
69-
});
68+
const emptyWarning = await waitFor(
69+
() =>
70+
screen.findByTestId(DataTestIds.CHECKS_EMPTY_STATE, {
71+
exact: false,
72+
}),
73+
{ timeout: 10000 }
74+
);
75+
7076
expect(emptyWarning).toBeInTheDocument();
7177
});
7278

src/page/CheckList/CheckList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import { useSuspenseProbes } from 'data/useProbes';
1414
import { useChecksReachabilitySuccessRate } from 'data/useSuccessRates';
1515
import { findCheckinMetrics } from 'data/utils';
1616
import { useQueryParametersState } from 'hooks/useQueryParametersState';
17+
import { ChecksEmptyState } from 'components/ChecksEmptyState';
1718
import { QueryErrorBoundary } from 'components/QueryErrorBoundary';
1819
import { CHECK_LIST_STATUS_OPTIONS } from 'page/CheckList/CheckList.constants';
1920
import { useCheckFilters } from 'page/CheckList/CheckList.hooks';
2021
import { matchesAllFilters } from 'page/CheckList/CheckList.utils';
2122
import { CheckListHeader } from 'page/CheckList/components/CheckListHeader';
2223
import { CheckListItem } from 'page/CheckList/components/CheckListItem';
2324
import { CheckListScene } from 'page/CheckList/components/CheckListScene';
24-
import { EmptyCheckList } from 'page/CheckList/components/EmptyCheckList';
2525

2626
const CHECKS_PER_PAGE_CARD = 15;
2727
const CHECKS_PER_PAGE_LIST = 50;
@@ -175,7 +175,7 @@ const CheckListContent = ({ onChangeViewType, viewType }: CheckListContentProps)
175175
};
176176

177177
if (checks.length === 0) {
178-
return <EmptyCheckList />;
178+
return <ChecksEmptyState />;
179179
}
180180

181181
const showHeaders = viewType !== CheckListViewType.Viz;

src/page/CheckList/components/EmptyCheckList.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

src/scenes/Common/emptyScene.tsx

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,17 @@
1-
import React from 'react';
2-
import { GrafanaTheme2 } from '@grafana/data';
31
import { EmbeddedScene, SceneReactObject } from '@grafana/scenes';
4-
import { Button, Card, TextLink, useStyles2 } from '@grafana/ui';
52
import { css } from '@emotion/css';
63

7-
import { CheckType } from 'types';
8-
import { ROUTES } from 'routing/types';
9-
import { useNavigation } from 'hooks/useNavigation';
4+
import { ChecksEmptyState } from 'components/ChecksEmptyState';
105

11-
function getStyles(theme: GrafanaTheme2) {
12-
return {
13-
container: css`
14-
display: flex;
15-
align-items: center;
16-
justify-content: center;
17-
width: 100%;
18-
min-height: 100%;
19-
`,
20-
cardHeader: css`
21-
text-align: center;
22-
justify-content: center;
23-
`,
24-
emptyCard: css`
25-
min-height: 200px;
26-
max-width: 800px;
27-
padding: ${theme.spacing(4)};
28-
`,
29-
cardButtons: css`
30-
display: flex;
31-
justify-content: center;
32-
`,
33-
};
34-
}
35-
36-
function EmptyScene({ checkType }: { checkType?: CheckType }) {
37-
const styles = useStyles2(getStyles);
38-
const navigate = useNavigation();
39-
40-
return (
41-
<div className={styles.container}>
42-
<Card className={styles.emptyCard}>
43-
<Card.Heading className={styles.cardHeader}>
44-
<p>
45-
You don&apos;t have any {checkType ? checkType.toUpperCase() : ''} checks running. Click the Create a check
46-
button to start monitoring your services with Grafana Cloud, or{' '}
47-
<TextLink href="https://grafana.com/docs/grafana-cloud/synthetic-monitoring/" external={true}>
48-
check out the Synthetic Monitoring docs.
49-
</TextLink>
50-
</p>
51-
</Card.Heading>
52-
<Card.Actions className={styles.cardButtons}>
53-
<Button
54-
onClick={() => {
55-
navigate(checkType ? ROUTES.NewCheck + '/' + checkType : ROUTES.ChooseCheckGroup);
56-
}}
57-
>
58-
Create a check
59-
</Button>
60-
</Card.Actions>
61-
</Card>
62-
</div>
63-
);
64-
}
6+
const wrapperStyles = css`
7+
width: 100%;
8+
`;
659

66-
export function getEmptyScene(checkType?: CheckType) {
10+
export function getEmptyScene() {
6711
return new EmbeddedScene({
6812
body: new SceneReactObject({
69-
component: EmptyScene,
70-
props: { checkType },
13+
component: ChecksEmptyState,
14+
props: { className: wrapperStyles },
7115
}),
7216
});
7317
}

src/test/dataTestIds.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ export enum DataTestIds {
2424
PREFORMATTED = 'preformatted',
2525
CHECK_FORM_SUBMIT_BUTTON = 'check-form-submit-button',
2626
CONFIRM_UNSAVED_MODAL_HEADING = 'confirm-unsaved-modal-heading',
27+
CHECK_USAGE = 'check-usage',
28+
CHECKS_EMPTY_STATE = 'checks-empty-state',
2729
}

0 commit comments

Comments
 (0)