Skip to content

Commit 8d177a3

Browse files
committed
Merge remote-tracking branch 'origin/main' into 254889-lens-include-empty-rows-defaults-simple
2 parents 67a6d11 + 3790a2e commit 8d177a3

15 files changed

Lines changed: 430 additions & 68 deletions

File tree

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/lib/use_display_options.test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,24 @@ describe('buildDisplayOptions', () => {
4343
};
4444

4545
it('shows Add step + Commands sections when no search is active', () => {
46-
const result = buildDisplayOptions(base);
46+
const result = buildDisplayOptions({ ...base, options: [makeAction('a', 'A')] });
4747
expect(groupLabels(result)).toEqual(['Add step', 'Commands']);
48-
expect(dataKinds(result)).toEqual(['command', 'command']);
48+
expect(dataKinds(result)).toEqual(['action', 'command', 'command']);
49+
});
50+
51+
it('hides Add step section when no actions match the search', () => {
52+
const result = buildDisplayOptions({ ...base, options: [], searchTerm: 'zzz' });
53+
expect(groupLabels(result)).not.toContain('Add step');
54+
});
55+
56+
it('hides Add step section in Steps: mode when no actions match', () => {
57+
const result = buildDisplayOptions({
58+
...base,
59+
options: [],
60+
searchTerm: `${STEPS_PREFIX}zzz`,
61+
});
62+
expect(groupLabels(result)).not.toContain('Add step');
63+
expect(result).toHaveLength(0);
4964
});
5065

5166
it('returns action items directly when inside a sub-group', () => {

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/lib/use_display_options.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -91,37 +91,41 @@ export function buildDisplayOptions({
9191
}
9292

9393
if (isStepsMode) {
94+
if (options.length > 0) {
95+
result.push({
96+
label: i18n.translate('workflows.actionsMenu.addStepGroupLabel', {
97+
defaultMessage: 'Add step',
98+
}),
99+
isGroupLabel: true,
100+
});
101+
for (const opt of options) {
102+
result.push({ label: opt.label, data: { menuItem: { kind: 'action', action: opt } } });
103+
}
104+
}
105+
return result;
106+
}
107+
108+
const visibleOptions = hasSearch ? options.slice(0, MAX_VISIBLE_STEPS) : options;
109+
if (visibleOptions.length > 0) {
94110
result.push({
95111
label: i18n.translate('workflows.actionsMenu.addStepGroupLabel', {
96112
defaultMessage: 'Add step',
97113
}),
98114
isGroupLabel: true,
99115
});
100-
for (const opt of options) {
116+
for (const opt of visibleOptions) {
101117
result.push({ label: opt.label, data: { menuItem: { kind: 'action', action: opt } } });
102118
}
103-
return result;
104-
}
105-
106-
result.push({
107-
label: i18n.translate('workflows.actionsMenu.addStepGroupLabel', {
108-
defaultMessage: 'Add step',
109-
}),
110-
isGroupLabel: true,
111-
});
112-
const visibleOptions = hasSearch ? options.slice(0, MAX_VISIBLE_STEPS) : options;
113-
for (const opt of visibleOptions) {
114-
result.push({ label: opt.label, data: { menuItem: { kind: 'action', action: opt } } });
115-
}
116119

117-
if (hasSearch && options.length > MAX_VISIBLE_STEPS) {
118-
result.push({
119-
label: i18n.translate('workflows.actionsMenu.viewAllSteps', {
120-
defaultMessage: 'View all steps to add',
121-
}),
122-
className: 'compactOption',
123-
data: { menuItem: { kind: 'nav', target: 'viewAll' } },
124-
});
120+
if (hasSearch && options.length > MAX_VISIBLE_STEPS) {
121+
result.push({
122+
label: i18n.translate('workflows.actionsMenu.viewAllSteps', {
123+
defaultMessage: 'View all steps to add',
124+
}),
125+
className: 'compactOption',
126+
data: { menuItem: { kind: 'nav', target: 'viewAll' } },
127+
});
128+
}
125129
}
126130

127131
const filteredCmds = (commands ?? []).filter(

src/platform/plugins/shared/workflows_management/public/features/actions_menu_popover/ui/actions_menu.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,26 @@ const componentStyles = {
417417
'& .euiSelectableListItem.compactOption': {
418418
paddingBlock: euiTheme.size.s,
419419
},
420+
// EUI 116 routes EuiSelectableListItem through EuiListItemLayout, which
421+
// adds gap on __content and vertical padding on __text and drops the
422+
// between-row border. renderActionOption owns its own spacing, so zero
423+
// the new gap/padding out and re-add the row border to match the design.
424+
'& .euiSelectableListItem__content': {
425+
gap: 0,
426+
},
427+
'& .euiSelectableListItem__text': {
428+
paddingBlock: 0,
429+
},
430+
'& .euiSelectableListItem:not(:last-of-type)': {
431+
borderBottom: euiTheme.border.thin,
432+
},
420433
'& .euiSelectableList': {
421434
maxHeight: '420px',
422435
overflowY: 'auto',
423436
},
424437
'& .euiSelectableList__groupLabel': {
425438
borderBottom: euiTheme.border.thin,
439+
paddingInline: '16px',
426440
},
427441
'& .euiSelectableList__groupLabel ~ .euiSelectableList__groupLabel': {
428442
marginTop: '24px',
@@ -435,7 +449,8 @@ const componentStyles = {
435449
}),
436450
header: ({ euiTheme }: UseEuiTheme) =>
437451
css({
438-
padding: euiTheme.size.m,
452+
paddingBlock: euiTheme.size.m,
453+
paddingInline: '16px',
439454
}),
440455
actionOption: css({
441456
gap: '12px',

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ describe('useWorkflowBulkActions', () => {
101101

102102
beforeEach(() => {
103103
jest.clearAllMocks();
104+
mockApplication.capabilities.workflowsManagement.deleteWorkflow = true;
105+
mockApplication.capabilities.workflowsManagement.updateWorkflow = true;
106+
mockApplication.capabilities.workflowsManagement.readWorkflow = true;
104107
});
105108

106109
it('returns panels and modals', () => {
@@ -158,6 +161,25 @@ describe('useWorkflowBulkActions', () => {
158161
expect(deleteItem).toBeDefined();
159162
});
160163

164+
it('does not include delete action when a managed workflow is selected', () => {
165+
const managedWorkflow = createMockWorkflow({ managed: true });
166+
const { result } = renderHook(
167+
() =>
168+
useWorkflowBulkActions({
169+
...defaultProps,
170+
selectedWorkflows: [managedWorkflow],
171+
allWorkflows: [managedWorkflow],
172+
}),
173+
{ wrapper }
174+
);
175+
176+
const mainPanel = result.current.panels[0];
177+
const deleteItem = mainPanel.items?.find(
178+
(item) => 'key' in item && item.key === 'workflows-bulk-action-delete'
179+
);
180+
expect(deleteItem).toBeUndefined();
181+
});
182+
161183
it('includes export action when there are exportable workflows', () => {
162184
const { result } = renderHook(() => useWorkflowBulkActions(defaultProps), { wrapper });
163185

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,27 @@ export const useWorkflowBulkActions = ({
6969
const canReadWorkflow = application?.capabilities.workflowsManagement.readWorkflow;
7070

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

7375
const handleDeleteWorkflows = useCallback(() => {
76+
if (hasManagedWorkflows) {
77+
return;
78+
}
7479
onAction();
7580
setShowDeleteModal(true);
76-
}, [onAction]);
81+
}, [hasManagedWorkflows, onAction]);
7782

7883
const confirmDelete = useCallback(() => {
79-
const ids = selectedWorkflows.map((workflow) => workflow.id);
84+
const ids = selectedWorkflows
85+
.filter((workflow) => workflow.managed !== true)
86+
.map((workflow) => workflow.id);
8087
const count = ids.length;
88+
if (count === 0) {
89+
setShowDeleteModal(false);
90+
deselectWorkflows();
91+
return;
92+
}
8193

8294
setShowDeleteModal(false);
8395
deselectWorkflows();
@@ -231,15 +243,15 @@ export const useWorkflowBulkActions = ({
231243
});
232244
}
233245

234-
if (mainPanelItems.length > 0 && canDeleteWorkflow) {
246+
if (mainPanelItems.length > 0 && canDeleteSelectedWorkflows) {
235247
mainPanelItems.push({
236248
isSeparator: true as const,
237249
key: 'bulk-actions-separator',
238250
'data-test-subj': 'bulk-actions-separator',
239251
});
240252
}
241253

242-
if (canDeleteWorkflow) {
254+
if (canDeleteSelectedWorkflows) {
243255
mainPanelItems.push({
244256
name: (
245257
<EuiTextColor color="danger">
@@ -268,7 +280,7 @@ export const useWorkflowBulkActions = ({
268280
}, [
269281
selectedWorkflows,
270282
canUpdateWorkflow,
271-
canDeleteWorkflow,
283+
canDeleteSelectedWorkflows,
272284
canReadWorkflow,
273285
isDisabled,
274286
handleEnableWorkflows,

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,36 @@ describe('Authorization matrix', () => {
284284
}
285285
);
286286

287+
it('disables the delete row action for managed workflows', async () => {
288+
setKibanaCapabilities({
289+
createWorkflow: true,
290+
updateWorkflow: true,
291+
deleteWorkflow: true,
292+
executeWorkflow: true,
293+
});
294+
renderList({ item: createWorkflowListItem({ id: 'managed-wf', managed: true }) });
295+
296+
await openFirstRowCollapsedActions();
297+
298+
expect(screen.getByTestId('deleteWorkflowAction')).toBeDisabled();
299+
});
300+
301+
it('explains why managed workflows cannot be deleted', async () => {
302+
setKibanaCapabilities({
303+
createWorkflow: true,
304+
updateWorkflow: true,
305+
deleteWorkflow: true,
306+
executeWorkflow: true,
307+
});
308+
renderList({ item: createWorkflowListItem({ id: 'managed-wf', managed: true }) });
309+
310+
await openFirstRowCollapsedActions();
311+
const deleteAction = screen.getByTestId('deleteWorkflowAction');
312+
await userEvent.hover(deleteAction.parentElement ?? deleteAction);
313+
314+
expect(await screen.findByText('Managed workflows cannot be deleted')).toBeInTheDocument();
315+
});
316+
287317
it('disables the enabled switch when the workflow is invalid even if update is granted', () => {
288318
setKibanaCapabilities({
289319
createWorkflow: false,

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

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

107107
jest.mock('../../../shared/ui', () => ({
108108
getRunTooltipContent: () => 'Run',
109+
ManagedWorkflowBadge: ({ dataTestSubj = 'managedWorkflowBadge' }: { dataTestSubj?: string }) => (
110+
<span data-test-subj={dataTestSubj}>{'Managed'}</span>
111+
),
109112
StatusBadge: ({ status }: { status: string }) => <span>{status}</span>,
110113
WorkflowStatus: ({ valid }: { valid: boolean }) => <span>{valid ? 'Valid' : 'Invalid'}</span>,
111114
}));
@@ -287,6 +290,38 @@ describe('WorkflowList', () => {
287290
expect(screen.getByText('A workflow for testing')).toBeInTheDocument();
288291
});
289292

293+
it('renders the managed badge as the first tag for managed workflows', () => {
294+
const workflow = createMockWorkflow({
295+
managed: true,
296+
definition: {
297+
version: '1',
298+
name: 'My Test Workflow',
299+
enabled: true,
300+
triggers: [],
301+
steps: [],
302+
tags: ['custom', 'second'],
303+
},
304+
});
305+
306+
mockUseWorkflows.mockReturnValue({
307+
data: createMockWorkflowListDto([workflow]),
308+
isLoading: false,
309+
error: null,
310+
refetch: mockRefetch,
311+
});
312+
313+
renderComponent();
314+
315+
const tagsCell = screen.getByTestId('workflowTags');
316+
const managedBadge = within(tagsCell).getByTestId('managedWorkflowBadge');
317+
const firstWorkflowTag = within(tagsCell).getByText('custom');
318+
319+
expect(managedBadge).toHaveTextContent('Managed');
320+
expect(managedBadge.compareDocumentPosition(firstWorkflowTag)).toEqual(
321+
Node.DOCUMENT_POSITION_FOLLOWING
322+
);
323+
});
324+
290325
it('shows "No description" for workflows without description', () => {
291326
mockUseWorkflows.mockReturnValue({
292327
data: createMockWorkflowListDto([createMockWorkflow({ description: '' })]),

0 commit comments

Comments
 (0)