From 60a4e2cc59a08c1d0eccfe13bf36a7e5abe71a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Apr 2023 16:08:03 +0100 Subject: [PATCH 01/15] Update TableListView component to allow row action to be disabled --- .../table_list/src/components/table.tsx | 37 ++++-- .../table_list/src/table_list_view.test.tsx | 105 ++++++++++++++++++ .../table_list/src/table_list_view.tsx | 17 +++ .../table_list/src/types.ts | 13 +++ 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 330eb67be4278..7b59ce76db03a 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -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'; @@ -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'; @@ -51,6 +54,7 @@ interface Props extends State, TagManageme tableColumns: Array>; hasUpdatedAtMetadata: boolean; deleteItems: TableListViewProps['deleteItems']; + tableItemsRowActions: TableItemsRowActions; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; @@ -70,6 +74,7 @@ export function Table({ entityName, entityNamePlural, tagsToTableItemMap, + tableItemsRowActions, deleteItems, tableCaption, onTableChange, @@ -105,13 +110,31 @@ export function Table({ ); }, [deleteItems, dispatch, entityName, entityNamePlural, selectedIds.length]); - const selection = deleteItems - ? { - onSelectionChange: (obj: T[]) => { - dispatch({ type: 'onSelectionChange', data: obj }); - }, - } - : undefined; + let selection: EuiTableSelectionType | undefined; + + if (deleteItems) { + selection = { + onSelectionChange: (obj: T[]) => { + dispatch({ type: 'onSelectionChange', data: obj }); + }, + 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 ''; + }, + }; + } const { isPopoverOpen, diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list/src/table_list_view.test.tsx index 62c83fb5b9454..0245af450fb8a 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list/src/table_list_view.test.tsx @@ -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) => { + 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); + }); + }); }); diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 1612649f80bda..9cfe18cf68db7 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -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 { @@ -67,6 +68,11 @@ export interface Props RowActions | undefined; children?: ReactNode | undefined; findItems( searchQuery: string, @@ -241,6 +247,7 @@ function TableListViewComp({ urlStateEnabled = true, customTableColumn, emptyPrompt, + rowItemActions, findItems, createItem, editItem, @@ -580,6 +587,15 @@ function TableListViewComp({ return selectedIds.map((selectedId) => itemsById[selectedId]); }, [selectedIds, itemsById]); + const tableItemsRowActions = useMemo(() => { + return items.reduce((acc, item) => { + return { + ...acc, + [item.id]: rowItemActions ? rowItemActions(item) : undefined, + }; + }, {}); + }, [items, rowItemActions]); + // ------------ // Callbacks // ------------ @@ -929,6 +945,7 @@ function TableListViewComp({ tagsToTableItemMap={tagsToTableItemMap} deleteItems={deleteItems} tableCaption={tableListTitle} + tableItemsRowActions={tableItemsRowActions} onTableChange={onTableChange} onTableSearchChange={onTableSearchChange} onSortChange={onSortChange} diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list/src/types.ts index 0e716e6d59cf3..177f3d47c34c0 100644 --- a/packages/content-management/table_list/src/types.ts +++ b/packages/content-management/table_list/src/types.ts @@ -12,3 +12,16 @@ export interface Tag { description: string; color: string; } + +export type TableRowAction = 'list' | 'delete'; + +export type RowActions = { + [action in TableRowAction]?: { + enabled: boolean; + reason?: string; + }; +}; + +export interface TableItemsRowActions { + [id: string]: RowActions | undefined; +} From b730850aa337d5adbbe97fb0875513ab79ee2315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Apr 2023 16:33:01 +0100 Subject: [PATCH 02/15] Export type from table-list package --- packages/content-management/table_list/index.ts | 2 +- packages/content-management/table_list/src/index.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/content-management/table_list/index.ts b/packages/content-management/table_list/index.ts index 532b35450d541..9a608b2d6dda3 100644 --- a/packages/content-management/table_list/index.ts +++ b/packages/content-management/table_list/index.ts @@ -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'; diff --git a/packages/content-management/table_list/src/index.ts b/packages/content-management/table_list/src/index.ts index df0d1e22bc106..d1e83d7dd2e93 100644 --- a/packages/content-management/table_list/src/index.ts +++ b/packages/content-management/table_list/src/index.ts @@ -15,3 +15,5 @@ export type { } from './table_list_view'; export { TableListViewProvider, TableListViewKibanaProvider } from './services'; + +export type { RowActions } from './types'; From 8ebb07a677222a343e72365554e373b957054eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Apr 2023 16:37:35 +0100 Subject: [PATCH 03/15] Update files management to allow "managementUiActions" config to be registered --- packages/shared-ux/file/types/index.ts | 11 +++++++++++ src/plugins/files/public/plugin.ts | 7 ++++++- src/plugins/files_management/public/app.tsx | 17 +++++++++++++++-- src/plugins/files_management/public/context.tsx | 8 ++++++-- .../files_management/public/i18n_texts.ts | 3 +++ .../public/mount_management_section.tsx | 1 + src/plugins/files_management/public/types.ts | 1 + 7 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/shared-ux/file/types/index.ts b/packages/shared-ux/file/types/index.ts index 4c49124f7149f..7cbc5a79efeee 100644 --- a/packages/shared-ux/file/types/index.ts +++ b/packages/shared-ux/file/types/index.ts @@ -250,6 +250,17 @@ 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?: { + delete: { + enabled: boolean; + /** If delete is not enabled in management UI, specify the reason. */ + reason?: string; + }; + }; } /** diff --git a/src/plugins/files/public/plugin.ts b/src/plugins/files/public/plugin.ts index 54646e9199f9a..5b9c71e89b28a 100644 --- a/src/plugins/files/public/plugin.ts +++ b/src/plugins/files/public/plugin.ts @@ -35,7 +35,9 @@ export interface FilesSetup { registerFileKind(fileKind: FileKindBrowser): void; } -export type FilesStart = Pick; +export type FilesStart = Pick & { + getFileKindDefinition: (id: string) => FileKindBrowser; +}; /** * Bringing files to Kibana @@ -77,6 +79,9 @@ export class FilesPlugin implements Plugin { start(core: CoreStart): FilesStart { return { filesClientFactory: this.filesClientFactory!, + getFileKindDefinition: (id: string): FileKindBrowser => { + return this.registry.get(id); + }, }; } } diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index becdd05fa0e2c..3f207f9f4bede 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -12,18 +12,25 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list'; import numeral from '@elastic/numeral'; import type { FileJSON } from '@kbn/files-plugin/common'; + import { useFilesManagementContext } from './context'; import { i18nTexts } from './i18n_texts'; import { EmptyPrompt, DiagnosticsFlyout, FileFlyout } from './components'; -type FilesUserContentSchema = UserContentCommonSchema; +type FilesUserContentSchema = Omit & { + attributes: { + title: string; + description?: string; + fileKind: string; + }; +}; function naivelyFuzzify(query: string): string { return query.includes('*') ? query : `*${query}*`; } export const App: FunctionComponent = () => { - const { filesClient } = useFilesManagementContext(); + const { filesClient, getFileKindDefinition } = useFilesManagementContext(); const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState(false); const [selectedFile, setSelectedFile] = useState(undefined); return ( @@ -71,6 +78,12 @@ export const App: FunctionComponent = () => { {i18nTexts.diagnosticsFlyoutTitle} , ]} + rowItemActions={({ attributes }) => { + const definition = getFileKindDefinition(attributes.fileKind); + return { + delete: definition?.managementUiActions?.delete, + }; + }} /> {showDiagnosticsFlyout && ( setShowDiagnosticsFlyout(false)} /> diff --git a/src/plugins/files_management/public/context.tsx b/src/plugins/files_management/public/context.tsx index 18f031b84e5c1..93675b4a43734 100644 --- a/src/plugins/files_management/public/context.tsx +++ b/src/plugins/files_management/public/context.tsx @@ -12,9 +12,13 @@ import type { AppContext } from './types'; const FilesManagementAppContext = createContext(null as unknown as AppContext); -export const FilesManagementAppContextProvider: FC = ({ children, filesClient }) => { +export const FilesManagementAppContextProvider: FC = ({ + children, + filesClient, + getFileKindDefinition, +}) => { return ( - + {children} ); diff --git a/src/plugins/files_management/public/i18n_texts.ts b/src/plugins/files_management/public/i18n_texts.ts index c5f4956af372f..d430038dcdddc 100644 --- a/src/plugins/files_management/public/i18n_texts.ts +++ b/src/plugins/files_management/public/i18n_texts.ts @@ -101,4 +101,7 @@ export const i18nTexts = { defaultMessage: 'Upload error', }), } as Record, + rowCheckboxDisabled: i18n.translate('filesManagement.table.checkBoxDisabledLabel', { + defaultMessage: 'This file cannot be deleted.', + }), }; diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 7dce1986237a7..9de7340158940 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -42,6 +42,7 @@ export const mountManagementSection = ( > diff --git a/src/plugins/files_management/public/types.ts b/src/plugins/files_management/public/types.ts index 2a73b69bea017..ddbe17e948751 100755 --- a/src/plugins/files_management/public/types.ts +++ b/src/plugins/files_management/public/types.ts @@ -11,6 +11,7 @@ import { ManagementSetup } from '@kbn/management-plugin/public'; export interface AppContext { filesClient: FilesClient; + getFileKindDefinition: FilesStart['getFileKindDefinition']; } export interface SetupDependencies { From 96ab13a95b269b7e2aecddeb12391f52c5592583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 18 Apr 2023 16:42:36 +0100 Subject: [PATCH 04/15] Update files example with table of files not deletable --- examples/files_example/common/index.ts | 11 +- .../files_example/public/components/app.tsx | 147 +++++++++++++++--- .../public/components/file_picker.tsx | 7 +- .../files_example/public/components/modal.tsx | 12 +- examples/files_example/public/plugin.ts | 21 ++- examples/files_example/public/types.ts | 2 + examples/files_example/server/plugin.ts | 3 +- 7 files changed, 165 insertions(+), 38 deletions(-) diff --git a/examples/files_example/common/index.ts b/examples/files_example/common/index.ts index b3ef90f9dfd6e..4ac246cf07f2f 100644 --- a/examples/files_example/common/index.ts +++ b/examples/files_example/common/index.ts @@ -9,8 +9,12 @@ import type { FileKind } from '@kbn/files-plugin/common'; import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; -export const PLUGIN_ID = 'filesExample'; +export const PLUGIN_ID = 'filesExample' as const; export const PLUGIN_NAME = 'Files example'; +export const NO_MGT_DELETE_FILE_KIND = `${PLUGIN_ID}NoMgtDelete` as const; +export const FILE_KIND_IDS = [PLUGIN_ID, NO_MGT_DELETE_FILE_KIND] as const; + +export type FileTypeId = typeof FILE_KIND_IDS[number]; const httpTags = { tags: [`access:${PLUGIN_ID}`], @@ -30,4 +34,9 @@ export const exampleFileKind: FileKind = { }, }; +export const exampleFileKindNotDeletableInMangementUI: FileKind = { + ...exampleFileKind, + id: NO_MGT_DELETE_FILE_KIND, +}; + export type MyImageMetadata = FileImageMetadata; diff --git a/examples/files_example/public/components/app.tsx b/examples/files_example/public/components/app.tsx index db0968d7b43f2..9555859658a9e 100644 --- a/examples/files_example/public/components/app.tsx +++ b/examples/files_example/public/components/app.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React, { FC, useState, type Dispatch, type SetStateAction } from 'react'; import { useQuery } from '@tanstack/react-query'; import type { FileJSON } from '@kbn/files-plugin/common'; import type { FilesClientResponses } from '@kbn/files-plugin/public'; @@ -19,11 +19,14 @@ import { EuiIcon, EuiButtonIcon, EuiLink, + EuiTitle, + EuiSpacer, + EuiText, } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; import { MyFilePicker } from './file_picker'; -import type { MyImageMetadata } from '../../common'; +import type { FileTypeId, MyImageMetadata } from '../../common'; import type { FileClients } from '../types'; import { DetailsFlyout } from './details_flyout'; import { ConfirmButtonIcon } from './confirm_button'; @@ -36,16 +39,27 @@ interface FilesExampleAppDeps { type ListResponse = FilesClientResponses['list']; -export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) => { - const { data, isLoading, error, refetch } = useQuery( - ['files'], - () => files.example.list(), - { refetchOnWindowFocus: false } - ); - const [showUploadModal, setShowUploadModal] = useState(false); - const [showFilePickerModal, setShowFilePickerModal] = useState(false); +interface FilesTableProps extends FilesExampleAppDeps { + data?: ListResponse; + isLoading: boolean; + error: unknown; + refetch: () => Promise; + setShowUploadModal: Dispatch>; + setShowFilePickerModal: Dispatch>; + setSelectedItem: Dispatch>>; +} + +const FilesTable: FC = ({ + files, + data, + isLoading, + error, + refetch, + setSelectedItem, + setShowFilePickerModal, + setShowUploadModal, +}) => { const [isDeletingFile, setIsDeletingFile] = useState(false); - const [selectedItem, setSelectedItem] = useState>(); const renderToolsRight = () => { return [ @@ -130,22 +144,99 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = }, ]; + return ( + + ); +}; + +export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) => { + const exampleFilesQuery = useQuery(['files'], () => files.example.list(), { + refetchOnWindowFocus: false, + }); + + const exampleFilesNotDeletableQuery = useQuery( + ['filesNotDeletable'], + () => files.exampleNotDeletable.list(), + { + refetchOnWindowFocus: false, + } + ); + + const [showUploadModal, setShowUploadModal] = useState(false); + const [showFilePickerModal, setShowFilePickerModal] = useState(false); + const [activeFileTypeId, setActiveFileTypeId] = useState('filesExample'); + const [selectedItem, setSelectedItem] = useState>(); + return ( <> - +

All UI actions in management UI

+ + + { + setActiveFileTypeId('filesExample'); + setShowFilePickerModal(value); + }, + setShowUploadModal: (value) => { + setActiveFileTypeId('filesExample'); + setShowUploadModal(value); + }, + }} + /> + + + + +

Files not deletable in management UI

+
+ + + The files uploaded in this table are not deletable in the Kibana {'>'} Management {'>'}{' '} + Files UI + + + + { + setActiveFileTypeId('filesExampleNoMgtDelete'); + setShowFilePickerModal(value); + }, + setShowUploadModal: (value) => { + setActiveFileTypeId('filesExampleNoMgtDelete'); + setShowUploadModal(value); + }, }} - pagination />
@@ -159,10 +250,15 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = {showUploadModal && ( setShowUploadModal(false)} onUploaded={() => { notifications.toasts.addSuccess('Uploaded file!'); - refetch(); + if (activeFileTypeId === 'filesExample') { + exampleFilesQuery.refetch(); + } else { + exampleFilesNotDeletableQuery.refetch(); + } setShowUploadModal(false); }} /> @@ -170,11 +266,16 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = {showFilePickerModal && ( setShowFilePickerModal(false)} + fileKind={activeFileTypeId} onUpload={() => { notifications.toasts.addSuccess({ title: 'Uploaded files', }); - refetch(); + if (activeFileTypeId === 'filesExample') { + exampleFilesQuery.refetch(); + } else { + exampleFilesNotDeletableQuery.refetch(); + } }} onDone={(ids) => { notifications.toasts.addSuccess({ diff --git a/examples/files_example/public/components/file_picker.tsx b/examples/files_example/public/components/file_picker.tsx index 3e94029f3a0f8..061463bd66e90 100644 --- a/examples/files_example/public/components/file_picker.tsx +++ b/examples/files_example/public/components/file_picker.tsx @@ -9,20 +9,19 @@ import React from 'react'; import type { FunctionComponent } from 'react'; -import { exampleFileKind } from '../../common'; - import { FilePicker } from '../imports'; interface Props { + fileKind: string; onClose: () => void; onUpload: (ids: string[]) => void; onDone: (ids: string[]) => void; } -export const MyFilePicker: FunctionComponent = ({ onClose, onDone, onUpload }) => { +export const MyFilePicker: FunctionComponent = ({ fileKind, onClose, onDone, onUpload }) => { return ( onDone(files.map((f) => f.id))} onUpload={(n) => onUpload(n.map(({ id }) => id))} diff --git a/examples/files_example/public/components/modal.tsx b/examples/files_example/public/components/modal.tsx index 1471c469b6351..0126ae0f3dcc5 100644 --- a/examples/files_example/public/components/modal.tsx +++ b/examples/files_example/public/components/modal.tsx @@ -9,16 +9,17 @@ import type { FunctionComponent } from 'react'; import React from 'react'; import { EuiModal, EuiModalHeader, EuiModalBody, EuiText } from '@elastic/eui'; -import { exampleFileKind, MyImageMetadata } from '../../common'; +import type { MyImageMetadata } from '../../common'; import { FilesClient, FileUpload } from '../imports'; interface Props { client: FilesClient; + fileKind: string; onDismiss: () => void; onUploaded: () => void; } -export const Modal: FunctionComponent = ({ onDismiss, onUploaded, client }) => { +export const Modal: FunctionComponent = ({ fileKind, onDismiss, onUploaded, client }) => { return ( @@ -27,12 +28,7 @@ export const Modal: FunctionComponent = ({ onDismiss, onUploaded, client - + ); diff --git a/examples/files_example/public/plugin.ts b/examples/files_example/public/plugin.ts index b36928e5f5566..65f159fb460fb 100644 --- a/examples/files_example/public/plugin.ts +++ b/examples/files_example/public/plugin.ts @@ -8,7 +8,13 @@ import { AppNavLinkStatus } from '@kbn/core-application-browser'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; -import { PLUGIN_ID, PLUGIN_NAME, exampleFileKind, MyImageMetadata } from '../common'; +import { + PLUGIN_ID, + PLUGIN_NAME, + exampleFileKind, + MyImageMetadata, + exampleFileKindNotDeletableInMangementUI, +} from '../common'; import { FilesExamplePluginsStart, FilesExamplePluginsSetup } from './types'; export class FilesExamplePlugin @@ -22,6 +28,16 @@ export class FilesExamplePlugin id: exampleFileKind.id, allowedMimeTypes: exampleFileKind.allowedMimeTypes, }); + files.registerFileKind({ + id: exampleFileKindNotDeletableInMangementUI.id, + allowedMimeTypes: exampleFileKindNotDeletableInMangementUI.allowedMimeTypes, + managementUiActions: { + delete: { + enabled: false, + reason: 'This file is too cool to be deleted.', + }, + }, + }); developerExamples.register({ appId: PLUGIN_ID, @@ -45,6 +61,9 @@ export class FilesExamplePlugin files: { unscoped: deps.files.filesClientFactory.asUnscoped(), example: deps.files.filesClientFactory.asScoped(exampleFileKind.id), + exampleNotDeletable: deps.files.filesClientFactory.asScoped( + exampleFileKindNotDeletableInMangementUI.id + ), }, }, params diff --git a/examples/files_example/public/types.ts b/examples/files_example/public/types.ts index 4ca9eb148872f..c4aac2da51574 100644 --- a/examples/files_example/public/types.ts +++ b/examples/files_example/public/types.ts @@ -28,6 +28,8 @@ export interface FileClients { unscoped: FilesClient; // Example file kind example: ScopedFilesClient; + // Example file kind not deletable in the management UI + exampleNotDeletable: ScopedFilesClient; } export interface AppPluginStartDependencies { diff --git a/examples/files_example/server/plugin.ts b/examples/files_example/server/plugin.ts index 5ab9571a64207..56e663480ffe9 100644 --- a/examples/files_example/server/plugin.ts +++ b/examples/files_example/server/plugin.ts @@ -7,7 +7,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; -import { exampleFileKind } from '../common'; +import { exampleFileKind, exampleFileKindNotDeletableInMangementUI } from '../common'; import type { FilesExamplePluginsSetup, FilesExamplePluginsStart } from './types'; export class FilesExamplePlugin @@ -23,6 +23,7 @@ export class FilesExamplePlugin this.logger.debug('filesExample: Setup'); files.registerFileKind(exampleFileKind); + files.registerFileKind(exampleFileKindNotDeletableInMangementUI); return {}; } From 608293b41b730ca5309f30ec3aa7dd8b3088fb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Apr 2023 15:28:31 +0100 Subject: [PATCH 05/15] Add "kindToExclude" filter to find() arguments --- .../shared-ux/file/types/base_file_client.ts | 1 + .../adapters/query_filters.ts | 16 ++++++++++++++++ .../server/file_service/file_action_types.ts | 4 ++++ src/plugins/files/server/routes/find.ts | 4 +++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/shared-ux/file/types/base_file_client.ts b/packages/shared-ux/file/types/base_file_client.ts index 4a00f2de00516..52d1ca09fd170 100644 --- a/packages/shared-ux/file/types/base_file_client.ts +++ b/packages/shared-ux/file/types/base_file_client.ts @@ -27,6 +27,7 @@ export interface BaseFilesClient { find: ( args: { kind?: string | string[]; + kindToExclude?: string | string[]; status?: string | string[]; extension?: string | string[]; name?: string | string[]; diff --git a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts index 0f453a1b81e6a..014e57b41d2b1 100644 --- a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts +++ b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts @@ -24,6 +24,7 @@ export function filterArgsToKuery({ extension, mimeType, kind, + kindToExclude, meta, name, status, @@ -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( diff --git a/src/plugins/files/server/file_service/file_action_types.ts b/src/plugins/files/server/file_service/file_action_types.ts index 4247f567802ed..96795ac93b387 100644 --- a/src/plugins/files/server/file_service/file_action_types.ts +++ b/src/plugins/files/server/file_service/file_action_types.ts @@ -82,6 +82,10 @@ export interface FindFileArgs extends Pagination { * File kind(s), see {@link FileKind}. */ kind?: string[]; + /** + * File kind(s) to exclude from search, see {@link FileKind}. + */ + kindToExclude?: string[]; /** * File name(s). */ diff --git a/src/plugins/files/server/routes/find.ts b/src/plugins/files/server/routes/find.ts index a81a9d2ea5220..6749e06254100 100644 --- a/src/plugins/files/server/routes/find.ts +++ b/src/plugins/files/server/routes/find.ts @@ -30,6 +30,7 @@ export function toArrayOrUndefined(val?: string | string[]): undefined | string[ const rt = { body: schema.object({ kind: schema.maybe(stringOrArrayOfStrings), + kindToExclude: schema.maybe(stringOrArrayOfStrings), status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), @@ -50,12 +51,13 @@ export type Endpoint = CreateRouteDefinition< const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; const { - body: { meta, extension, kind, name, status }, + body: { meta, extension, kind, name, status, kindToExclude }, query, } = req; const { files: results, total } = await fileService.asCurrentUser().find({ kind: toArrayOrUndefined(kind), + kindToExclude: toArrayOrUndefined(kindToExclude), name: toArrayOrUndefined(name), status: toArrayOrUndefined(status), extension: toArrayOrUndefined(extension), From 7b6f50c53639093689da948c57dbfaf2ed39ac3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Apr 2023 15:28:58 +0100 Subject: [PATCH 06/15] Fix react console error using unknown prop on Fragment --- .../table_list/src/table_list_view.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 9cfe18cf68db7..3d9c97b0ae8a0 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -878,7 +878,15 @@ function TableListViewComp({ } const PageTemplate = withoutPageTemplateWrapper - ? (React.Fragment as unknown as typeof KibanaPageTemplate) + ? ((({ + children: _children, + 'data-test-subj': dataTestSubj, + }: { + children: React.ReactNode; + ['data-test-subj']?: string; + }) => ( +
{_children}
+ )) as unknown as typeof KibanaPageTemplate) : KibanaPageTemplate; if (!showFetchError && hasNoItems) { From 0424a51b71e95e59e5c7feb35a8036add3da847f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Apr 2023 15:29:53 +0100 Subject: [PATCH 07/15] Add "list" action config to managementUiActions --- packages/shared-ux/file/types/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/shared-ux/file/types/index.ts b/packages/shared-ux/file/types/index.ts index 7cbc5a79efeee..9a8204fdc867a 100644 --- a/packages/shared-ux/file/types/index.ts +++ b/packages/shared-ux/file/types/index.ts @@ -255,9 +255,12 @@ export interface FileKindBrowser extends FileKindBase { * */ managementUiActions?: { - delete: { + list?: { enabled: boolean; - /** If delete is not enabled in management UI, specify the reason. */ + }; + delete?: { + enabled: boolean; + /** If delete is not enabled in management UI, specify the reason (will appear in a tooltip). */ reason?: string; }; }; From 898ecb75bec2db8c68168f16a1a2e6deb241de0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Apr 2023 15:30:31 +0100 Subject: [PATCH 08/15] Exclude files in table list that have "list" config disabled --- src/plugins/files/public/plugin.ts | 4 ++++ src/plugins/files_management/public/app.tsx | 13 +++++++++++-- src/plugins/files_management/public/context.tsx | 5 ++++- .../public/mount_management_section.tsx | 9 +++++++-- src/plugins/files_management/public/types.ts | 1 + 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/plugins/files/public/plugin.ts b/src/plugins/files/public/plugin.ts index 5b9c71e89b28a..13828d0ee366c 100644 --- a/src/plugins/files/public/plugin.ts +++ b/src/plugins/files/public/plugin.ts @@ -37,6 +37,7 @@ export interface FilesSetup { export type FilesStart = Pick & { getFileKindDefinition: (id: string) => FileKindBrowser; + getAllFindKindDefinitions: () => FileKindBrowser[]; }; /** @@ -82,6 +83,9 @@ export class FilesPlugin implements Plugin { getFileKindDefinition: (id: string): FileKindBrowser => { return this.registry.get(id); }, + getAllFindKindDefinitions: (): FileKindBrowser[] => { + return this.registry.getAll(); + }, }; } } diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index 3f207f9f4bede..3ee4e5f52720c 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -30,9 +30,15 @@ function naivelyFuzzify(query: string): string { } export const App: FunctionComponent = () => { - const { filesClient, getFileKindDefinition } = useFilesManagementContext(); + const { filesClient, getFileKindDefinition, getAllFindKindDefinitions } = + useFilesManagementContext(); const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState(false); const [selectedFile, setSelectedFile] = useState(undefined); + + const kindToExcludeFromSearch = getAllFindKindDefinitions() + .filter(({ managementUiActions }) => managementUiActions?.list?.enabled === false) + .map(({ id }) => id); + return (
@@ -44,7 +50,10 @@ export const App: FunctionComponent = () => { entityNamePlural={i18nTexts.entityNamePlural} findItems={(searchQuery) => filesClient - .find({ name: searchQuery ? naivelyFuzzify(searchQuery) : undefined }) + .find({ + name: searchQuery ? naivelyFuzzify(searchQuery) : undefined, + kindToExclude: kindToExcludeFromSearch, + }) .then(({ files, total }) => ({ hits: files.map((file) => ({ id: file.id, diff --git a/src/plugins/files_management/public/context.tsx b/src/plugins/files_management/public/context.tsx index 93675b4a43734..0688c5a7edecb 100644 --- a/src/plugins/files_management/public/context.tsx +++ b/src/plugins/files_management/public/context.tsx @@ -16,9 +16,12 @@ export const FilesManagementAppContextProvider: FC = ({ children, filesClient, getFileKindDefinition, + getAllFindKindDefinitions, }) => { return ( - + {children} ); diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 9de7340158940..9c7091516d46e 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -30,6 +30,10 @@ export const mountManagementSection = ( startDeps: StartDependencies, { element, history }: ManagementAppMountParams ) => { + const { + files: { filesClientFactory, getAllFindKindDefinitions, getFileKindDefinition }, + } = startDeps; + ReactDOM.render( @@ -41,8 +45,9 @@ export const mountManagementSection = ( }} > diff --git a/src/plugins/files_management/public/types.ts b/src/plugins/files_management/public/types.ts index ddbe17e948751..303d5e1c5d1a7 100755 --- a/src/plugins/files_management/public/types.ts +++ b/src/plugins/files_management/public/types.ts @@ -12,6 +12,7 @@ import { ManagementSetup } from '@kbn/management-plugin/public'; export interface AppContext { filesClient: FilesClient; getFileKindDefinition: FilesStart['getFileKindDefinition']; + getAllFindKindDefinitions: FilesStart['getAllFindKindDefinitions']; } export interface SetupDependencies { From 7a79d7f633b0b785a27920cb5fd4748edb85aa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Apr 2023 15:31:18 +0100 Subject: [PATCH 09/15] Add files_example table for files not to be listed --- examples/files_example/common/index.ts | 8 +- .../files_example/public/components/app.tsx | 174 +++++++++++------- examples/files_example/public/plugin.ts | 13 ++ examples/files_example/public/types.ts | 2 + examples/files_example/server/plugin.ts | 7 +- 5 files changed, 131 insertions(+), 73 deletions(-) diff --git a/examples/files_example/common/index.ts b/examples/files_example/common/index.ts index 4ac246cf07f2f..885fc665fec34 100644 --- a/examples/files_example/common/index.ts +++ b/examples/files_example/common/index.ts @@ -11,8 +11,9 @@ import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; export const PLUGIN_ID = 'filesExample' as const; export const PLUGIN_NAME = 'Files example'; +export const NO_MGT_LIST_FILE_KIND = `${PLUGIN_ID}NoMgtList` as const; export const NO_MGT_DELETE_FILE_KIND = `${PLUGIN_ID}NoMgtDelete` as const; -export const FILE_KIND_IDS = [PLUGIN_ID, NO_MGT_DELETE_FILE_KIND] as const; +export const FILE_KIND_IDS = [PLUGIN_ID, NO_MGT_LIST_FILE_KIND, NO_MGT_DELETE_FILE_KIND] as const; export type FileTypeId = typeof FILE_KIND_IDS[number]; @@ -34,6 +35,11 @@ export const exampleFileKind: FileKind = { }, }; +export const exampleFileKindNotListedInMangementUI: FileKind = { + ...exampleFileKind, + id: NO_MGT_LIST_FILE_KIND, +}; + export const exampleFileKindNotDeletableInMangementUI: FileKind = { ...exampleFileKind, id: NO_MGT_DELETE_FILE_KIND, diff --git a/examples/files_example/public/components/app.tsx b/examples/files_example/public/components/app.tsx index 9555859658a9e..2cd43ed0341e5 100644 --- a/examples/files_example/public/components/app.tsx +++ b/examples/files_example/public/components/app.tsx @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import React, { FC, useState, type Dispatch, type SetStateAction } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import React, { FC, useState } from 'react'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; import type { FileJSON } from '@kbn/files-plugin/common'; import type { FilesClientResponses } from '@kbn/files-plugin/public'; @@ -40,16 +40,20 @@ interface FilesExampleAppDeps { type ListResponse = FilesClientResponses['list']; interface FilesTableProps extends FilesExampleAppDeps { + title: string; + description?: string | React.ReactNode; data?: ListResponse; isLoading: boolean; error: unknown; refetch: () => Promise; - setShowUploadModal: Dispatch>; - setShowFilePickerModal: Dispatch>; - setSelectedItem: Dispatch>>; + setShowUploadModal: (value: boolean) => void; + setShowFilePickerModal: (value: boolean) => void; + setSelectedItem: (value: FileJSON) => void; } const FilesTable: FC = ({ + title, + description, files, data, isLoading, @@ -145,18 +149,30 @@ const FilesTable: FC = ({ ]; return ( - + <> + +

{title}

+
+ + {!!description && ( + <> + + {description} + + )} + + ); }; @@ -165,6 +181,14 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = refetchOnWindowFocus: false, }); + const exampleFilesNotListedQuery = useQuery( + ['filesNotListed'], + () => files.exampleNotListed.list(), + { + refetchOnWindowFocus: false, + } + ); + const exampleFilesNotDeletableQuery = useQuery( ['filesNotDeletable'], () => files.exampleNotDeletable.list(), @@ -178,64 +202,80 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = const [activeFileTypeId, setActiveFileTypeId] = useState('filesExample'); const [selectedItem, setSelectedItem] = useState>(); + const commonProps = { + files, + notifications, + setSelectedItem, + }; + + const getOpenModalHandlers = (fileTypeId: FileTypeId) => ({ + setShowFilePickerModal: (value: boolean) => { + setActiveFileTypeId(fileTypeId); + setShowFilePickerModal(value); + }, + setShowUploadModal: (value: boolean) => { + setActiveFileTypeId(fileTypeId); + setShowUploadModal(value); + }, + }); + + const getUseQueryResult = (fileTypeId: FileTypeId): UseQueryResult => { + switch (fileTypeId) { + case 'filesExampleNoMgtList': + return exampleFilesNotListedQuery; + case 'filesExampleNoMgtDelete': + return exampleFilesNotDeletableQuery; + default: + return exampleFilesQuery; + } + }; + return ( <> - -

All UI actions in management UI

-
- + {/* Table of files with ALL UI actions in Management UI */} { - setActiveFileTypeId('filesExample'); - setShowFilePickerModal(value); - }, - setShowUploadModal: (value) => { - setActiveFileTypeId('filesExample'); - setShowUploadModal(value); - }, + ...commonProps, + ...exampleFilesQuery, + ...getOpenModalHandlers('filesExample'), + title: 'All UI actions in management UI', }} /> + {/* Table of files that are not listed in the Management UI */} + + The files uploaded in this table are not listed in the Management {'>'} Kibana{' '} + {'>'} Files UI + + ), + }} + /> - -

Files not deletable in management UI

-
- - - The files uploaded in this table are not deletable in the Kibana {'>'} Management {'>'}{' '} - Files UI - - - + + {/* Table of files that are not deletable in the Management UI */} { - setActiveFileTypeId('filesExampleNoMgtDelete'); - setShowFilePickerModal(value); - }, - setShowUploadModal: (value) => { - setActiveFileTypeId('filesExampleNoMgtDelete'); - setShowUploadModal(value); - }, + ...commonProps, + ...getOpenModalHandlers('filesExampleNoMgtDelete'), + ...exampleFilesNotDeletableQuery, + title: 'Files not deletable in management UI', + description: ( + + The files uploaded in this table are not deletable in the Management {'>'} Kibana{' '} + {'>'} Files UI + + ), }} />
@@ -254,11 +294,7 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = onDismiss={() => setShowUploadModal(false)} onUploaded={() => { notifications.toasts.addSuccess('Uploaded file!'); - if (activeFileTypeId === 'filesExample') { - exampleFilesQuery.refetch(); - } else { - exampleFilesNotDeletableQuery.refetch(); - } + getUseQueryResult(activeFileTypeId).refetch(); setShowUploadModal(false); }} /> @@ -271,11 +307,7 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = notifications.toasts.addSuccess({ title: 'Uploaded files', }); - if (activeFileTypeId === 'filesExample') { - exampleFilesQuery.refetch(); - } else { - exampleFilesNotDeletableQuery.refetch(); - } + getUseQueryResult(activeFileTypeId).refetch(); }} onDone={(ids) => { notifications.toasts.addSuccess({ diff --git a/examples/files_example/public/plugin.ts b/examples/files_example/public/plugin.ts index 65f159fb460fb..a3c0e8a129ec6 100644 --- a/examples/files_example/public/plugin.ts +++ b/examples/files_example/public/plugin.ts @@ -13,6 +13,7 @@ import { PLUGIN_NAME, exampleFileKind, MyImageMetadata, + exampleFileKindNotListedInMangementUI, exampleFileKindNotDeletableInMangementUI, } from '../common'; import { FilesExamplePluginsStart, FilesExamplePluginsSetup } from './types'; @@ -28,6 +29,15 @@ export class FilesExamplePlugin id: exampleFileKind.id, allowedMimeTypes: exampleFileKind.allowedMimeTypes, }); + files.registerFileKind({ + id: exampleFileKindNotListedInMangementUI.id, + allowedMimeTypes: exampleFileKindNotListedInMangementUI.allowedMimeTypes, + managementUiActions: { + list: { + enabled: false, + }, + }, + }); files.registerFileKind({ id: exampleFileKindNotDeletableInMangementUI.id, allowedMimeTypes: exampleFileKindNotDeletableInMangementUI.allowedMimeTypes, @@ -61,6 +71,9 @@ export class FilesExamplePlugin files: { unscoped: deps.files.filesClientFactory.asUnscoped(), example: deps.files.filesClientFactory.asScoped(exampleFileKind.id), + exampleNotListed: deps.files.filesClientFactory.asScoped( + exampleFileKindNotListedInMangementUI.id + ), exampleNotDeletable: deps.files.filesClientFactory.asScoped( exampleFileKindNotDeletableInMangementUI.id ), diff --git a/examples/files_example/public/types.ts b/examples/files_example/public/types.ts index c4aac2da51574..4c3a6329bda8c 100644 --- a/examples/files_example/public/types.ts +++ b/examples/files_example/public/types.ts @@ -28,6 +28,8 @@ export interface FileClients { unscoped: FilesClient; // Example file kind example: ScopedFilesClient; + // Example file kind not listed in the management UI + exampleNotListed: ScopedFilesClient; // Example file kind not deletable in the management UI exampleNotDeletable: ScopedFilesClient; } diff --git a/examples/files_example/server/plugin.ts b/examples/files_example/server/plugin.ts index 56e663480ffe9..ba5a1cd0b5510 100644 --- a/examples/files_example/server/plugin.ts +++ b/examples/files_example/server/plugin.ts @@ -7,7 +7,11 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; -import { exampleFileKind, exampleFileKindNotDeletableInMangementUI } from '../common'; +import { + exampleFileKind, + exampleFileKindNotListedInMangementUI, + exampleFileKindNotDeletableInMangementUI, +} from '../common'; import type { FilesExamplePluginsSetup, FilesExamplePluginsStart } from './types'; export class FilesExamplePlugin @@ -23,6 +27,7 @@ export class FilesExamplePlugin this.logger.debug('filesExample: Setup'); files.registerFileKind(exampleFileKind); + files.registerFileKind(exampleFileKindNotListedInMangementUI); files.registerFileKind(exampleFileKindNotDeletableInMangementUI); return {}; From f145adb232aea2769207a855d3cf42bcb4751b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Apr 2023 17:34:45 +0100 Subject: [PATCH 10/15] Update functional test --- .../integration_tests/file_service.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/plugins/files/server/integration_tests/file_service.test.ts b/src/plugins/files/server/integration_tests/file_service.test.ts index 25d7f463de03a..baf2b0a5c0e14 100644 --- a/src/plugins/files/server/integration_tests/file_service.test.ts +++ b/src/plugins/files/server/integration_tests/file_service.test.ts @@ -157,6 +157,7 @@ describe('FileService', () => { createDisposableFile({ fileKind, name: 'foo-2' }), createDisposableFile({ fileKind, name: 'foo-3' }), createDisposableFile({ fileKind, name: 'test-3' }), + createDisposableFile({ fileKind: fileKindNonDefault, name: 'foo-1' }), ]); { const { files, total } = await fileService.find({ @@ -166,7 +167,7 @@ describe('FileService', () => { page: 1, }); expect(files.length).toBe(2); - expect(total).toBe(3); + expect(total).toBe(4); } { @@ -176,7 +177,19 @@ describe('FileService', () => { perPage: 2, page: 2, }); - expect(files.length).toBe(1); + expect(files.length).toBe(2); + expect(total).toBe(4); + } + + // Filter out fileKind + { + const { files, total } = await fileService.find({ + kindToExclude: [fileKindNonDefault], + name: ['foo*'], + perPage: 10, + page: 1, + }); + expect(files.length).toBe(3); // foo-1 from fileKindNonDefault not returned expect(total).toBe(3); } }); From 52b5b5ff991c96128b6be57f9af150cc76c9ede0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 20 Apr 2023 12:19:58 +0100 Subject: [PATCH 11/15] Fix integration test --- .../files/server/integration_tests/file_service.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/files/server/integration_tests/file_service.test.ts b/src/plugins/files/server/integration_tests/file_service.test.ts index baf2b0a5c0e14..3492eb8e5f12c 100644 --- a/src/plugins/files/server/integration_tests/file_service.test.ts +++ b/src/plugins/files/server/integration_tests/file_service.test.ts @@ -161,7 +161,7 @@ describe('FileService', () => { ]); { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 1, @@ -172,7 +172,7 @@ describe('FileService', () => { { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 2, From fcd224c4b3f623cac680ea0406eb813a018781a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 24 Apr 2023 12:19:23 +0100 Subject: [PATCH 12/15] Wrap PageTemplate in useMemo to prevent state reinitialization --- .../table_list/src/components/table.tsx | 50 ++++++++++--------- .../table_list/src/table_list_view.tsx | 26 +++++----- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 7b59ce76db03a..3214e7bf00a72 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -110,31 +110,32 @@ export function Table({ ); }, [deleteItems, dispatch, entityName, entityNamePlural, selectedIds.length]); - let selection: EuiTableSelectionType | undefined; - - if (deleteItems) { - selection = { - onSelectionChange: (obj: T[]) => { - dispatch({ type: 'onSelectionChange', data: obj }); - }, - selectable: (obj) => { - const actions = tableItemsRowActions[obj.id]; - return actions?.delete?.enabled !== false; - }, - selectableMessage: (selectable, obj) => { - if (!selectable) { + const selection = useMemo | undefined>(() => { + if (deleteItems) { + return { + onSelectionChange: (obj: T[]) => { + dispatch({ type: 'onSelectionChange', data: obj }); + }, + selectable: (obj) => { const actions = tableItemsRowActions[obj.id]; - return ( - actions?.delete?.reason ?? - i18n.translate('contentManagement.tableList.actionsDisabledLabel', { - defaultMessage: 'Actions disabled for this item', - }) - ); - } - return ''; - }, - }; - } + 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, @@ -237,6 +238,7 @@ export function Table({ data-test-subj="itemsInMemTable" rowHeader="attributes.title" tableCaption={tableCaption} + isSelectable /> ); } diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 3d9c97b0ae8a0..2191a3c9b7eee 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -870,6 +870,20 @@ function TableListViewComp({ }; }, []); + const PageTemplate = useMemo(() => { + return withoutPageTemplateWrapper + ? ((({ + children: _children, + 'data-test-subj': dataTestSubj, + }: { + children: React.ReactNode; + ['data-test-subj']?: string; + }) => ( +
{_children}
+ )) as unknown as typeof KibanaPageTemplate) + : KibanaPageTemplate; + }, [withoutPageTemplateWrapper]); + // ------------ // Render // ------------ @@ -877,18 +891,6 @@ function TableListViewComp({ return null; } - const PageTemplate = withoutPageTemplateWrapper - ? ((({ - children: _children, - 'data-test-subj': dataTestSubj, - }: { - children: React.ReactNode; - ['data-test-subj']?: string; - }) => ( -
{_children}
- )) as unknown as typeof KibanaPageTemplate) - : KibanaPageTemplate; - if (!showFetchError && hasNoItems) { return ( From dc48629f54d55f4f8b118f34d6c145391e8dcdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 24 Apr 2023 12:29:38 +0100 Subject: [PATCH 13/15] Revert changes to files_examples --- examples/files_example/common/index.ts | 17 +- .../files_example/public/components/app.tsx | 181 +++--------------- .../public/components/file_picker.tsx | 7 +- .../files_example/public/components/modal.tsx | 12 +- examples/files_example/public/plugin.ts | 34 +--- examples/files_example/public/types.ts | 4 - examples/files_example/server/plugin.ts | 8 +- 7 files changed, 39 insertions(+), 224 deletions(-) diff --git a/examples/files_example/common/index.ts b/examples/files_example/common/index.ts index 885fc665fec34..b3ef90f9dfd6e 100644 --- a/examples/files_example/common/index.ts +++ b/examples/files_example/common/index.ts @@ -9,13 +9,8 @@ import type { FileKind } from '@kbn/files-plugin/common'; import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; -export const PLUGIN_ID = 'filesExample' as const; +export const PLUGIN_ID = 'filesExample'; export const PLUGIN_NAME = 'Files example'; -export const NO_MGT_LIST_FILE_KIND = `${PLUGIN_ID}NoMgtList` as const; -export const NO_MGT_DELETE_FILE_KIND = `${PLUGIN_ID}NoMgtDelete` as const; -export const FILE_KIND_IDS = [PLUGIN_ID, NO_MGT_LIST_FILE_KIND, NO_MGT_DELETE_FILE_KIND] as const; - -export type FileTypeId = typeof FILE_KIND_IDS[number]; const httpTags = { tags: [`access:${PLUGIN_ID}`], @@ -35,14 +30,4 @@ export const exampleFileKind: FileKind = { }, }; -export const exampleFileKindNotListedInMangementUI: FileKind = { - ...exampleFileKind, - id: NO_MGT_LIST_FILE_KIND, -}; - -export const exampleFileKindNotDeletableInMangementUI: FileKind = { - ...exampleFileKind, - id: NO_MGT_DELETE_FILE_KIND, -}; - export type MyImageMetadata = FileImageMetadata; diff --git a/examples/files_example/public/components/app.tsx b/examples/files_example/public/components/app.tsx index 2cd43ed0341e5..db0968d7b43f2 100644 --- a/examples/files_example/public/components/app.tsx +++ b/examples/files_example/public/components/app.tsx @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import React, { FC, useState } from 'react'; -import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import React, { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; import type { FileJSON } from '@kbn/files-plugin/common'; import type { FilesClientResponses } from '@kbn/files-plugin/public'; @@ -19,14 +19,11 @@ import { EuiIcon, EuiButtonIcon, EuiLink, - EuiTitle, - EuiSpacer, - EuiText, } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; import { MyFilePicker } from './file_picker'; -import type { FileTypeId, MyImageMetadata } from '../../common'; +import type { MyImageMetadata } from '../../common'; import type { FileClients } from '../types'; import { DetailsFlyout } from './details_flyout'; import { ConfirmButtonIcon } from './confirm_button'; @@ -39,31 +36,16 @@ interface FilesExampleAppDeps { type ListResponse = FilesClientResponses['list']; -interface FilesTableProps extends FilesExampleAppDeps { - title: string; - description?: string | React.ReactNode; - data?: ListResponse; - isLoading: boolean; - error: unknown; - refetch: () => Promise; - setShowUploadModal: (value: boolean) => void; - setShowFilePickerModal: (value: boolean) => void; - setSelectedItem: (value: FileJSON) => void; -} - -const FilesTable: FC = ({ - title, - description, - files, - data, - isLoading, - error, - refetch, - setSelectedItem, - setShowFilePickerModal, - setShowUploadModal, -}) => { +export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) => { + const { data, isLoading, error, refetch } = useQuery( + ['files'], + () => files.example.list(), + { refetchOnWindowFocus: false } + ); + const [showUploadModal, setShowUploadModal] = useState(false); + const [showFilePickerModal, setShowFilePickerModal] = useState(false); const [isDeletingFile, setIsDeletingFile] = useState(false); + const [selectedItem, setSelectedItem] = useState>(); const renderToolsRight = () => { return [ @@ -148,135 +130,22 @@ const FilesTable: FC = ({ }, ]; - return ( - <> - -

{title}

-
- - {!!description && ( - <> - - {description} - - )} - - - ); -}; - -export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) => { - const exampleFilesQuery = useQuery(['files'], () => files.example.list(), { - refetchOnWindowFocus: false, - }); - - const exampleFilesNotListedQuery = useQuery( - ['filesNotListed'], - () => files.exampleNotListed.list(), - { - refetchOnWindowFocus: false, - } - ); - - const exampleFilesNotDeletableQuery = useQuery( - ['filesNotDeletable'], - () => files.exampleNotDeletable.list(), - { - refetchOnWindowFocus: false, - } - ); - - const [showUploadModal, setShowUploadModal] = useState(false); - const [showFilePickerModal, setShowFilePickerModal] = useState(false); - const [activeFileTypeId, setActiveFileTypeId] = useState('filesExample'); - const [selectedItem, setSelectedItem] = useState>(); - - const commonProps = { - files, - notifications, - setSelectedItem, - }; - - const getOpenModalHandlers = (fileTypeId: FileTypeId) => ({ - setShowFilePickerModal: (value: boolean) => { - setActiveFileTypeId(fileTypeId); - setShowFilePickerModal(value); - }, - setShowUploadModal: (value: boolean) => { - setActiveFileTypeId(fileTypeId); - setShowUploadModal(value); - }, - }); - - const getUseQueryResult = (fileTypeId: FileTypeId): UseQueryResult => { - switch (fileTypeId) { - case 'filesExampleNoMgtList': - return exampleFilesNotListedQuery; - case 'filesExampleNoMgtDelete': - return exampleFilesNotDeletableQuery; - default: - return exampleFilesQuery; - } - }; - return ( <> - {/* Table of files with ALL UI actions in Management UI */} - - - - {/* Table of files that are not listed in the Management UI */} - - The files uploaded in this table are not listed in the Management {'>'} Kibana{' '} - {'>'} Files UI - - ), - }} - /> - - - {/* Table of files that are not deletable in the Management UI */} - - The files uploaded in this table are not deletable in the Management {'>'} Kibana{' '} - {'>'} Files UI - - ), + @@ -290,11 +159,10 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = {showUploadModal && ( setShowUploadModal(false)} onUploaded={() => { notifications.toasts.addSuccess('Uploaded file!'); - getUseQueryResult(activeFileTypeId).refetch(); + refetch(); setShowUploadModal(false); }} /> @@ -302,12 +170,11 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) = {showFilePickerModal && ( setShowFilePickerModal(false)} - fileKind={activeFileTypeId} onUpload={() => { notifications.toasts.addSuccess({ title: 'Uploaded files', }); - getUseQueryResult(activeFileTypeId).refetch(); + refetch(); }} onDone={(ids) => { notifications.toasts.addSuccess({ diff --git a/examples/files_example/public/components/file_picker.tsx b/examples/files_example/public/components/file_picker.tsx index 061463bd66e90..3e94029f3a0f8 100644 --- a/examples/files_example/public/components/file_picker.tsx +++ b/examples/files_example/public/components/file_picker.tsx @@ -9,19 +9,20 @@ import React from 'react'; import type { FunctionComponent } from 'react'; +import { exampleFileKind } from '../../common'; + import { FilePicker } from '../imports'; interface Props { - fileKind: string; onClose: () => void; onUpload: (ids: string[]) => void; onDone: (ids: string[]) => void; } -export const MyFilePicker: FunctionComponent = ({ fileKind, onClose, onDone, onUpload }) => { +export const MyFilePicker: FunctionComponent = ({ onClose, onDone, onUpload }) => { return ( onDone(files.map((f) => f.id))} onUpload={(n) => onUpload(n.map(({ id }) => id))} diff --git a/examples/files_example/public/components/modal.tsx b/examples/files_example/public/components/modal.tsx index 0126ae0f3dcc5..1471c469b6351 100644 --- a/examples/files_example/public/components/modal.tsx +++ b/examples/files_example/public/components/modal.tsx @@ -9,17 +9,16 @@ import type { FunctionComponent } from 'react'; import React from 'react'; import { EuiModal, EuiModalHeader, EuiModalBody, EuiText } from '@elastic/eui'; -import type { MyImageMetadata } from '../../common'; +import { exampleFileKind, MyImageMetadata } from '../../common'; import { FilesClient, FileUpload } from '../imports'; interface Props { client: FilesClient; - fileKind: string; onDismiss: () => void; onUploaded: () => void; } -export const Modal: FunctionComponent = ({ fileKind, onDismiss, onUploaded, client }) => { +export const Modal: FunctionComponent = ({ onDismiss, onUploaded, client }) => { return ( @@ -28,7 +27,12 @@ export const Modal: FunctionComponent = ({ fileKind, onDismiss, onUploade - + ); diff --git a/examples/files_example/public/plugin.ts b/examples/files_example/public/plugin.ts index a3c0e8a129ec6..b36928e5f5566 100644 --- a/examples/files_example/public/plugin.ts +++ b/examples/files_example/public/plugin.ts @@ -8,14 +8,7 @@ import { AppNavLinkStatus } from '@kbn/core-application-browser'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; -import { - PLUGIN_ID, - PLUGIN_NAME, - exampleFileKind, - MyImageMetadata, - exampleFileKindNotListedInMangementUI, - exampleFileKindNotDeletableInMangementUI, -} from '../common'; +import { PLUGIN_ID, PLUGIN_NAME, exampleFileKind, MyImageMetadata } from '../common'; import { FilesExamplePluginsStart, FilesExamplePluginsSetup } from './types'; export class FilesExamplePlugin @@ -29,25 +22,6 @@ export class FilesExamplePlugin id: exampleFileKind.id, allowedMimeTypes: exampleFileKind.allowedMimeTypes, }); - files.registerFileKind({ - id: exampleFileKindNotListedInMangementUI.id, - allowedMimeTypes: exampleFileKindNotListedInMangementUI.allowedMimeTypes, - managementUiActions: { - list: { - enabled: false, - }, - }, - }); - files.registerFileKind({ - id: exampleFileKindNotDeletableInMangementUI.id, - allowedMimeTypes: exampleFileKindNotDeletableInMangementUI.allowedMimeTypes, - managementUiActions: { - delete: { - enabled: false, - reason: 'This file is too cool to be deleted.', - }, - }, - }); developerExamples.register({ appId: PLUGIN_ID, @@ -71,12 +45,6 @@ export class FilesExamplePlugin files: { unscoped: deps.files.filesClientFactory.asUnscoped(), example: deps.files.filesClientFactory.asScoped(exampleFileKind.id), - exampleNotListed: deps.files.filesClientFactory.asScoped( - exampleFileKindNotListedInMangementUI.id - ), - exampleNotDeletable: deps.files.filesClientFactory.asScoped( - exampleFileKindNotDeletableInMangementUI.id - ), }, }, params diff --git a/examples/files_example/public/types.ts b/examples/files_example/public/types.ts index 4c3a6329bda8c..4ca9eb148872f 100644 --- a/examples/files_example/public/types.ts +++ b/examples/files_example/public/types.ts @@ -28,10 +28,6 @@ export interface FileClients { unscoped: FilesClient; // Example file kind example: ScopedFilesClient; - // Example file kind not listed in the management UI - exampleNotListed: ScopedFilesClient; - // Example file kind not deletable in the management UI - exampleNotDeletable: ScopedFilesClient; } export interface AppPluginStartDependencies { diff --git a/examples/files_example/server/plugin.ts b/examples/files_example/server/plugin.ts index ba5a1cd0b5510..5ab9571a64207 100644 --- a/examples/files_example/server/plugin.ts +++ b/examples/files_example/server/plugin.ts @@ -7,11 +7,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; -import { - exampleFileKind, - exampleFileKindNotListedInMangementUI, - exampleFileKindNotDeletableInMangementUI, -} from '../common'; +import { exampleFileKind } from '../common'; import type { FilesExamplePluginsSetup, FilesExamplePluginsStart } from './types'; export class FilesExamplePlugin @@ -27,8 +23,6 @@ export class FilesExamplePlugin this.logger.debug('filesExample: Setup'); files.registerFileKind(exampleFileKind); - files.registerFileKind(exampleFileKindNotListedInMangementUI); - files.registerFileKind(exampleFileKindNotDeletableInMangementUI); return {}; } From 1e1f24cb9bf8fca78def6a8e8b4f102d1d511c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 24 Apr 2023 12:29:47 +0100 Subject: [PATCH 14/15] Add TS comments --- packages/shared-ux/file/types/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared-ux/file/types/index.ts b/packages/shared-ux/file/types/index.ts index 9a8204fdc867a..86b9e47fdab43 100644 --- a/packages/shared-ux/file/types/index.ts +++ b/packages/shared-ux/file/types/index.ts @@ -255,9 +255,11 @@ export interface FileKindBrowser extends FileKindBase { * */ 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). */ From ac2153b29d43c2f45d914dbdfc8f089715a29d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 24 Apr 2023 13:39:23 +0100 Subject: [PATCH 15/15] Remove row action type --- packages/content-management/table_list/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list/src/types.ts index 177f3d47c34c0..c8e734a289451 100644 --- a/packages/content-management/table_list/src/types.ts +++ b/packages/content-management/table_list/src/types.ts @@ -13,7 +13,7 @@ export interface Tag { color: string; } -export type TableRowAction = 'list' | 'delete'; +export type TableRowAction = 'delete'; export type RowActions = { [action in TableRowAction]?: {