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
Expand Up @@ -101,6 +101,9 @@ describe('useWorkflowBulkActions', () => {

beforeEach(() => {
jest.clearAllMocks();
mockApplication.capabilities.workflowsManagement.deleteWorkflow = true;
mockApplication.capabilities.workflowsManagement.updateWorkflow = true;
mockApplication.capabilities.workflowsManagement.readWorkflow = true;
});

it('returns panels and modals', () => {
Expand Down Expand Up @@ -158,6 +161,25 @@ describe('useWorkflowBulkActions', () => {
expect(deleteItem).toBeDefined();
});

it('does not include delete action when a managed workflow is selected', () => {
const managedWorkflow = createMockWorkflow({ managed: true });
const { result } = renderHook(
() =>
useWorkflowBulkActions({
...defaultProps,
selectedWorkflows: [managedWorkflow],
allWorkflows: [managedWorkflow],
}),
{ wrapper }
);

const mainPanel = result.current.panels[0];
const deleteItem = mainPanel.items?.find(
(item) => 'key' in item && item.key === 'workflows-bulk-action-delete'
);
expect(deleteItem).toBeUndefined();
});

it('includes export action when there are exportable workflows', () => {
const { result } = renderHook(() => useWorkflowBulkActions(defaultProps), { wrapper });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,27 @@ export const useWorkflowBulkActions = ({
const canReadWorkflow = application?.capabilities.workflowsManagement.readWorkflow;

const isDisabled = selectedWorkflows.length === 0;
const hasManagedWorkflows = selectedWorkflows.some((workflow) => workflow.managed === true);
const canDeleteSelectedWorkflows = canDeleteWorkflow && !hasManagedWorkflows;

const handleDeleteWorkflows = useCallback(() => {
if (hasManagedWorkflows) {
return;
}
onAction();
setShowDeleteModal(true);
}, [onAction]);
}, [hasManagedWorkflows, onAction]);

const confirmDelete = useCallback(() => {
const ids = selectedWorkflows.map((workflow) => workflow.id);
const ids = selectedWorkflows
.filter((workflow) => workflow.managed !== true)
.map((workflow) => workflow.id);
const count = ids.length;
if (count === 0) {
setShowDeleteModal(false);
deselectWorkflows();
return;
}

setShowDeleteModal(false);
deselectWorkflows();
Expand Down Expand Up @@ -231,15 +243,15 @@ export const useWorkflowBulkActions = ({
});
}

if (mainPanelItems.length > 0 && canDeleteWorkflow) {
if (mainPanelItems.length > 0 && canDeleteSelectedWorkflows) {
mainPanelItems.push({
isSeparator: true as const,
key: 'bulk-actions-separator',
'data-test-subj': 'bulk-actions-separator',
});
}

if (canDeleteWorkflow) {
if (canDeleteSelectedWorkflows) {
mainPanelItems.push({
name: (
<EuiTextColor color="danger">
Expand Down Expand Up @@ -268,7 +280,7 @@ export const useWorkflowBulkActions = ({
}, [
selectedWorkflows,
canUpdateWorkflow,
canDeleteWorkflow,
canDeleteSelectedWorkflows,
canReadWorkflow,
isDisabled,
handleEnableWorkflows,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,36 @@ describe('Authorization matrix', () => {
}
);

it('disables the delete row action for managed workflows', async () => {
setKibanaCapabilities({
createWorkflow: true,
updateWorkflow: true,
deleteWorkflow: true,
executeWorkflow: true,
});
renderList({ item: createWorkflowListItem({ id: 'managed-wf', managed: true }) });

await openFirstRowCollapsedActions();

expect(screen.getByTestId('deleteWorkflowAction')).toBeDisabled();
});

it('explains why managed workflows cannot be deleted', async () => {
setKibanaCapabilities({
createWorkflow: true,
updateWorkflow: true,
deleteWorkflow: true,
executeWorkflow: true,
});
renderList({ item: createWorkflowListItem({ id: 'managed-wf', managed: true }) });

await openFirstRowCollapsedActions();
const deleteAction = screen.getByTestId('deleteWorkflowAction');
await userEvent.hover(deleteAction.parentElement ?? deleteAction);

expect(await screen.findByText('Managed workflows cannot be deleted')).toBeInTheDocument();
});

it('disables the enabled switch when the workflow is invalid even if update is granted', () => {
setKibanaCapabilities({
createWorkflow: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import React from 'react';
import type { WorkflowListDto, WorkflowListItemDto, WorkflowsSearchParams } from '@kbn/workflows';
import { createMockWorkflowsCapabilities as mockCreateMockWorkflowsCapabilities } from '@kbn/workflows-ui/mocks';
Expand Down Expand Up @@ -106,6 +106,9 @@ jest.mock('../../run_workflow/ui/workflow_execute_modal', () => ({

jest.mock('../../../shared/ui', () => ({
getRunTooltipContent: () => 'Run',
ManagedWorkflowBadge: ({ dataTestSubj = 'managedWorkflowBadge' }: { dataTestSubj?: string }) => (
<span data-test-subj={dataTestSubj}>{'Managed'}</span>
),
StatusBadge: ({ status }: { status: string }) => <span>{status}</span>,
WorkflowStatus: ({ valid }: { valid: boolean }) => <span>{valid ? 'Valid' : 'Invalid'}</span>,
}));
Expand Down Expand Up @@ -287,6 +290,38 @@ describe('WorkflowList', () => {
expect(screen.getByText('A workflow for testing')).toBeInTheDocument();
});

it('renders the managed badge as the first tag for managed workflows', () => {
const workflow = createMockWorkflow({
managed: true,
definition: {
version: '1',
name: 'My Test Workflow',
enabled: true,
triggers: [],
steps: [],
tags: ['custom', 'second'],
},
});

mockUseWorkflows.mockReturnValue({
data: createMockWorkflowListDto([workflow]),
isLoading: false,
error: null,
refetch: mockRefetch,
});

renderComponent();

const tagsCell = screen.getByTestId('workflowTags');
const managedBadge = within(tagsCell).getByTestId('managedWorkflowBadge');
const firstWorkflowTag = within(tagsCell).getByText('custom');

expect(managedBadge).toHaveTextContent('Managed');
expect(managedBadge.compareDocumentPosition(firstWorkflowTag)).toEqual(
Node.DOCUMENT_POSITION_FOLLOWING
);
});

it('shows "No description" for workflows without description', () => {
mockUseWorkflows.mockReturnValue({
data: createMockWorkflowListDto([createMockWorkflow({ description: '' })]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { WorkflowListItemDto } from '@kbn/workflows';
import { WorkflowTriggersAndSteps } from './workflow_triggers_and_steps';
import { getRunTooltipContent, StatusBadge, WorkflowStatus } from '../../../shared/ui';
import {
getRunTooltipContent,
ManagedWorkflowBadge,
StatusBadge,
WorkflowStatus,
} from '../../../shared/ui';
import { NextExecutionTime } from '../../../shared/ui/next_execution_time';
import { WORKFLOWS_TABLE_PAGE_SIZE_OPTIONS } from '../constants';

Expand Down Expand Up @@ -171,7 +176,7 @@ export const WorkflowListTable = ({
}),
width: '18%',
render: (value: unknown, item: WorkflowListItemDto) => (
<WorkflowTagsCell tags={item.definition?.tags} />
<WorkflowTagsCell tags={item.definition?.tags} isManaged={item.managed === true} />
),
},
{
Expand Down Expand Up @@ -324,15 +329,20 @@ export const WorkflowListTable = ({
onClick: (item: WorkflowListItemDto) => onExportWorkflow(item),
},
{
enabled: () => canDeleteWorkflow,
enabled: (item) => canDeleteWorkflow && item.managed !== true,
type: 'icon',
color: 'danger',
name: i18n.translate('workflows.workflowList.delete', { defaultMessage: 'Delete' }),
'data-test-subj': 'deleteWorkflowAction',
icon: 'trash',
description: i18n.translate('workflows.workflowList.delete', {
defaultMessage: 'Delete workflow',
}),
description: (item: WorkflowListItemDto) =>
item.managed === true
? i18n.translate('workflows.workflowList.deleteManagedDisabled', {
defaultMessage: 'Managed workflows cannot be deleted',
})
: i18n.translate('workflows.workflowList.deleteDescription', {
defaultMessage: 'Delete workflow',
}),
onClick: (item: WorkflowListItemDto) => onDeleteWorkflow(item),
},
],
Expand Down Expand Up @@ -416,15 +426,25 @@ const overflowPopoverStyle = css`
overflow: auto;
`;

const WorkflowTagsCell = ({ tags }: { tags: readonly string[] | undefined }) => {
const WorkflowTagsCell = ({
tags,
isManaged,
}: {
tags: readonly string[] | undefined;
isManaged: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
const close = useCallback(() => setIsOpen(false), []);

if (!tags || tags.length === 0) return null;
if (!isManaged && (!tags || tags.length === 0)) return null;

const visible = tags.slice(0, MAX_VISIBLE_TAGS);
const hidden = tags.slice(MAX_VISIBLE_TAGS);
const workflowTags = tags ?? [];
const visibleWorkflowTags = workflowTags.slice(
0,
isManaged ? MAX_VISIBLE_TAGS - 1 : MAX_VISIBLE_TAGS
);
const hidden = workflowTags.slice(visibleWorkflowTags.length);

return (
<EuiFlexGroup
Expand All @@ -434,7 +454,12 @@ const WorkflowTagsCell = ({ tags }: { tags: readonly string[] | undefined }) =>
css={tagsRowStyle}
data-test-subj="workflowTags"
>
{visible.map((tag) => (
{isManaged ? (
<EuiFlexItem grow={false} css={visibleTagStyle}>
<ManagedWorkflowBadge />
</EuiFlexItem>
) : null}
{visibleWorkflowTags.map((tag) => (
<EuiFlexItem key={tag} grow={false} css={visibleTagStyle}>
<EuiBadge color="hollow" title={tag}>
{tag}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,20 @@ describe('WorkflowDetailHeader', () => {
hasYamlSchemaValidationErrors = false,
serverValid = true,
isSaving = false,
isManaged = false,
}: {
isValid?: boolean;
hasChanges?: boolean;
hasYamlSchemaValidationErrors?: boolean;
serverValid?: boolean;
isSaving?: boolean;
isManaged?: boolean;
} = {}
) => {
const store = createMockStore();

// Set up the workflow in the store (with server-side valid flag)
store.dispatch(setWorkflow({ ...mockWorkflow, valid: serverValid }));
store.dispatch(setWorkflow({ ...mockWorkflow, managed: isManaged, valid: serverValid }));
store.dispatch(setYamlString(hasChanges ? 'modified yaml' : mockWorkflow.yaml));

if (!isValid) {
Expand Down Expand Up @@ -216,6 +218,31 @@ describe('WorkflowDetailHeader', () => {
expect(button).toBeEnabled();
});

it('shows the managed badge for managed workflows', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
isManaged: true,
});

expect(result.getByTestId('workflowDetailManagedBadge')).toHaveTextContent('Managed');
});

it('keeps the enabled toggle editable for managed workflows', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
isManaged: true,
});

expect(result.getByRole('switch')).not.toBeDisabled();
});

it('disables saving managed workflow YAML', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
hasChanges: true,
isManaged: true,
});

expect(result.getByTestId('saveWorkflowHeaderButton')).toBeDisabled();
});

it('shows the unsaved changes confirmation when running with unsaved changes', () => {
const result = renderWithProviders(<WorkflowDetailHeader {...defaultProps} />, {
hasChanges: true,
Expand Down
Loading
Loading