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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { WorkflowsEmptyState } from './workflows_empty_state';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { WorkflowsEmptyState } from './workflows_empty_state';
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { I18nProvider } from '@kbn/i18n-react';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { WorkflowsEmptyState } from './workflows_empty_state';

// Mock useKibana hook
jest.mock('@kbn/kibana-react-plugin/public', () => ({
useKibana: () => ({
services: {
http: {
basePath: {
prepend: (path: string) => `/mock-base-path${path}`,
},
},
},
}),
}));

const renderWithIntl = (component: React.ReactElement) => {
return render(<I18nProvider>{component}</I18nProvider>);
};

describe('WorkflowsEmptyState', () => {
it('renders the empty state with title and description', () => {
renderWithIntl(<WorkflowsEmptyState />);

expect(screen.getByText('Get Started with Workflows')).toBeInTheDocument();
expect(screen.getByText(/Workflows let you automate and orchestrate/)).toBeInTheDocument();
expect(screen.getByText(/Start by creating a workflow/)).toBeInTheDocument();
});

it('renders the create button when user can create workflows', () => {
const onCreateWorkflow = jest.fn();
renderWithIntl(
<WorkflowsEmptyState canCreateWorkflow={true} onCreateWorkflow={onCreateWorkflow} />
);

const createButton = screen.getByText('Create a new workflow');
expect(createButton).toBeInTheDocument();

fireEvent.click(createButton);
expect(onCreateWorkflow).toHaveBeenCalledTimes(1);
});

it('does not render the create button when user cannot create workflows', () => {
renderWithIntl(<WorkflowsEmptyState canCreateWorkflow={false} />);

expect(screen.queryByText('Create a new workflow')).not.toBeInTheDocument();
});

it('does not render the create button when onCreateWorkflow is not provided', () => {
renderWithIntl(<WorkflowsEmptyState canCreateWorkflow={true} />);

expect(screen.queryByText('Create a new workflow')).not.toBeInTheDocument();
});

it('renders the footer with documentation link', () => {
renderWithIntl(<WorkflowsEmptyState />);

expect(screen.getByText('Need help?')).toBeInTheDocument();
expect(screen.getByText('Read documentation')).toBeInTheDocument();
});

it('renders the illustration image', () => {
renderWithIntl(<WorkflowsEmptyState />);

const images = screen.getAllByRole('presentation');
const mainImage = images.find((img) => img.tagName === 'IMG');
expect(mainImage).toBeInTheDocument();
expect(mainImage).toHaveAttribute(
'src',
'/mock-base-path/plugins/workflowsManagement/assets/empty_state.svg'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';

interface WorkflowsEmptyStateProps {
onCreateWorkflow?: () => void;
canCreateWorkflow?: boolean;
}

export function WorkflowsEmptyState({
onCreateWorkflow,
canCreateWorkflow = false,
}: WorkflowsEmptyStateProps) {
const { http } = useKibana().services;
return (
<EuiEmptyPrompt
icon={
<EuiImage
size="fullWidth"
src={http!.basePath.prepend('/plugins/workflowsManagement/assets/empty_state.svg')}
alt=""
/>
}
title={
<h2>
<FormattedMessage
id="workflows.emptyState.title"
defaultMessage="Get Started with Workflows"
/>
</h2>
}
layout="horizontal"
color="plain"
body={
<>
<p>
<FormattedMessage
id="workflows.emptyState.body.firstParagraph"
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."
/>
</p>
<p>
<FormattedMessage
id="workflows.emptyState.body.secondParagraph"
defaultMessage="Start by creating a workflow to simplify repetitive tasks and improve efficiency."
/>
</p>
</>
}
actions={
canCreateWorkflow && onCreateWorkflow ? (
<EuiButton color="primary" fill onClick={onCreateWorkflow} iconType="plusInCircle">
<FormattedMessage
id="workflows.emptyState.createButton"
defaultMessage="Create a new workflow"
/>
</EuiButton>
) : null
}
footer={
<>
<EuiTitle size="xxs">
<span>
<FormattedMessage
id="workflows.emptyState.footer.title"
defaultMessage="Need help?"
/>
</span>
</EuiTitle>{' '}
<EuiLink href="#" target="_blank">
<FormattedMessage
id="workflows.emptyState.footer.link"
defaultMessage="Read documentation"
/>
</EuiLink>
</>
}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,29 @@ import {
EuiText,
useEuiTheme,
} from '@elastic/eui';
import type { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table';
import { i18n } from '@kbn/i18n';
import { FormattedRelative } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { WorkflowListItemDto } from '@kbn/workflows';
import React, { useCallback, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { FormattedRelative } from '@kbn/i18n-react';
import type { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table';
import { i18n } from '@kbn/i18n';
import { WorkflowsEmptyState } from '../../../components';
import { useWorkflowActions } from '../../../entities/workflows/model/use_workflow_actions';
import { useWorkflows } from '../../../entities/workflows/model/use_workflows';
import { getStatusLabel } from '../../../shared/translations';
import { getExecutionStatusIcon } from '../../../shared/ui';
import { shouldShowWorkflowsEmptyState } from '../../../shared/utils/workflow_utils';
import type { WorkflowsSearchParams } from '../../../types';
import { WORKFLOWS_TABLE_PAGE_SIZE_OPTIONS } from '../constants';
import { getExecutionStatusIcon } from '../../../shared/ui';
import { getStatusLabel } from '../../../shared/translations';

interface WorkflowListProps {
search: WorkflowsSearchParams;
setSearch: (search: WorkflowsSearchParams) => void;
onCreateWorkflow?: () => void;
}

export function WorkflowList({ search, setSearch }: WorkflowListProps) {
export function WorkflowList({ search, setSearch, onCreateWorkflow }: WorkflowListProps) {
const { euiTheme } = useEuiTheme();
const { application, notifications } = useKibana().services;
const { data: workflows, isLoading: isLoadingWorkflows, error } = useWorkflows(search);
Expand Down Expand Up @@ -299,6 +302,20 @@ export function WorkflowList({ search, setSearch }: WorkflowListProps) {
return <EuiText>Error loading workflows</EuiText>;
}

// Show empty state if no workflows exist and no filters are applied
if (shouldShowWorkflowsEmptyState(workflows, search)) {
return (
<EuiFlexGroup justifyContent="center" alignItems="center" style={{ minHeight: '60vh' }}>
<EuiFlexItem grow={false}>
<WorkflowsEmptyState
onCreateWorkflow={onCreateWorkflow}
canCreateWorkflow={!!canCreateWorkflow}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

const showStart = (search.page - 1) * search.limit + 1;
let showEnd = search.page * search.limit;
if (showEnd > (workflows!._pagination.total || 0)) {
Expand Down
Loading