mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[Files] Files management (#144425)
## Summary Files management UI that rounds out the files MVP. This is UI is intended to be progressively enhanced and provides a way for system administrators get some insight and manage the files created and stored in Kibana. ## To reviewers * This is UI for retrieval and deletion of files (the R+D of CRUD) * Creating and deleting tags to be supported in a future version * This UI is intended to form part of the broader content management experience * We use the `TableListView` component as far as possible ## How to test 1. Start Kibana with `yarn start --run-examples` 2. Go to the "Developer Examples" from the left nav menu 3. Go to the "Files example" plugin 4. Click the "Upload file" button, upload a few different image types (PNG, JPG and WEBP) 5. Go to "Stack management" > "Files" 6. Behold your files in the management UI 7. (Bonus) check that the UI and API `GET /api/files/find`, `GET /api/files/metrics` and `DELETE /api/files/blobs` are not accessible to non-admin or appropriately privileged users (i.e., those with "Files management" access). ## List of functionality - [x] List all saved objects (scoped to admin) - [x] Is able to bulk-delete files - [x] Shows basic storage diagnostics - [x] Is able to search and filter files ## Screenshots <details> <summary>screenshots</summary> <img width="1545" alt="Screenshot 2022-11-08 at 13 56 54" src="https://user-images.githubusercontent.com/8155004/200570783-cfefdbf3-c5ff-4ece-ba24-48a455fcca75.png"> <img width="910" alt="Screenshot 2022-11-10 at 12 52 35" src="https://user-images.githubusercontent.com/8155004/201083812-bc9f25f5-b423-43a6-9229-5e2a4cdd943a.png"> <img width="451" alt="Screenshot 2022-11-10 at 12 37 07" src="https://user-images.githubusercontent.com/8155004/201081039-832a1980-684c-4abb-bb05-0c7c6a849d4d.png"> <img width="959" alt="Screenshot 2022-11-08 at 13 57 15" src="https://user-images.githubusercontent.com/8155004/200570797-f122cff5-7043-4e01-9b51-d5663c1b26d6.png"> <img width="500" alt="Screenshot 2022-11-08 at 13 57 38" src="https://user-images.githubusercontent.com/8155004/200570801-35cdbd99-0256-4dee-9f78-2f6ad853305f.png"> </details> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
759fb032f7
commit
a166fba83d
45 changed files with 1031 additions and 59 deletions
|
@ -39,6 +39,7 @@
|
|||
"eventAnnotation": "src/plugins/event_annotation",
|
||||
"fieldFormats": "src/plugins/field_formats",
|
||||
"files": "src/plugins/files",
|
||||
"filesManagement": "src/plugins/files_management",
|
||||
"flot": "packages/kbn-flot-charts/lib",
|
||||
"guidedOnboarding": "src/plugins/guided_onboarding",
|
||||
"guidedOnboardingPackage": "packages/kbn-guided-onboarding",
|
||||
|
|
|
@ -180,6 +180,10 @@ for use in their own application.
|
|||
|The files service provides functionality to manage, retrieve, share files in Kibana.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/files_management/README.md[filesManagement]
|
||||
|Minimal interface for admins to manage files in Kibana.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/guided_onboarding/README.md[guidedOnboarding]
|
||||
|This plugin contains the code for the Guided Onboarding project. Guided onboarding consists of guides for Solutions (Enterprise Search, Observability, Security) that can be completed as a checklist of steps. The guides help users to ingest their data and to navigate to the correct Solutions pages.
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"prefix": "filesExample",
|
||||
"paths": {
|
||||
"filesExample": "."
|
||||
},
|
||||
"translations": ["translations/ja-JP.json"]
|
||||
}
|
|
@ -17,7 +17,7 @@ const httpTags = {
|
|||
|
||||
export const exampleFileKind: FileKind = {
|
||||
id: PLUGIN_ID,
|
||||
allowedMimeTypes: ['image/png'],
|
||||
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'text/plain'],
|
||||
http: {
|
||||
create: httpTags,
|
||||
delete: httpTags,
|
||||
|
|
|
@ -140,4 +140,8 @@ export const getStoryArgTypes = () => ({
|
|||
},
|
||||
defaultValue: 20,
|
||||
},
|
||||
asManagementSection: {
|
||||
control: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -79,6 +79,21 @@ export interface Props<T extends UserContentCommonSchema = UserContentCommonSche
|
|||
createItem?(): void;
|
||||
deleteItems?(items: T[]): Promise<void>;
|
||||
editItem?(item: T): void;
|
||||
/**
|
||||
* Name for the column containing the "title" value.
|
||||
*/
|
||||
titleColumnName?: string;
|
||||
/**
|
||||
* Additional actions (buttons) to be placed in the page header.
|
||||
* @note only the first two values will be used.
|
||||
*/
|
||||
additionalRightSideActions?: ReactNode[];
|
||||
/**
|
||||
* This assumes the content is already wrapped in an outer PageTemplate component.
|
||||
* @note Hack! This is being used as a workaround so that this page can be rendered in the Kibana management UI
|
||||
* @deprecated
|
||||
*/
|
||||
withoutPageTemplateWrapper?: boolean;
|
||||
inspector?: InspectorConfig;
|
||||
}
|
||||
|
||||
|
@ -135,6 +150,9 @@ function TableListViewComp<T extends UserContentCommonSchema>({
|
|||
id = 'userContent',
|
||||
inspector = { enabled: false },
|
||||
children,
|
||||
titleColumnName,
|
||||
additionalRightSideActions = [],
|
||||
withoutPageTemplateWrapper,
|
||||
}: Props<T>) {
|
||||
if (!getDetailViewLink && !onClickTitle) {
|
||||
throw new Error(
|
||||
|
@ -260,9 +278,11 @@ function TableListViewComp<T extends UserContentCommonSchema>({
|
|||
const columns: Array<EuiBasicTableColumn<T>> = [
|
||||
{
|
||||
field: 'attributes.title',
|
||||
name: i18n.translate('contentManagement.tableList.mainColumnName', {
|
||||
defaultMessage: 'Name, description, tags',
|
||||
}),
|
||||
name:
|
||||
titleColumnName ??
|
||||
i18n.translate('contentManagement.tableList.mainColumnName', {
|
||||
defaultMessage: 'Name, description, tags',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (field: keyof T, record: T) => {
|
||||
return (
|
||||
|
@ -363,6 +383,7 @@ function TableListViewComp<T extends UserContentCommonSchema>({
|
|||
|
||||
return columns;
|
||||
}, [
|
||||
titleColumnName,
|
||||
customTableColumn,
|
||||
hasUpdatedAtMetadata,
|
||||
editItem,
|
||||
|
@ -551,23 +572,30 @@ function TableListViewComp<T extends UserContentCommonSchema>({
|
|||
return null;
|
||||
}
|
||||
|
||||
const PageTemplate = withoutPageTemplateWrapper
|
||||
? (React.Fragment as unknown as typeof KibanaPageTemplate)
|
||||
: KibanaPageTemplate;
|
||||
|
||||
if (!showFetchError && hasNoItems) {
|
||||
return (
|
||||
<KibanaPageTemplate panelled isEmptyState={true} data-test-subj={pageDataTestSubject}>
|
||||
<PageTemplate panelled isEmptyState={true} data-test-subj={pageDataTestSubject}>
|
||||
<KibanaPageTemplate.Section
|
||||
aria-labelledby={hasInitialFetchReturned ? headingId : undefined}
|
||||
>
|
||||
{renderNoItemsMessage()}
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate panelled data-test-subj={pageDataTestSubject}>
|
||||
<PageTemplate panelled data-test-subj={pageDataTestSubject}>
|
||||
<KibanaPageTemplate.Header
|
||||
pageTitle={<span id={headingId}>{tableListTitle}</span>}
|
||||
rightSideItems={[renderCreateButton() ?? <span />]}
|
||||
rightSideItems={[
|
||||
renderCreateButton() ?? <span />,
|
||||
...additionalRightSideActions?.slice(0, 2),
|
||||
]}
|
||||
data-test-subj="top-nav"
|
||||
/>
|
||||
<KibanaPageTemplate.Section aria-labelledby={hasInitialFetchReturned ? headingId : undefined}>
|
||||
|
@ -623,7 +651,7 @@ function TableListViewComp<T extends UserContentCommonSchema>({
|
|||
/>
|
||||
)}
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ pageLoadAssetSize:
|
|||
features: 21723
|
||||
fieldFormats: 65209
|
||||
files: 22673
|
||||
filesManagement: 18683
|
||||
fileUpload: 25664
|
||||
fleet: 126917
|
||||
globalSearch: 29696
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"prefix": "files",
|
||||
"paths": {
|
||||
"files": "."
|
||||
},
|
||||
"translations": ["translations/ja-JP.json"]
|
||||
}
|
|
@ -72,3 +72,4 @@ export type { Endpoint as FileUnshareHttpEndpoint } from '../server/routes/file_
|
|||
export type { Endpoint as FileGetShareHttpEndpoint } from '../server/routes/file_kind/share/get';
|
||||
export type { Endpoint as FileListSharesHttpEndpoint } from '../server/routes/file_kind/share/list';
|
||||
export type { Endpoint as FilePublicDownloadHttpEndpoint } from '../server/routes/public_facing/download';
|
||||
export type { Endpoint as BulkDeleteHttpEndpoint } from '../server/routes/bulk_delete';
|
||||
|
|
|
@ -48,6 +48,7 @@ export const apiRoutes = {
|
|||
*/
|
||||
getFindRoute: () => `${API_BASE_PATH}/find`,
|
||||
getMetricsRoute: () => `${API_BASE_PATH}/metrics`,
|
||||
getBulkDeleteRoute: () => `${API_BASE_PATH}/blobs`,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -86,6 +87,12 @@ export function createFilesClient({
|
|||
fileKind?: string;
|
||||
}): FilesClient | ScopedFilesClient {
|
||||
const api: FilesClient = {
|
||||
bulkDelete: (args) => {
|
||||
return http.delete(apiRoutes.getBulkDeleteRoute(), {
|
||||
headers: commonBodyHeaders,
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
},
|
||||
create: ({ kind, ...args }) => {
|
||||
return http.post(apiRoutes.getCreateFileRoute(scopedFileKind ?? kind), {
|
||||
headers: commonBodyHeaders,
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { FilesClient } from './types';
|
|||
// TODO: Remove this once we have access to the shared file client mock
|
||||
export const createMockFilesClient = (): DeeplyMockedKeys<FilesClient> => ({
|
||||
create: jest.fn(),
|
||||
bulkDelete: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
download: jest.fn(),
|
||||
find: jest.fn(),
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FileJSON } from '../common';
|
|||
import type {
|
||||
FindFilesHttpEndpoint,
|
||||
FileShareHttpEndpoint,
|
||||
BulkDeleteHttpEndpoint,
|
||||
FileUnshareHttpEndpoint,
|
||||
FileGetShareHttpEndpoint,
|
||||
FilesMetricsHttpEndpoint,
|
||||
|
@ -58,6 +59,12 @@ interface GlobalEndpoints {
|
|||
* @param args - File filters
|
||||
*/
|
||||
find: UnscopedClientMethodFrom<FindFilesHttpEndpoint>;
|
||||
/**
|
||||
* Bulk a delete a set of files given their IDs.
|
||||
*
|
||||
* @param args - Bulk delete args
|
||||
*/
|
||||
bulkDelete: UnscopedClientMethodFrom<BulkDeleteHttpEndpoint>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,10 +46,6 @@ export interface UpdateFileArgs {
|
|||
* File ID.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* File kind, must correspond to a registered {@link FileKind}.
|
||||
*/
|
||||
fileKind: string;
|
||||
/**
|
||||
* Attributes to update.
|
||||
*/
|
||||
|
@ -64,10 +60,6 @@ export interface DeleteFileArgs {
|
|||
* File ID.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* File kind, must correspond to a registered {@link FileKind}.
|
||||
*/
|
||||
fileKind: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,10 +70,6 @@ export interface GetByIdArgs {
|
|||
* File ID.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* File kind, must correspond to a registered {@link FileKind}.
|
||||
*/
|
||||
fileKind: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -54,13 +54,13 @@ export class InternalFileService {
|
|||
}
|
||||
}
|
||||
|
||||
public async updateFile({ attributes, fileKind, id }: UpdateFileArgs): Promise<IFile> {
|
||||
const file = await this.getById({ fileKind, id });
|
||||
public async updateFile({ attributes, id }: UpdateFileArgs): Promise<IFile> {
|
||||
const file = await this.getById({ id });
|
||||
return await file.update(attributes);
|
||||
}
|
||||
|
||||
public async deleteFile({ id, fileKind }: DeleteFileArgs): Promise<void> {
|
||||
const file = await this.getById({ id, fileKind });
|
||||
public async deleteFile({ id }: DeleteFileArgs): Promise<void> {
|
||||
const file = await this.getById({ id });
|
||||
await file.delete();
|
||||
}
|
||||
|
||||
|
@ -80,12 +80,8 @@ export class InternalFileService {
|
|||
}
|
||||
}
|
||||
|
||||
public async getById({ fileKind, id }: GetByIdArgs): Promise<IFile> {
|
||||
const file = await this.get(id);
|
||||
if (file.data.fileKind !== fileKind) {
|
||||
throw new Error(`Unexpected file kind "${file.data.fileKind}", expected "${fileKind}".`);
|
||||
}
|
||||
return file;
|
||||
public async getById({ id }: GetByIdArgs): Promise<IFile> {
|
||||
return await this.get(id);
|
||||
}
|
||||
|
||||
public getFileKind(id: string): FileKind {
|
||||
|
|
|
@ -134,7 +134,7 @@ describe('FileService', () => {
|
|||
|
||||
it('retrieves a file', async () => {
|
||||
const { id } = await createDisposableFile({ fileKind, name: 'test' });
|
||||
const myFile = await fileService.getById({ id, fileKind });
|
||||
const myFile = await fileService.getById({ id });
|
||||
expect(myFile?.id).toMatch(id);
|
||||
});
|
||||
|
||||
|
@ -203,7 +203,7 @@ describe('FileService', () => {
|
|||
expect(updatedFile1.data.alt).toBe(updatableFields.alt);
|
||||
|
||||
// Fetch the file anew to be doubly sure
|
||||
const updatedFile2 = await fileService.getById<CustomMeta>({ fileKind, id: file.id });
|
||||
const updatedFile2 = await fileService.getById<CustomMeta>({ id: file.id });
|
||||
expect(updatedFile2.data.meta).toEqual(expect.objectContaining(updatableFields.meta));
|
||||
// Below also tests that our meta type is work as expected by using `some` field.
|
||||
expect(updatedFile2.data.meta?.some).toBe(updatableFields.meta.some);
|
||||
|
|
|
@ -17,6 +17,7 @@ export * from '../../common/api_routes';
|
|||
|
||||
export const FILES_API_ROUTES = {
|
||||
find: `${API_BASE_PATH}/find`,
|
||||
bulkDelete: `${API_BASE_PATH}/blobs`,
|
||||
metrics: `${API_BASE_PATH}/metrics`,
|
||||
public: {
|
||||
download: `${FILES_PUBLIC_API_BASE_PATH}/blob/{fileName?}`,
|
||||
|
|
73
src/plugins/files/server/routes/bulk_delete.ts
Normal file
73
src/plugins/files/server/routes/bulk_delete.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { CreateHandler, FilesRouter } from './types';
|
||||
import { FILES_MANAGE_PRIVILEGE } from '../../common/constants';
|
||||
import { FILES_API_ROUTES, CreateRouteDefinition } from './api_routes';
|
||||
|
||||
const method = 'delete' as const;
|
||||
|
||||
const rt = {
|
||||
body: schema.object({
|
||||
ids: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1, maxSize: 100 }),
|
||||
}),
|
||||
};
|
||||
|
||||
interface Result {
|
||||
/**
|
||||
* The files that were deleted
|
||||
*/
|
||||
succeeded: string[];
|
||||
/**
|
||||
* Any failed deletions. Only included in the response if there were failures.
|
||||
*/
|
||||
failed?: Array<[id: string, reason: string]>;
|
||||
}
|
||||
|
||||
export type Endpoint = CreateRouteDefinition<typeof rt, Result>;
|
||||
|
||||
const handler: CreateHandler<Endpoint> = async ({ files }, req, res) => {
|
||||
const fileService = (await files).fileService.asCurrentUser();
|
||||
const {
|
||||
body: { ids },
|
||||
} = req;
|
||||
|
||||
const succeeded: Result['succeeded'] = [];
|
||||
const failed: Result['failed'] = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await fileService.delete({ id });
|
||||
succeeded.push(id);
|
||||
} catch (e) {
|
||||
failed.push([id, e.message]);
|
||||
}
|
||||
}
|
||||
|
||||
const body: Endpoint['output'] = {
|
||||
succeeded,
|
||||
failed: failed.length ? failed : undefined,
|
||||
};
|
||||
|
||||
return res.ok({
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
export function register(router: FilesRouter) {
|
||||
router[method](
|
||||
{
|
||||
path: FILES_API_ROUTES.bulkDelete,
|
||||
validate: { ...rt },
|
||||
options: {
|
||||
tags: [`access:${FILES_MANAGE_PRIVILEGE}`],
|
||||
},
|
||||
},
|
||||
handler
|
||||
);
|
||||
}
|
|
@ -24,7 +24,7 @@ export async function getById(
|
|||
): Promise<ResultOrHttpError> {
|
||||
let result: undefined | File;
|
||||
try {
|
||||
result = await fileService.getById({ id, fileKind });
|
||||
result = await fileService.getById({ id });
|
||||
} catch (e) {
|
||||
let error: undefined | IKibanaResponse;
|
||||
if (e instanceof errors.FileNotFoundError) {
|
||||
|
|
|
@ -15,8 +15,8 @@ import { page, pageSize } from './common_schemas';
|
|||
|
||||
const method = 'post' as const;
|
||||
|
||||
const string64 = schema.string({ maxLength: 64 });
|
||||
const string256 = schema.string({ maxLength: 256 });
|
||||
const string64 = schema.string({ minLength: 1, maxLength: 64 });
|
||||
const string256 = schema.string({ minLength: 1, maxLength: 256 });
|
||||
|
||||
export const stringOrArrayOfStrings = schema.oneOf([string64, schema.arrayOf(string64)]);
|
||||
export const nameStringOrArrayOfNameStrings = schema.oneOf([string256, schema.arrayOf(string256)]);
|
||||
|
|
|
@ -10,12 +10,13 @@ import { FilesRouter } from './types';
|
|||
|
||||
import * as find from './find';
|
||||
import * as metrics from './metrics';
|
||||
import * as bulkDelete from './bulk_delete';
|
||||
import * as publicDownload from './public_facing/download';
|
||||
|
||||
export { registerFileKindRoutes } from './file_kind';
|
||||
|
||||
export function registerRoutes(router: FilesRouter) {
|
||||
[find, metrics, publicDownload].forEach((endpoint) => {
|
||||
[find, metrics, bulkDelete, publicDownload].forEach((endpoint) => {
|
||||
endpoint.register(router);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { FileJSON } from '../../../common';
|
||||
import type { CreateFileKindHttpEndpoint } from '../../../common/api_routes';
|
||||
import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils';
|
||||
|
||||
|
@ -201,6 +202,32 @@ describe('File HTTP API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('bulk delete', () => {
|
||||
afterEach(async () => {
|
||||
await testHarness.cleanupAfterEach();
|
||||
});
|
||||
it('bulk deletes files', async () => {
|
||||
const [file1] = await Promise.all([
|
||||
createFile({}, { deleteAfterTest: false }),
|
||||
createFile(),
|
||||
createFile(),
|
||||
]);
|
||||
{
|
||||
const { body: response } = await request
|
||||
.delete(root, `/api/files/blobs`)
|
||||
.send({ ids: [file1.id, 'unknown'] })
|
||||
.expect(200);
|
||||
expect(response.succeeded).toEqual([file1.id]);
|
||||
expect(response.failed).toEqual([['unknown', 'File not found']]);
|
||||
}
|
||||
{
|
||||
const { body: response } = await request.post(root, `/api/files/find`).send({}).expect(200);
|
||||
expect(response.files).toHaveLength(2);
|
||||
expect(response.files.find((file: FileJSON) => file.id === file1.id)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('public download', () => {
|
||||
afterEach(async () => {
|
||||
await testHarness.cleanupAfterEach();
|
||||
|
|
|
@ -32,7 +32,8 @@ export async function setupIntegrationEnvironment() {
|
|||
alt: string;
|
||||
meta: Record<string, any>;
|
||||
mimeType: string;
|
||||
}> = {}
|
||||
}> = {},
|
||||
{ deleteAfterTest = true }: { deleteAfterTest?: boolean } = {}
|
||||
): Promise<FileJSON> => {
|
||||
const result = await request
|
||||
.post(root, `/api/files/files/${fileKind}`)
|
||||
|
@ -45,12 +46,14 @@ export async function setupIntegrationEnvironment() {
|
|||
})
|
||||
)
|
||||
.expect(200);
|
||||
disposables.push(async () => {
|
||||
await request
|
||||
.delete(root, `/api/files/files/${fileKind}/${result.body.file.id}`)
|
||||
.send()
|
||||
.expect(200);
|
||||
});
|
||||
if (deleteAfterTest) {
|
||||
disposables.push(async () => {
|
||||
await request
|
||||
.delete(root, `/api/files/files/${fileKind}/${result.body.file.id}`)
|
||||
.send()
|
||||
.expect(200);
|
||||
});
|
||||
}
|
||||
return result.body.file;
|
||||
};
|
||||
|
||||
|
|
12
src/plugins/files_management/README.md
Executable file
12
src/plugins/files_management/README.md
Executable file
|
@ -0,0 +1,12 @@
|
|||
# Files management
|
||||
|
||||
Minimal interface for admins to manage files in Kibana.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
This UI is intended to become part of the Kibana Content Management UI. This will
|
||||
be a broader user-experience than only files, i.e., aimed at "content" more generally.
|
||||
|
||||
Do not add new file-specific features that does not fit with this vision.
|
||||
|
14
src/plugins/files_management/common/index.ts
Executable file
14
src/plugins/files_management/common/index.ts
Executable file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const PLUGIN_ID = 'filesManagement';
|
||||
export const PLUGIN_NAME = i18n.translate('filesManagement.name', {
|
||||
defaultMessage: 'Files',
|
||||
});
|
15
src/plugins/files_management/kibana.json
Executable file
15
src/plugins/files_management/kibana.json
Executable file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "filesManagement",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "@elastic/kibana-global-experience",
|
||||
"githubTeam": "@elastic/kibana-global-experience"
|
||||
},
|
||||
"description": "Simple UI for managing files in Kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["files", "management"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["kibanaReact"]
|
||||
}
|
82
src/plugins/files_management/public/app.tsx
Normal file
82
src/plugins/files_management/public/app.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
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;
|
||||
|
||||
function naivelyFuzzify(query: string): string {
|
||||
return query.includes('*') ? query : `*${query}*`;
|
||||
}
|
||||
|
||||
export const App: FunctionComponent = () => {
|
||||
const { filesClient } = useFilesManagementContext();
|
||||
const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<undefined | FileJSON>(undefined);
|
||||
return (
|
||||
<>
|
||||
<TableListView<FilesUserContentSchema>
|
||||
titleColumnName={i18nTexts.titleColumnName}
|
||||
emptyPrompt={<EmptyPrompt />}
|
||||
entityName={i18nTexts.entityName}
|
||||
entityNamePlural={i18nTexts.entityNamePlural}
|
||||
findItems={(searchQuery) =>
|
||||
filesClient
|
||||
.find({ name: searchQuery ? naivelyFuzzify(searchQuery) : undefined })
|
||||
.then(({ files, total }) => ({
|
||||
hits: files.map((file) => ({
|
||||
id: file.id,
|
||||
updatedAt: file.updated,
|
||||
references: [],
|
||||
type: 'file',
|
||||
attributes: {
|
||||
title: file.name + (file.extension ? `.${file.extension}` : ''),
|
||||
...file,
|
||||
},
|
||||
})),
|
||||
total,
|
||||
}))
|
||||
}
|
||||
customTableColumn={{
|
||||
name: i18nTexts.size,
|
||||
field: 'attributes.size',
|
||||
render: (value: any) => value && numeral(value).format('0[.]0 b'),
|
||||
sortable: true,
|
||||
}}
|
||||
initialFilter=""
|
||||
initialPageSize={50}
|
||||
listingLimit={1000}
|
||||
tableListTitle={i18nTexts.tableListTitle}
|
||||
onClickTitle={({ attributes }) => setSelectedFile(attributes as unknown as FileJSON)}
|
||||
deleteItems={async (items) => {
|
||||
await filesClient.bulkDelete({ ids: items.map(({ id }) => id) });
|
||||
}}
|
||||
withoutPageTemplateWrapper
|
||||
additionalRightSideActions={[
|
||||
<EuiButtonEmpty onClick={() => setShowDiagnosticsFlyout(true)}>
|
||||
{i18nTexts.diagnosticsFlyoutTitle}
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
/>
|
||||
{showDiagnosticsFlyout && (
|
||||
<DiagnosticsFlyout onClose={() => setShowDiagnosticsFlyout(false)} />
|
||||
)}
|
||||
{Boolean(selectedFile) && (
|
||||
<FileFlyout file={selectedFile!} onClose={() => setSelectedFile(undefined)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiEmptyPrompt,
|
||||
EuiStat,
|
||||
EuiFlexGroup,
|
||||
EuiSpacer,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { Chart, Axis, Position, HistogramBarSeries, ScaleType } from '@elastic/charts';
|
||||
import numeral from '@elastic/numeral';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { i18nTexts } from '../i18n_texts';
|
||||
import { useFilesManagementContext } from '../context';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DiagnosticsFlyout: FunctionComponent<Props> = ({ onClose }) => {
|
||||
const { filesClient } = useFilesManagementContext();
|
||||
const { status, refetch, data, isLoading, error } = useQuery(['filesDiagnostics'], async () => {
|
||||
return filesClient.getMetrics();
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlyout ownFocus onClose={onClose} size="s">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{i18nTexts.diagnosticsFlyoutTitle}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{status === 'error' ? (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="xs"
|
||||
title={<h3>{i18nTexts.failedToFetchDiagnostics}</h3>}
|
||||
body={(error as Error)?.message ?? ''}
|
||||
color="danger"
|
||||
actions={[
|
||||
<EuiButton isLoading={isLoading} color="danger" onClick={() => refetch()}>
|
||||
{i18nTexts.retry}
|
||||
</EuiButton>,
|
||||
]}
|
||||
/>
|
||||
) : status === 'loading' ? (
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
) : (
|
||||
<>
|
||||
<EuiPanel hasBorder hasShadow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{i18nTexts.diagnosticsFlyoutSummarySectionTitle}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiStat
|
||||
title={numeral(data.storage.esFixedSizeIndex.used).format('0[.]0 b')}
|
||||
description={i18nTexts.diagnosticsSpaceUsed}
|
||||
titleSize="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiStat
|
||||
title={Object.values(data.countByStatus).reduce((acc, value) => acc + value, 0)}
|
||||
description={i18nTexts.diagnosticsTotalCount}
|
||||
titleSize="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel hasBorder hasShadow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{i18nTexts.diagnosticsBreakdownsStatus}</h3>
|
||||
</EuiTitle>
|
||||
<Chart size={{ height: 200, width: '100%' }}>
|
||||
<Axis id="y" position={Position.Left} showOverlappingTicks />
|
||||
<Axis id="x" position={Position.Bottom} showOverlappingTicks />
|
||||
<HistogramBarSeries
|
||||
data={Object.entries(data.countByStatus).map(([key, count]) => ({
|
||||
key,
|
||||
count,
|
||||
}))}
|
||||
id="Status"
|
||||
xAccessor={'key'}
|
||||
yAccessors={['count']}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
timeZone="local"
|
||||
/>
|
||||
</Chart>
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
<EuiPanel hasBorder hasShadow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{i18nTexts.diagnosticsBreakdownsExtension}</h3>
|
||||
</EuiTitle>
|
||||
<Chart size={{ height: 200, width: '100%' }}>
|
||||
<Axis id="y" position={Position.Left} showOverlappingTicks />
|
||||
<Axis id="x" position={Position.Bottom} showOverlappingTicks />
|
||||
<HistogramBarSeries
|
||||
data={Object.entries(data.countByExtension).map(([key, count]) => ({
|
||||
key,
|
||||
count,
|
||||
}))}
|
||||
id="Extension"
|
||||
xAccessor={'key'}
|
||||
yAccessors={['count']}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
timeZone="local"
|
||||
/>
|
||||
</Chart>
|
||||
</EuiPanel>
|
||||
</>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18nTexts } from '../i18n_texts';
|
||||
|
||||
export const EmptyPrompt: FunctionComponent = () => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<h3>{i18nTexts.emptyPromptTitle}</h3>}
|
||||
body={i18nTexts.emptyPromptDescription}
|
||||
/>
|
||||
);
|
||||
};
|
121
src/plugins/files_management/public/components/file_flyout.tsx
Normal file
121
src/plugins/files_management/public/components/file_flyout.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiDescriptionList,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiSpacer,
|
||||
EuiFlyoutFooter,
|
||||
EuiButtonEmpty,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import type { FileJSON } from '@kbn/files-plugin/common';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import { Image } from '@kbn/files-plugin/public';
|
||||
import React from 'react';
|
||||
import { i18nTexts } from '../i18n_texts';
|
||||
import { useFilesManagementContext } from '../context';
|
||||
|
||||
interface Props {
|
||||
file: FileJSON;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const FileFlyout: FunctionComponent<Props> = ({ onClose, file }) => {
|
||||
const { filesClient } = useFilesManagementContext();
|
||||
return (
|
||||
<EuiFlyout ownFocus onClose={onClose} size="m">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2>{file.name}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={[
|
||||
{
|
||||
title: i18nTexts.filesFlyoutStatus,
|
||||
description: (
|
||||
<EuiHealth
|
||||
color={
|
||||
file.status === 'READY'
|
||||
? 'success'
|
||||
: file.status === 'AWAITING_UPLOAD' || file.status === 'UPLOADING'
|
||||
? 'primary'
|
||||
: 'warning'
|
||||
}
|
||||
>
|
||||
{i18nTexts.filesStatus[file.status]}
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18nTexts.filesFlyoutSize,
|
||||
description: numeral(file.size).format('0[.]0 b'),
|
||||
},
|
||||
{
|
||||
title: i18nTexts.filesFlyoutExtension,
|
||||
description: file.extension?.toUpperCase() ?? '',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={[
|
||||
{
|
||||
title: i18nTexts.filesFlyoutMimeType,
|
||||
description: file.mimeType ?? '',
|
||||
},
|
||||
{
|
||||
title: i18nTexts.filesFlyoutCreated,
|
||||
description: file.created,
|
||||
},
|
||||
{
|
||||
title: i18nTexts.filesFlyoutUpdated,
|
||||
description: file.updated,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{file.mimeType?.startsWith('image/') && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiHorizontalRule />
|
||||
<EuiTitle size="s">
|
||||
<h3>{i18nTexts.filesFlyoutPreview}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="center" gutterSize="none">
|
||||
<Image size="xl" alt={file.alt ?? ''} src={filesClient.getDownloadHref(file)} />
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiButtonEmpty href={filesClient.getDownloadHref(file)} iconType="download">
|
||||
{i18nTexts.filesFlyoutDownload}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
11
src/plugins/files_management/public/components/index.ts
Normal file
11
src/plugins/files_management/public/components/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { EmptyPrompt } from './empty_prompt';
|
||||
export { DiagnosticsFlyout } from './diagnostics_flyout';
|
||||
export { FileFlyout } from './file_flyout';
|
31
src/plugins/files_management/public/context.tsx
Normal file
31
src/plugins/files_management/public/context.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, FC } from 'react';
|
||||
|
||||
import type { AppContext } from './types';
|
||||
|
||||
const FilesManagementAppContext = createContext<AppContext>(null as unknown as AppContext);
|
||||
|
||||
export const FilesManagementAppContextProvider: FC<AppContext> = ({ children, filesClient }) => {
|
||||
return (
|
||||
<FilesManagementAppContext.Provider value={{ filesClient }}>
|
||||
{children}
|
||||
</FilesManagementAppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFilesManagementContext = () => {
|
||||
const ctx = useContext(FilesManagementAppContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
'useFilesManagementContext must be used within a FilesManagementAppContextProvider'
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
};
|
101
src/plugins/files_management/public/i18n_texts.ts
Normal file
101
src/plugins/files_management/public/i18n_texts.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FileStatus } from '@kbn/files-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const i18nTexts = {
|
||||
titleColumnName: i18n.translate('filesManagement.table.titleColumnName', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
tableListTitle: i18n.translate('filesManagement.table.title', { defaultMessage: 'Files' }),
|
||||
entityName: i18n.translate('filesManagement.entityName.title', { defaultMessage: 'file' }),
|
||||
retry: i18n.translate('filesManagement.button.retry', {
|
||||
defaultMessage: 'Retry',
|
||||
}),
|
||||
entityNamePlural: i18n.translate('filesManagement.entityNamePlural.title', {
|
||||
defaultMessage: 'files',
|
||||
}),
|
||||
emptyPromptTitle: i18n.translate('filesManagement.emptyPrompt.title', {
|
||||
defaultMessage: 'No files found',
|
||||
}),
|
||||
emptyPromptDescription: i18n.translate('filesManagement.emptyPrompt.description', {
|
||||
defaultMessage: 'Any files created in Kibana will be listed here.',
|
||||
}),
|
||||
size: i18n.translate('filesManagement.table.sizeColumnName', {
|
||||
defaultMessage: 'Size',
|
||||
}),
|
||||
diagnosticsFlyoutTitle: i18n.translate('filesManagement.diagnostics.flyoutTitle', {
|
||||
defaultMessage: 'Statistics',
|
||||
}),
|
||||
diagnosticsFlyoutSummarySectionTitle: i18n.translate(
|
||||
'filesManagement.diagnostics.summarySectionTitle',
|
||||
{
|
||||
defaultMessage: 'Summary',
|
||||
}
|
||||
),
|
||||
failedToFetchDiagnostics: i18n.translate('filesManagement.diagnostics.errorMessage', {
|
||||
defaultMessage: 'Could not fetch statistics',
|
||||
}),
|
||||
diagnosticsSpaceUsed: i18n.translate('filesManagement.diagnostics.spaceUsedLabel', {
|
||||
defaultMessage: 'Disk space used',
|
||||
}),
|
||||
diagnosticsTotalCount: i18n.translate('filesManagement.diagnostics.totalCountLabel', {
|
||||
defaultMessage: 'Number of files',
|
||||
}),
|
||||
diagnosticsBreakdownsStatus: i18n.translate('filesManagement.diagnostics.breakdownStatusTitle', {
|
||||
defaultMessage: 'Count by status',
|
||||
}),
|
||||
diagnosticsBreakdownsExtension: i18n.translate(
|
||||
'filesManagement.diagnostics.breakdownExtensionTitle',
|
||||
{
|
||||
defaultMessage: 'Count by extension',
|
||||
}
|
||||
),
|
||||
filesFlyoutSize: i18n.translate('filesManagement.filesFlyout.sizeLabel', {
|
||||
defaultMessage: 'Size',
|
||||
}),
|
||||
filesFlyoutExtension: i18n.translate('filesManagement.filesFlyout.extensionLabel', {
|
||||
defaultMessage: 'Extension',
|
||||
}),
|
||||
filesFlyoutMimeType: i18n.translate('filesManagement.filesFlyout.mimeTypeLabel', {
|
||||
defaultMessage: 'MIME type',
|
||||
}),
|
||||
filesFlyoutStatus: i18n.translate('filesManagement.filesFlyout.statusLabel', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
filesFlyoutCreated: i18n.translate('filesManagement.filesFlyout.createdLabel', {
|
||||
defaultMessage: 'Created',
|
||||
}),
|
||||
filesFlyoutUpdated: i18n.translate('filesManagement.filesFlyout.updatedLabel', {
|
||||
defaultMessage: 'Updated',
|
||||
}),
|
||||
filesFlyoutDownload: i18n.translate('filesManagement.filesFlyout.downloadButtonLabel', {
|
||||
defaultMessage: 'Download',
|
||||
}),
|
||||
filesFlyoutPreview: i18n.translate('filesManagement.filesFlyout.previewSectionTitle', {
|
||||
defaultMessage: 'Preview',
|
||||
}),
|
||||
filesStatus: {
|
||||
AWAITING_UPLOAD: i18n.translate('filesManagement.filesFlyout.status.awaitingUpload', {
|
||||
defaultMessage: 'Awaiting upload',
|
||||
}),
|
||||
DELETED: i18n.translate('filesManagement.filesFlyout.status.deleted', {
|
||||
defaultMessage: 'Deleted',
|
||||
}),
|
||||
READY: i18n.translate('filesManagement.filesFlyout.status.ready', {
|
||||
defaultMessage: 'Ready to download',
|
||||
}),
|
||||
UPLOADING: i18n.translate('filesManagement.filesFlyout.status.uploading', {
|
||||
defaultMessage: 'Uploading',
|
||||
}),
|
||||
UPLOAD_ERROR: i18n.translate('filesManagement.filesFlyout.status.uploadError', {
|
||||
defaultMessage: 'Upload error',
|
||||
}),
|
||||
} as Record<FileStatus, string>,
|
||||
};
|
13
src/plugins/files_management/public/index.ts
Executable file
13
src/plugins/files_management/public/index.ts
Executable file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FilesManagementPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new FilesManagementPlugin();
|
||||
}
|
55
src/plugins/files_management/public/mount_management_section.tsx
Executable file
55
src/plugins/files_management/public/mount_management_section.tsx
Executable file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { I18nProvider, FormattedRelative } from '@kbn/i18n-react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
import {
|
||||
TableListViewKibanaProvider,
|
||||
TableListViewKibanaDependencies,
|
||||
} from '@kbn/content-management-table-list';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { StartDependencies } from './types';
|
||||
import { App } from './app';
|
||||
import { FilesManagementAppContextProvider } from './context';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const mountManagementSection = (
|
||||
coreStart: CoreStart,
|
||||
startDeps: StartDependencies,
|
||||
{ element }: ManagementAppMountParams
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TableListViewKibanaProvider
|
||||
{...{
|
||||
core: coreStart as unknown as TableListViewKibanaDependencies['core'],
|
||||
toMountPoint,
|
||||
FormattedRelative,
|
||||
}}
|
||||
>
|
||||
<FilesManagementAppContextProvider
|
||||
filesClient={startDeps.files.filesClientFactory.asUnscoped()}
|
||||
>
|
||||
<App />
|
||||
</FilesManagementAppContextProvider>
|
||||
</TableListViewKibanaProvider>
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
element
|
||||
);
|
||||
|
||||
return () => {
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
};
|
31
src/plugins/files_management/public/plugin.ts
Executable file
31
src/plugins/files_management/public/plugin.ts
Executable file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { CoreSetup, Plugin } from '@kbn/core/public';
|
||||
import type { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
|
||||
import type { SetupDependencies, StartDependencies } from './types';
|
||||
|
||||
export class FilesManagementPlugin
|
||||
implements Plugin<void, void, SetupDependencies, StartDependencies>
|
||||
{
|
||||
public setup(core: CoreSetup<StartDependencies>, { management }: SetupDependencies): void {
|
||||
management.sections.section.kibana.registerApp({
|
||||
id: PLUGIN_ID,
|
||||
title: PLUGIN_NAME,
|
||||
order: 1,
|
||||
async mount(params: ManagementAppMountParams) {
|
||||
const { mountManagementSection } = await import('./mount_management_section');
|
||||
const [coreStart, depsStart] = await core.getStartServices();
|
||||
return mountManagementSection(coreStart, depsStart, params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {}
|
||||
}
|
22
src/plugins/files_management/public/types.ts
Executable file
22
src/plugins/files_management/public/types.ts
Executable file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FilesClient, FilesSetup, FilesStart } from '@kbn/files-plugin/public';
|
||||
import { ManagementSetup } from '@kbn/management-plugin/public';
|
||||
|
||||
export interface AppContext {
|
||||
filesClient: FilesClient;
|
||||
}
|
||||
|
||||
export interface SetupDependencies {
|
||||
files: FilesSetup;
|
||||
management: ManagementSetup;
|
||||
}
|
||||
export interface StartDependencies {
|
||||
files: FilesStart;
|
||||
}
|
14
src/plugins/files_management/tsconfig.json
Normal file
14
src/plugins/files_management/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*", ".storybook/**/*"],
|
||||
"kbn_references": [
|
||||
{ "path": "../../core/tsconfig.json" },
|
||||
{ "path": "../files/tsconfig.json" },
|
||||
{ "path": "../management/tsconfig.json" }
|
||||
]
|
||||
}
|
|
@ -828,6 +828,8 @@
|
|||
"@kbn/expressions-plugin/*": ["src/plugins/expressions/*"],
|
||||
"@kbn/field-formats-plugin": ["src/plugins/field_formats"],
|
||||
"@kbn/field-formats-plugin/*": ["src/plugins/field_formats/*"],
|
||||
"@kbn/files-management-plugin": ["src/plugins/files_management"],
|
||||
"@kbn/files-management-plugin/*": ["src/plugins/files_management/*"],
|
||||
"@kbn/files-plugin": ["src/plugins/files"],
|
||||
"@kbn/files-plugin/*": ["src/plugins/files/*"],
|
||||
"@kbn/guided-onboarding-plugin": ["src/plugins/guided_onboarding"],
|
||||
|
|
|
@ -165,6 +165,10 @@ Array [
|
|||
"id": "indexPatterns",
|
||||
"subFeatures": undefined,
|
||||
},
|
||||
Object {
|
||||
"id": "filesManagement",
|
||||
"subFeatures": undefined,
|
||||
},
|
||||
Object {
|
||||
"id": "savedObjectsManagement",
|
||||
"subFeatures": undefined,
|
||||
|
@ -449,6 +453,10 @@ Array [
|
|||
"id": "indexPatterns",
|
||||
"subFeatures": undefined,
|
||||
},
|
||||
Object {
|
||||
"id": "filesManagement",
|
||||
"subFeatures": undefined,
|
||||
},
|
||||
Object {
|
||||
"id": "savedObjectsManagement",
|
||||
"subFeatures": undefined,
|
||||
|
@ -759,6 +767,57 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures with a basic license returns the filesManagement feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"files:manageFiles",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"filesManagement",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"files",
|
||||
],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"files:manageFiles",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"filesManagement",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"files",
|
||||
],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures with a basic license returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
|
@ -1267,6 +1326,57 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures with a enterprise license returns the filesManagement feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"files:manageFiles",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"filesManagement",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"files",
|
||||
],
|
||||
"read": Array [],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "all",
|
||||
},
|
||||
Object {
|
||||
"privilege": Object {
|
||||
"api": Array [
|
||||
"files:manageFiles",
|
||||
],
|
||||
"app": Array [
|
||||
"kibana",
|
||||
],
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"filesManagement",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [],
|
||||
"read": Array [
|
||||
"files",
|
||||
],
|
||||
},
|
||||
"ui": Array [],
|
||||
},
|
||||
"privilegeId": "read",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`buildOSSFeatures with a enterprise license returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -422,6 +422,45 @@ export const buildOSSFeatures = ({
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'filesManagement',
|
||||
name: i18n.translate('xpack.features.filesManagementFeatureName', {
|
||||
defaultMessage: 'Files Management',
|
||||
}),
|
||||
order: 1600,
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
app: ['kibana'],
|
||||
catalogue: [],
|
||||
management: {
|
||||
kibana: ['filesManagement'],
|
||||
},
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['kibana'],
|
||||
management: {
|
||||
kibana: ['filesManagement'],
|
||||
},
|
||||
savedObject: {
|
||||
all: ['files'],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
api: ['files:manageFiles'],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
management: {
|
||||
kibana: ['filesManagement'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['files'],
|
||||
},
|
||||
ui: [],
|
||||
api: ['files:manageFiles'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'savedObjectsManagement',
|
||||
name: i18n.translate('xpack.features.savedObjectsManagementFeatureName', {
|
||||
|
|
|
@ -66,6 +66,7 @@ describe('Features Plugin', () => {
|
|||
"dev_tools",
|
||||
"advancedSettings",
|
||||
"indexPatterns",
|
||||
"filesManagement",
|
||||
"savedObjectsManagement",
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'dev_tools',
|
||||
'actions',
|
||||
'enterpriseSearch',
|
||||
'filesManagement',
|
||||
'advancedSettings',
|
||||
'indexPatterns',
|
||||
'graph',
|
||||
|
|
|
@ -78,6 +78,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'packs_all',
|
||||
'packs_read',
|
||||
],
|
||||
filesManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
},
|
||||
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
};
|
||||
|
|
|
@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
filesManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
},
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
|
@ -131,6 +132,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
dev_tools: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
filesManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
osquery: [
|
||||
'all',
|
||||
|
|
|
@ -68,7 +68,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
});
|
||||
expect(sections[1]).to.eql({
|
||||
sectionId: 'kibana',
|
||||
sectionLinks: ['dataViews', 'objects', 'tags', 'search_sessions', 'spaces', 'settings'],
|
||||
sectionLinks: [
|
||||
'dataViews',
|
||||
'filesManagement',
|
||||
'objects',
|
||||
'tags',
|
||||
'search_sessions',
|
||||
'spaces',
|
||||
'settings',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue