Skip to content

Commit

Permalink
[Files] Allow option to disable delete action in mgt UI (#155179)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored Apr 24, 2023
1 parent 3d78370 commit 29a10fd
Show file tree
Hide file tree
Showing 18 changed files with 294 additions and 21 deletions.
2 changes: 1 addition & 1 deletion packages/content-management/table_list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@

export { TableListView, TableListViewProvider, TableListViewKibanaProvider } from './src';

export type { UserContentCommonSchema } from './src';
export type { UserContentCommonSchema, RowActions } from './src';
export type { TableListViewKibanaDependencies } from './src/services';
33 changes: 29 additions & 4 deletions packages/content-management/table_list/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
SearchFilterConfig,
Direction,
Query,
type EuiTableSelectionType,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { useServices } from '../services';
import type { Action } from '../actions';
Expand All @@ -26,6 +28,7 @@ import type {
Props as TableListViewProps,
UserContentCommonSchema,
} from '../table_list_view';
import type { TableItemsRowActions } from '../types';
import { TableSortSelect } from './table_sort_select';
import { TagFilterPanel } from './tag_filter_panel';
import { useTagFilterPanel } from './use_tag_filter_panel';
Expand All @@ -51,6 +54,7 @@ interface Props<T extends UserContentCommonSchema> extends State<T>, TagManageme
tableColumns: Array<EuiBasicTableColumn<T>>;
hasUpdatedAtMetadata: boolean;
deleteItems: TableListViewProps<T>['deleteItems'];
tableItemsRowActions: TableItemsRowActions;
onSortChange: (column: SortColumnField, direction: Direction) => void;
onTableChange: (criteria: CriteriaWithPagination<T>) => void;
onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void;
Expand All @@ -70,6 +74,7 @@ export function Table<T extends UserContentCommonSchema>({
entityName,
entityNamePlural,
tagsToTableItemMap,
tableItemsRowActions,
deleteItems,
tableCaption,
onTableChange,
Expand Down Expand Up @@ -105,13 +110,32 @@ export function Table<T extends UserContentCommonSchema>({
);
}, [deleteItems, dispatch, entityName, entityNamePlural, selectedIds.length]);

const selection = deleteItems
? {
const selection = useMemo<EuiTableSelectionType<T> | undefined>(() => {
if (deleteItems) {
return {
onSelectionChange: (obj: T[]) => {
dispatch({ type: 'onSelectionChange', data: obj });
},
}
: undefined;
selectable: (obj) => {
const actions = tableItemsRowActions[obj.id];
return actions?.delete?.enabled !== false;
},
selectableMessage: (selectable, obj) => {
if (!selectable) {
const actions = tableItemsRowActions[obj.id];
return (
actions?.delete?.reason ??
i18n.translate('contentManagement.tableList.actionsDisabledLabel', {
defaultMessage: 'Actions disabled for this item',
})
);
}
return '';
},
initialSelected: [],
};
}
}, [deleteItems, dispatch, tableItemsRowActions]);

const {
isPopoverOpen,
Expand Down Expand Up @@ -214,6 +238,7 @@ export function Table<T extends UserContentCommonSchema>({
data-test-subj="itemsInMemTable"
rowHeader="attributes.title"
tableCaption={tableCaption}
isSelectable
/>
);
}
2 changes: 2 additions & 0 deletions packages/content-management/table_list/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export type {
} from './table_list_view';

export { TableListViewProvider, TableListViewKibanaProvider } from './services';

export type { RowActions } from './types';
105 changes: 105 additions & 0 deletions packages/content-management/table_list/src/table_list_view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1067,4 +1067,109 @@ describe('TableListView', () => {
expect(router?.history.location?.search).toBe('?sort=title&sortdir=desc');
});
});

describe('row item actions', () => {
const hits: UserContentCommonSchema[] = [
{
id: '123',
updatedAt: twoDaysAgo.toISOString(),
type: 'dashboard',
attributes: {
title: 'Item 1',
description: 'Item 1 description',
},
references: [],
},
{
id: '456',
updatedAt: yesterday.toISOString(),
type: 'dashboard',
attributes: {
title: 'Item 2',
description: 'Item 2 description',
},
references: [],
},
];

const setupTest = async (props?: Partial<TableListViewProps>) => {
let testBed: TestBed | undefined;
const deleteItems = jest.fn();
await act(async () => {
testBed = await setup({
findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }),
deleteItems,
...props,
});
});

testBed!.component.update();
return { testBed: testBed!, deleteItems };
};

test('should allow select items to be deleted', async () => {
const {
testBed: { table, find, exists, component, form },
deleteItems,
} = await setupTest();

const { tableCellsValues } = table.getMetaData('itemsInMemTable');

expect(tableCellsValues).toEqual([
['', 'Item 2Item 2 description', yesterdayToString], // First empty col is the "checkbox"
['', 'Item 1Item 1 description', twoDaysAgoToString],
]);

const selectedHit = hits[1];

expect(exists('deleteSelectedItems')).toBe(false);
act(() => {
// Select the second item
form.selectCheckBox(`checkboxSelectRow-${selectedHit.id}`);
});
component.update();
// Delete button is now visible
expect(exists('deleteSelectedItems')).toBe(true);

// Click delete and validate that confirm modal opens
expect(component.exists('.euiModal--confirmation')).toBe(false);
act(() => {
find('deleteSelectedItems').simulate('click');
});
component.update();
expect(component.exists('.euiModal--confirmation')).toBe(true);

await act(async () => {
find('confirmModalConfirmButton').simulate('click');
});
expect(deleteItems).toHaveBeenCalledWith([selectedHit]);
});

test('should allow to disable the "delete" action for a row', async () => {
const reasonMessage = 'This file cannot be deleted.';

const {
testBed: { find },
} = await setupTest({
rowItemActions: (obj) => {
if (obj.id === hits[1].id) {
return {
delete: {
enabled: false,
reason: reasonMessage,
},
};
}
},
});

const firstCheckBox = find(`checkboxSelectRow-${hits[0].id}`);
const secondCheckBox = find(`checkboxSelectRow-${hits[1].id}`);

expect(firstCheckBox.props().disabled).toBe(false);
expect(secondCheckBox.props().disabled).toBe(true);
// EUI changes the check "title" from "Select this row" to the reason to disable the checkbox
expect(secondCheckBox.props().title).toBe(reasonMessage);
});
});
});
35 changes: 31 additions & 4 deletions packages/content-management/table_list/src/table_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { getReducer } from './reducer';
import type { SortColumnField } from './components';
import { useTags } from './use_tags';
import { useInRouterContext, useUrlState } from './use_url_state';
import { RowActions, TableItemsRowActions } from './types';

interface ContentEditorConfig
extends Pick<OpenContentEditorParams, 'isReadonly' | 'onSave' | 'customValidators'> {
Expand All @@ -67,6 +68,11 @@ export interface Props<T extends UserContentCommonSchema = UserContentCommonSche
headingId?: string;
/** An optional id for the listing. Used to generate unique data-test-subj. Default: "userContent" */
id?: string;
/**
* Configuration of the table row item actions. Disable specific action for a table row item.
* Currently only the "delete" ite action can be disabled.
*/
rowItemActions?: (obj: T) => RowActions | undefined;
children?: ReactNode | undefined;
findItems(
searchQuery: string,
Expand Down Expand Up @@ -241,6 +247,7 @@ function TableListViewComp<T extends UserContentCommonSchema>({
urlStateEnabled = true,
customTableColumn,
emptyPrompt,
rowItemActions,
findItems,
createItem,
editItem,
Expand Down Expand Up @@ -580,6 +587,15 @@ function TableListViewComp<T extends UserContentCommonSchema>({
return selectedIds.map((selectedId) => itemsById[selectedId]);
}, [selectedIds, itemsById]);

const tableItemsRowActions = useMemo(() => {
return items.reduce<TableItemsRowActions>((acc, item) => {
return {
...acc,
[item.id]: rowItemActions ? rowItemActions(item) : undefined,
};
}, {});
}, [items, rowItemActions]);

// ------------
// Callbacks
// ------------
Expand Down Expand Up @@ -854,17 +870,27 @@ function TableListViewComp<T extends UserContentCommonSchema>({
};
}, []);

const PageTemplate = useMemo<typeof KibanaPageTemplate>(() => {
return withoutPageTemplateWrapper
? ((({
children: _children,
'data-test-subj': dataTestSubj,
}: {
children: React.ReactNode;
['data-test-subj']?: string;
}) => (
<div data-test-subj={dataTestSubj}>{_children}</div>
)) as unknown as typeof KibanaPageTemplate)
: KibanaPageTemplate;
}, [withoutPageTemplateWrapper]);

// ------------
// Render
// ------------
if (!hasInitialFetchReturned) {
return null;
}

const PageTemplate = withoutPageTemplateWrapper
? (React.Fragment as unknown as typeof KibanaPageTemplate)
: KibanaPageTemplate;

if (!showFetchError && hasNoItems) {
return (
<PageTemplate panelled isEmptyState={true} data-test-subj={pageDataTestSubject}>
Expand Down Expand Up @@ -929,6 +955,7 @@ function TableListViewComp<T extends UserContentCommonSchema>({
tagsToTableItemMap={tagsToTableItemMap}
deleteItems={deleteItems}
tableCaption={tableListTitle}
tableItemsRowActions={tableItemsRowActions}
onTableChange={onTableChange}
onTableSearchChange={onTableSearchChange}
onSortChange={onSortChange}
Expand Down
13 changes: 13 additions & 0 deletions packages/content-management/table_list/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ export interface Tag {
description: string;
color: string;
}

export type TableRowAction = 'delete';

export type RowActions = {
[action in TableRowAction]?: {
enabled: boolean;
reason?: string;
};
};

export interface TableItemsRowActions {
[id: string]: RowActions | undefined;
}
1 change: 1 addition & 0 deletions packages/shared-ux/file/types/base_file_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface BaseFilesClient<M = unknown> {
find: (
args: {
kind?: string | string[];
kindToExclude?: string | string[];
status?: string | string[];
extension?: string | string[];
name?: string | string[];
Expand Down
16 changes: 16 additions & 0 deletions packages/shared-ux/file/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,22 @@ export interface FileKindBrowser extends FileKindBase {
* @default 4MiB
*/
maxSizeBytes?: number;
/**
* Allowed actions that can be done in the File Management UI. If not provided, all actions are allowed
*
*/
managementUiActions?: {
/** Allow files to be listed in management UI */
list?: {
enabled: boolean;
};
/** Allow files to be deleted in management UI */
delete?: {
enabled: boolean;
/** If delete is not enabled in management UI, specify the reason (will appear in a tooltip). */
reason?: string;
};
};
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/plugins/files/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export interface FilesSetup {
registerFileKind(fileKind: FileKindBrowser): void;
}

export type FilesStart = Pick<FilesSetup, 'filesClientFactory'>;
export type FilesStart = Pick<FilesSetup, 'filesClientFactory'> & {
getFileKindDefinition: (id: string) => FileKindBrowser;
getAllFindKindDefinitions: () => FileKindBrowser[];
};

/**
* Bringing files to Kibana
Expand Down Expand Up @@ -77,6 +80,12 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart> {
start(core: CoreStart): FilesStart {
return {
filesClientFactory: this.filesClientFactory!,
getFileKindDefinition: (id: string): FileKindBrowser => {
return this.registry.get(id);
},
getAllFindKindDefinitions: (): FileKindBrowser[] => {
return this.registry.getAll();
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function filterArgsToKuery({
extension,
mimeType,
kind,
kindToExclude,
meta,
name,
status,
Expand All @@ -50,12 +51,27 @@ export function filterArgsToKuery({
}
};

const addExcludeFilters = (fieldName: keyof FileMetadata | string, values: string[] = []) => {
if (values.length) {
const andExpressions = values
.filter(Boolean)
.map((value) =>
nodeTypes.function.buildNode(
'not',
nodeBuilder.is(`${attrPrefix}.${fieldName}`, escapeKuery(value))
)
);
kueryExpressions.push(nodeBuilder.and(andExpressions));
}
};

addFilters('name', name, true);
addFilters('FileKind', kind);
addFilters('Status', status);
addFilters('extension', extension);
addFilters('mime_type', mimeType);
addFilters('user.id', user);
addExcludeFilters('FileKind', kindToExclude);

if (meta) {
const addMetaFilters = pipe(
Expand Down
Loading

0 comments on commit 29a10fd

Please sign in to comment.