Skip to content

Commit 501654a

Browse files
authored
Workflows Empty State (#233575)
Implemented empty state for workflows list page according to Figma design using EUI EmptyPrompt component. <img width="3024" height="1570" alt="CleanShot 2025-08-31 at 13 32 25@2x" src="https://github.com/user-attachments/assets/882e8e0b-08ed-4b19-b9b9-2b3289fb9cae" />
1 parent 3043305 commit 501654a

File tree

8 files changed

+326
-60
lines changed

8 files changed

+326
-60
lines changed

src/platform/plugins/shared/workflows_management/public/assets/empty_state.svg

Lines changed: 14 additions & 0 deletions
Loading
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export { WorkflowsEmptyState } from './workflows_empty_state';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export { WorkflowsEmptyState } from './workflows_empty_state';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { I18nProvider } from '@kbn/i18n-react';
11+
import { fireEvent, render, screen } from '@testing-library/react';
12+
import React from 'react';
13+
import { WorkflowsEmptyState } from './workflows_empty_state';
14+
15+
// Mock useKibana hook
16+
jest.mock('@kbn/kibana-react-plugin/public', () => ({
17+
useKibana: () => ({
18+
services: {
19+
http: {
20+
basePath: {
21+
prepend: (path: string) => `/mock-base-path${path}`,
22+
},
23+
},
24+
},
25+
}),
26+
}));
27+
28+
const renderWithIntl = (component: React.ReactElement) => {
29+
return render(<I18nProvider>{component}</I18nProvider>);
30+
};
31+
32+
describe('WorkflowsEmptyState', () => {
33+
it('renders the empty state with title and description', () => {
34+
renderWithIntl(<WorkflowsEmptyState />);
35+
36+
expect(screen.getByText('Get Started with Workflows')).toBeInTheDocument();
37+
expect(screen.getByText(/Workflows let you automate and orchestrate/)).toBeInTheDocument();
38+
expect(screen.getByText(/Start by creating a workflow/)).toBeInTheDocument();
39+
});
40+
41+
it('renders the create button when user can create workflows', () => {
42+
const onCreateWorkflow = jest.fn();
43+
renderWithIntl(
44+
<WorkflowsEmptyState canCreateWorkflow={true} onCreateWorkflow={onCreateWorkflow} />
45+
);
46+
47+
const createButton = screen.getByText('Create a new workflow');
48+
expect(createButton).toBeInTheDocument();
49+
50+
fireEvent.click(createButton);
51+
expect(onCreateWorkflow).toHaveBeenCalledTimes(1);
52+
});
53+
54+
it('does not render the create button when user cannot create workflows', () => {
55+
renderWithIntl(<WorkflowsEmptyState canCreateWorkflow={false} />);
56+
57+
expect(screen.queryByText('Create a new workflow')).not.toBeInTheDocument();
58+
});
59+
60+
it('does not render the create button when onCreateWorkflow is not provided', () => {
61+
renderWithIntl(<WorkflowsEmptyState canCreateWorkflow={true} />);
62+
63+
expect(screen.queryByText('Create a new workflow')).not.toBeInTheDocument();
64+
});
65+
66+
it('renders the footer with documentation link', () => {
67+
renderWithIntl(<WorkflowsEmptyState />);
68+
69+
expect(screen.getByText('Need help?')).toBeInTheDocument();
70+
expect(screen.getByText('Read documentation')).toBeInTheDocument();
71+
});
72+
73+
it('renders the illustration image', () => {
74+
renderWithIntl(<WorkflowsEmptyState />);
75+
76+
const images = screen.getAllByRole('presentation');
77+
const mainImage = images.find((img) => img.tagName === 'IMG');
78+
expect(mainImage).toBeInTheDocument();
79+
expect(mainImage).toHaveAttribute(
80+
'src',
81+
'/mock-base-path/plugins/workflowsManagement/assets/empty_state.svg'
82+
);
83+
});
84+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink, EuiTitle } from '@elastic/eui';
11+
import { FormattedMessage } from '@kbn/i18n-react';
12+
import { useKibana } from '@kbn/kibana-react-plugin/public';
13+
import React from 'react';
14+
15+
interface WorkflowsEmptyStateProps {
16+
onCreateWorkflow?: () => void;
17+
canCreateWorkflow?: boolean;
18+
}
19+
20+
export function WorkflowsEmptyState({
21+
onCreateWorkflow,
22+
canCreateWorkflow = false,
23+
}: WorkflowsEmptyStateProps) {
24+
const { http } = useKibana().services;
25+
return (
26+
<EuiEmptyPrompt
27+
icon={
28+
<EuiImage
29+
size="fullWidth"
30+
src={http!.basePath.prepend('/plugins/workflowsManagement/assets/empty_state.svg')}
31+
alt=""
32+
/>
33+
}
34+
title={
35+
<h2>
36+
<FormattedMessage
37+
id="workflows.emptyState.title"
38+
defaultMessage="Get Started with Workflows"
39+
/>
40+
</h2>
41+
}
42+
layout="horizontal"
43+
color="plain"
44+
body={
45+
<>
46+
<p>
47+
<FormattedMessage
48+
id="workflows.emptyState.body.firstParagraph"
49+
defaultMessage="Workflows let you automate and orchestrate security actions across your environment. Build step-by-step processes to enrich alerts, trigger responses, or streamline investigations—all in one place."
50+
/>
51+
</p>
52+
<p>
53+
<FormattedMessage
54+
id="workflows.emptyState.body.secondParagraph"
55+
defaultMessage="Start by creating a workflow to simplify repetitive tasks and improve efficiency."
56+
/>
57+
</p>
58+
</>
59+
}
60+
actions={
61+
canCreateWorkflow && onCreateWorkflow ? (
62+
<EuiButton color="primary" fill onClick={onCreateWorkflow} iconType="plusInCircle">
63+
<FormattedMessage
64+
id="workflows.emptyState.createButton"
65+
defaultMessage="Create a new workflow"
66+
/>
67+
</EuiButton>
68+
) : null
69+
}
70+
footer={
71+
<>
72+
<EuiTitle size="xxs">
73+
<span>
74+
<FormattedMessage
75+
id="workflows.emptyState.footer.title"
76+
defaultMessage="Need help?"
77+
/>
78+
</span>
79+
</EuiTitle>{' '}
80+
<EuiLink href="#" target="_blank">
81+
<FormattedMessage
82+
id="workflows.emptyState.footer.link"
83+
defaultMessage="Read documentation"
84+
/>
85+
</EuiLink>
86+
</>
87+
}
88+
/>
89+
);
90+
}

src/platform/plugins/shared/workflows_management/public/features/workflow_list/ui/index.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,29 @@ import {
2020
EuiText,
2121
useEuiTheme,
2222
} from '@elastic/eui';
23+
import type { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table';
24+
import { i18n } from '@kbn/i18n';
25+
import { FormattedRelative } from '@kbn/i18n-react';
2326
import { useKibana } from '@kbn/kibana-react-plugin/public';
2427
import type { WorkflowListItemDto } from '@kbn/workflows';
2528
import React, { useCallback, useMemo, useState } from 'react';
2629
import { Link } from 'react-router-dom';
27-
import { FormattedRelative } from '@kbn/i18n-react';
28-
import type { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table';
29-
import { i18n } from '@kbn/i18n';
30+
import { WorkflowsEmptyState } from '../../../components';
3031
import { useWorkflowActions } from '../../../entities/workflows/model/use_workflow_actions';
3132
import { useWorkflows } from '../../../entities/workflows/model/use_workflows';
33+
import { getStatusLabel } from '../../../shared/translations';
34+
import { getExecutionStatusIcon } from '../../../shared/ui';
35+
import { shouldShowWorkflowsEmptyState } from '../../../shared/utils/workflow_utils';
3236
import type { WorkflowsSearchParams } from '../../../types';
3337
import { WORKFLOWS_TABLE_PAGE_SIZE_OPTIONS } from '../constants';
34-
import { getExecutionStatusIcon } from '../../../shared/ui';
35-
import { getStatusLabel } from '../../../shared/translations';
3638

3739
interface WorkflowListProps {
3840
search: WorkflowsSearchParams;
3941
setSearch: (search: WorkflowsSearchParams) => void;
42+
onCreateWorkflow?: () => void;
4043
}
4144

42-
export function WorkflowList({ search, setSearch }: WorkflowListProps) {
45+
export function WorkflowList({ search, setSearch, onCreateWorkflow }: WorkflowListProps) {
4346
const { euiTheme } = useEuiTheme();
4447
const { application, notifications } = useKibana().services;
4548
const { data: workflows, isLoading: isLoadingWorkflows, error } = useWorkflows(search);
@@ -299,6 +302,20 @@ export function WorkflowList({ search, setSearch }: WorkflowListProps) {
299302
return <EuiText>Error loading workflows</EuiText>;
300303
}
301304

305+
// Show empty state if no workflows exist and no filters are applied
306+
if (shouldShowWorkflowsEmptyState(workflows, search)) {
307+
return (
308+
<EuiFlexGroup justifyContent="center" alignItems="center" style={{ minHeight: '60vh' }}>
309+
<EuiFlexItem grow={false}>
310+
<WorkflowsEmptyState
311+
onCreateWorkflow={onCreateWorkflow}
312+
canCreateWorkflow={!!canCreateWorkflow}
313+
/>
314+
</EuiFlexItem>
315+
</EuiFlexGroup>
316+
);
317+
}
318+
302319
const showStart = (search.page - 1) * search.limit + 1;
303320
let showEnd = search.page * search.limit;
304321
if (showEnd > (workflows!._pagination.total || 0)) {

0 commit comments

Comments
 (0)