Skip to content

Commit

Permalink
[Files] Filepicker (#143111)
Browse files Browse the repository at this point in the history
* added unscoped file client to the files components context

* wip: created some basic files and stories for new filepicker component

* fix some types after require filesclient to be passed in

* added file picker state and some basic tests

* added file picker context

* added missing file client value

* updated file picker stories

* expanded files context;

* remove the size observable and also added a test for file removal and adding of duplicates

* added error content component

* refactor upload component to not take files client as prop

* updated shared file compoennts context value

* added test for adding multiple files at once

* allow passing in string or array of strings

* added some i18n texts

* move the creation of a file kinds registry to a common location

* set file kinds only once

* a bunch of stuff: added title component, using grid, removed responsive on upload controls

* refactor layour components to own component using grid, added new i18n texts for empty state prompt

* minor copy tweak

* added basic story with some files

* added file grid and refactored picker to only exist in modal for now

* get the css grid algo that we want: auto-fill not auto-fit!

* override styling for content area of file

* split stories of files

* delete commented out code

* give the modal a fixed width

* fix upload file state, where we do not want a fixed width modal

* moved styles down to card, and combined margin removal rules

* optimize for filtering files, first pass just filter on names

* include xxl

* moved debounceTime to rxjs land, added test for filtering behaviour

* added story with more images

* big ol wip

* empty prompt when uploading a file

* added pagination

* fixed tests and added some comments

* address lint

* moved copy to i18n and updated size and color of empty error prompt

* remove use of css`

* remove non existant prop

* also reload files

* fileUpload -> files

* update logic for watching if selected

* disambiguate i18n ids

* use abort signal and call sendRequest from file$, filtering done server side now, update tests

* fix a few off by one errors and hook up the new system to the ui

* added test for in flight requests behaviour

* update the files example app

* fix minor card layout styling to make all cards the same size regardless of text or image conten

* added new file picker component

* make file cards a bit wider and text a bit smaller so that it wraps...

* fix issue of throwing abort error and prematurely setting request to completed...

* remove unused import

* replace filter i18n

* a bunch of cool changes

* added export for the file picker component

* updated example app to use multiple file upload

* added some comments and made images load eagerly in file picker for now...

* complete ux for examples

* only files that are "READY" should be in the file picker

* set loading to false if error

* install data-test-subj everywhere!

* added some react component tests

* remove unused import

* fix storybook case

* fix up where the files example plugin is listed, moved it to the developer examples area

* fix potential flashing of loader by debouncing

* do not create new observable on every render

* i18n

* have only filepicker ctx used in filepicker components

* refactor loadImageEagerly -> lazy

* useObservable instead of useEffect

* factor modal footer to own component and remove css util

* use the middle dot luke

* copy update in files example app

* added filter story

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
jloleysens and kibanamachine authored Oct 24, 2022
1 parent ffc8fb9 commit a42f182
Show file tree
Hide file tree
Showing 44 changed files with 1,611 additions and 90 deletions.
4 changes: 2 additions & 2 deletions x-pack/examples/files_example/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
import type { FileKind, FileImageMetadata } from '@kbn/files-plugin/common';

export const PLUGIN_ID = 'filesExample';
export const PLUGIN_NAME = 'filesExample';
export const PLUGIN_NAME = 'Files example';

const httpTags = {
tags: [`access:${PLUGIN_ID}`],
};

export const exampleFileKind: FileKind = {
id: 'filesExample',
id: PLUGIN_ID,
allowedMimeTypes: ['image/png'],
http: {
create: httpTags,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/examples/files_example/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"description": "Example plugin integrating with files plugin",
"server": true,
"ui": true,
"requiredPlugins": ["files"],
"requiredPlugins": ["files", "developerExamples"],
"optionalPlugins": []
}
2 changes: 1 addition & 1 deletion x-pack/examples/files_example/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const renderApp = (
) => {
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<FilesContext>
<FilesContext client={files.unscoped}>
<FilesExampleApp files={files} notifications={notifications} />
</FilesContext>
</QueryClientProvider>,
Expand Down
21 changes: 21 additions & 0 deletions x-pack/examples/files_example/public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@elastic/eui';

import { CoreStart } from '@kbn/core/public';
import { MyFilePicker } from './file_picker';
import type { MyImageMetadata } from '../../common';
import type { FileClients } from '../types';
import { DetailsFlyout } from './details_flyout';
Expand All @@ -39,11 +40,19 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
files.example.list()
);
const [showUploadModal, setShowUploadModal] = useState(false);
const [showFilePickerModal, setShowFilePickerModal] = useState(false);
const [isDeletingFile, setIsDeletingFile] = useState(false);
const [selectedItem, setSelectedItem] = useState<undefined | FileJSON<MyImageMetadata>>();

const renderToolsRight = () => {
return [
<EuiButton
onClick={() => setShowFilePickerModal(true)}
isDisabled={isLoading || isDeletingFile}
iconType="eye"
>
Select a file
</EuiButton>,
<EuiButton
onClick={() => setShowUploadModal(true)}
isDisabled={isLoading || isDeletingFile}
Expand Down Expand Up @@ -155,6 +164,18 @@ export const FilesExampleApp = ({ files, notifications }: FilesExampleAppDeps) =
}}
/>
)}
{showFilePickerModal && (
<MyFilePicker
onClose={() => setShowFilePickerModal(false)}
onDone={(ids) => {
notifications.toasts.addSuccess({
title: 'Selected files!',
text: 'IDS:' + JSON.stringify(ids, null, 2),
});
setShowFilePickerModal(false);
}}
/>
)}
</>
);
};
22 changes: 22 additions & 0 deletions x-pack/examples/files_example/public/components/file_picker.tsx
Original file line number Diff line number Diff line change
@@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import type { FunctionComponent } from 'react';

import { exampleFileKind } from '../../common';

import { FilePicker } from '../imports';

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

export const MyFilePicker: FunctionComponent<Props> = ({ onClose, onDone }) => {
return <FilePicker kind={exampleFileKind.id} onClose={onClose} onDone={onDone} />;
};
2 changes: 1 addition & 1 deletion x-pack/examples/files_example/public/components/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export const Modal: FunctionComponent<Props> = ({ onDismiss, onUploaded, client
</EuiModalHeader>
<EuiModalBody>
<UploadFile
multiple
kind={exampleFileKind.id}
client={client}
onDone={onUploaded}
meta={{ custom: 'meta' }}
/>
Expand Down
3 changes: 3 additions & 0 deletions x-pack/examples/files_example/public/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ export {
UploadFile,
FilesContext,
ScopedFilesClient,
FilePicker,
Image,
} from '@kbn/files-plugin/public';

export type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
13 changes: 12 additions & 1 deletion x-pack/examples/files_example/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,30 @@
* 2.0.
*/

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 { FilesExamplePluginsStart, FilesExamplePluginsSetup } from './types';

export class FilesExamplePlugin
implements Plugin<unknown, unknown, FilesExamplePluginsSetup, FilesExamplePluginsStart>
{
public setup(core: CoreSetup<FilesExamplePluginsStart>, { files }: FilesExamplePluginsSetup) {
public setup(
core: CoreSetup<FilesExamplePluginsStart>,
{ files, developerExamples }: FilesExamplePluginsSetup
) {
files.registerFileKind(exampleFileKind);

developerExamples.register({
appId: PLUGIN_ID,
title: PLUGIN_NAME,
description: 'Example plugin for the files plugin',
});

core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
// Load application bundle
const { renderApp } = await import('./application');
Expand Down
9 changes: 8 additions & 1 deletion x-pack/examples/files_example/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
*/

import { MyImageMetadata } from '../common';
import type { FilesSetup, FilesStart, ScopedFilesClient, FilesClient } from './imports';
import type {
FilesSetup,
FilesStart,
ScopedFilesClient,
FilesClient,
DeveloperExamplesSetup,
} from './imports';

export interface FilesExamplePluginsSetup {
files: FilesSetup;
developerExamples: DeveloperExamplesSetup;
}

export interface FilesExamplePluginsStart {
Expand Down
9 changes: 3 additions & 6 deletions x-pack/examples/files_example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
],
"exclude": [],
"references": [
{
"path": "../../../src/core/tsconfig.json"
},
{
"path": "../../plugins/files/tsconfig.json"
}
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../plugins/files/tsconfig.json" },
{ "path": "../../../examples/developer_examples/tsconfig.json" }
]
}
15 changes: 14 additions & 1 deletion x-pack/plugins/files/public/components/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@

import React, { createContext, useContext, type FunctionComponent } from 'react';
import { FileKindsRegistry, getFileKindsRegistry } from '../../common/file_kinds_registry';
import type { FilesClient } from '../types';

export interface FilesContextValue {
registry: FileKindsRegistry;
/**
* A files client that will be used process uploads.
*/
client: FilesClient<any>;
}

const FilesContextObject = createContext<FilesContextValue>(null as unknown as FilesContextValue);
Expand All @@ -21,10 +26,18 @@ export const useFilesContext = () => {
}
return ctx;
};
export const FilesContext: FunctionComponent = ({ children }) => {

interface ContextProps {
/**
* A files client that will be used process uploads.
*/
client: FilesClient<any>;
}
export const FilesContext: FunctionComponent<ContextProps> = ({ client, children }) => {
return (
<FilesContextObject.Provider
value={{
client,
registry: getFileKindsRegistry(),
}}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 React from 'react';
import type { FunctionComponent } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiLink } from '@elastic/eui';
import { css } from '@emotion/react';
import { useFilePickerContext } from '../context';

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

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

export const ClearFilterButton: FunctionComponent<Props> = ({ onClick }) => {
const { state } = useFilePickerContext();
const query = useObservable(state.queryDebounced$);
if (!query) {
return null;
}
return (
<div
css={css`
display: grid;
place-items: center;
`}
>
<EuiLink onClick={onClick}>{i18nTexts.clearFilterButton}</EuiLink>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 React from 'react';
import type { FunctionComponent } from 'react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { i18nTexts } from '../i18n_texts';
import { useFilePickerContext } from '../context';
import { useBehaviorSubject } from '../../use_behavior_subject';

interface Props {
error: Error;
}

export const ErrorContent: FunctionComponent<Props> = ({ error }) => {
const { state } = useFilePickerContext();
const isLoading = useBehaviorSubject(state.isLoading$);
return (
<EuiEmptyPrompt
data-test-subj="errorPrompt"
iconType="alert"
iconColor="danger"
titleSize="xs"
title={<h3>{i18nTexts.loadingFilesErrorTitle}</h3>}
body={error.message}
actions={
<EuiButton disabled={isLoading} onClick={state.retry}>
{i18nTexts.retryButtonLabel}
</EuiButton>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.filesFilePicker {
.euiCard__content, .euiCard__description {
margin :0; // make the cards a little bit more compact
}
}
Loading

0 comments on commit a42f182

Please sign in to comment.