[Files] Use upload component in files example (#141362) (#141388)

* added modal and removed hardcoded image upload

* remove unused var

* added shared imports file and hooked up modal component to example app

* use the Image component from files and update the title for the files modal

* set and show custom metadata

* fix content type not being passed through to upload or to picker component

* remove old commented out code

(cherry picked from commit 50df4630f5)

Co-authored-by: Jean-Louis Leysens <jeanlouis.leysens@elastic.co>
This commit is contained in:
Kibana Machine 2022-09-22 06:40:43 -06:00 committed by GitHub
parent 8876f57332
commit 8ae26a1c89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 130 additions and 62 deletions

View file

@ -16,6 +16,7 @@ const httpTags = {
export const exampleFileKind: FileKind = {
id: 'filesExample',
allowedMimeTypes: ['image/png'],
http: {
create: httpTags,
delete: httpTags,

View file

@ -11,6 +11,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { AppPluginStartDependencies } from './types';
import { FilesExampleApp } from './components/app';
import { FilesContext } from './imports';
const queryClient = new QueryClient();
@ -21,7 +22,9 @@ export const renderApp = (
) => {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<FilesExampleApp files={files} notifications={notifications} />
<FilesContext>
<FilesExampleApp files={files} notifications={notifications} />
</FilesContext>
</QueryClientProvider>,
element
);

File diff suppressed because one or more lines are too long

View file

@ -10,10 +10,8 @@ import { useQuery } from '@tanstack/react-query';
import type { FileJSON } from '@kbn/files-plugin/common';
import type { FilesClientResponses } from '@kbn/files-plugin/public';
const names = ['foo', 'bar', 'baz'];
import {
EuiPageTemplate_Deprecated as EuiPageTemplate,
EuiPageTemplate,
EuiInMemoryTable,
EuiInMemoryTableProps,
EuiButton,
@ -26,8 +24,7 @@ import { CoreStart } from '@kbn/core/public';
import { DetailsFlyout } from './details_flyout';
import type { FileClients } from '../types';
import { ConfirmButtonIcon } from './confirm_button';
// @ts-ignore
import imageBase64 from '!!raw-loader!../assets/image.png.base64';
import { Modal } from './modal';
interface FilesExampleAppDeps {
files: FileClients;
@ -40,37 +37,15 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
const { data, isLoading, error, refetch } = useQuery<ListResponse>(['files'], () =>
files.example.list()
);
const [isUploadingImage, setIsUploadingImage] = useState(false);
const [showUploadModal, setShowUploadModal] = useState(false);
const [isDeletingFile, setIsDeletingFile] = useState(false);
const [selectedItem, setSelectedItem] = useState<undefined | FileJSON>();
const uploadImage = async () => {
try {
setIsUploadingImage(true);
const { file } = await files.example.create({
name: names[Math.floor(Math.random() * names.length)],
alt: 'My image',
meta: { myValue: 'test' },
mimeType: 'image/png',
});
await refetch();
const blob = new Blob([Uint8Array.from(atob(imageBase64), (c) => c.charCodeAt(0))], {
type: 'image/png',
});
await files.example.upload({ id: file.id, body: blob });
await refetch();
notifications.toasts.addSuccess('Sucessfully uploaded image');
} finally {
setIsUploadingImage(false);
}
};
const renderToolsRight = () => {
return [
<EuiButton
onClick={uploadImage}
isDisabled={isUploadingImage || isLoading || isDeletingFile}
isLoading={isUploadingImage}
onClick={() => setShowUploadModal(true)}
isDisabled={isLoading || isDeletingFile}
iconType="exportAction"
>
Upload image
@ -84,7 +59,11 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
{
field: 'name',
name: 'Name',
render: (name, item) => <EuiLink onClick={() => setSelectedItem(item)}>{name}</EuiLink>,
render: (name, item) => (
<EuiLink disabled={isDeletingFile} onClick={() => setSelectedItem(item)}>
{name}
</EuiLink>
),
},
{
field: 'status',
@ -107,6 +86,7 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
isPrimary: true,
render: (item) => (
<EuiButtonIcon
disabled={isDeletingFile}
aria-label="View file details"
iconType="eye"
onClick={() => setSelectedItem(item)}
@ -139,23 +119,22 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
return (
<>
<EuiPageTemplate
pageHeader={{
pageTitle: 'Files example',
}}
>
<EuiInMemoryTable
columns={columns}
items={items}
itemId="id"
loading={isLoading || isDeletingFile}
error={error ? JSON.stringify(error) : undefined}
sorting
search={{
toolsRight: renderToolsRight(),
}}
pagination
/>
<EuiPageTemplate restrictWidth>
<EuiPageTemplate.Header pageTitle="Files example" />
<EuiPageTemplate.Section>
<EuiInMemoryTable
columns={columns}
items={items}
itemId="id"
loading={isLoading || isDeletingFile}
error={error ? JSON.stringify(error) : undefined}
sorting
search={{
toolsRight: renderToolsRight(),
}}
pagination
/>
</EuiPageTemplate.Section>
</EuiPageTemplate>
{selectedItem && (
<DetailsFlyout
@ -164,6 +143,17 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
onDismiss={() => setSelectedItem(undefined)}
/>
)}
{showUploadModal && (
<Modal
client={files.unscoped}
onDismiss={() => setShowUploadModal(false)}
onUploaded={() => {
notifications.toasts.addSuccess('Uploaded file!');
refetch();
setShowUploadModal(false);
}}
/>
)}
</>
);
};

View file

@ -23,6 +23,7 @@ import {
} from '@elastic/eui';
import type { FileJSON } from '@kbn/files-plugin/common';
import { FileClients } from '../types';
import { Image } from '../imports';
interface Props {
file: FileJSON;
@ -66,10 +67,16 @@ export const DetailsFlyout: FunctionComponent<Props> = ({ files, file, onDismiss
title: 'Last updated',
description: moment(file.updated).fromNow(),
},
{
title: 'Custom meta',
description: (
<pre>{file.meta ? JSON.stringify(file.meta, null, 2) : '<no custom metadata>'}</pre>
),
},
]}
/>
<EuiSpacer size="xl" />
<img
<Image
css={css`
height: 400px;
`}

View file

@ -0,0 +1,38 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FunctionComponent } from 'react';
import React from 'react';
import { EuiModal, EuiModalHeader, EuiModalBody, EuiText } from '@elastic/eui';
import { exampleFileKind } from '../../common';
import { FilesClient, UploadFile } from '../imports';
interface Props {
client: FilesClient;
onDismiss: () => void;
onUploaded: () => void;
}
export const Modal: FunctionComponent<Props> = ({ onDismiss, onUploaded, client }) => {
return (
<EuiModal onClose={onDismiss}>
<EuiModalHeader>
<EuiText>
<h2>Upload image</h2>
</EuiText>
</EuiModalHeader>
<EuiModalBody>
<UploadFile
kind={exampleFileKind.id}
client={client}
onDone={onUploaded}
meta={{ custom: 'meta' }}
/>
</EuiModalBody>
</EuiModal>
);
};

View file

@ -0,0 +1,16 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export {
FilesClient,
FilesSetup,
FilesStart,
UploadFile,
FilesContext,
ScopedFilesClient,
Image,
} from '@kbn/files-plugin/public';

View file

@ -12,8 +12,9 @@ import { FilesExamplePluginsStart, FilesExamplePluginsSetup } from './types';
export class FilesExamplePlugin
implements Plugin<unknown, unknown, FilesExamplePluginsSetup, FilesExamplePluginsStart>
{
public setup(core: CoreSetup<FilesExamplePluginsStart>) {
// Register an application into the side navigation menu
public setup(core: CoreSetup<FilesExamplePluginsStart>, { files }: FilesExamplePluginsSetup) {
files.registerFileKind(exampleFileKind);
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
@ -21,13 +22,14 @@ export class FilesExamplePlugin
// Load application bundle
const { renderApp } = await import('./application');
// Get start services as specified in kibana.json
const [coreStart, { files }] = await core.getStartServices();
const [coreStart, deps] = await core.getStartServices();
// Render the application
return renderApp(
coreStart,
{
files: {
example: files.filesClientFactory.asScoped(exampleFileKind.id),
unscoped: deps.files.filesClientFactory.asUnscoped(),
example: deps.files.filesClientFactory.asScoped(exampleFileKind.id),
},
},
params

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { FilesSetup, FilesStart, ScopedFilesClient } from '@kbn/files-plugin/public';
import type { FilesSetup, FilesStart, ScopedFilesClient, FilesClient } from './imports';
export interface FilesExamplePluginsSetup {
files: FilesSetup;
@ -16,6 +16,7 @@ export interface FilesExamplePluginsStart {
}
export interface FileClients {
unscoped: FilesClient;
// Example file kind
example: ScopedFilesClient;
}

View file

@ -92,14 +92,15 @@ export const UploadFile = <Kind extends string = string>({
}: Props<Kind>): ReturnType<FunctionComponent> => {
const { registry } = useFilesContext();
const ref = useRef<null | EuiFilePicker>(null);
const fileKind = registry.get(kindId);
const uploadState = useMemo(
() =>
createUploadState({
client,
fileKind: registry.get(kindId),
fileKind,
allowRepeatedUploads,
}),
[client, kindId, allowRepeatedUploads, registry]
[client, allowRepeatedUploads, fileKind]
);
/**
@ -118,7 +119,13 @@ export const UploadFile = <Kind extends string = string>({
return (
<context.Provider value={uploadState}>
<Component ref={ref} meta={meta} immediate={immediate} allowClear={allowClear} />
<Component
ref={ref}
accept={fileKind.allowedMimeTypes?.join(',')}
meta={meta}
immediate={immediate}
allowClear={allowClear}
/>
</context.Provider>
);
};

View file

@ -190,6 +190,7 @@ export class UploadState {
id: uploadTarget.id,
kind: this.fileKind.id,
abortSignal,
contentType: mime,
})
);
}),

View file

@ -127,10 +127,10 @@ export function createFilesClient({
body: JSON.stringify(body),
});
},
upload: ({ kind, abortSignal, ...args }) => {
upload: ({ kind, abortSignal, contentType, ...args }) => {
return http.put(apiRoutes.getUploadRoute(scopedFileKind ?? kind, args.id), {
headers: {
'Content-Type': 'application/octet-stream',
'Content-Type': contentType ?? 'application/octet-stream',
},
signal: abortSignal,
body: args.body as BodyInit,

View file

@ -96,7 +96,10 @@ export interface FilesClient extends GlobalEndpoints {
*
* @param args - upload file args
*/
upload: ClientMethodFrom<UploadFileKindHttpEndpoint, { abortSignal?: AbortSignal }>;
upload: ClientMethodFrom<
UploadFileKindHttpEndpoint,
{ abortSignal?: AbortSignal; contentType?: string }
>;
/**
* Stream a download of the file object's content.
*