Skip to content

Commit

Permalink
[Files] Add file upload to file picker (#143969)
Browse files Browse the repository at this point in the history
* memoize is selected observable

* added on upload start and upload end cbs

* listen for uploading state, also i18n for search field

* added uploading state

* updated the files example plugin

* rework the footer to include the compressed upload component

* prevent change page when uploading

* revert change to search field

* added pass through the on upload callback so that consumers can respond to upload events

* updated i18n

* export DoneNotification type from upload files state

* added test for uploading state

* added common page and page size schemas

* make sure the file$ does not collapse on error

* hide pagination if there are no files

* added a small test to check that pagination is hidden when there are no files

* do not error in send request method
  • Loading branch information
jloleysens committed Oct 27, 2022
1 parent fd1ad82 commit f59c6da
Show file tree
Hide file tree
Showing 20 changed files with 188 additions and 42 deletions.
5 changes: 5 additions & 0 deletions x-pack/examples/files_example/public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
{showFilePickerModal && (
<MyFilePicker
onClose={() => setShowFilePickerModal(false)}
onUpload={() => {
notifications.toasts.addSuccess({
title: 'Uploaded files',
});
}}
onDone={(ids) => {
notifications.toasts.addSuccess({
title: 'Selected files!',
Expand Down
13 changes: 11 additions & 2 deletions x-pack/examples/files_example/public/components/file_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ import { FilePicker } from '../imports';

interface Props {
onClose: () => void;
onUpload: (ids: string[]) => void;
onDone: (ids: string[]) => void;
}

export const MyFilePicker: FunctionComponent<Props> = ({ onClose, onDone }) => {
return <FilePicker kind={exampleFileKind.id} onClose={onClose} onDone={onDone} pageSize={50} />;
export const MyFilePicker: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => {
return (
<FilePicker
kind={exampleFileKind.id}
onClose={onClose}
onDone={onDone}
onUpload={(n) => onUpload(n.map(({ id }) => id))}
pageSize={50}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { css } from '@emotion/react';
import { useFilePickerContext } from '../context';

import { i18nTexts } from '../i18n_texts';
import { useBehaviorSubject } from '../../use_behavior_subject';

interface Props {
onClick: () => void;
}

export const ClearFilterButton: FunctionComponent<Props> = ({ onClick }) => {
const { state } = useFilePickerContext();
const isUploading = useBehaviorSubject(state.isUploading$);
const query = useObservable(state.queryDebounced$);
if (!query) {
return null;
Expand All @@ -30,7 +32,9 @@ export const ClearFilterButton: FunctionComponent<Props> = ({ onClick }) => {
place-items: center;
`}
>
<EuiLink onClick={onClick}>{i18nTexts.clearFilterButton}</EuiLink>
<EuiLink disabled={isUploading} onClick={onClick}>
{i18nTexts.clearFilterButton}
</EuiLink>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React from 'react';
import React, { useMemo } from 'react';
import type { FunctionComponent } from 'react';
import numeral from '@elastic/numeral';
import useObservable from 'react-use/lib/useObservable';
Expand All @@ -26,8 +26,8 @@ export const FileCard: FunctionComponent<Props> = ({ file }) => {
const { kind, state, client } = useFilePickerContext();
const { euiTheme } = useEuiTheme();
const displayImage = isImage({ type: file.mimeType });

const isSelected = useObservable(state.watchFileSelected$(file.id), false);
const isSelected$ = useMemo(() => state.watchFileSelected$(file.id), [file.id, state]);
const isSelected = useObservable(isSelected$, false);

const imageHeight = `calc(${euiTheme.size.xxxl} * 2)`;
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,72 @@
* 2.0.
*/

import { EuiFlexGroup, EuiModalFooter } from '@elastic/eui';
import { EuiModalFooter } from '@elastic/eui';
import { css } from '@emotion/react';
import type { FunctionComponent } from 'react';
import React from 'react';
import React, { useCallback } from 'react';

import { UploadFile } from '../../upload_file';
import type { Props as FilePickerProps } from '../file_picker';
import { useFilePickerContext } from '../context';
import { i18nTexts } from '../i18n_texts';
import { Pagination } from './pagination';
import { SelectButton, Props as SelectButtonProps } from './select_button';

interface Props {
kind: string;
onDone: SelectButtonProps['onClick'];
onUpload?: FilePickerProps['onUpload'];
}

export const ModalFooter: FunctionComponent<Props> = ({ onDone }) => {
export const ModalFooter: FunctionComponent<Props> = ({ kind, onDone, onUpload }) => {
const { state } = useFilePickerContext();
const onUploadStart = useCallback(() => state.setIsUploading(true), [state]);
const onUploadEnd = useCallback(() => state.setIsUploading(false), [state]);
return (
<EuiModalFooter>
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween" alignItems="center">
<Pagination />
<SelectButton onClick={onDone} />
</EuiFlexGroup>
<div
css={css`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
width: 100%;
`}
>
<div
css={css`
place-self: stretch;
`}
>
<UploadFile
onDone={(n) => {
state.selectFile(n.map(({ id }) => id));
state.resetFilters();
onUpload?.(n);
}}
onUploadStart={onUploadStart}
onUploadEnd={onUploadEnd}
kind={kind}
initialPromptText={i18nTexts.uploadFilePlaceholderText}
multiple
compressed
/>
</div>
<div
css={css`
place-self: center;
`}
>
<Pagination />
</div>
<div
css={css`
place-self: end;
`}
>
<SelectButton onClick={onDone} />
</div>
</div>
</EuiModalFooter>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@
import React from 'react';
import type { FunctionComponent } from 'react';
import { EuiPagination } from '@elastic/eui';
import useObservable from 'react-use/lib/useObservable';
import { useFilePickerContext } from '../context';
import { useBehaviorSubject } from '../../use_behavior_subject';

export const Pagination: FunctionComponent = () => {
const { state } = useFilePickerContext();
const page = useBehaviorSubject(state.currentPage$);
const files = useObservable(state.files$, []);
const pageCount = useBehaviorSubject(state.totalPages$);
return <EuiPagination onPageClick={state.setPage} pageCount={pageCount} activePage={page} />;
const isUploading = useBehaviorSubject(state.isUploading$);
if (files.length === 0) {
return null;
}
return (
<EuiPagination
data-test-subj="paginationControls"
onPageClick={isUploading ? () => {} : state.setPage}
pageCount={pageCount}
activePage={page}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export const SearchField: FunctionComponent = () => {
const query = useBehaviorSubject(state.query$);
const isLoading = useBehaviorSubject(state.isLoading$);
const hasFiles = useBehaviorSubject(state.hasFiles$);
const isUploading = useBehaviorSubject(state.isUploading$);
return (
<EuiFieldSearch
data-test-subj="searchField"
disabled={!query && !hasFiles}
disabled={isUploading || (!query && !hasFiles)}
isLoading={isLoading}
value={query ?? ''}
placeholder={i18nTexts.searchFieldPlaceholder}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ export interface Props {

export const SelectButton: FunctionComponent<Props> = ({ onClick }) => {
const { state } = useFilePickerContext();
const isUploading = useBehaviorSubject(state.isUploading$);
const selectedFiles = useBehaviorSubject(state.selectedFileIds$);
return (
<EuiButton
data-test-subj="selectButton"
disabled={!state.hasFilesSelected()}
disabled={isUploading || !state.hasFilesSelected()}
onClick={() => onClick(selectedFiles)}
>
{selectedFiles.length > 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('FilePicker', () => {
selectButton: `${baseTestSubj}.selectButton`,
loadingSpinner: `${baseTestSubj}.loadingSpinner`,
fileGrid: `${baseTestSubj}.fileGrid`,
paginationControls: `${baseTestSubj}.paginationControls`,
};

return {
Expand Down Expand Up @@ -126,4 +127,10 @@ describe('FilePicker', () => {
expect(onDone).toHaveBeenCalledTimes(1);
expect(onDone).toHaveBeenNthCalledWith(1, ['a', 'b']);
});
it('hides pagination if there are no files', async () => {
client.list.mockImplementation(() => Promise.resolve({ files: [] as FileJSON[], total: 2 }));
const { actions, testSubjects, exists } = await initTestBed();
await actions.waitUntilLoaded();
expect(exists(testSubjects.paginationControls)).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
EuiFlexGroup,
} from '@elastic/eui';

import type { DoneNotification } from '../upload_file';
import { useBehaviorSubject } from '../use_behavior_subject';
import { useFilePickerContext, FilePickerContext } from './context';

Expand All @@ -43,13 +44,17 @@ export interface Props<Kind extends string = string> {
* Will be called after a user has a selected a set of files
*/
onDone: (fileIds: string[]) => void;
/**
* When a user has succesfully uploaded some files this callback will be called
*/
onUpload?: (done: DoneNotification[]) => void;
/**
* The number of results to show per page.
*/
pageSize?: number;
}

const Component: FunctionComponent<Props> = ({ onClose, onDone }) => {
const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => {
const { state, kind } = useFilePickerContext();

const hasFiles = useBehaviorSubject(state.hasFiles$);
Expand All @@ -59,7 +64,7 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone }) => {

useObservable(state.files$);

const renderFooter = () => <ModalFooter onDone={onDone} />;
const renderFooter = () => <ModalFooter kind={kind} onDone={onDone} onUpload={onUpload} />;

return (
<EuiModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,16 @@ describe('FilePickerState', () => {
expectObservable(filePickerState.loadingError$).toBe('a-b---c-', {});
});
});
it('does not allow fetching files while an upload is in progress', () => {
getTestScheduler().run(({ expectObservable, cold }) => {
const files = [] as FileJSON[];
filesClient.list.mockImplementation(() => of({ files }) as any);
const uploadInput = '---a|';
const queryInput = ' -----a|';
const upload$ = cold(uploadInput).pipe(tap(() => filePickerState.setIsUploading(true)));
const query$ = cold(queryInput).pipe(tap((q) => filePickerState.setQuery(q)));
expectObservable(merge(upload$, query$)).toBe('---a-a|');
expectObservable(filePickerState.files$).toBe('a------', { a: [] });
});
});
});
Loading

0 comments on commit f59c6da

Please sign in to comment.