diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index cbd746f89951..e315c7fbcceb 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -31,7 +31,6 @@ const STORYBOOKS = [ 'expression_reveal_image', 'expression_shape', 'expression_tagcloud', - 'files', 'fleet', 'home', 'infra', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b3cae787acc7..38644589d40b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1028,9 +1028,13 @@ packages/shared-ux/button/exit_full_screen/types @elastic/kibana-global-experien packages/shared-ux/card/no_data/impl @elastic/kibana-global-experience packages/shared-ux/card/no_data/mocks @elastic/kibana-global-experience packages/shared-ux/card/no_data/types @elastic/kibana-global-experience +packages/shared-ux/file/context @elastic/kibana-global-experience +packages/shared-ux/file/file_picker/impl @elastic/kibana-global-experience +packages/shared-ux/file/file_upload/impl @elastic/kibana-global-experience packages/shared-ux/file/image/impl @elastic/kibana-global-experience packages/shared-ux/file/image/mocks @elastic/kibana-global-experience -packages/shared-ux/file/image/types @elastic/kibana-global-experience +packages/shared-ux/file/mocks @elastic/kibana-global-experience +packages/shared-ux/file/types @elastic/kibana-global-experience packages/shared-ux/file/util @elastic/kibana-global-experience packages/shared-ux/link/redirect_app/impl @elastic/kibana-global-experience packages/shared-ux/link/redirect_app/mocks @elastic/kibana-global-experience diff --git a/examples/files_example/common/index.ts b/examples/files_example/common/index.ts index 29b2c97c9532..b3ef90f9dfd6 100644 --- a/examples/files_example/common/index.ts +++ b/examples/files_example/common/index.ts @@ -7,7 +7,7 @@ */ import type { FileKind } from '@kbn/files-plugin/common'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; export const PLUGIN_ID = 'filesExample'; export const PLUGIN_NAME = 'Files example'; diff --git a/examples/files_example/public/components/modal.tsx b/examples/files_example/public/components/modal.tsx index a314e5bd8ea4..1471c469b635 100644 --- a/examples/files_example/public/components/modal.tsx +++ b/examples/files_example/public/components/modal.tsx @@ -10,7 +10,7 @@ import type { FunctionComponent } from 'react'; import React from 'react'; import { EuiModal, EuiModalHeader, EuiModalBody, EuiText } from '@elastic/eui'; import { exampleFileKind, MyImageMetadata } from '../../common'; -import { FilesClient, UploadFile } from '../imports'; +import { FilesClient, FileUpload } from '../imports'; interface Props { client: FilesClient; @@ -27,7 +27,7 @@ export const Modal: FunctionComponent = ({ onDismiss, onUploaded, client - () +... + + + +``` \ No newline at end of file diff --git a/src/plugins/files/public/components/util/index.ts b/packages/shared-ux/file/context/index.ts similarity index 70% rename from src/plugins/files/public/components/util/index.ts rename to packages/shared-ux/file/context/index.ts index 9e0dccc6c336..a1011302a2a3 100644 --- a/src/plugins/files/public/components/util/index.ts +++ b/packages/shared-ux/file/context/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { getImageMetadata, isImage, fitToBox, getBlurhashSrc } from './image_metadata'; -export type { ImageMetadataFactory } from './image_metadata'; +export { FilesContext, useFilesContext, type FilesContextValue } from './src'; diff --git a/packages/shared-ux/file/image/types/kibana.jsonc b/packages/shared-ux/file/context/kibana.jsonc similarity index 70% rename from packages/shared-ux/file/image/types/kibana.jsonc rename to packages/shared-ux/file/context/kibana.jsonc index c337cfd46035..55921ceec305 100644 --- a/packages/shared-ux/file/image/types/kibana.jsonc +++ b/packages/shared-ux/file/context/kibana.jsonc @@ -1,6 +1,6 @@ { "type": "shared-common", - "id": "@kbn/shared-ux-link-redirect-app-types", + "id": "@kbn/shared-ux-file-context", "owner": "@elastic/kibana-global-experience", "runtimeDeps": [], "typeDeps": [] diff --git a/packages/shared-ux/file/context/package.json b/packages/shared-ux/file/context/package.json new file mode 100644 index 000000000000..025c4b810137 --- /dev/null +++ b/packages/shared-ux/file/context/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-context", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/src/plugins/files/public/components/context.tsx b/packages/shared-ux/file/context/src/index.tsx similarity index 85% rename from src/plugins/files/public/components/context.tsx rename to packages/shared-ux/file/context/src/index.tsx index fbf73999b625..da0c70df6c01 100644 --- a/src/plugins/files/public/components/context.tsx +++ b/packages/shared-ux/file/context/src/index.tsx @@ -7,11 +7,9 @@ */ import React, { createContext, useContext, type FunctionComponent } from 'react'; -import { FileKindsRegistry, getFileKindsRegistry } from '../../common/file_kinds_registry'; -import type { FilesClient } from '../types'; +import type { BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types'; export interface FilesContextValue { - registry: FileKindsRegistry; /** * A files client that will be used process uploads. */ @@ -39,7 +37,6 @@ export const FilesContext: FunctionComponent = ({ client, children {children} diff --git a/packages/shared-ux/file/context/tsconfig.json b/packages/shared-ux/file/context/tsconfig.json new file mode 100644 index 000000000000..dc13d1aced52 --- /dev/null +++ b/packages/shared-ux/file/context/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/shared-ux/file/file_picker/impl/BUILD.bazel b/packages/shared-ux/file/file_picker/impl/BUILD.bazel new file mode 100644 index 000000000000..f8bb2f8804ca --- /dev/null +++ b/packages/shared-ux/file/file_picker/impl/BUILD.bazel @@ -0,0 +1,158 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "impl" +PKG_REQUIRE_NAME = "@kbn/shared-ux-file-picker" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + "**/*.scss", + "**/*.mdx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//@elastic/eui", + "@npm//@elastic/numeral", + "@npm//react", + "@npm//@emotion/react", + "@npm//@emotion/css", + "@npm//rxjs", + "//packages/kbn-i18n", + "//packages/shared-ux/file/util", + "//packages/shared-ux/file/context", + "//packages/shared-ux/file/file_upload/impl", + "//packages/shared-ux/file/image/impl", + "//packages/kbn-shared-ux-utility", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@elastic/eui", + "@npm//@elastic/numeral", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", + "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-ambient-ui-types", + "//packages/shared-ux/file/util:npm_module_types", + "//packages/shared-ux/file/context:npm_module_types", + "//packages/shared-ux/file/file_upload/impl:npm_module_types", + "//packages/shared-ux/file/image/impl:npm_module_types", + "//packages/shared-ux/file/types:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/file_picker/impl/README.mdx b/packages/shared-ux/file/file_picker/impl/README.mdx new file mode 100644 index 000000000000..655f1e1f9557 --- /dev/null +++ b/packages/shared-ux/file/file_picker/impl/README.mdx @@ -0,0 +1,27 @@ +--- +id: sharedUX/Components/FilePicker +slug: /shared-ux/components/file-picker +title: File picker +description: Pick or upload files from Kibana. +tags: ['shared-ux', 'component', 'files'] +date: 2022-11-22 +--- + +## Description + +A component designed to capture the UX of picking files. Use cases include: + +* A dashboard user wanting to upload an image to their dashboard +* A user picking a new avatar +* A cases user selecting from a set of text files they want to attach to a case +* ...and many more + +## Usage + +Must be wrapped in the `FilesContext`. + +```tsx + + + +``` \ No newline at end of file diff --git a/src/plugins/files/public/components/index.ts b/packages/shared-ux/file/file_picker/impl/index.tsx similarity index 67% rename from src/plugins/files/public/components/index.ts rename to packages/shared-ux/file/file_picker/impl/index.tsx index 87354bd934f3..3db12d1ca665 100644 --- a/src/plugins/files/public/components/index.ts +++ b/packages/shared-ux/file/file_picker/impl/index.tsx @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { UploadFile, type UploadFileProps } from './upload_file'; -export { FilePicker, type FilePickerProps } from './file_picker'; -export { FilesContext } from './context'; +export { FilePicker } from './src'; +export type { FilePickerProps } from './src'; diff --git a/src/plugins/files/.storybook/main.ts b/packages/shared-ux/file/file_picker/impl/jest.config.js similarity index 71% rename from src/plugins/files/.storybook/main.ts rename to packages/shared-ux/file/file_picker/impl/jest.config.js index f9d5b3ea3edd..fa51ce8bbd88 100644 --- a/src/plugins/files/.storybook/main.ts +++ b/packages/shared-ux/file/file_picker/impl/jest.config.js @@ -6,12 +6,8 @@ * Side Public License, v 1. */ -import { defaultConfig } from '@kbn/storybook'; - module.exports = { - ...defaultConfig, - stories: ['../**/*.stories.tsx'], - reactOptions: { - strictMode: true, - }, + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/packages/shared-ux/file/file_picker/impl'], }; diff --git a/packages/shared-ux/file/file_picker/impl/kibana.jsonc b/packages/shared-ux/file/file_picker/impl/kibana.jsonc new file mode 100644 index 000000000000..9188feb7ebc1 --- /dev/null +++ b/packages/shared-ux/file/file_picker/impl/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-picker", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/file_picker/impl/package.json b/packages/shared-ux/file/file_picker/impl/package.json new file mode 100644 index 000000000000..80f28c0ccf7f --- /dev/null +++ b/packages/shared-ux/file/file_picker/impl/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-picker", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/src/plugins/files/public/components/file_picker/components/clear_filter_button.tsx b/packages/shared-ux/file/file_picker/impl/src/components/clear_filter_button.tsx similarity index 94% rename from src/plugins/files/public/components/file_picker/components/clear_filter_button.tsx rename to packages/shared-ux/file/file_picker/impl/src/components/clear_filter_button.tsx index c8a373f70cd5..d0370d00f194 100644 --- a/src/plugins/files/public/components/file_picker/components/clear_filter_button.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/components/clear_filter_button.tsx @@ -11,10 +11,10 @@ import type { FunctionComponent } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { EuiLink } from '@elastic/eui'; import { css } from '@emotion/react'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { useFilePickerContext } from '../context'; import { i18nTexts } from '../i18n_texts'; -import { useBehaviorSubject } from '../../use_behavior_subject'; interface Props { onClick: () => void; diff --git a/src/plugins/files/public/components/file_picker/components/empty_prompt.tsx b/packages/shared-ux/file/file_picker/impl/src/components/empty_prompt.tsx similarity index 94% rename from src/plugins/files/public/components/file_picker/components/empty_prompt.tsx rename to packages/shared-ux/file/file_picker/impl/src/components/empty_prompt.tsx index 3ee463067d6f..b32f2426281c 100644 --- a/src/plugins/files/public/components/file_picker/components/empty_prompt.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/components/empty_prompt.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { css } from '@emotion/react'; import { EuiEmptyPrompt, useEuiTheme } from '@elastic/eui'; import type { FunctionComponent } from 'react'; -import { UploadFile } from '../../upload_file'; +import { FileUpload } from '@kbn/shared-ux-file-upload'; import { useFilePickerContext } from '../context'; import { i18nTexts } from '../i18n_texts'; @@ -28,7 +28,7 @@ export const EmptyPrompt: FunctionComponent = ({ kind, multiple }) => { title={

{i18nTexts.emptyStatePrompt}

} titleSize="s" actions={[ - = ({ kind, onDone, onUpload, place-self: stretch; `} > - { state.selectFile(n.map(({ fileJSON }) => fileJSON)); state.resetFilters(); diff --git a/src/plugins/files/public/components/file_picker/components/pagination.tsx b/packages/shared-ux/file/file_picker/impl/src/components/pagination.tsx similarity index 94% rename from src/plugins/files/public/components/file_picker/components/pagination.tsx rename to packages/shared-ux/file/file_picker/impl/src/components/pagination.tsx index 3384edcab16c..d23c98bc9d3f 100644 --- a/src/plugins/files/public/components/file_picker/components/pagination.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/components/pagination.tsx @@ -10,8 +10,8 @@ import React from 'react'; import type { FunctionComponent } from 'react'; import { EuiPagination } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { useFilePickerContext } from '../context'; -import { useBehaviorSubject } from '../../use_behavior_subject'; export const Pagination: FunctionComponent = () => { const { state } = useFilePickerContext(); diff --git a/src/plugins/files/public/components/file_picker/components/search_field.tsx b/packages/shared-ux/file/file_picker/impl/src/components/search_field.tsx similarity index 94% rename from src/plugins/files/public/components/file_picker/components/search_field.tsx rename to packages/shared-ux/file/file_picker/impl/src/components/search_field.tsx index e1feb83800ab..687221b1abf8 100644 --- a/src/plugins/files/public/components/file_picker/components/search_field.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/components/search_field.tsx @@ -9,9 +9,9 @@ import type { FunctionComponent } from 'react'; import React from 'react'; import { EuiFieldSearch } from '@elastic/eui'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { i18nTexts } from '../i18n_texts'; import { useFilePickerContext } from '../context'; -import { useBehaviorSubject } from '../../use_behavior_subject'; export const SearchField: FunctionComponent = () => { const { state } = useFilePickerContext(); diff --git a/src/plugins/files/public/components/file_picker/components/select_button.tsx b/packages/shared-ux/file/file_picker/impl/src/components/select_button.tsx similarity index 90% rename from src/plugins/files/public/components/file_picker/components/select_button.tsx rename to packages/shared-ux/file/file_picker/impl/src/components/select_button.tsx index 46110f7a4660..414cd24c5eb5 100644 --- a/src/plugins/files/public/components/file_picker/components/select_button.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/components/select_button.tsx @@ -9,10 +9,10 @@ import { EuiButton } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import { useBehaviorSubject } from '../../use_behavior_subject'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; import { useFilePickerContext } from '../context'; import { i18nTexts } from '../i18n_texts'; -import type { FileJSON } from '../../../../common'; export interface Props { onClick: (selectedFiles: FileJSON[]) => void; diff --git a/src/plugins/files/public/components/file_picker/components/title.tsx b/packages/shared-ux/file/file_picker/impl/src/components/title.tsx similarity index 100% rename from src/plugins/files/public/components/file_picker/components/title.tsx rename to packages/shared-ux/file/file_picker/impl/src/components/title.tsx diff --git a/src/plugins/files/public/components/file_picker/context.tsx b/packages/shared-ux/file/file_picker/impl/src/context.tsx similarity index 95% rename from src/plugins/files/public/components/file_picker/context.tsx rename to packages/shared-ux/file/file_picker/impl/src/context.tsx index c17fe601e487..a494c12dc776 100644 --- a/src/plugins/files/public/components/file_picker/context.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/context.tsx @@ -8,7 +8,7 @@ import React, { createContext, useContext, useMemo, useEffect } from 'react'; import type { FunctionComponent } from 'react'; -import { useFilesContext, FilesContextValue } from '../context'; +import { useFilesContext, FilesContextValue } from '@kbn/shared-ux-file-context'; import { FilePickerState, createFilePickerState } from './file_picker_state'; interface FilePickerContextValue extends FilesContextValue { diff --git a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx b/packages/shared-ux/file/file_picker/impl/src/file_picker.stories.tsx similarity index 85% rename from src/plugins/files/public/components/file_picker/file_picker.stories.tsx rename to packages/shared-ux/file/file_picker/impl/src/file_picker.stories.tsx index e0de316d60b2..c314bfb1db65 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/file_picker.stories.tsx @@ -10,19 +10,20 @@ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { base64dLogo } from '@kbn/shared-ux-file-image-mocks'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; -import type { FileJSON } from '../../../common'; -import { FilesClient, FilesClientResponses } from '../../types'; -import { register } from '../stories_shared'; -import { FilesContext } from '../context'; +import type { FileImageMetadata, FileKind } from '@kbn/shared-ux-file-types'; +import type { FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types'; +import { FilesContext } from '@kbn/shared-ux-file-context'; import { FilePicker, Props as FilePickerProps } from './file_picker'; +type ListResponse = ReturnType; + const kind = 'filepicker'; -register({ - id: kind, - http: {}, - allowedMimeTypes: ['*'], -}); +const getFileKind = (id: string) => + ({ + id: kind, + http: {}, + allowedMimeTypes: ['*'], + } as FileKind); const defaultProps: FilePickerProps = { kind, @@ -32,7 +33,7 @@ const defaultProps: FilePickerProps = { }; export default { - title: 'components/FilePicker', + title: 'files/FilePicker', component: FilePicker, args: defaultProps, decorators: [ @@ -44,10 +45,11 @@ export default { new Promise((res, rej) => setTimeout(() => rej(new Error('not so fast buster!')), 3000) ), - list: async (): Promise => ({ + list: async (): ListResponse => ({ files: [], total: 0, }), + getFileKind, } as unknown as FilesClient } > @@ -89,10 +91,11 @@ BasicOne.decorators = [ client={ { getDownloadHref: () => `data:image/png;base64,${base64dLogo}`, - list: async (): Promise => ({ + list: async (): ListResponse => ({ files: [createFileJSON()], total: 1, }), + getFileKind, } as unknown as FilesClient } > @@ -119,10 +122,11 @@ BasicMany.decorators = [ client={ { getDownloadHref: () => `data:image/png;base64,${base64dLogo}`, - list: async (): Promise => ({ + list: async (): ListResponse => ({ files, total: files.length, }), + getFileKind, } as unknown as FilesClient } > @@ -142,10 +146,11 @@ BasicManyMany.decorators = [ client={ { getDownloadHref: () => `data:image/png;base64,${base64dLogo}`, - list: async (): Promise => ({ + list: async (): ListResponse => ({ files: array.map((_, idx) => createFileJSON({ id: String(idx) })), total: array.length, }), + getFileKind, } as unknown as FilesClient } > @@ -168,6 +173,7 @@ ErrorLoading.decorators = [ list: async () => { throw new Error('stop'); }, + getFileKind, } as unknown as FilesClient } > @@ -194,6 +200,7 @@ TryFilter.decorators = [ } return array; }, + getFileKind, } as unknown as FilesClient } > @@ -211,10 +218,11 @@ SingleSelect.decorators = [ client={ { getDownloadHref: () => `data:image/png;base64,${base64dLogo}`, - list: async (): Promise => ({ + list: async (): ListResponse => ({ files: [createFileJSON(), createFileJSON(), createFileJSON()], total: 1, }), + getFileKind, } as unknown as FilesClient } > diff --git a/src/plugins/files/public/components/file_picker/file_picker.test.tsx b/packages/shared-ux/file/file_picker/impl/src/file_picker.test.tsx similarity index 91% rename from src/plugins/files/public/components/file_picker/file_picker.test.tsx rename to packages/shared-ux/file/file_picker/impl/src/file_picker.test.tsx index 398f105e6c62..c57c5f0a52c4 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.test.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/file_picker.test.tsx @@ -10,15 +10,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed } from '@kbn/test-jest-helpers'; -import { createMockFilesClient } from '../../mocks'; -import { FilesContext } from '../context'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { FilesContext } from '@kbn/shared-ux-file-context'; import { FilePicker, Props } from './file_picker'; -import { - FileKindsRegistryImpl, - getFileKindsRegistry, - setFileKindsRegistry, -} from '../../../common/file_kinds_registry'; -import { FileJSON } from '../../../common'; describe('FilePicker', () => { const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); @@ -85,18 +80,14 @@ describe('FilePicker', () => { }; } - beforeAll(() => { - setFileKindsRegistry(new FileKindsRegistryImpl()); - getFileKindsRegistry().register({ - id: 'test', - maxSizeBytes: 10000, - http: {}, - }); - }); - beforeEach(() => { jest.resetAllMocks(); client = createMockFilesClient(); + client.getFileKind.mockImplementation(() => ({ + id: 'test', + maxSizeBytes: 10000, + http: {}, + })); onDone = jest.fn(); onClose = jest.fn(); }); diff --git a/src/plugins/files/public/components/file_picker/file_picker.tsx b/packages/shared-ux/file/file_picker/impl/src/file_picker.tsx similarity index 95% rename from src/plugins/files/public/components/file_picker/file_picker.tsx rename to packages/shared-ux/file/file_picker/impl/src/file_picker.tsx index 434a0da9967a..e14df066764e 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.tsx +++ b/packages/shared-ux/file/file_picker/impl/src/file_picker.tsx @@ -20,8 +20,9 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; -import type { DoneNotification } from '../upload_file'; -import { useBehaviorSubject } from '../use_behavior_subject'; +import type { DoneNotification } from '@kbn/shared-ux-file-upload'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; import { useFilePickerContext, FilePickerContext } from './context'; import { Title } from './components/title'; @@ -32,7 +33,6 @@ import { SearchField } from './components/search_field'; import { ModalFooter } from './components/modal_footer'; import { ClearFilterButton } from './components/clear_filter_button'; -import type { FileJSON } from '../../../common'; export interface Props { /** diff --git a/src/plugins/files/public/components/file_picker/file_picker_state.test.ts b/packages/shared-ux/file/file_picker/impl/src/file_picker_state.test.ts similarity index 98% rename from src/plugins/files/public/components/file_picker/file_picker_state.test.ts rename to packages/shared-ux/file/file_picker/impl/src/file_picker_state.test.ts index f60f804b3923..7523512d0116 100644 --- a/src/plugins/files/public/components/file_picker/file_picker_state.test.ts +++ b/packages/shared-ux/file/file_picker/impl/src/file_picker_state.test.ts @@ -16,9 +16,9 @@ jest.mock('rxjs', () => { import { TestScheduler } from 'rxjs/testing'; import { merge, tap, of, NEVER } from 'rxjs'; -import { FileJSON } from '../../../common'; +import { FileJSON } from '@kbn/shared-ux-file-types'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; import { FilePickerState, createFilePickerState } from './file_picker_state'; -import { createMockFilesClient } from '../../mocks'; const getTestScheduler = () => new TestScheduler((actual, expected) => expect(actual).toEqual(expected)); diff --git a/src/plugins/files/public/components/file_picker/file_picker_state.ts b/packages/shared-ux/file/file_picker/impl/src/file_picker_state.ts similarity index 66% rename from src/plugins/files/public/components/file_picker/file_picker_state.ts rename to packages/shared-ux/file/file_picker/impl/src/file_picker_state.ts index a40606509adb..63c537828019 100644 --- a/src/plugins/files/public/components/file_picker/file_picker_state.ts +++ b/packages/shared-ux/file/file_picker/impl/src/file_picker_state.ts @@ -6,23 +6,8 @@ * Side Public License, v 1. */ -import { - map, - tap, - from, - EMPTY, - switchMap, - catchError, - Observable, - shareReplay, - debounceTime, - Subscription, - combineLatest, - BehaviorSubject, - distinctUntilChanged, -} from 'rxjs'; -import type { FileJSON } from '../../../common'; -import type { FilesClient } from '../../types'; +import * as Rx from 'rxjs'; +import type { FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types'; function naivelyFuzzify(query: string): string { return query.includes('*') ? query : `*${query}*`; @@ -32,25 +17,25 @@ export class FilePickerState { /** * Files the user has selected */ - public readonly selectedFiles$ = new BehaviorSubject([]); + public readonly selectedFiles$ = new Rx.BehaviorSubject([]); public readonly selectedFileIds$ = this.selectedFiles$.pipe( - map((files) => files.map((file) => file.id)) + Rx.map((files) => files.map((file) => file.id)) ); - public readonly isLoading$ = new BehaviorSubject(true); - public readonly loadingError$ = new BehaviorSubject(undefined); - public readonly hasFiles$ = new BehaviorSubject(false); - public readonly hasQuery$ = new BehaviorSubject(false); - public readonly query$ = new BehaviorSubject(undefined); - public readonly queryDebounced$ = this.query$.pipe(debounceTime(100)); - public readonly currentPage$ = new BehaviorSubject(0); - public readonly totalPages$ = new BehaviorSubject(undefined); - public readonly isUploading$ = new BehaviorSubject(false); + public readonly isLoading$ = new Rx.BehaviorSubject(true); + public readonly loadingError$ = new Rx.BehaviorSubject(undefined); + public readonly hasFiles$ = new Rx.BehaviorSubject(false); + public readonly hasQuery$ = new Rx.BehaviorSubject(false); + public readonly query$ = new Rx.BehaviorSubject(undefined); + public readonly queryDebounced$ = this.query$.pipe(Rx.debounceTime(100)); + public readonly currentPage$ = new Rx.BehaviorSubject(0); + public readonly totalPages$ = new Rx.BehaviorSubject(undefined); + public readonly isUploading$ = new Rx.BehaviorSubject(false); private readonly selectedFiles = new Map(); - private readonly retry$ = new BehaviorSubject(undefined); - private readonly subscriptions: Subscription[] = []; - private readonly internalIsLoading$ = new BehaviorSubject(true); + private readonly retry$ = new Rx.BehaviorSubject(undefined); + private readonly subscriptions: Rx.Subscription[] = []; + private readonly internalIsLoading$ = new Rx.BehaviorSubject(true); constructor( private readonly client: FilesClient, @@ -61,21 +46,21 @@ export class FilePickerState { this.subscriptions = [ this.query$ .pipe( - map((query) => Boolean(query)), - distinctUntilChanged() + Rx.map((query) => Boolean(query)), + Rx.distinctUntilChanged() ) .subscribe(this.hasQuery$), - this.internalIsLoading$.pipe(distinctUntilChanged()).subscribe(this.isLoading$), + this.internalIsLoading$.pipe(Rx.distinctUntilChanged()).subscribe(this.isLoading$), ]; } - private readonly requests$ = combineLatest([ - this.currentPage$.pipe(distinctUntilChanged()), - this.query$.pipe(distinctUntilChanged()), + private readonly requests$ = Rx.combineLatest([ + this.currentPage$.pipe(Rx.distinctUntilChanged()), + this.query$.pipe(Rx.distinctUntilChanged()), this.retry$, ]).pipe( - tap(() => this.setIsLoading(true)), // set loading state as early as possible - debounceTime(100) + Rx.tap(() => this.setIsLoading(true)), // set loading state as early as possible + Rx.debounceTime(100) ); /** @@ -85,11 +70,11 @@ export class FilePickerState { * @note This is not explicitly kept in sync with the selected files! */ public readonly files$ = this.requests$.pipe( - switchMap(([page, query]) => this.sendRequest(page, query)), - tap(({ total }) => this.updateTotalPages({ total })), - tap(({ total }) => this.hasFiles$.next(Boolean(total))), - map(({ files }) => files), - shareReplay() + Rx.switchMap(([page, query]) => this.sendRequest(page, query)), + Rx.tap(({ total }) => this.updateTotalPages({ total })), + Rx.tap(({ total }) => this.hasFiles$.next(Boolean(total))), + Rx.map(({ files }) => files), + Rx.shareReplay() ); private updateTotalPages = ({ total }: { total: number }): void => { @@ -123,8 +108,8 @@ export class FilePickerState { private sendRequest = ( page: number, query: undefined | string - ): Observable<{ files: FileJSON[]; total: number }> => { - if (this.isUploading$.getValue()) return EMPTY; + ): Rx.Observable<{ files: FileJSON[]; total: number }> => { + if (this.isUploading$.getValue()) return Rx.EMPTY; if (this.abort) this.abort(); this.setIsLoading(true); this.loadingError$.next(undefined); @@ -138,7 +123,7 @@ export class FilePickerState { } }; - const request$ = from( + const request$ = Rx.from( this.client.list({ kind: this.kind, name: query ? [naivelyFuzzify(query)] : undefined, @@ -148,20 +133,20 @@ export class FilePickerState { abortSignal: abortController.signal, }) ).pipe( - catchError((e) => { + Rx.catchError((e) => { if (e.name !== 'AbortError') { this.setIsLoading(false); this.loadingError$.next(e); } else { // If the request was aborted, we assume another request is now in progress } - return EMPTY; + return Rx.EMPTY; }), - tap(() => { + Rx.tap(() => { this.setIsLoading(false); this.abort = undefined; }), - shareReplay() + Rx.shareReplay() ); request$.subscribe(); @@ -213,10 +198,10 @@ export class FilePickerState { for (const sub of this.subscriptions) sub.unsubscribe(); }; - watchFileSelected$ = (id: string): Observable => { + watchFileSelected$ = (id: string): Rx.Observable => { return this.selectedFiles$.pipe( - map(() => this.selectedFiles.has(id)), - distinctUntilChanged() + Rx.map(() => this.selectedFiles.has(id)), + Rx.distinctUntilChanged() ); }; } diff --git a/packages/shared-ux/file/file_picker/impl/src/i18n_texts.ts b/packages/shared-ux/file/file_picker/impl/src/i18n_texts.ts new file mode 100644 index 000000000000..0f1f3281b0f9 --- /dev/null +++ b/packages/shared-ux/file/file_picker/impl/src/i18n_texts.ts @@ -0,0 +1,53 @@ +/* + * 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 i18nTexts = { + title: i18n.translate('sharedUXPackages.filePicker.title', { + defaultMessage: 'Select a file', + }), + titleMultiple: i18n.translate('sharedUXPackages.filePicker.titleMultiple', { + defaultMessage: 'Select files', + }), + loadingFilesErrorTitle: i18n.translate('sharedUXPackages.filePicker.error.loadingTitle', { + defaultMessage: 'Could not load files', + }), + retryButtonLabel: i18n.translate('sharedUXPackages.filePicker.error.retryButtonLabel', { + defaultMessage: 'Retry', + }), + emptyStatePrompt: i18n.translate('sharedUXPackages.filePicker.emptyStatePromptTitle', { + defaultMessage: 'Upload your first file', + }), + selectFileLabel: i18n.translate('sharedUXPackages.filePicker.selectFileButtonLable', { + defaultMessage: 'Select file', + }), + selectFilesLabel: (nrOfFiles: number) => + i18n.translate('sharedUXPackages.filePicker.selectFilesButtonLable', { + defaultMessage: 'Select {nrOfFiles} files', + values: { nrOfFiles }, + }), + searchFieldPlaceholder: i18n.translate('sharedUXPackages.filePicker.searchFieldPlaceholder', { + defaultMessage: 'my-file-*', + }), + emptyFileGridPrompt: i18n.translate('sharedUXPackages.filePicker.emptyGridPrompt', { + defaultMessage: 'No files match your filter', + }), + loadMoreButtonLabel: i18n.translate('sharedUXPackages.filePicker.loadMoreButtonLabel', { + defaultMessage: 'Load more', + }), + clearFilterButton: i18n.translate('sharedUXPackages.filePicker.clearFilterButtonLabel', { + defaultMessage: 'Clear filter', + }), + uploadFilePlaceholderText: i18n.translate( + 'sharedUXPackages.filePicker.uploadFilePlaceholderText', + { + defaultMessage: 'Drag and drop to upload new files', + } + ), +}; diff --git a/src/plugins/files/public/components/file_picker/index.tsx b/packages/shared-ux/file/file_picker/impl/src/index.tsx similarity index 100% rename from src/plugins/files/public/components/file_picker/index.tsx rename to packages/shared-ux/file/file_picker/impl/src/index.tsx diff --git a/packages/shared-ux/file/image/types/tsconfig.json b/packages/shared-ux/file/file_picker/impl/tsconfig.json similarity index 65% rename from packages/shared-ux/file/image/types/tsconfig.json rename to packages/shared-ux/file/file_picker/impl/tsconfig.json index f566d00dd270..0b9ca147ee59 100644 --- a/packages/shared-ux/file/image/types/tsconfig.json +++ b/packages/shared-ux/file/file_picker/impl/tsconfig.json @@ -5,11 +5,14 @@ "emitDeclarationOnly": true, "outDir": "target_types", "types": [ - "rxjs", - "@types/react", + "node", + "jest", + "react", + "@emotion/react/types/css-prop", ] }, "include": [ - "*.d.ts" + "**/*.ts", + "**/*.tsx", ] } diff --git a/packages/shared-ux/file/file_upload/impl/BUILD.bazel b/packages/shared-ux/file/file_upload/impl/BUILD.bazel new file mode 100644 index 000000000000..6a16aeb63dbf --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/BUILD.bazel @@ -0,0 +1,155 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "impl" +PKG_REQUIRE_NAME = "@kbn/shared-ux-file-upload" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + "**/*.mdx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//@elastic/eui", + "@npm//react", + "@npm//lodash", + "@npm//@emotion/react", + "@npm//@emotion/css", + "@npm//rxjs", + "//packages/kbn-i18n", + "//packages/kbn-ui-theme", + "//packages/shared-ux/file/util", + "//packages/shared-ux/file/context", + "//packages/kbn-shared-ux-utility", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@elastic/eui", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", + "@npm//@types/lodash", + "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-ui-theme:npm_module_types", + "//packages/kbn-ambient-ui-types", + "//packages/shared-ux/file/context:npm_module_types", + "//packages/shared-ux/file/util:npm_module_types", + "//packages/shared-ux/file/types:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/file_upload/impl/README.mdx b/packages/shared-ux/file/file_upload/impl/README.mdx new file mode 100644 index 000000000000..9c6f7db56a71 --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/README.mdx @@ -0,0 +1,32 @@ +--- +id: sharedUX/Components/FileUpload +slug: /shared-ux/components/file-upload +title: File upload +description: Upload file(s) to Kibana. +tags: ['shared-ux', 'component', 'files'] +date: 2022-11-22 +--- + +## Description + +A wrapper around `` that provides state management for the upload process using the `FileClient`. + +## Usage + +Must be wrapped in the `FilesContext`. + +```tsx + + + +``` + +## Variants + +### Default + +The default layout should sit nicely in a form, modal or flyout. + +### Compressed + +When space is constrained you can render a smaller version of the UI by passing the `compressed` prop. This variant will be smaller in size, and start uploading a file immediately once it receives it. \ No newline at end of file diff --git a/packages/shared-ux/file/file_upload/impl/index.tsx b/packages/shared-ux/file/file_upload/impl/index.tsx new file mode 100644 index 000000000000..025954ca5b63 --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/index.tsx @@ -0,0 +1,10 @@ +/* + * 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 { FileUpload } from './src'; +export type { FileUploadProps, DoneNotification } from './src'; diff --git a/packages/shared-ux/file/file_upload/impl/jest.config.js b/packages/shared-ux/file/file_upload/impl/jest.config.js new file mode 100644 index 000000000000..7f297c7c7996 --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/packages/shared-ux/file/file_upload/impl'], +}; diff --git a/packages/shared-ux/file/file_upload/impl/kibana.jsonc b/packages/shared-ux/file/file_upload/impl/kibana.jsonc new file mode 100644 index 000000000000..11ad99c84ef5 --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-upload", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/file_upload/impl/package.json b/packages/shared-ux/file/file_upload/impl/package.json new file mode 100644 index 000000000000..060044a0775a --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-upload", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/src/plugins/files/public/components/upload_file/components/cancel_button.tsx b/packages/shared-ux/file/file_upload/impl/src/components/cancel_button.tsx similarity index 95% rename from src/plugins/files/public/components/upload_file/components/cancel_button.tsx rename to packages/shared-ux/file/file_upload/impl/src/components/cancel_button.tsx index 4bf6aead0912..b4d79ab40656 100644 --- a/src/plugins/files/public/components/upload_file/components/cancel_button.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/components/cancel_button.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiButtonIcon } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import { useBehaviorSubject } from '../../use_behavior_subject'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { useUploadState } from '../context'; import { i18nTexts } from '../i18n_texts'; diff --git a/src/plugins/files/public/components/upload_file/components/clear_button.tsx b/packages/shared-ux/file/file_upload/impl/src/components/clear_button.tsx similarity index 100% rename from src/plugins/files/public/components/upload_file/components/clear_button.tsx rename to packages/shared-ux/file/file_upload/impl/src/components/clear_button.tsx diff --git a/src/plugins/files/public/components/upload_file/components/control_button.tsx b/packages/shared-ux/file/file_upload/impl/src/components/control_button.tsx similarity index 95% rename from src/plugins/files/public/components/upload_file/components/control_button.tsx rename to packages/shared-ux/file/file_upload/impl/src/components/control_button.tsx index c52d72545e04..85dc1be6db8a 100644 --- a/src/plugins/files/public/components/upload_file/components/control_button.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/components/control_button.tsx @@ -9,7 +9,7 @@ import type { FunctionComponent } from 'react'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { useBehaviorSubject } from '../../use_behavior_subject'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { useUploadState } from '../context'; import { UploadButton } from './upload_button'; import { RetryButton } from './retry_button'; diff --git a/src/plugins/files/public/components/upload_file/components/index.ts b/packages/shared-ux/file/file_upload/impl/src/components/index.ts similarity index 100% rename from src/plugins/files/public/components/upload_file/components/index.ts rename to packages/shared-ux/file/file_upload/impl/src/components/index.ts diff --git a/src/plugins/files/public/components/upload_file/components/retry_button.tsx b/packages/shared-ux/file/file_upload/impl/src/components/retry_button.tsx similarity index 93% rename from src/plugins/files/public/components/upload_file/components/retry_button.tsx rename to packages/shared-ux/file/file_upload/impl/src/components/retry_button.tsx index 273515331b3c..295764994d97 100644 --- a/src/plugins/files/public/components/upload_file/components/retry_button.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/components/retry_button.tsx @@ -9,7 +9,7 @@ import { EuiButton } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; -import { useBehaviorSubject } from '../../use_behavior_subject'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { useUploadState } from '../context'; import { i18nTexts } from '../i18n_texts'; diff --git a/src/plugins/files/public/components/upload_file/components/upload_button.tsx b/packages/shared-ux/file/file_upload/impl/src/components/upload_button.tsx similarity index 95% rename from src/plugins/files/public/components/upload_file/components/upload_button.tsx rename to packages/shared-ux/file/file_upload/impl/src/components/upload_button.tsx index 2bd024ad2e1a..9475efb97cf6 100644 --- a/src/plugins/files/public/components/upload_file/components/upload_button.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/components/upload_button.tsx @@ -9,10 +9,10 @@ import { EuiButton } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import useObservable from 'react-use/lib/useObservable'; import { i18nTexts } from '../i18n_texts'; import { useUploadState } from '../context'; -import { useBehaviorSubject } from '../../use_behavior_subject'; interface Props { onClick: () => void; diff --git a/src/plugins/files/public/components/upload_file/context.ts b/packages/shared-ux/file/file_upload/impl/src/context.ts similarity index 100% rename from src/plugins/files/public/components/upload_file/context.ts rename to packages/shared-ux/file/file_upload/impl/src/context.ts diff --git a/src/plugins/files/public/components/upload_file/upload_file.component.tsx b/packages/shared-ux/file/file_upload/impl/src/file_upload.component.tsx similarity index 95% rename from src/plugins/files/public/components/upload_file/upload_file.component.tsx rename to packages/shared-ux/file/file_upload/impl/src/file_upload.component.tsx index dc6af5ebd845..06cd2c6d27de 100644 --- a/src/plugins/files/public/components/upload_file/upload_file.component.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/file_upload.component.tsx @@ -17,9 +17,9 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; +import { useBehaviorSubject } from '@kbn/shared-ux-file-util'; import { css } from '@emotion/react'; import useObservable from 'react-use/lib/useObservable'; -import { useBehaviorSubject } from '../use_behavior_subject'; import { i18nTexts } from './i18n_texts'; import { ControlButton, ClearButton } from './components'; import { useUploadState } from './context'; @@ -48,7 +48,7 @@ const styles = { `, }; -export const UploadFile = React.forwardRef( +export const FileUpload = React.forwardRef( ( { compressed, @@ -71,12 +71,12 @@ export const UploadFile = React.forwardRef( const isInvalid = Boolean(error); const errorMessage = error?.message; - const id = useGeneratedHtmlId({ prefix: 'filesUploadFile' }); + const id = useGeneratedHtmlId({ prefix: 'filesFileUpload' }); const errorId = `${id}_error`; return (
new Promise((res) => setTimeout(res, ms)); + const kind = 'test'; +const miniFile = 'miniFile'; +const zipOnly = 'zipOnly'; +const fileKinds = { + [kind]: { + id: kind, + http: {}, + allowedMimeTypes: ['*'], + }, + [miniFile]: { + id: miniFile, + http: {}, + maxSizeBytes: 1, + allowedMimeTypes: ['*'], + }, + [zipOnly]: { + id: zipOnly, + http: {}, + allowedMimeTypes: ['application/zip'], + }, +}; +const getFileKind = (id: string) => (fileKinds as any)[id] as FileKind; const defaultArgs: Props = { kind, @@ -25,8 +46,8 @@ const defaultArgs: Props = { }; export default { - title: 'stateful/UploadFile', - component: UploadFile, + title: 'files/FileUpload', + component: FileUpload, args: defaultArgs, decorators: [ (Story) => ( @@ -35,6 +56,7 @@ export default { { create: async () => ({ file: { id: 'test' } }), upload: () => sleep(1000), + getFileKind, } as unknown as FilesClient } > @@ -42,30 +64,9 @@ export default { ), ], -} as ComponentMeta; +} as ComponentMeta; -register({ - id: kind, - http: {}, - allowedMimeTypes: ['*'], -}); - -const miniFile = 'miniFile'; -register({ - id: miniFile, - http: {}, - maxSizeBytes: 1, - allowedMimeTypes: ['*'], -}); - -const zipOnly = 'zipOnly'; -register({ - id: zipOnly, - http: {}, - allowedMimeTypes: ['application/zip'], -}); - -const Template: ComponentStory = (props: Props) => ; +const Template: ComponentStory = (props: Props) => ; export const Basic = Template.bind({}); @@ -86,6 +87,7 @@ LongErrorUX.decorators = [ throw new Error('Something went wrong while uploading! '.repeat(10).trim()); }, delete: async () => {}, + getFileKind, } as unknown as FilesClient } > @@ -105,6 +107,7 @@ Abort.decorators = [ await sleep(60000); }, delete: async () => {}, + getFileKind, } as unknown as FilesClient } > @@ -148,6 +151,7 @@ ImmediateUploadError.decorators = [ throw new Error('Something went wrong while uploading!'); }, delete: async () => {}, + getFileKind, } as unknown as FilesClient } > @@ -167,6 +171,7 @@ ImmediateUploadAbort.decorators = [ await sleep(60000); }, delete: async () => {}, + getFileKind, } as unknown as FilesClient } > @@ -198,6 +203,7 @@ CompressedError.decorators = [ throw new Error('Something went wrong while uploading! '.repeat(10).trim()); }, delete: async () => {}, + getFileKind, } as unknown as FilesClient } > diff --git a/src/plugins/files/public/components/upload_file/upload_file.test.tsx b/packages/shared-ux/file/file_upload/impl/src/file_upload.test.tsx similarity index 92% rename from src/plugins/files/public/components/upload_file/upload_file.test.tsx rename to packages/shared-ux/file/file_upload/impl/src/file_upload.test.tsx index 795ba00d4b67..b5118eca52ea 100644 --- a/src/plugins/files/public/components/upload_file/upload_file.test.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/file_upload.test.tsx @@ -10,20 +10,12 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed } from '@kbn/test-jest-helpers'; import { EuiFilePicker } from '@elastic/eui'; +import { FilesContext } from '@kbn/shared-ux-file-context'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; +import { FileUpload, Props } from './file_upload'; -import { - FileKindsRegistryImpl, - setFileKindsRegistry, - getFileKindsRegistry, -} from '../../../common/file_kinds_registry'; - -import { createMockFilesClient } from '../../mocks'; - -import { FileJSON } from '../../../common'; -import { FilesContext } from '../context'; -import { UploadFile, Props } from './upload_file'; - -describe('UploadFile', () => { +describe('FileUpload', () => { const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); let onDone: jest.Mock; let onError: jest.Mock; @@ -32,7 +24,7 @@ describe('UploadFile', () => { async function initTestBed(props?: Partial) { const createTestBed = registerTestBed((p: Props) => ( - + )); @@ -43,7 +35,7 @@ describe('UploadFile', () => { ...props, }); - const baseTestSubj = `filesUploadFile`; + const baseTestSubj = `filesFileUpload`; const testSubjects = { base: baseTestSubj, @@ -86,17 +78,13 @@ describe('UploadFile', () => { }; } - beforeAll(() => { - setFileKindsRegistry(new FileKindsRegistryImpl()); - getFileKindsRegistry().register({ + beforeEach(() => { + client = createMockFilesClient(); + client.getFileKind.mockImplementation(() => ({ id: 'test', maxSizeBytes: 10000, http: {}, - }); - }); - - beforeEach(() => { - client = createMockFilesClient(); + })); onDone = jest.fn(); onError = jest.fn(); }); diff --git a/src/plugins/files/public/components/upload_file/upload_file.tsx b/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx similarity index 92% rename from src/plugins/files/public/components/upload_file/upload_file.tsx rename to packages/shared-ux/file/file_upload/impl/src/file_upload.tsx index 7ade7500fde1..7b7d54b425fc 100644 --- a/src/plugins/files/public/components/upload_file/upload_file.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/file_upload.tsx @@ -9,12 +9,12 @@ import { EuiFilePicker } from '@elastic/eui'; import React, { type FunctionComponent, useRef, useEffect, useMemo } from 'react'; -import { useFilesContext } from '../context'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { useFilesContext } from '@kbn/shared-ux-file-context'; -import { UploadFile as Component } from './upload_file.component'; +import { FileUpload as Component } from './file_upload.component'; import { createUploadState } from './upload_state'; import { context } from './context'; -import type { FileJSON } from '../../../common'; /** * An object representing an uploaded file @@ -35,7 +35,7 @@ interface UploadedFile { } /** - * UploadFile component props + * FileUpload component props */ export interface Props { /** @@ -119,7 +119,7 @@ export interface Props { * * In order to use this component you must register your file kind with {@link FileKindsRegistry} */ -export const UploadFile = ({ +export const FileUpload = ({ meta, onDone, onError, @@ -135,9 +135,9 @@ export const UploadFile = ({ allowRepeatedUploads = false, className, }: Props): ReturnType => { - const { registry, client } = useFilesContext(); + const { client } = useFilesContext(); const ref = useRef(null); - const fileKind = registry.get(kindId); + const fileKind = client.getFileKind(kindId); const repeatedUploads = compressed || allowRepeatedUploads; const uploadState = useMemo( () => @@ -187,4 +187,4 @@ export const UploadFile = ({ }; /* eslint-disable import/no-default-export */ -export default UploadFile; +export default FileUpload; diff --git a/src/plugins/files/public/components/upload_file/i18n_texts.ts b/packages/shared-ux/file/file_upload/impl/src/i18n_texts.ts similarity index 56% rename from src/plugins/files/public/components/upload_file/i18n_texts.ts rename to packages/shared-ux/file/file_upload/impl/src/i18n_texts.ts index 19ac5b3e0a67..d1b6d206e00f 100644 --- a/src/plugins/files/public/components/upload_file/i18n_texts.ts +++ b/packages/shared-ux/file/file_upload/impl/src/i18n_texts.ts @@ -9,32 +9,32 @@ import { i18n } from '@kbn/i18n'; export const i18nTexts = { - defaultPickerLabel: i18n.translate('files.uploadFile.defaultFilePickerLabel', { + defaultPickerLabel: i18n.translate('sharedUXPackages.fileUpload.defaultFilePickerLabel', { defaultMessage: 'Upload a file', }), - upload: i18n.translate('files.uploadFile.uploadButtonLabel', { + upload: i18n.translate('sharedUXPackages.fileUpload.uploadButtonLabel', { defaultMessage: 'Upload', }), - uploading: i18n.translate('files.uploadFile.uploadingButtonLabel', { + uploading: i18n.translate('sharedUXPackages.fileUpload.uploadingButtonLabel', { defaultMessage: 'Uploading', }), - uploadComplete: i18n.translate('files.uploadFile.uploadCompleteButtonLabel', { + uploadComplete: i18n.translate('sharedUXPackages.fileUpload.uploadCompleteButtonLabel', { defaultMessage: 'Upload complete', }), - retry: i18n.translate('files.uploadFile.retryButtonLabel', { + retry: i18n.translate('sharedUXPackages.fileUpload.retryButtonLabel', { defaultMessage: 'Retry', }), - clear: i18n.translate('files.uploadFile.clearButtonLabel', { + clear: i18n.translate('sharedUXPackages.fileUpload.clearButtonLabel', { defaultMessage: 'Clear', }), - cancel: i18n.translate('files.uploadFile.cancelButtonLabel', { + cancel: i18n.translate('sharedUXPackages.fileUpload.cancelButtonLabel', { defaultMessage: 'Cancel', }), - uploadDone: i18n.translate('files.uploadFile.uploadDoneToolTipContent', { + uploadDone: i18n.translate('sharedUXPackages.fileUpload.uploadDoneToolTipContent', { defaultMessage: 'Your file was successfully uploaded!', }), fileTooLarge: (expectedSize: string) => - i18n.translate('files.uploadFile.fileTooLargeErrorMessage', { + i18n.translate('sharedUXPackages.fileUpload.fileTooLargeErrorMessage', { defaultMessage: 'File is too large. Maximum size is {expectedSize, plural, one {# byte} other {# bytes} }.', values: { expectedSize }, diff --git a/src/plugins/files/public/components/upload_file/index.tsx b/packages/shared-ux/file/file_upload/impl/src/index.tsx similarity index 76% rename from src/plugins/files/public/components/upload_file/index.tsx rename to packages/shared-ux/file/file_upload/impl/src/index.tsx index 3ddde57b71b3..8f8f0de5b90d 100644 --- a/src/plugins/files/public/components/upload_file/index.tsx +++ b/packages/shared-ux/file/file_upload/impl/src/index.tsx @@ -8,11 +8,11 @@ import React, { lazy, Suspense, ReactNode } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import type { Props } from './upload_file'; +import type { Props } from './file_upload'; export type { DoneNotification } from './upload_state'; -export type UploadFileProps = Props & { +export type FileUploadProps = Props & { /** * A custom fallback for when component is lazy loading, * If not provided, is used @@ -20,10 +20,10 @@ export type UploadFileProps = Props & { lazyLoadFallback?: ReactNode; }; -const UploadFileContainer = lazy(() => import('./upload_file')); +const FileUploadContainer = lazy(() => import('./file_upload')); -export const UploadFile = (props: UploadFileProps) => ( +export const FileUpload = (props: FileUploadProps) => ( }> - + ); diff --git a/src/plugins/files/public/components/upload_file/upload_state.test.ts b/packages/shared-ux/file/file_upload/impl/src/upload_state.test.ts similarity index 96% rename from src/plugins/files/public/components/upload_file/upload_state.test.ts rename to packages/shared-ux/file/file_upload/impl/src/upload_state.test.ts index b5695f01ea0f..08e50faf1bd7 100644 --- a/src/plugins/files/public/components/upload_file/upload_state.test.ts +++ b/packages/shared-ux/file/file_upload/impl/src/upload_state.test.ts @@ -9,10 +9,9 @@ import { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { of, delay, merge, tap, mergeMap } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import type { FileKind, FileJSON } from '../../../common'; -import { createMockFilesClient } from '../../mocks'; -import type { FilesClient } from '../../types'; -import { ImageMetadataFactory } from '../util/image_metadata'; +import type { FileKind, FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; +import { ImageMetadataFactory } from '@kbn/shared-ux-file-util'; import { UploadState } from './upload_state'; diff --git a/src/plugins/files/public/components/upload_file/upload_state.ts b/packages/shared-ux/file/file_upload/impl/src/upload_state.ts similarity index 74% rename from src/plugins/files/public/components/upload_file/upload_state.ts rename to packages/shared-ux/file/file_upload/impl/src/upload_state.ts index 30a0dc38a75d..fc8a743c3d20 100644 --- a/src/plugins/files/public/components/upload_file/upload_state.ts +++ b/packages/shared-ux/file/file_upload/impl/src/upload_state.ts @@ -6,31 +6,9 @@ * Side Public License, v 1. */ -import { - of, - map, - zip, - from, - race, - take, - filter, - Subject, - finalize, - forkJoin, - mergeMap, - switchMap, - catchError, - shareReplay, - ReplaySubject, - BehaviorSubject, - type Observable, - combineLatest, - distinctUntilChanged, - Subscription, -} from 'rxjs'; -import type { FileKind, FileJSON } from '../../../common/types'; -import type { FilesClient } from '../../types'; -import { ImageMetadataFactory, getImageMetadata, isImage } from '../util'; +import * as Rx from 'rxjs'; +import { ImageMetadataFactory, getImageMetadata, isImage } from '@kbn/shared-ux-file-util'; +import type { FileKind, FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types'; import { i18nTexts } from './i18n_texts'; import { createStateSubject, type SimpleStateSubject, parseFileName } from './util'; @@ -56,18 +34,18 @@ interface UploadOptions { } export class UploadState { - private readonly abort$ = new Subject(); - private readonly files$$ = new BehaviorSubject([]); + private readonly abort$ = new Rx.Subject(); + private readonly files$$ = new Rx.BehaviorSubject([]); public readonly files$ = this.files$$.pipe( - switchMap((files$) => (files$.length ? zip(...files$) : of([]))) + Rx.switchMap((files$) => (files$.length ? Rx.zip(...files$) : Rx.of([]))) ); - public readonly clear$ = new Subject(); - public readonly error$ = new BehaviorSubject(undefined); - public readonly uploading$ = new BehaviorSubject(false); - public readonly done$ = new Subject(); + public readonly clear$ = new Rx.Subject(); + public readonly error$ = new Rx.BehaviorSubject(undefined); + public readonly uploading$ = new Rx.BehaviorSubject(false); + public readonly done$ = new Rx.Subject(); - private subscriptions: Subscription[]; + private subscriptions: Rx.Subscription[]; constructor( private readonly fileKind: FileKind, @@ -75,31 +53,31 @@ export class UploadState { private readonly opts: UploadOptions = { allowRepeatedUploads: false }, private readonly loadImageMetadata: ImageMetadataFactory = getImageMetadata ) { - const latestFiles$ = this.files$$.pipe(switchMap((files$) => combineLatest(files$))); + const latestFiles$ = this.files$$.pipe(Rx.switchMap((files$) => Rx.combineLatest(files$))); this.subscriptions = [ latestFiles$ .pipe( - map((files) => files.some((file) => file.status === 'uploading')), - distinctUntilChanged() + Rx.map((files) => files.some((file) => file.status === 'uploading')), + Rx.distinctUntilChanged() ) .subscribe(this.uploading$), latestFiles$ .pipe( - map((files) => { + Rx.map((files) => { const errorFile = files.find((file) => Boolean(file.error)); return errorFile ? errorFile.error : undefined; }), - filter(Boolean) + Rx.filter(Boolean) ) .subscribe(this.error$), latestFiles$ .pipe( - filter( + Rx.filter( (files) => Boolean(files.length) && files.every((file) => file.status === 'uploaded') ), - map((files) => + Rx.map((files) => files.map((file) => ({ id: file.id!, kind: this.fileKind.id, @@ -166,14 +144,14 @@ export class UploadState { */ private uploadFile = ( file$: SimpleStateSubject, - abort$: Observable, + abort$: Rx.Observable, meta?: unknown - ): Observable => { + ): Rx.Observable => { const abortController = new AbortController(); const abortSignal = abortController.signal; const { file, status } = file$.getValue(); if (!['idle', 'upload_failed'].includes(status)) { - return of(undefined); + return Rx.of(undefined); } let uploadTarget: undefined | FileJSON; @@ -184,8 +162,8 @@ export class UploadState { const mime = file.type || undefined; const _meta = meta as Record; - return from(isImage(file) ? this.loadImageMetadata(file) : of(undefined)).pipe( - mergeMap((imageMetadata) => + return Rx.from(isImage(file) ? this.loadImageMetadata(file) : Rx.of(undefined)).pipe( + Rx.mergeMap((imageMetadata) => this.client.create({ kind: this.fileKind.id, name, @@ -193,11 +171,11 @@ export class UploadState { meta: imageMetadata ? { ...imageMetadata, ..._meta } : _meta, }) ), - mergeMap((result) => { + Rx.mergeMap((result) => { uploadTarget = result.file; - return race( + return Rx.race( abort$.pipe( - map(() => { + Rx.map(() => { abortController.abort(); throw new Error('Abort!'); }) @@ -212,32 +190,34 @@ export class UploadState { }) ); }), - map(() => { + Rx.map(() => { file$.setState({ status: 'uploaded', id: uploadTarget?.id, fileJSON: uploadTarget }); }), - catchError((e) => { + Rx.catchError((e) => { const isAbortError = e.message === 'Abort!'; file$.setState({ status: 'upload_failed', error: isAbortError ? undefined : e }); - return of(isAbortError ? undefined : e); + return Rx.of(isAbortError ? undefined : e); }) ); }; - public upload = (meta?: unknown): Observable => { + public upload = (meta?: unknown): Rx.Observable => { if (this.isUploading()) { throw new Error('Upload already in progress'); } - const abort$ = new ReplaySubject(1); + const abort$ = new Rx.ReplaySubject(1); const sub = this.abort$.subscribe(abort$); const upload$ = this.files$$.pipe( - take(1), - switchMap((files$) => forkJoin(files$.map((file$) => this.uploadFile(file$, abort$, meta)))), - map(() => undefined), - finalize(() => { + Rx.take(1), + Rx.switchMap((files$) => + Rx.forkJoin(files$.map((file$) => this.uploadFile(file$, abort$, meta))) + ), + Rx.map(() => undefined), + Rx.finalize(() => { if (this.opts.allowRepeatedUploads) this.clear(); sub.unsubscribe(); }), - shareReplay() + Rx.shareReplay() ); upload$.subscribe(); // Kick off the upload diff --git a/src/plugins/files/public/components/upload_file/util/index.ts b/packages/shared-ux/file/file_upload/impl/src/util/index.ts similarity index 100% rename from src/plugins/files/public/components/upload_file/util/index.ts rename to packages/shared-ux/file/file_upload/impl/src/util/index.ts diff --git a/src/plugins/files/public/components/upload_file/util/parse_file_name.test.ts b/packages/shared-ux/file/file_upload/impl/src/util/parse_file_name.test.ts similarity index 100% rename from src/plugins/files/public/components/upload_file/util/parse_file_name.test.ts rename to packages/shared-ux/file/file_upload/impl/src/util/parse_file_name.test.ts diff --git a/src/plugins/files/public/components/upload_file/util/parse_file_name.ts b/packages/shared-ux/file/file_upload/impl/src/util/parse_file_name.ts similarity index 100% rename from src/plugins/files/public/components/upload_file/util/parse_file_name.ts rename to packages/shared-ux/file/file_upload/impl/src/util/parse_file_name.ts diff --git a/src/plugins/files/public/components/upload_file/util/simple_state_subject.ts b/packages/shared-ux/file/file_upload/impl/src/util/simple_state_subject.ts similarity index 100% rename from src/plugins/files/public/components/upload_file/util/simple_state_subject.ts rename to packages/shared-ux/file/file_upload/impl/src/util/simple_state_subject.ts diff --git a/packages/shared-ux/file/file_upload/impl/tsconfig.json b/packages/shared-ux/file/file_upload/impl/tsconfig.json new file mode 100644 index 000000000000..0b9ca147ee59 --- /dev/null +++ b/packages/shared-ux/file/file_upload/impl/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "node", + "jest", + "react", + "@emotion/react/types/css-prop", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/shared-ux/file/image/impl/BUILD.bazel b/packages/shared-ux/file/image/impl/BUILD.bazel index 95f8b36032d8..dde834269edd 100644 --- a/packages/shared-ux/file/image/impl/BUILD.bazel +++ b/packages/shared-ux/file/image/impl/BUILD.bazel @@ -74,7 +74,7 @@ TYPES_DEPS = [ "@npm//@types/classnames", "//packages/kbn-ambient-ui-types", "//packages/shared-ux/file/util:npm_module_types", - "//packages/shared-ux/file/image/types:npm_module_types", + "//packages/shared-ux/file/types:npm_module_types", ] jsts_transpiler( diff --git a/packages/shared-ux/file/image/impl/src/image.tsx b/packages/shared-ux/file/image/impl/src/image.tsx index 59dc56d4ddc1..40f9099df955 100644 --- a/packages/shared-ux/file/image/impl/src/image.tsx +++ b/packages/shared-ux/file/image/impl/src/image.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useState } from 'react'; import { EuiImage, EuiImageProps } from '@elastic/eui'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; import { getBlurhashSrc } from '@kbn/shared-ux-file-util'; import classNames from 'classnames'; import { css } from '@emotion/react'; diff --git a/packages/shared-ux/file/image/types/README.md b/packages/shared-ux/file/image/types/README.md deleted file mode 100644 index 6bef9e4caac2..000000000000 --- a/packages/shared-ux/file/image/types/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/shared-ux-link-redirect-app-types - -TODO diff --git a/packages/shared-ux/file/image/types/index.d.ts b/packages/shared-ux/file/image/types/index.d.ts deleted file mode 100644 index 0635982d12a7..000000000000 --- a/packages/shared-ux/file/image/types/index.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - */ - -/** - * Set of metadata captured for every image uploaded via the file services' - * public components. - */ -export interface FileImageMetadata { - /** - * The blurhash that can be displayed while the image is loading - */ - blurhash?: string; - /** - * Width, in px, of the original image - */ - width: number; - /** - * Height, in px, of the original image - */ - height: number; -} diff --git a/packages/shared-ux/file/mocks/BUILD.bazel b/packages/shared-ux/file/mocks/BUILD.bazel new file mode 100644 index 000000000000..e0df2b7fb962 --- /dev/null +++ b/packages/shared-ux/file/mocks/BUILD.bazel @@ -0,0 +1,134 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "mocks" +PKG_REQUIRE_NAME = "@kbn/shared-ux-file-mocks" + +SOURCE_FILES = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//jest", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", + "//packages/kbn-utility-types-jest:npm_module_types", + "//packages/shared-ux/file/types:npm_module_types", + +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/mocks/README.md b/packages/shared-ux/file/mocks/README.md new file mode 100644 index 000000000000..876bde04cb8a --- /dev/null +++ b/packages/shared-ux/file/mocks/README.md @@ -0,0 +1,3 @@ +# @kbn/shared-ux-markdown-mocks + +TODO \ No newline at end of file diff --git a/packages/shared-ux/file/mocks/index.ts b/packages/shared-ux/file/mocks/index.ts new file mode 100644 index 000000000000..412e320fd21f --- /dev/null +++ b/packages/shared-ux/file/mocks/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { DeeplyMockedKeys } from '@kbn/utility-types-jest'; +import type { BaseFilesClient } from '@kbn/shared-ux-file-types'; + +export const createMockFilesClient = (): DeeplyMockedKeys => ({ + create: jest.fn(), + delete: jest.fn(), + list: jest.fn(), + upload: jest.fn(), + getFileKind: jest.fn(), + getDownloadHref: jest.fn(), + bulkDelete: jest.fn(), + download: jest.fn(), + find: jest.fn(), + getById: jest.fn(), + getShare: jest.fn(), + listShares: jest.fn(), + share: jest.fn(), + unshare: jest.fn(), + update: jest.fn(), +}); diff --git a/packages/shared-ux/file/mocks/kibana.jsonc b/packages/shared-ux/file/mocks/kibana.jsonc new file mode 100644 index 000000000000..79247cea3183 --- /dev/null +++ b/packages/shared-ux/file/mocks/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-mocks", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/shared-ux/file/mocks/package.json b/packages/shared-ux/file/mocks/package.json new file mode 100644 index 000000000000..6be31776d191 --- /dev/null +++ b/packages/shared-ux/file/mocks/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-mocks", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/packages/shared-ux/file/mocks/tsconfig.json b/packages/shared-ux/file/mocks/tsconfig.json new file mode 100644 index 000000000000..6711daf2036c --- /dev/null +++ b/packages/shared-ux/file/mocks/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/shared-ux/file/image/types/BUILD.bazel b/packages/shared-ux/file/types/BUILD.bazel similarity index 95% rename from packages/shared-ux/file/image/types/BUILD.bazel rename to packages/shared-ux/file/types/BUILD.bazel index d328c3c4fc76..5ebe604a3fe9 100644 --- a/packages/shared-ux/file/image/types/BUILD.bazel +++ b/packages/shared-ux/file/types/BUILD.bazel @@ -3,7 +3,7 @@ load("@build_bazel_rules_nodejs//:index.bzl", "js_library") load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_DIRNAME = "types" -PKG_REQUIRE_NAME = "@kbn/shared-ux-file-image-types" +PKG_REQUIRE_NAME = "@kbn/shared-ux-file-types" SRCS = glob( [ diff --git a/packages/shared-ux/file/types/README.md b/packages/shared-ux/file/types/README.md new file mode 100644 index 000000000000..66e069f50592 --- /dev/null +++ b/packages/shared-ux/file/types/README.md @@ -0,0 +1,5 @@ +# @kbn/shared-ux-link-redirect-app-types + +To generate the types for the file client run. See ./build_file_client.ts + +`yarn ts-node ./packages/shared-ux/file/types/build_file_client.ts` diff --git a/packages/shared-ux/file/types/base_file_client.d.ts b/packages/shared-ux/file/types/base_file_client.d.ts new file mode 100644 index 000000000000..87805f63cfc3 --- /dev/null +++ b/packages/shared-ux/file/types/base_file_client.d.ts @@ -0,0 +1,158 @@ +/* + * 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 { FileJSON, FileKind } from '.'; + +export interface Pagination { + page?: number; + perPage?: number; +} + +export interface Abortable { + abortSignal?: AbortSignal; +} + +export interface BaseFilesClient { + /** + * Find a set of files given some filters. + * + * @param args - File filters + */ + find: ( + args: { + kind?: string | string[]; + status?: string | string[]; + extension?: string | string[]; + name?: string | string[]; + meta?: M; + } & Pagination & + Abortable + ) => Promise<{ files: Array>; total: number }>; + /** + * Bulk a delete a set of files given their IDs. + * + * @param args - Bulk delete args + */ + bulkDelete: ( + args: { ids: string[] } & Abortable + ) => Promise<{ succeeded: string[]; failed?: Array<[id: string, reason: string]> }>; + /** + * Create a new file object with the provided metadata. + * + * @param args - create file args + */ + create: ( + args: { name: string; meta?: M; alt?: string; mimeType?: string; kind: string } & Abortable + ) => Promise<{ file: FileJSON }>; + /** + * Delete a file object and all associated share and content objects. + * + * @param args - delete file args + */ + delete: (args: { id: string; kind: string } & Abortable) => Promise<{ ok: true }>; + /** + * Get a file object by ID. + * + * @param args - get file by ID args + */ + getById: (args: { id: string; kind: string } & Abortable) => Promise<{ file: FileJSON }>; + /** + * List all file objects, of a given {@link FileKind}. + * + * @param args - list files args + */ + list: ( + args: { + kind: string; + status?: string | string[]; + extension?: string | string[]; + name?: string | string[]; + meta?: M; + } & Pagination & + Abortable + ) => Promise<{ files: Array>; total: number }>; + /** + * Update a set of of metadata values of the file object. + * + * @param args - update file args + */ + update: ( + args: { id: string; kind: string; name?: string; meta?: M; alt?: string } & Abortable + ) => Promise<{ file: FileJSON }>; + /** + * Stream the contents of the file to Kibana server for storage. + * + * @param args - upload file args + */ + upload: ( + args: { + id: string; + /** + * Should be blob or ReadableStream of some kind. + */ + body: unknown; + kind: string; + abortSignal?: AbortSignal; + contentType?: string; + selfDestructOnAbort?: boolean; + } & Abortable + ) => Promise<{ + ok: true; + size: number; + }>; + /** + * Stream a download of the file object's content. + * + * @param args - download file args + */ + download: (args: { fileName?: string; id: string; kind: string } & Abortable) => Promise; + /** + * Get a string for downloading a file that can be passed to a button element's + * href for download. + * + * @param args - get download URL args + */ + getDownloadHref: (args: Pick, 'id' | 'fileKind'>) => string; + /** + * Share a file by creating a new file share instance. + * + * @note This returns the secret token that can be used + * to access a file via the public download enpoint. + * + * @param args - File share arguments + */ + share: ( + args: { name?: string; validUntil?: number; fileId: string; kind: string } & Abortable + ) => Promise; + /** + * Delete a file share instance. + * + * @param args - File unshare arguments + */ + unshare: (args: { id: string; kind: string } & Abortable) => Promise<{ ok: true }>; + /** + * Get a file share instance. + * + * @param args - Get file share arguments + */ + getShare: (args: { id: string; kind: string } & Abortable) => Promise<{ share: FileShareJSON }>; + /** + * List all file shares. Optionally scoping to a specific + * file. + * + * @param args - Get file share arguments + */ + listShares: ( + args: { forFileId?: string; kind: string } & Pagination & Abortable + ) => Promise<{ shares: FileShareJSON[] }>; + /** + * Get a file kind + * @param id The id of the file kind + */ + getFileKind: (id: string) => FileKind; +} diff --git a/packages/shared-ux/file/types/index.d.ts b/packages/shared-ux/file/types/index.d.ts new file mode 100644 index 000000000000..ff9d83b2cfdd --- /dev/null +++ b/packages/shared-ux/file/types/index.d.ts @@ -0,0 +1,319 @@ +/* + * 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 type { BaseFilesClient, Abortable, Pagination } from './base_file_client'; + +/* Status of a file. + * + * AWAITING_UPLOAD - A file object has been created but does not have any contents. + * UPLOADING - File contents are being uploaded. + * READY - File contents have been uploaded and are ready for to be downloaded. + * UPLOAD_ERROR - An attempt was made to upload file contents but failed. + * DELETED - The file contents have been or are being deleted. + */ +export type FileStatus = 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERROR' | 'DELETED'; + +/** + * Supported file compression algorithms + */ +export type FileCompression = 'br' | 'gzip' | 'deflate' | 'none'; + +/** + * File metadata fields are defined per the ECS specification: + * + * https://www.elastic.co/guide/en/ecs/current/ecs-file.html + * + * Custom fields are named according to the custom field convention: "CustomFieldName". + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type BaseFileMetadata = { + /** + * Name of the file + * + * @note This field is recommended since it will provide a better UX + */ + name?: string; + + /** + * MIME type of the file contents + */ + mime_type?: string; + + /** + * ISO string representing the file creation date + */ + created?: string; + /** + * Size of the file + */ + size?: number; + /** + * Hash of the file's contents + */ + hash?: { + /** + * UTF-8 string representing MD5 hash + */ + md5?: string; + /** + * UTF-8 string representing sha1 hash + */ + sha1?: string; + /** + * UTF-8 string representing sha256 hash + */ + sha256?: string; + /** + * UTF-8 string representing sha384 hash + */ + sha384?: string; + /** + * UTF-8 string representing sha512 hash + */ + sha512?: string; + /** + * UTF-8 string representing shadeep hash + */ + ssdeep?: string; + /** + * UTF-8 string representing tlsh hash + */ + tlsh?: string; + [hashName: string]: string | undefined; + }; + + /** + * Data about the user that created the file + */ + user?: { + /** + * The human-friendly user name of the owner of the file + * + * @note this field cannot be used to uniquely ID a user. See {@link BaseFileMetadata['user']['id']}. + */ + name?: string; + /** + * The unique ID of the user who created the file, taken from the user profile + * ID. + * + * See https://www.elastic.co/guide/en/elasticsearch/reference/master/user-profile.html. + */ + id?: string; + }; + + /** + * The file extension, for example "jpg", "png", "svg" and so forth + */ + extension?: string; + + /** + * Alternate text that can be used used to describe the contents of the file + * in human-friendly language + */ + Alt?: string; + + /** + * ISO string representing when the file was last updated + */ + Updated?: string; + + /** + * The file's current status + */ + Status?: FileStatus; + + /** + * The maximum number of bytes per file chunk + */ + ChunkSize?: number; + + /** + * Compression algorithm used to transform chunks before they were stored. + */ + Compression?: FileCompression; +}; + +/** + * Extra metadata on a file object specific to Kibana implementation. + */ +export type FileMetadata = Required< + Pick +> & + BaseFileMetadata & { + /** + * Unique identifier of the kind of file. Kibana applications can register + * these at runtime. + */ + FileKind: string; + + /** + * User-defined metadata. + */ + Meta?: Meta; + }; + +/** + * Attributes of a file that represent a serialised version of the file. + */ +export interface FileJSON { + /** + * Unique file ID. + */ + id: string; + /** + * ISO string of when this file was created + */ + created: FileMetadata['created']; + /** + * ISO string of when the file was updated + */ + updated: FileMetadata['Updated']; + /** + * File name. + * + * @note Does not have to be unique. + */ + name: FileMetadata['name']; + /** + * MIME type of the file's contents. + */ + mimeType: FileMetadata['mime_type']; + /** + * The size, in bytes, of the file content. + */ + size: FileMetadata['size']; + /** + * The file extension (dot suffix). + * + * @note this value can be derived from MIME type but is stored for search + * convenience. + */ + extension: FileMetadata['extension']; + + /** + * A consumer defined set of attributes. + * + * Consumers of the file service can add their own tags and identifiers to + * a file using the "meta" object. + */ + meta: FileMetadata['Meta']; + /** + * Use this text to describe the file contents for display and accessibility. + */ + alt: FileMetadata['Alt']; + /** + * A unique kind that governs various aspects of the file. A consumer of the + * files service must register a file kind and link their files to a specific + * kind. + * + * @note This enables stricter access controls to CRUD and other functionality + * exposed by the files service. + */ + fileKind: FileMetadata['FileKind']; + /** + * The current status of the file. + * + * See {@link FileStatus} for more details. + */ + status: FileMetadata['Status']; + /** + * User data associated with this file + */ + user?: FileMetadata['user']; +} + +/* + * A descriptor of meta values associated with a set or "kind" of files. + * + * @note In order to use the file service consumers must register a {@link FileKind} + * in the {@link FileKindsRegistry}. + */ +export interface FileKind { + /** + * Unique file kind ID + */ + id: string; + /** + * Maximum size, in bytes, a file of this kind can be. + * + * @default 4MiB + */ + maxSizeBytes?: number; + + /** + * The MIME type of the file content. + * + * @default accept all mime types + */ + allowedMimeTypes?: string[]; + + /** + * Blob store specific settings that enable configuration of storage + * details. + */ + blobStoreSettings?: BlobStorageSettings; + + /** + * Specify which HTTP routes to create for the file kind. + * + * You can always create your own HTTP routes for working with files but + * this interface allows you to expose basic CRUD operations, upload, download + * and sharing of files over a RESTful-like interface. + * + * @note The public {@link FileClient} uses these endpoints. + */ + http: { + /** + * Expose file creation (and upload) over HTTP. + */ + create?: HttpEndpointDefinition; + /** + * Expose file updates over HTTP. + */ + update?: HttpEndpointDefinition; + /** + * Expose file deletion over HTTP. + */ + delete?: HttpEndpointDefinition; + /** + * Expose "get by ID" functionality over HTTP. + */ + getById?: HttpEndpointDefinition; + /** + * Expose the ability to list all files of this kind over HTTP. + */ + list?: HttpEndpointDefinition; + /** + * Expose the ability to download a file's contents over HTTP. + */ + download?: HttpEndpointDefinition; + /** + * Expose file share functionality over HTTP. + */ + share?: HttpEndpointDefinition; + }; +} + +/** + * Set of metadata captured for every image uploaded via the file services' + * public components. + */ +export interface FileImageMetadata { + /** + * The blurhash that can be displayed while the image is loading + */ + blurhash?: string; + /** + * Width, in px, of the original image + */ + width: number; + /** + * Height, in px, of the original image + */ + height: number; +} diff --git a/packages/shared-ux/file/types/kibana.jsonc b/packages/shared-ux/file/types/kibana.jsonc new file mode 100644 index 000000000000..f40bdacc6802 --- /dev/null +++ b/packages/shared-ux/file/types/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-types", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/image/types/package.json b/packages/shared-ux/file/types/package.json similarity index 63% rename from packages/shared-ux/file/image/types/package.json rename to packages/shared-ux/file/types/package.json index 66726e55b51f..3bcdaed63f50 100644 --- a/packages/shared-ux/file/image/types/package.json +++ b/packages/shared-ux/file/types/package.json @@ -1,5 +1,5 @@ { - "name": "@kbn/shared-ux-link-redirect-app-types", + "name": "@kbn/shared-ux-file-types", "private": true, "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0" diff --git a/packages/shared-ux/file/types/tsconfig.json b/packages/shared-ux/file/types/tsconfig.json new file mode 100644 index 000000000000..7b2ef816db91 --- /dev/null +++ b/packages/shared-ux/file/types/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "node" + ], + }, + "include": [ + "*.d.ts" + ] +} diff --git a/packages/shared-ux/file/util/BUILD.bazel b/packages/shared-ux/file/util/BUILD.bazel index e7aeb058410b..1fdb6e0500ed 100644 --- a/packages/shared-ux/file/util/BUILD.bazel +++ b/packages/shared-ux/file/util/BUILD.bazel @@ -50,6 +50,8 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "@npm//jest", "@npm//blurhash", + "@npm//rxjs", + "@npm//react-use", ] # In this array place dependencies necessary to build the types, which will include the @@ -64,7 +66,9 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "@npm//@types/jest", "@npm//blurhash", - "//packages/shared-ux/file/image/types:npm_module_types", + "@npm//rxjs", + "@npm//react-use", + "//packages/shared-ux/file/types:npm_module_types", ] jsts_transpiler( diff --git a/packages/shared-ux/file/util/index.ts b/packages/shared-ux/file/util/index.ts index 25ce4d9f13cb..1dce5607ac64 100644 --- a/packages/shared-ux/file/util/index.ts +++ b/packages/shared-ux/file/util/index.ts @@ -12,4 +12,5 @@ export { getImageMetadata, isImage, type ImageMetadataFactory, + useBehaviorSubject, } from './src'; diff --git a/packages/shared-ux/file/util/src/image_metadata.ts b/packages/shared-ux/file/util/src/image_metadata.ts index f19abcf14c05..f7e361d10d71 100644 --- a/packages/shared-ux/file/util/src/image_metadata.ts +++ b/packages/shared-ux/file/util/src/image_metadata.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import bh from 'blurhash'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; +import * as bh from 'blurhash'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-types'; export function isImage(file: { type?: string }): boolean { return Boolean(file.type?.startsWith('image/')); diff --git a/packages/shared-ux/file/util/src/index.ts b/packages/shared-ux/file/util/src/index.ts index 9e0dccc6c336..9f7f2b1f443d 100644 --- a/packages/shared-ux/file/util/src/index.ts +++ b/packages/shared-ux/file/util/src/index.ts @@ -8,3 +8,4 @@ export { getImageMetadata, isImage, fitToBox, getBlurhashSrc } from './image_metadata'; export type { ImageMetadataFactory } from './image_metadata'; +export { useBehaviorSubject } from './use_behavior_subject'; diff --git a/src/plugins/files/public/components/use_behavior_subject.ts b/packages/shared-ux/file/util/src/use_behavior_subject.ts similarity index 100% rename from src/plugins/files/public/components/use_behavior_subject.ts rename to packages/shared-ux/file/util/src/use_behavior_subject.ts diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 7ad2c16224d6..7c37284bc3e7 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -33,7 +33,6 @@ export const storybookAliases = { expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', - files: 'src/plugins/files/.storybook', fleet: 'x-pack/plugins/fleet/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/infra/.storybook', diff --git a/src/plugins/files/.storybook/manager.ts b/src/plugins/files/.storybook/manager.ts deleted file mode 100644 index d49eea178479..000000000000 --- a/src/plugins/files/.storybook/manager.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { addons } from '@storybook/addons'; -import { create } from '@storybook/theming'; -import { PANEL_ID } from '@storybook/addon-actions'; - -addons.setConfig({ - theme: create({ - base: 'light', - brandTitle: 'Kibana React Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/main/src/plugins/files', - }), - showPanel: true.valueOf, - selectedPanel: PANEL_ID, -}); diff --git a/src/plugins/files/common/api_routes.ts b/src/plugins/files/common/api_routes.ts index 90ddd5d8b6e7..ad6f11ca541d 100644 --- a/src/plugins/files/common/api_routes.ts +++ b/src/plugins/files/common/api_routes.ts @@ -27,49 +27,46 @@ export interface EndpointInputs< body?: B; } -export interface CreateRouteDefinition { - inputs: { - params: TypeOf>; - query: TypeOf>; - body: TypeOf>; - }; - output: R; -} - -export type AnyEndpoint = CreateRouteDefinition; +type Extends = X extends Y ? Y : unknown; /** - * Abstract type definition for API route inputs and outputs. + * Use this when creating file service endpoints to ensure that the client methods + * are receiving the types they expect as well as providing the expected inputs. * - * These definitions should be shared between the public and server - * as the single source of truth. + * For example, consider create route: + * + * const rt = configSchema.object({...}); + * + * export type Endpoint = CreateRouteDefinition< + * typeof rt, // We pass in our runtime types + * { file: FileJSON }, // We pass in return type + * FilesClient['create'] // We pass in client method + * >; + * + * This will return `unknown` for param, query or body if client-server types + * are out-of-sync. + * + * The very best would be if the client was auto-generated from the server + * endpoint declarations. */ -export interface HttpApiInterfaceEntryDefinition< - P = unknown, - Q = unknown, - B = unknown, - R = unknown +export interface CreateRouteDefinition< + Inputs extends EndpointInputs, + R, + ClientMethod extends (arg: any) => Promise = () => Promise > { inputs: { - params: P; - query: Q; - body: B; + params: Extends[0], TypeOf>>; + query: Extends[0], TypeOf>>; + body: Extends[0], TypeOf>>; }; - output: R; + output: Extends>>; } -export type { Endpoint as CreateFileKindHttpEndpoint } from '../server/routes/file_kind/create'; -export type { Endpoint as DeleteFileKindHttpEndpoint } from '../server/routes/file_kind/delete'; -export type { Endpoint as DownloadFileKindHttpEndpoint } from '../server/routes/file_kind/download'; -export type { Endpoint as GetByIdFileKindHttpEndpoint } from '../server/routes/file_kind/get_by_id'; -export type { Endpoint as ListFileKindHttpEndpoint } from '../server/routes/file_kind/list'; -export type { Endpoint as UpdateFileKindHttpEndpoint } from '../server/routes/file_kind/update'; -export type { Endpoint as UploadFileKindHttpEndpoint } from '../server/routes/file_kind/upload'; -export type { Endpoint as FindFilesHttpEndpoint } from '../server/routes/find'; -export type { Endpoint as FilesMetricsHttpEndpoint } from '../server/routes/metrics'; -export type { Endpoint as FileShareHttpEndpoint } from '../server/routes/file_kind/share/share'; -export type { Endpoint as FileUnshareHttpEndpoint } from '../server/routes/file_kind/share/unshare'; -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'; +export interface AnyEndpoint { + inputs: { + params: any; + query: any; + body: any; + }; + output: any; +} diff --git a/src/plugins/files/common/files_client.ts b/src/plugins/files/common/files_client.ts new file mode 100644 index 000000000000..5914f93b8ca7 --- /dev/null +++ b/src/plugins/files/common/files_client.ts @@ -0,0 +1,61 @@ +/* + * 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 { BaseFilesClient } from '@kbn/shared-ux-file-types'; +import type { FilesMetrics } from './types'; + +/** + * A client that can be used to manage a specific {@link FileKind}. + */ +export interface FilesClient extends BaseFilesClient { + /** + * Get metrics of file system, like storage usage. + * + * @param args - Get metrics arguments + */ + getMetrics: () => Promise; + /** + * Download a file, bypassing regular security by way of a + * secret share token. + * + * @param args - Get public download arguments. + */ + publicDownload: (args: { token: string; fileName?: string }) => any; +} + +export type FilesClientResponses = { + [K in keyof FilesClient]: Awaited[K]>>; +}; + +/** + * A files client that is scoped to a specific {@link FileKind}. + * + * More convenient if you want to re-use the same client for the same file kind + * and not specify the kind every time. + */ +export type ScopedFilesClient = { + [K in keyof FilesClient]: K extends 'list' + ? (arg?: Omit[K]>[0], 'kind'>) => ReturnType[K]> + : (arg: Omit[K]>[0], 'kind'>) => ReturnType[K]>; +}; + +/** + * A factory for creating a {@link ScopedFilesClient} + */ +export interface FilesClientFactory { + /** + * Create a files client. + */ + asUnscoped(): FilesClient; + /** + * Create a {@link ScopedFileClient} for a given {@link FileKind}. + * + * @param fileKind - The {@link FileKind} to create a client for. + */ + asScoped(fileKind: string): ScopedFilesClient; +} diff --git a/src/plugins/files/common/types.ts b/src/plugins/files/common/types.ts index 370be9028317..b0f9b7e0b9b6 100644 --- a/src/plugins/files/common/types.ts +++ b/src/plugins/files/common/types.ts @@ -9,8 +9,20 @@ import type { SavedObject } from '@kbn/core/server'; import type { Observable } from 'rxjs'; import type { Readable } from 'stream'; +import type { FileJSON, FileStatus, FileMetadata } from '@kbn/shared-ux-file-types'; import type { ES_FIXED_SIZE_INDEX_BLOB_STORE } from './constants'; +export type { + FileKind, + FileJSON, + FileStatus, + FileMetadata, + BaseFilesClient, + FileCompression, + BaseFileMetadata, + FileImageMetadata, +} from '@kbn/shared-ux-file-types'; + /** * Values for paginating through results. */ @@ -25,226 +37,6 @@ export interface Pagination { perPage?: number; } -/** - * Status of a file. - * - * AWAITING_UPLOAD - A file object has been created but does not have any contents. - * UPLOADING - File contents are being uploaded. - * READY - File contents have been uploaded and are ready for to be downloaded. - * UPLOAD_ERROR - An attempt was made to upload file contents but failed. - * DELETED - The file contents have been or are being deleted. - */ -export type FileStatus = 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERROR' | 'DELETED'; - -/** - * Supported file compression algorithms - */ -export type FileCompression = 'br' | 'gzip' | 'deflate' | 'none'; - -/** - * File metadata fields are defined per the ECS specification: - * - * https://www.elastic.co/guide/en/ecs/current/ecs-file.html - * - * Custom fields are named according to the custom field convention: "CustomFieldName". - */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type BaseFileMetadata = { - /** - * Name of the file - * - * @note This field is recommended since it will provide a better UX - */ - name?: string; - - /** - * MIME type of the file contents - */ - mime_type?: string; - - /** - * ISO string representing the file creation date - */ - created?: string; - /** - * Size of the file - */ - size?: number; - /** - * Hash of the file's contents - */ - hash?: { - /** - * UTF-8 string representing MD5 hash - */ - md5?: string; - /** - * UTF-8 string representing sha1 hash - */ - sha1?: string; - /** - * UTF-8 string representing sha256 hash - */ - sha256?: string; - /** - * UTF-8 string representing sha384 hash - */ - sha384?: string; - /** - * UTF-8 string representing sha512 hash - */ - sha512?: string; - /** - * UTF-8 string representing shadeep hash - */ - ssdeep?: string; - /** - * UTF-8 string representing tlsh hash - */ - tlsh?: string; - [hashName: string]: string | undefined; - }; - - /** - * Data about the user that created the file - */ - user?: { - /** - * The human-friendly user name of the owner of the file - * - * @note this field cannot be used to uniquely ID a user. See {@link BaseFileMetadata['user']['id']}. - */ - name?: string; - /** - * The unique ID of the user who created the file, taken from the user profile - * ID. - * - * See https://www.elastic.co/guide/en/elasticsearch/reference/master/user-profile.html. - */ - id?: string; - }; - - /** - * The file extension, for example "jpg", "png", "svg" and so forth - */ - extension?: string; - - /** - * Alternate text that can be used used to describe the contents of the file - * in human-friendly language - */ - Alt?: string; - - /** - * ISO string representing when the file was last updated - */ - Updated?: string; - - /** - * The file's current status - */ - Status?: FileStatus; - - /** - * The maximum number of bytes per file chunk - */ - ChunkSize?: number; - - /** - * Compression algorithm used to transform chunks before they were stored. - */ - Compression?: FileCompression; -}; - -/** - * Extra metadata on a file object specific to Kibana implementation. - */ -export type FileMetadata = Required< - Pick -> & - BaseFileMetadata & { - /** - * Unique identifier of the kind of file. Kibana applications can register - * these at runtime. - */ - FileKind: string; - - /** - * User-defined metadata. - */ - Meta?: Meta; - }; - -/** - * Attributes of a file that represent a serialised version of the file. - */ -export interface FileJSON { - /** - * Unique file ID. - */ - id: string; - /** - * ISO string of when this file was created - */ - created: FileMetadata['created']; - /** - * ISO string of when the file was updated - */ - updated: FileMetadata['Updated']; - /** - * File name. - * - * @note Does not have to be unique. - */ - name: FileMetadata['name']; - /** - * MIME type of the file's contents. - */ - mimeType: FileMetadata['mime_type']; - /** - * The size, in bytes, of the file content. - */ - size: FileMetadata['size']; - /** - * The file extension (dot suffix). - * - * @note this value can be derived from MIME type but is stored for search - * convenience. - */ - extension: FileMetadata['extension']; - - /** - * A consumer defined set of attributes. - * - * Consumers of the file service can add their own tags and identifiers to - * a file using the "meta" object. - */ - meta: FileMetadata['Meta']; - /** - * Use this text to describe the file contents for display and accessibility. - */ - alt: FileMetadata['Alt']; - /** - * A unique kind that governs various aspects of the file. A consumer of the - * files service must register a file kind and link their files to a specific - * kind. - * - * @note This enables stricter access controls to CRUD and other functionality - * exposed by the files service. - */ - fileKind: FileMetadata['FileKind']; - /** - * The current status of the file. - * - * See {@link FileStatus} for more details. - */ - status: FileMetadata['Status']; - /** - * User data associated with this file - */ - user?: FileMetadata['user']; -} - /** * An {@link SavedObject} containing a file object (i.e., metadata only). */ @@ -440,90 +232,6 @@ export interface BlobStorageSettings { // Other blob store settings will go here once available } -interface HttpEndpointDefinition { - /** - * Specify the tags for this endpoint. - * - * @example - * // This will enable access control to this endpoint for users that can access "myApp" only. - * { tags: ['access:myApp'] } - * - */ - tags: string[]; -} - -/** - * A descriptor of meta values associated with a set or "kind" of files. - * - * @note In order to use the file service consumers must register a {@link FileKind} - * in the {@link FileKindsRegistry}. - */ -export interface FileKind { - /** - * Unique file kind ID - */ - id: string; - /** - * Maximum size, in bytes, a file of this kind can be. - * - * @default 4MiB - */ - maxSizeBytes?: number; - - /** - * The MIME type of the file content. - * - * @default accept all mime types - */ - allowedMimeTypes?: string[]; - - /** - * Blob store specific settings that enable configuration of storage - * details. - */ - blobStoreSettings?: BlobStorageSettings; - - /** - * Specify which HTTP routes to create for the file kind. - * - * You can always create your own HTTP routes for working with files but - * this interface allows you to expose basic CRUD operations, upload, download - * and sharing of files over a RESTful-like interface. - * - * @note The public {@link FileClient} uses these endpoints. - */ - http: { - /** - * Expose file creation (and upload) over HTTP. - */ - create?: HttpEndpointDefinition; - /** - * Expose file updates over HTTP. - */ - update?: HttpEndpointDefinition; - /** - * Expose file deletion over HTTP. - */ - delete?: HttpEndpointDefinition; - /** - * Expose "get by ID" functionality over HTTP. - */ - getById?: HttpEndpointDefinition; - /** - * Expose the ability to list all files of this kind over HTTP. - */ - list?: HttpEndpointDefinition; - /** - * Expose the ability to download a file's contents over HTTP. - */ - download?: HttpEndpointDefinition; - /** - * Expose file share functionality over HTTP. - */ - share?: HttpEndpointDefinition; - }; -} - /** * A collection of generally useful metrics about files. */ diff --git a/src/plugins/files/public/components/file_picker/i18n_texts.ts b/src/plugins/files/public/components/file_picker/i18n_texts.ts deleted file mode 100644 index 9bc4b4642cd6..000000000000 --- a/src/plugins/files/public/components/file_picker/i18n_texts.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 i18nTexts = { - title: i18n.translate('files.filePicker.title', { - defaultMessage: 'Select a file', - }), - titleMultiple: i18n.translate('files.filePicker.titleMultiple', { - defaultMessage: 'Select files', - }), - loadingFilesErrorTitle: i18n.translate('files.filePicker.error.loadingTitle', { - defaultMessage: 'Could not load files', - }), - retryButtonLabel: i18n.translate('files.filePicker.error.retryButtonLabel', { - defaultMessage: 'Retry', - }), - emptyStatePrompt: i18n.translate('files.filePicker.emptyStatePromptTitle', { - defaultMessage: 'Upload your first file', - }), - selectFileLabel: i18n.translate('files.filePicker.selectFileButtonLable', { - defaultMessage: 'Select file', - }), - selectFilesLabel: (nrOfFiles: number) => - i18n.translate('files.filePicker.selectFilesButtonLable', { - defaultMessage: 'Select {nrOfFiles} files', - values: { nrOfFiles }, - }), - searchFieldPlaceholder: i18n.translate('files.filePicker.searchFieldPlaceholder', { - defaultMessage: 'my-file-*', - }), - emptyFileGridPrompt: i18n.translate('files.filePicker.emptyGridPrompt', { - defaultMessage: 'No files match your filter', - }), - loadMoreButtonLabel: i18n.translate('files.filePicker.loadMoreButtonLabel', { - defaultMessage: 'Load more', - }), - clearFilterButton: i18n.translate('files.filePicker.clearFilterButtonLabel', { - defaultMessage: 'Clear filter', - }), - uploadFilePlaceholderText: i18n.translate('files.filePicker.uploadFilePlaceholderText', { - defaultMessage: 'Drag and drop to upload new files', - }), -}; diff --git a/src/plugins/files/public/components/stories_shared.ts b/src/plugins/files/public/components/stories_shared.ts deleted file mode 100644 index f20058ea0f58..000000000000 --- a/src/plugins/files/public/components/stories_shared.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { FileKind } from '../../common'; -import { - setFileKindsRegistry, - getFileKindsRegistry, - FileKindsRegistryImpl, -} from '../../common/file_kinds_registry'; - -setFileKindsRegistry(new FileKindsRegistryImpl()); -const fileKindsRegistry = getFileKindsRegistry(); -export const register: FileKindsRegistryImpl['register'] = (fileKind: FileKind) => { - if (!fileKindsRegistry.getAll().find((kind) => kind.id === fileKind.id)) { - getFileKindsRegistry().register(fileKind); - } -}; diff --git a/src/plugins/files/public/components/util/image_metadata.test.ts b/src/plugins/files/public/components/util/image_metadata.test.ts deleted file mode 100644 index d91b9be36f6b..000000000000 --- a/src/plugins/files/public/components/util/image_metadata.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 { fitToBox } from './image_metadata'; -describe('util', () => { - describe('fitToBox', () => { - test('300x300', () => { - expect(fitToBox(300, 300)).toMatchInlineSnapshot(` - Object { - "height": 120, - "width": 120, - } - `); - }); - - test('300x150', () => { - expect(fitToBox(300, 150)).toMatchInlineSnapshot(` - Object { - "height": 60, - "width": 120, - } - `); - }); - - test('4500x9000', () => { - expect(fitToBox(4500, 9000)).toMatchInlineSnapshot(` - Object { - "height": 120, - "width": 60, - } - `); - }); - - test('1000x300', () => { - expect(fitToBox(1000, 300)).toMatchInlineSnapshot(` - Object { - "height": 36, - "width": 120, - } - `); - }); - - test('0x0', () => { - expect(fitToBox(0, 0)).toMatchInlineSnapshot(` - Object { - "height": 0, - "width": 0, - } - `); - }); - }); -}); diff --git a/src/plugins/files/public/components/util/image_metadata.ts b/src/plugins/files/public/components/util/image_metadata.ts deleted file mode 100644 index 891448b99bbb..000000000000 --- a/src/plugins/files/public/components/util/image_metadata.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 * as bh from 'blurhash'; -import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; - -export function isImage(file: { type?: string }): boolean { - return Boolean(file.type?.startsWith('image/')); -} - -export const boxDimensions = { - width: 120, - height: 120, -}; - -/** - * Calculate the size of an image, fitting to our limits see {@link boxDimensions}, - * while preserving the aspect ratio. - */ -export function fitToBox(width: number, height: number): { width: number; height: number } { - const offsetRatio = Math.abs( - Math.min( - // Find the aspect at which our box is smallest, if less than 1, it means we exceed the limits - Math.min(boxDimensions.width / width, boxDimensions.height / height), - // Values greater than 1 are within our limits - 1 - ) - 1 // Get the percentage we are exceeding. E.g., 0.3 - 1 = -0.7 means the image needs to shrink by 70% to fit - ); - return { - width: Math.floor(width - offsetRatio * width), - height: Math.floor(height - offsetRatio * height), - }; -} - -/** - * Get the native size of the image - */ -function loadImage(src: string): Promise { - return new Promise((res, rej) => { - const image = new window.Image(); - image.src = src; - image.onload = () => res(image); - image.onerror = rej; - }); -} - -/** - * Extract image metadata, assumes that file or blob as an image! - */ -export async function getImageMetadata(file: File | Blob): Promise { - const imgUrl = window.URL.createObjectURL(file); - try { - const image = await loadImage(imgUrl); - const canvas = document.createElement('canvas'); - // blurhash encoding and decoding is an expensive algorithm, - // so we have to shrink the image to speed up the calculation - const { width, height } = fitToBox(image.width, image.height); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - if (!ctx) throw new Error('Could not get 2d canvas context!'); - ctx.drawImage(image, 0, 0, width, height); - const imgData = ctx.getImageData(0, 0, width, height); - return { - blurhash: bh.encode(imgData.data, imgData.width, imgData.height, 4, 4), - width: image.width, - height: image.height, - }; - } catch (e) { - // Don't error out if we cannot generate the blurhash - return undefined; - } finally { - window.URL.revokeObjectURL(imgUrl); - } -} - -export type ImageMetadataFactory = typeof getImageMetadata; - -export function getBlurhashSrc({ - width, - height, - hash, -}: { - width: number; - height: number; - hash: string; -}): string { - const smallSizeImageCanvas = document.createElement('canvas'); - const { width: blurWidth, height: blurHeight } = fitToBox(width, height); - smallSizeImageCanvas.width = blurWidth; - smallSizeImageCanvas.height = blurHeight; - - const smallSizeImageCtx = smallSizeImageCanvas.getContext('2d')!; - const imageData = smallSizeImageCtx.createImageData(blurWidth, blurHeight); - imageData.data.set(bh.decode(hash, blurWidth, blurHeight)); - smallSizeImageCtx.putImageData(imageData, 0, 0); - - // scale back the blurred image to the size of the original image, - // so it is sized and positioned the same as the original image when used with an `` tag - const originalSizeImageCanvas = document.createElement('canvas'); - originalSizeImageCanvas.width = width; - originalSizeImageCanvas.height = height; - const originalSizeImageCtx = originalSizeImageCanvas.getContext('2d')!; - originalSizeImageCtx.drawImage(smallSizeImageCanvas, 0, 0, width, height); - return originalSizeImageCanvas.toDataURL(); -} diff --git a/src/plugins/files/public/files_client/files_client.ts b/src/plugins/files/public/files_client/files_client.ts index a842c702c242..9044ab58cbf7 100644 --- a/src/plugins/files/public/files_client/files_client.ts +++ b/src/plugins/files/public/files_client/files_client.ts @@ -8,6 +8,7 @@ import type { HttpStart } from '@kbn/core/public'; import type { ScopedFilesClient, FilesClient } from '../types'; +import { getFileKindsRegistry } from '../../common/file_kinds_registry'; import { API_BASE_PATH, FILES_API_BASE_PATH, @@ -170,6 +171,9 @@ export function createFilesClient({ publicDownload: ({ token, fileName }) => { return http.get(apiRoutes.getPublicDownloadRoute(fileName), { query: { token } }); }, + getFileKind(id: string) { + return getFileKindsRegistry().get(id); + }, }; return api; } diff --git a/src/plugins/files/public/index.ts b/src/plugins/files/public/index.ts index dc36b4d29d25..168780bd9238 100644 --- a/src/plugins/files/public/index.ts +++ b/src/plugins/files/public/index.ts @@ -15,13 +15,6 @@ export type { FilesClientFactory, FilesClientResponses, } from './types'; -export { - FilesContext, - UploadFile, - type UploadFileProps, - FilePicker, - type FilePickerProps, -} from './components'; export function plugin() { return new FilesPlugin(); diff --git a/src/plugins/files/public/mocks.ts b/src/plugins/files/public/mocks.ts index 909f1c02c54c..c22d9bda2460 100644 --- a/src/plugins/files/public/mocks.ts +++ b/src/plugins/files/public/mocks.ts @@ -6,25 +6,12 @@ * Side Public License, v 1. */ +import { createMockFilesClient as createBaseMocksFilesClient } from '@kbn/shared-ux-file-mocks'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import type { FilesClient } from './types'; -// TODO: Remove this once we have access to the shared file client mock export const createMockFilesClient = (): DeeplyMockedKeys => ({ - create: jest.fn(), - bulkDelete: jest.fn(), - delete: jest.fn(), - download: jest.fn(), - find: jest.fn(), - getById: jest.fn(), - getDownloadHref: jest.fn(), + ...createBaseMocksFilesClient(), getMetrics: jest.fn(), - getShare: jest.fn(), - list: jest.fn(), - listShares: jest.fn(), publicDownload: jest.fn(), - share: jest.fn(), - unshare: jest.fn(), - update: jest.fn(), - upload: jest.fn(), }); diff --git a/src/plugins/files/public/types.ts b/src/plugins/files/public/types.ts index 950446810585..ee4f622a8ddb 100644 --- a/src/plugins/files/public/types.ts +++ b/src/plugins/files/public/types.ts @@ -6,189 +6,9 @@ * Side Public License, v 1. */ -import { FileJSON } from '../common'; -import type { - FindFilesHttpEndpoint, - FileShareHttpEndpoint, - BulkDeleteHttpEndpoint, - FileUnshareHttpEndpoint, - FileGetShareHttpEndpoint, - FilesMetricsHttpEndpoint, - ListFileKindHttpEndpoint, - CreateFileKindHttpEndpoint, - FileListSharesHttpEndpoint, - UpdateFileKindHttpEndpoint, - UploadFileKindHttpEndpoint, - DeleteFileKindHttpEndpoint, - GetByIdFileKindHttpEndpoint, - DownloadFileKindHttpEndpoint, - FilePublicDownloadHttpEndpoint, - HttpApiInterfaceEntryDefinition, -} from '../common/api_routes'; - -type UnscopedClientMethodFrom = ( - args: E['inputs']['body'] & - E['inputs']['params'] & - E['inputs']['query'] & { abortSignal?: AbortSignal } -) => Promise; - -/** - * @param args - Input to the endpoint which includes body, params and query of the RESTful endpoint. - */ -type ClientMethodFrom = ( - args: Parameters>[0] & { kind: string } & ExtraArgs -) => Promise; - -interface GlobalEndpoints { - /** - * Get metrics of file system, like storage usage. - * - * @param args - Get metrics arguments - */ - getMetrics: () => Promise; - /** - * Download a file, bypassing regular security by way of a - * secret share token. - * - * @param args - Get public download arguments. - */ - publicDownload: UnscopedClientMethodFrom; - /** - * Find a set of files given some filters. - * - * @param args - File filters - */ - find: UnscopedClientMethodFrom; - /** - * Bulk a delete a set of files given their IDs. - * - * @param args - Bulk delete args - */ - bulkDelete: UnscopedClientMethodFrom; -} - -/** - * A client that can be used to manage a specific {@link FileKind}. - */ -export interface FilesClient extends GlobalEndpoints { - /** - * Create a new file object with the provided metadata. - * - * @param args - create file args - */ - create: ClientMethodFrom>; - /** - * Delete a file object and all associated share and content objects. - * - * @param args - delete file args - */ - delete: ClientMethodFrom; - /** - * Get a file object by ID. - * - * @param args - get file by ID args - */ - getById: ClientMethodFrom>; - /** - * List all file objects, of a given {@link FileKind}. - * - * @param args - list files args - */ - list: ClientMethodFrom>; - /** - * Update a set of of metadata values of the file object. - * - * @param args - update file args - */ - update: ClientMethodFrom>; - /** - * Stream the contents of the file to Kibana server for storage. - * - * @param args - upload file args - */ - upload: ( - args: UploadFileKindHttpEndpoint['inputs']['params'] & - UploadFileKindHttpEndpoint['inputs']['query'] & { - /** - * Should be blob or ReadableStream of some kind. - */ - body: unknown; - kind: string; - abortSignal?: AbortSignal; - contentType?: string; - } - ) => Promise; - /** - * Stream a download of the file object's content. - * - * @param args - download file args - */ - download: ClientMethodFrom; - /** - * Get a string for downloading a file that can be passed to a button element's - * href for download. - * - * @param args - get download URL args - */ - getDownloadHref: (args: Pick) => string; - /** - * Share a file by creating a new file share instance. - * - * @note This returns the secret token that can be used - * to access a file via the public download enpoint. - * - * @param args - File share arguments - */ - share: ClientMethodFrom; - /** - * Delete a file share instance. - * - * @param args - File unshare arguments - */ - unshare: ClientMethodFrom; - /** - * Get a file share instance. - * - * @param args - Get file share arguments - */ - getShare: ClientMethodFrom; - /** - * List all file shares. Optionally scoping to a specific - * file. - * - * @param args - Get file share arguments - */ - listShares: ClientMethodFrom; -} - -export type FilesClientResponses = { - [K in keyof FilesClient]: Awaited[K]>>; -}; - -/** - * A files client that is scoped to a specific {@link FileKind}. - * - * More convenient if you want to re-use the same client for the same file kind - * and not specify the kind every time. - */ -export type ScopedFilesClient = { - [K in keyof FilesClient]: K extends 'list' - ? (arg?: Omit[K]>[0], 'kind'>) => ReturnType[K]> - : (arg: Omit[K]>[0], 'kind'>) => ReturnType[K]>; -}; - -/** - * A factory for creating a {@link ScopedFilesClient} - */ -export interface FilesClientFactory { - /** - * Create a files client. - */ - asUnscoped(): FilesClient; - /** - * Create a {@link ScopedFileClient} for a given {@link FileKind}. - * - * @param fileKind - The {@link FileKind} to create a client for. - */ - asScoped(fileKind: string): ScopedFilesClient; -} +export type { + FilesClient, + ScopedFilesClient, + FilesClientFactory, + FilesClientResponses, +} from '../common/files_client'; diff --git a/src/plugins/files/server/routes/bulk_delete.ts b/src/plugins/files/server/routes/bulk_delete.ts index efb5161dec59..280855bd2b9f 100644 --- a/src/plugins/files/server/routes/bulk_delete.ts +++ b/src/plugins/files/server/routes/bulk_delete.ts @@ -7,8 +7,9 @@ */ import { schema } from '@kbn/config-schema'; -import type { CreateHandler, FilesRouter } from './types'; import { FILES_MANAGE_PRIVILEGE } from '../../common/constants'; +import { FilesClient } from '../../common/files_client'; +import type { CreateHandler, FilesRouter } from './types'; import { FILES_API_ROUTES, CreateRouteDefinition } from './api_routes'; const method = 'delete' as const; @@ -19,18 +20,20 @@ const rt = { }), }; -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; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { + /** + * 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]>; + }, + FilesClient['bulkDelete'] +>; const handler: CreateHandler = async ({ files }, req, res) => { const fileService = (await files).fileService.asCurrentUser(); @@ -38,8 +41,8 @@ const handler: CreateHandler = async ({ files }, req, res) => { body: { ids }, } = req; - const succeeded: Result['succeeded'] = []; - const failed: Result['failed'] = []; + const succeeded: string[] = []; + const failed: Array<[string, string]> = []; for (const id of ids) { try { await fileService.delete({ id }); diff --git a/src/plugins/files/server/routes/common_schemas.ts b/src/plugins/files/server/routes/common_schemas.ts index 3f99f1cca805..983caf931b9e 100644 --- a/src/plugins/files/server/routes/common_schemas.ts +++ b/src/plugins/files/server/routes/common_schemas.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; const ALPHA_NUMERIC_WITH_SPACES_REGEX = /^[a-z0-9\s_]+$/i; const ALPHA_NUMERIC_WITH_SPACES_EXT_REGEX = /^[a-z0-9\s\._]+$/i; @@ -46,4 +46,6 @@ export const fileAlt = schema.maybe( export const page = schema.number({ min: 1, defaultValue: 1 }); export const pageSize = schema.number({ min: 1, defaultValue: 100 }); -export const fileMeta = schema.maybe(schema.object({}, { unknowns: 'allow' })); +export const fileMeta = schema.maybe( + schema.object({}, { unknowns: 'allow' }) +) as unknown as Type; diff --git a/src/plugins/files/server/routes/file_kind/create.ts b/src/plugins/files/server/routes/file_kind/create.ts index d654b2c8c1c4..9121e27df0ac 100644 --- a/src/plugins/files/server/routes/file_kind/create.ts +++ b/src/plugins/files/server/routes/file_kind/create.ts @@ -7,6 +7,7 @@ */ import { schema } from '@kbn/config-schema'; +import { FilesClient } from '../../../common/files_client'; import type { FileJSON, FileKind } from '../../../common/types'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import type { FileKindRouter } from './types'; @@ -15,7 +16,7 @@ import { CreateHandler } from './types'; export const method = 'post' as const; -const rt = { +export const rt = { body: schema.object({ name: commonSchemas.fileName, alt: commonSchemas.fileAlt, @@ -24,7 +25,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition }>; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { file: FileJSON }, + FilesClient['create'] +>; export const handler: CreateHandler = async ({ fileKind, files }, req, res) => { const { fileService, security } = await files; diff --git a/src/plugins/files/server/routes/file_kind/delete.ts b/src/plugins/files/server/routes/file_kind/delete.ts index da7bb7bade21..ab1cc74cbae8 100644 --- a/src/plugins/files/server/routes/file_kind/delete.ts +++ b/src/plugins/files/server/routes/file_kind/delete.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { FileKind } from '../../../common/types'; +import { FilesClient } from '../../../common/files_client'; import { fileErrors } from '../../file'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import type { CreateHandler, FileKindRouter } from './types'; @@ -22,7 +23,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { diff --git a/src/plugins/files/server/routes/file_kind/download.ts b/src/plugins/files/server/routes/file_kind/download.ts index 4776d37485c9..854a520a6942 100644 --- a/src/plugins/files/server/routes/file_kind/download.ts +++ b/src/plugins/files/server/routes/file_kind/download.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { Readable } from 'stream'; - +import type { FilesClient } from '../../../common/files_client'; import type { FileKind } from '../../../common/types'; import { fileNameWithExt } from '../common_schemas'; import { fileErrors } from '../../file'; @@ -26,7 +26,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; type Response = Readable; diff --git a/src/plugins/files/server/routes/file_kind/get_by_id.ts b/src/plugins/files/server/routes/file_kind/get_by_id.ts index 914d1e70b77d..f8fa0ef2bd1d 100644 --- a/src/plugins/files/server/routes/file_kind/get_by_id.ts +++ b/src/plugins/files/server/routes/file_kind/get_by_id.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { FileJSON, FileKind } from '../../../common/types'; +import type { FilesClient } from '../../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import { getById } from './helpers'; import type { CreateHandler, FileKindRouter } from './types'; @@ -20,7 +21,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition }>; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { file: FileJSON }, + FilesClient['getById'] +>; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/list.ts b/src/plugins/files/server/routes/file_kind/list.ts index 54d8e98fc24f..d8f571e595d1 100644 --- a/src/plugins/files/server/routes/file_kind/list.ts +++ b/src/plugins/files/server/routes/file_kind/list.ts @@ -8,6 +8,8 @@ import { schema } from '@kbn/config-schema'; import type { FileJSON, FileKind } from '../../../common/types'; +import type { FilesClient } from '../../../common/files_client'; +import * as commonSchemas from '../common_schemas'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import * as cs from '../common_schemas'; import type { CreateHandler, FileKindRouter } from './types'; @@ -24,7 +26,7 @@ const rt = { status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), - meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + meta: commonSchemas.fileMeta, }), query: schema.object({ page: schema.maybe(cs.page), @@ -34,7 +36,8 @@ const rt = { export type Endpoint = CreateRouteDefinition< typeof rt, - { files: Array>; total: number } + { files: Array>; total: number }, + FilesClient['find'] >; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { @@ -50,7 +53,7 @@ export const handler: CreateHandler = async ({ files, fileKind }, req, extension: toArrayOrUndefined(extension), page, perPage, - meta, + meta: meta as Record, }); return res.ok({ body }); }; diff --git a/src/plugins/files/server/routes/file_kind/share/get.ts b/src/plugins/files/server/routes/file_kind/share/get.ts index 07e00d4ad800..0394bed5a791 100644 --- a/src/plugins/files/server/routes/file_kind/share/get.ts +++ b/src/plugins/files/server/routes/file_kind/share/get.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; +import type { FilesClient } from '../../../../common/files_client'; import { FileShareNotFoundError } from '../../../file_share_service/errors'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; import type { FileKind, FileShareJSON } from '../../../../common/types'; @@ -22,7 +23,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { share: FileShareJSON }, + FilesClient['getShare'] +>; export const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/share/list.ts b/src/plugins/files/server/routes/file_kind/share/list.ts index 470102cb815f..f8eb75b6451b 100644 --- a/src/plugins/files/server/routes/file_kind/share/list.ts +++ b/src/plugins/files/server/routes/file_kind/share/list.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; +import type { FilesClient } from '../../../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; import type { FileKind, FileShareJSON } from '../../../../common/types'; import { CreateHandler, FileKindRouter } from '../types'; @@ -23,7 +24,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { shares: FileShareJSON[] }, + FilesClient['listShares'] +>; export const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/share/share.ts b/src/plugins/files/server/routes/file_kind/share/share.ts index 3d3c5adb53e7..37f6a2e591a1 100644 --- a/src/plugins/files/server/routes/file_kind/share/share.ts +++ b/src/plugins/files/server/routes/file_kind/share/share.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { ExpiryDateInThePastError } from '../../../file_share_service/errors'; +import type { FilesClient } from '../../../../common/files_client'; import { CreateHandler, FileKindRouter } from '../types'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; @@ -34,7 +35,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + FileShareJSONWithToken, + FilesClient['share'] +>; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/share/unshare.ts b/src/plugins/files/server/routes/file_kind/share/unshare.ts index a41f5db8a597..a09a5fb8fcc2 100644 --- a/src/plugins/files/server/routes/file_kind/share/unshare.ts +++ b/src/plugins/files/server/routes/file_kind/share/unshare.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; +import type { FilesClient } from '../../../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../../api_routes'; import type { FileKind } from '../../../../common/types'; import { CreateHandler, FileKindRouter } from '../types'; @@ -21,7 +22,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; export const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/update.ts b/src/plugins/files/server/routes/file_kind/update.ts index 048e846322c5..0725fc0235f9 100644 --- a/src/plugins/files/server/routes/file_kind/update.ts +++ b/src/plugins/files/server/routes/file_kind/update.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { FileJSON, FileKind } from '../../../common/types'; +import type { FilesClient } from '../../../common/files_client'; import type { CreateHandler, FileKindRouter } from './types'; import { CreateRouteDefinition, FILES_API_ROUTES } from '../api_routes'; import { getById } from './helpers'; @@ -27,7 +28,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition }>; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { file: FileJSON }, + FilesClient['update'] +>; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/file_kind/upload.ts b/src/plugins/files/server/routes/file_kind/upload.ts index 88ef492ba11f..66b9c57e0df6 100644 --- a/src/plugins/files/server/routes/file_kind/upload.ts +++ b/src/plugins/files/server/routes/file_kind/upload.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; import { ReplaySubject } from 'rxjs'; import { Readable } from 'stream'; +import type { FilesClient } from '../../../common/files_client'; import type { FileKind } from '../../../common/types'; import type { CreateRouteDefinition } from '../../../common/api_routes'; import { FILES_API_ROUTES } from '../api_routes'; @@ -23,7 +24,7 @@ const rt = { params: schema.object({ id: schema.string(), }), - body: schema.stream(), + body: schema.stream() as Type, query: schema.object({ selfDestructOnAbort: schema.maybe(schema.boolean()), }), @@ -34,7 +35,8 @@ export type Endpoint = CreateRouteDefinition< { ok: true; size: number; - } + }, + FilesClient['upload'] >; export const handler: CreateHandler = async ({ files, fileKind }, req, res) => { diff --git a/src/plugins/files/server/routes/find.ts b/src/plugins/files/server/routes/find.ts index 458adc26ec4e..a81a9d2ea522 100644 --- a/src/plugins/files/server/routes/find.ts +++ b/src/plugins/files/server/routes/find.ts @@ -7,11 +7,12 @@ */ import { schema } from '@kbn/config-schema'; -import type { CreateHandler, FilesRouter } from './types'; +import { FilesClient } from '../../common/files_client'; import { FileJSON } from '../../common'; import { FILES_MANAGE_PRIVILEGE } from '../../common/constants'; import { FILES_API_ROUTES, CreateRouteDefinition } from './api_routes'; -import { page, pageSize } from './common_schemas'; +import { page, pageSize, fileMeta } from './common_schemas'; +import type { CreateHandler, FilesRouter } from './types'; const method = 'post' as const; @@ -32,7 +33,7 @@ const rt = { status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), - meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), + meta: fileMeta, }), query: schema.object({ page: schema.maybe(page), @@ -40,7 +41,11 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition< + typeof rt, + { files: FileJSON[]; total: number }, + FilesClient['find'] +>; const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; @@ -54,7 +59,7 @@ const handler: CreateHandler = async ({ files }, req, res) => { name: toArrayOrUndefined(name), status: toArrayOrUndefined(status), extension: toArrayOrUndefined(extension), - meta, + meta: meta as Record, ...query, }); diff --git a/src/plugins/files/server/routes/integration_tests/routes.test.ts b/src/plugins/files/server/routes/integration_tests/routes.test.ts index 133024a3e313..deaffdd73a7c 100644 --- a/src/plugins/files/server/routes/integration_tests/routes.test.ts +++ b/src/plugins/files/server/routes/integration_tests/routes.test.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ +import { TypeOf } from '@kbn/config-schema'; import type { FileJSON } from '../../../common'; -import type { CreateFileKindHttpEndpoint } from '../../../common/api_routes'; +import type { rt } from '../file_kind/create'; import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils'; describe('File HTTP API', () => { @@ -28,7 +29,7 @@ describe('File HTTP API', () => { describe('find', () => { beforeEach(async () => { - const args: Array = [ + const args: Array> = [ { name: 'firstFile', alt: 'my first alt', @@ -55,7 +56,7 @@ describe('File HTTP API', () => { }, ]; - const files = await Promise.all(args.map((arg) => createFile(arg))); + const files = await Promise.all(args.map((arg) => createFile(arg as any))); for (const file of files.slice(0, 2)) { await request diff --git a/src/plugins/files/server/routes/metrics.ts b/src/plugins/files/server/routes/metrics.ts index 6e3a63b7b67c..2e707ae1c8c1 100644 --- a/src/plugins/files/server/routes/metrics.ts +++ b/src/plugins/files/server/routes/metrics.ts @@ -8,14 +8,14 @@ import { FILES_MANAGE_PRIVILEGE } from '../../common/constants'; import type { FilesRouter } from './types'; - import { FilesMetrics } from '../../common'; +import { FilesClient } from '../../common/files_client'; import { CreateRouteDefinition, FILES_API_ROUTES } from './api_routes'; import type { FilesRequestHandler } from './types'; const method = 'get' as const; -export type Endpoint = CreateRouteDefinition<{}, FilesMetrics>; +export type Endpoint = CreateRouteDefinition<{}, FilesMetrics, FilesClient['getMetrics']>; const handler: FilesRequestHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/src/plugins/files/server/routes/public_facing/download.ts b/src/plugins/files/server/routes/public_facing/download.ts index 1635f9a7d39f..315598009522 100644 --- a/src/plugins/files/server/routes/public_facing/download.ts +++ b/src/plugins/files/server/routes/public_facing/download.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { Readable } from 'stream'; +import type { FilesClient } from '../../../common/files_client'; import { NoDownloadAvailableError } from '../../file/errors'; import { FileNotFoundError } from '../../file_service/errors'; import { @@ -31,7 +32,7 @@ const rt = { }), }; -export type Endpoint = CreateRouteDefinition; +export type Endpoint = CreateRouteDefinition; const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; diff --git a/tsconfig.base.json b/tsconfig.base.json index 3e3d5de4875b..6a236d215a6d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -654,12 +654,20 @@ "@kbn/shared-ux-card-no-data-mocks/*": ["packages/shared-ux/card/no_data/mocks/*"], "@kbn/shared-ux-card-no-data-types": ["packages/shared-ux/card/no_data/types"], "@kbn/shared-ux-card-no-data-types/*": ["packages/shared-ux/card/no_data/types/*"], + "@kbn/shared-ux-file-context": ["packages/shared-ux/file/context"], + "@kbn/shared-ux-file-context/*": ["packages/shared-ux/file/context/*"], + "@kbn/shared-ux-file-picker": ["packages/shared-ux/file/file_picker/impl"], + "@kbn/shared-ux-file-picker/*": ["packages/shared-ux/file/file_picker/impl/*"], + "@kbn/shared-ux-file-upload": ["packages/shared-ux/file/file_upload/impl"], + "@kbn/shared-ux-file-upload/*": ["packages/shared-ux/file/file_upload/impl/*"], "@kbn/shared-ux-file-image": ["packages/shared-ux/file/image/impl"], "@kbn/shared-ux-file-image/*": ["packages/shared-ux/file/image/impl/*"], "@kbn/shared-ux-file-image-mocks": ["packages/shared-ux/file/image/mocks"], "@kbn/shared-ux-file-image-mocks/*": ["packages/shared-ux/file/image/mocks/*"], - "@kbn/shared-ux-link-redirect-app-types": ["packages/shared-ux/file/image/types"], - "@kbn/shared-ux-link-redirect-app-types/*": ["packages/shared-ux/file/image/types/*"], + "@kbn/shared-ux-file-mocks": ["packages/shared-ux/file/mocks"], + "@kbn/shared-ux-file-mocks/*": ["packages/shared-ux/file/mocks/*"], + "@kbn/shared-ux-file-types": ["packages/shared-ux/file/types"], + "@kbn/shared-ux-file-types/*": ["packages/shared-ux/file/types/*"], "@kbn/shared-ux-file-util": ["packages/shared-ux/file/util"], "@kbn/shared-ux-file-util/*": ["packages/shared-ux/file/util/*"], "@kbn/shared-ux-link-redirect-app": ["packages/shared-ux/link/redirect_app/impl"], diff --git a/yarn.lock b/yarn.lock index 60ba69201ee1..a2b84a8845d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3901,15 +3901,31 @@ version "0.0.0" uid "" +"@kbn/shared-ux-file-context@link:bazel-bin/packages/shared-ux/file/context": + version "0.0.0" + uid "" + "@kbn/shared-ux-file-image-mocks@link:bazel-bin/packages/shared-ux/file/image/mocks": version "0.0.0" uid "" -"@kbn/shared-ux-file-image-types@link:bazel-bin/packages/shared-ux/file/image/types": +"@kbn/shared-ux-file-image@link:bazel-bin/packages/shared-ux/file/image/impl": version "0.0.0" uid "" -"@kbn/shared-ux-file-image@link:bazel-bin/packages/shared-ux/file/image/impl": +"@kbn/shared-ux-file-mocks@link:bazel-bin/packages/shared-ux/file/mocks": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-picker@link:bazel-bin/packages/shared-ux/file/file_picker/impl": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-types@link:bazel-bin/packages/shared-ux/file/types": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-upload@link:bazel-bin/packages/shared-ux/file/file_upload/impl": version "0.0.0" uid ""