mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Files] Move <FileUpload />
and <FilePicker />
👉🏻 packages/shared-ux/file
(#146284)
## Summary This is a refactor: * Move `FilesContext`, `FilePicker` and `UploadFile` components to `packages/shared-ux/file` as packages * Renamed `UploadFile` to `FileUpload` for more consistency * Also created `packages/shared-ux/file/types` and added `useBehaviourSubject` to `packages/shared-ux/file/util` (we can consider moving this elsewhere since that function is not necessarily tied to the files domain). * Removed the storybook config from `files` public plugin since there are no more components there ## How to test 👉🏻 `yarn storybook shared_ux` to see the components in a lab environment OR 👉🏻 `yarn start --run-examples` then "Developer examples" > "Files example" to see the components being used in Kibana Look out for any regressions: for example, in the `FileImage` component importing `import bh from 'blurhash'` caused a regression because blurhash does not expose a default export. This was fixed by doing: `import * as bh from 'blurhash`. ## Notes * With this change, we needed to move `FilesClient` interface to packages since it is used by the components. However, we also wanted to keep `FilesClient` interface as it is currently exported from `files` plugin because it exposes methods that only the server of `files` plugin should know about (e.g., the metrics endpoint). I created the `BaseFilesClient` in the packages directory that is extended in the `files` plugin as needed. This is a snapshot of the types as they are provided from the server implementation and will need to be updated/maintained by hand from here on out. * With `BaseFilesClient` in `packages`, we lost the type check between `files` server endpoints and the client methods. To re-establish this link the `CreateRouteDefinition` type helper got a parameter where the client method can be passed in to do checks that the server inputs (query, param and body) as well as outputs (the responses) match what the client expects using the `X extends Y ? X : unknown` capability of TS. See this in action in, for example `src/plugins/files/server/routes/find.ts`. DX will be: if these ever get out of sync, the server values for `query`, `param` or `body` will map to `unknown` causing a type issue when trying to use these values. This can only be fixed by bringing the `FilesClient` types in sync with the server types. * Server endpoints that should match expected `FilesClient` inputs/outputs should use the `CreateRouteDefinition` type helper, but if the endpoint does not need to map to a client method we can always skip using `CreateRouteDefinition`. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
115c250a9b
commit
5a86b583df
120 changed files with 1874 additions and 1176 deletions
|
@ -31,7 +31,6 @@ const STORYBOOKS = [
|
|||
'expression_reveal_image',
|
||||
'expression_shape',
|
||||
'expression_tagcloud',
|
||||
'files',
|
||||
'fleet',
|
||||
'home',
|
||||
'infra',
|
||||
|
|
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<MyImageMetadata>;
|
||||
|
@ -27,7 +27,7 @@ export const Modal: FunctionComponent<Props> = ({ onDismiss, onUploaded, client
|
|||
</EuiText>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<UploadFile
|
||||
<FileUpload
|
||||
multiple
|
||||
kind={exampleFileKind.id}
|
||||
onDone={onUploaded}
|
||||
|
|
|
@ -10,12 +10,12 @@ export {
|
|||
type FilesClient,
|
||||
type FilesSetup,
|
||||
type FilesStart,
|
||||
UploadFile,
|
||||
FilesContext,
|
||||
type ScopedFilesClient,
|
||||
FilePicker,
|
||||
} from '@kbn/files-plugin/public';
|
||||
|
||||
export { FilesContext } from '@kbn/shared-ux-file-context';
|
||||
export { FileImage as Image } from '@kbn/shared-ux-file-image';
|
||||
export { FileUpload } from '@kbn/shared-ux-file-upload';
|
||||
export { FilePicker } from '@kbn/shared-ux-file-picker';
|
||||
|
||||
export type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
|
|
|
@ -385,9 +385,13 @@
|
|||
"@kbn/shared-ux-card-no-data": "link:bazel-bin/packages/shared-ux/card/no_data/impl",
|
||||
"@kbn/shared-ux-card-no-data-mocks": "link:bazel-bin/packages/shared-ux/card/no_data/mocks",
|
||||
"@kbn/shared-ux-card-no-data-types": "link:bazel-bin/packages/shared-ux/card/no_data/types",
|
||||
"@kbn/shared-ux-file-context": "link:bazel-bin/packages/shared-ux/file/context",
|
||||
"@kbn/shared-ux-file-image": "link:bazel-bin/packages/shared-ux/file/image/impl",
|
||||
"@kbn/shared-ux-file-image-mocks": "link:bazel-bin/packages/shared-ux/file/image/mocks",
|
||||
"@kbn/shared-ux-file-image-types": "link:bazel-bin/packages/shared-ux/file/image/types",
|
||||
"@kbn/shared-ux-file-mocks": "link:bazel-bin/packages/shared-ux/file/mocks",
|
||||
"@kbn/shared-ux-file-picker": "link:bazel-bin/packages/shared-ux/file/file_picker/impl",
|
||||
"@kbn/shared-ux-file-types": "link:bazel-bin/packages/shared-ux/file/types",
|
||||
"@kbn/shared-ux-file-upload": "link:bazel-bin/packages/shared-ux/file/file_upload/impl",
|
||||
"@kbn/shared-ux-file-util": "link:bazel-bin/packages/shared-ux/file/util",
|
||||
"@kbn/shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/impl",
|
||||
"@kbn/shared-ux-link-redirect-app-mocks": "link:bazel-bin/packages/shared-ux/link/redirect_app/mocks",
|
||||
|
|
|
@ -333,9 +333,13 @@ filegroup(
|
|||
"//packages/shared-ux/card/no_data/impl:build",
|
||||
"//packages/shared-ux/card/no_data/mocks:build",
|
||||
"//packages/shared-ux/card/no_data/types:build",
|
||||
"//packages/shared-ux/file/context:build",
|
||||
"//packages/shared-ux/file/file_picker/impl:build",
|
||||
"//packages/shared-ux/file/file_upload/impl:build",
|
||||
"//packages/shared-ux/file/image/impl:build",
|
||||
"//packages/shared-ux/file/image/mocks:build",
|
||||
"//packages/shared-ux/file/image/types:build",
|
||||
"//packages/shared-ux/file/mocks:build",
|
||||
"//packages/shared-ux/file/types:build",
|
||||
"//packages/shared-ux/file/util:build",
|
||||
"//packages/shared-ux/link/redirect_app/impl:build",
|
||||
"//packages/shared-ux/link/redirect_app/mocks:build",
|
||||
|
@ -685,8 +689,12 @@ filegroup(
|
|||
"//packages/shared-ux/button/exit_full_screen/mocks:build_types",
|
||||
"//packages/shared-ux/card/no_data/impl:build_types",
|
||||
"//packages/shared-ux/card/no_data/mocks:build_types",
|
||||
"//packages/shared-ux/file/context:build_types",
|
||||
"//packages/shared-ux/file/file_picker/impl:build_types",
|
||||
"//packages/shared-ux/file/file_upload/impl:build_types",
|
||||
"//packages/shared-ux/file/image/impl:build_types",
|
||||
"//packages/shared-ux/file/image/mocks:build_types",
|
||||
"//packages/shared-ux/file/mocks:build_types",
|
||||
"//packages/shared-ux/file/util:build_types",
|
||||
"//packages/shared-ux/link/redirect_app/impl:build_types",
|
||||
"//packages/shared-ux/link/redirect_app/mocks:build_types",
|
||||
|
|
136
packages/shared-ux/file/context/BUILD.bazel
Normal file
136
packages/shared-ux/file/context/BUILD.bazel
Normal file
|
@ -0,0 +1,136 @@
|
|||
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 = "context"
|
||||
PKG_REQUIRE_NAME = "@kbn/shared-ux-file-context"
|
||||
|
||||
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//react",
|
||||
]
|
||||
|
||||
# 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/react",
|
||||
"//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"],
|
||||
)
|
22
packages/shared-ux/file/context/README.mdx
Normal file
22
packages/shared-ux/file/context/README.mdx
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
id: sharedUX/Components/FileContext
|
||||
slug: /shared-ux/components/context
|
||||
title: File component context
|
||||
description: Context for shared ux file components
|
||||
tags: ['shared-ux', 'context', 'files']
|
||||
date: 2022-11-22
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Context in which the Global Experience files components should live to access files client and more.
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
const client = files.filesClientFactory.asUnscoped<MyImageMetadata>()
|
||||
...
|
||||
<FilesClient client={client}>
|
||||
<MyApp />
|
||||
</FilesClient>
|
||||
```
|
|
@ -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';
|
|
@ -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": []
|
9
packages/shared-ux/file/context/package.json
Normal file
9
packages/shared-ux/file/context/package.json
Normal file
|
@ -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"
|
||||
}
|
|
@ -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<ContextProps> = ({ client, children
|
|||
<FilesContextObject.Provider
|
||||
value={{
|
||||
client,
|
||||
registry: getFileKindsRegistry(),
|
||||
}}
|
||||
>
|
||||
{children}
|
13
packages/shared-ux/file/context/tsconfig.json
Normal file
13
packages/shared-ux/file/context/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"types": [ ]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
]
|
||||
}
|
158
packages/shared-ux/file/file_picker/impl/BUILD.bazel
Normal file
158
packages/shared-ux/file/file_picker/impl/BUILD.bazel
Normal file
|
@ -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"],
|
||||
)
|
27
packages/shared-ux/file/file_picker/impl/README.mdx
Normal file
27
packages/shared-ux/file/file_picker/impl/README.mdx
Normal file
|
@ -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
|
||||
<FilesContext ...>
|
||||
<FilePicker ... />
|
||||
</FilesContext>
|
||||
```
|
|
@ -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';
|
|
@ -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: ['<rootDir>/packages/shared-ux/file/file_picker/impl'],
|
||||
};
|
7
packages/shared-ux/file/file_picker/impl/kibana.jsonc
Normal file
7
packages/shared-ux/file/file_picker/impl/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/shared-ux-file-picker",
|
||||
"owner": "@elastic/kibana-global-experience",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": []
|
||||
}
|
9
packages/shared-ux/file/file_picker/impl/package.json
Normal file
9
packages/shared-ux/file/file_picker/impl/package.json
Normal file
|
@ -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"
|
||||
}
|
|
@ -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;
|
|
@ -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<Props> = ({ kind, multiple }) => {
|
|||
title={<h3>{i18nTexts.emptyStatePrompt}</h3>}
|
||||
titleSize="s"
|
||||
actions={[
|
||||
<UploadFile
|
||||
<FileUpload
|
||||
css={css`
|
||||
min-width: calc(${euiTheme.size.xxxl} * 6);
|
||||
`}
|
|
@ -9,9 +9,9 @@
|
|||
import React from 'react';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import { EuiButton, EuiEmptyPrompt } 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';
|
||||
|
||||
interface Props {
|
||||
error: Error;
|
|
@ -12,10 +12,10 @@ import numeral from '@elastic/numeral';
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { EuiCard, EuiText, EuiIcon, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { FileJSON } from '@kbn/shared-ux-file-types';
|
||||
import { isImage } from '@kbn/shared-ux-file-util';
|
||||
import { FileImage as Image } from '@kbn/shared-ux-file-image';
|
||||
import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types';
|
||||
import { FileJSON } from '../../../../common';
|
||||
import { isImage } from '../../util';
|
||||
import type { FileImageMetadata } from '@kbn/shared-ux-file-types';
|
||||
import { useFilePickerContext } from '../context';
|
||||
|
||||
import './file_card.scss';
|
|
@ -10,8 +10,8 @@ import { EuiModalFooter } from '@elastic/eui';
|
|||
import { css } from '@emotion/react';
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FileUpload } from '@kbn/shared-ux-file-upload';
|
||||
|
||||
import { UploadFile } from '../../upload_file';
|
||||
import type { Props as FilePickerProps } from '../file_picker';
|
||||
import { useFilePickerContext } from '../context';
|
||||
import { i18nTexts } from '../i18n_texts';
|
||||
|
@ -44,7 +44,7 @@ export const ModalFooter: FunctionComponent<Props> = ({ kind, onDone, onUpload,
|
|||
place-self: stretch;
|
||||
`}
|
||||
>
|
||||
<UploadFile
|
||||
<FileUpload
|
||||
onDone={(n) => {
|
||||
state.selectFile(n.map(({ fileJSON }) => fileJSON));
|
||||
state.resetFilters();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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;
|
|
@ -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 {
|
|
@ -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<FilesClient['list']>;
|
||||
|
||||
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<FilesClientResponses['list']> => ({
|
||||
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<FilesClientResponses['list']> => ({
|
||||
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<FilesClientResponses['list']> => ({
|
||||
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<FilesClientResponses['list']> => ({
|
||||
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<FilesClientResponses['list']> => ({
|
||||
list: async (): ListResponse => ({
|
||||
files: [createFileJSON(), createFileJSON(), createFileJSON()],
|
||||
total: 1,
|
||||
}),
|
||||
getFileKind,
|
||||
} as unknown as FilesClient
|
||||
}
|
||||
>
|
|
@ -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();
|
||||
});
|
|
@ -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<Kind extends string = string> {
|
||||
/**
|
|
@ -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));
|
|
@ -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<FileJSON[]>([]);
|
||||
public readonly selectedFiles$ = new Rx.BehaviorSubject<FileJSON[]>([]);
|
||||
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<boolean>(true);
|
||||
public readonly loadingError$ = new BehaviorSubject<undefined | Error>(undefined);
|
||||
public readonly hasFiles$ = new BehaviorSubject<boolean>(false);
|
||||
public readonly hasQuery$ = new BehaviorSubject<boolean>(false);
|
||||
public readonly query$ = new BehaviorSubject<undefined | string>(undefined);
|
||||
public readonly queryDebounced$ = this.query$.pipe(debounceTime(100));
|
||||
public readonly currentPage$ = new BehaviorSubject<number>(0);
|
||||
public readonly totalPages$ = new BehaviorSubject<undefined | number>(undefined);
|
||||
public readonly isUploading$ = new BehaviorSubject<boolean>(false);
|
||||
public readonly isLoading$ = new Rx.BehaviorSubject<boolean>(true);
|
||||
public readonly loadingError$ = new Rx.BehaviorSubject<undefined | Error>(undefined);
|
||||
public readonly hasFiles$ = new Rx.BehaviorSubject<boolean>(false);
|
||||
public readonly hasQuery$ = new Rx.BehaviorSubject<boolean>(false);
|
||||
public readonly query$ = new Rx.BehaviorSubject<undefined | string>(undefined);
|
||||
public readonly queryDebounced$ = this.query$.pipe(Rx.debounceTime(100));
|
||||
public readonly currentPage$ = new Rx.BehaviorSubject<number>(0);
|
||||
public readonly totalPages$ = new Rx.BehaviorSubject<undefined | number>(undefined);
|
||||
public readonly isUploading$ = new Rx.BehaviorSubject<boolean>(false);
|
||||
|
||||
private readonly selectedFiles = new Map<string, FileJSON>();
|
||||
private readonly retry$ = new BehaviorSubject<void>(undefined);
|
||||
private readonly subscriptions: Subscription[] = [];
|
||||
private readonly internalIsLoading$ = new BehaviorSubject<boolean>(true);
|
||||
private readonly retry$ = new Rx.BehaviorSubject<void>(undefined);
|
||||
private readonly subscriptions: Rx.Subscription[] = [];
|
||||
private readonly internalIsLoading$ = new Rx.BehaviorSubject<boolean>(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<boolean> => {
|
||||
watchFileSelected$ = (id: string): Rx.Observable<boolean> => {
|
||||
return this.selectedFiles$.pipe(
|
||||
map(() => this.selectedFiles.has(id)),
|
||||
distinctUntilChanged()
|
||||
Rx.map(() => this.selectedFiles.has(id)),
|
||||
Rx.distinctUntilChanged()
|
||||
);
|
||||
};
|
||||
}
|
53
packages/shared-ux/file/file_picker/impl/src/i18n_texts.ts
Normal file
53
packages/shared-ux/file/file_picker/impl/src/i18n_texts.ts
Normal file
|
@ -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',
|
||||
}
|
||||
),
|
||||
};
|
|
@ -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",
|
||||
]
|
||||
}
|
155
packages/shared-ux/file/file_upload/impl/BUILD.bazel
Normal file
155
packages/shared-ux/file/file_upload/impl/BUILD.bazel
Normal file
|
@ -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"],
|
||||
)
|
32
packages/shared-ux/file/file_upload/impl/README.mdx
Normal file
32
packages/shared-ux/file/file_upload/impl/README.mdx
Normal file
|
@ -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 `<EuiFilePicker />` that provides state management for the upload process using the `FileClient`.
|
||||
|
||||
## Usage
|
||||
|
||||
Must be wrapped in the `FilesContext`.
|
||||
|
||||
```tsx
|
||||
<FilesContext ...>
|
||||
<FileUpload ... />
|
||||
</FilesContext>
|
||||
```
|
||||
|
||||
## 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.
|
10
packages/shared-ux/file/file_upload/impl/index.tsx
Normal file
10
packages/shared-ux/file/file_upload/impl/index.tsx
Normal file
|
@ -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';
|
13
packages/shared-ux/file/file_upload/impl/jest.config.js
Normal file
13
packages/shared-ux/file/file_upload/impl/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/packages/shared-ux/file/file_upload/impl'],
|
||||
};
|
7
packages/shared-ux/file/file_upload/impl/kibana.jsonc
Normal file
7
packages/shared-ux/file/file_upload/impl/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/shared-ux-file-upload",
|
||||
"owner": "@elastic/kibana-global-experience",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": []
|
||||
}
|
9
packages/shared-ux/file/file_upload/impl/package.json
Normal file
9
packages/shared-ux/file/file_upload/impl/package.json
Normal file
|
@ -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"
|
||||
}
|
|
@ -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';
|
||||
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
@ -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;
|
|
@ -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<EuiFilePicker, Props>(
|
||||
export const FileUpload = React.forwardRef<EuiFilePicker, Props>(
|
||||
(
|
||||
{
|
||||
compressed,
|
||||
|
@ -71,12 +71,12 @@ export const UploadFile = React.forwardRef<EuiFilePicker, Props>(
|
|||
const isInvalid = Boolean(error);
|
||||
const errorMessage = error?.message;
|
||||
|
||||
const id = useGeneratedHtmlId({ prefix: 'filesUploadFile' });
|
||||
const id = useGeneratedHtmlId({ prefix: 'filesFileUpload' });
|
||||
const errorId = `${id}_error`;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj="filesUploadFile"
|
||||
data-test-subj="filesFileUpload"
|
||||
css={[
|
||||
css`
|
||||
max-width: ${fullWidth ? '100%' : euiFormMaxWidth};
|
|
@ -9,14 +9,35 @@
|
|||
import React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { FileKind, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types';
|
||||
import { FilesContext } from '@kbn/shared-ux-file-context';
|
||||
|
||||
import { register } from '../stories_shared';
|
||||
import { FilesClient } from '../../types';
|
||||
import { FilesContext } from '../context';
|
||||
import { UploadFile, Props } from './upload_file';
|
||||
import { FileUpload, Props } from './file_upload';
|
||||
|
||||
const sleep = (ms: number) => 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 {
|
|||
</FilesContext>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof UploadFile>;
|
||||
} as ComponentMeta<typeof FileUpload>;
|
||||
|
||||
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<typeof UploadFile> = (props: Props) => <UploadFile {...props} />;
|
||||
const Template: ComponentStory<typeof FileUpload> = (props: Props) => <FileUpload {...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
|
||||
}
|
||||
>
|
|
@ -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<Props>) {
|
||||
const createTestBed = registerTestBed((p: Props) => (
|
||||
<FilesContext client={client}>
|
||||
<UploadFile {...p} />
|
||||
<FileUpload {...p} />
|
||||
</FilesContext>
|
||||
));
|
||||
|
||||
|
@ -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();
|
||||
});
|
|
@ -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<Meta = unknown> {
|
|||
}
|
||||
|
||||
/**
|
||||
* UploadFile component props
|
||||
* FileUpload component props
|
||||
*/
|
||||
export interface Props<Kind extends string = string> {
|
||||
/**
|
||||
|
@ -119,7 +119,7 @@ export interface Props<Kind extends string = string> {
|
|||
*
|
||||
* In order to use this component you must register your file kind with {@link FileKindsRegistry}
|
||||
*/
|
||||
export const UploadFile = <Kind extends string = string>({
|
||||
export const FileUpload = <Kind extends string = string>({
|
||||
meta,
|
||||
onDone,
|
||||
onError,
|
||||
|
@ -135,9 +135,9 @@ export const UploadFile = <Kind extends string = string>({
|
|||
allowRepeatedUploads = false,
|
||||
className,
|
||||
}: Props<Kind>): ReturnType<FunctionComponent> => {
|
||||
const { registry, client } = useFilesContext();
|
||||
const { client } = useFilesContext();
|
||||
const ref = useRef<null | EuiFilePicker>(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 = <Kind extends string = string>({
|
|||
};
|
||||
|
||||
/* eslint-disable import/no-default-export */
|
||||
export default UploadFile;
|
||||
export default FileUpload;
|
|
@ -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 },
|
|
@ -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, <EuiLoadingSpinner /> 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) => (
|
||||
<Suspense fallback={props.lazyLoadFallback ?? <EuiLoadingSpinner size="xl" />}>
|
||||
<UploadFileContainer {...props} />
|
||||
<FileUploadContainer {...props} />
|
||||
</Suspense>
|
||||
);
|
|
@ -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';
|
||||
|
|
@ -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<void>();
|
||||
private readonly files$$ = new BehaviorSubject<Upload[]>([]);
|
||||
private readonly abort$ = new Rx.Subject<void>();
|
||||
private readonly files$$ = new Rx.BehaviorSubject<Upload[]>([]);
|
||||
|
||||
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<void>();
|
||||
public readonly error$ = new BehaviorSubject<undefined | Error>(undefined);
|
||||
public readonly uploading$ = new BehaviorSubject(false);
|
||||
public readonly done$ = new Subject<undefined | DoneNotification[]>();
|
||||
public readonly clear$ = new Rx.Subject<void>();
|
||||
public readonly error$ = new Rx.BehaviorSubject<undefined | Error>(undefined);
|
||||
public readonly uploading$ = new Rx.BehaviorSubject(false);
|
||||
public readonly done$ = new Rx.Subject<undefined | DoneNotification[]>();
|
||||
|
||||
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<FileState>,
|
||||
abort$: Observable<void>,
|
||||
abort$: Rx.Observable<void>,
|
||||
meta?: unknown
|
||||
): Observable<void | Error> => {
|
||||
): Rx.Observable<void | Error> => {
|
||||
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<string, unknown>;
|
||||
|
||||
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<void> => {
|
||||
public upload = (meta?: unknown): Rx.Observable<void> => {
|
||||
if (this.isUploading()) {
|
||||
throw new Error('Upload already in progress');
|
||||
}
|
||||
const abort$ = new ReplaySubject<void>(1);
|
||||
const abort$ = new Rx.ReplaySubject<void>(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
|
18
packages/shared-ux/file/file_upload/impl/tsconfig.json
Normal file
18
packages/shared-ux/file/file_upload/impl/tsconfig.json
Normal file
|
@ -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",
|
||||
]
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# @kbn/shared-ux-link-redirect-app-types
|
||||
|
||||
TODO
|
26
packages/shared-ux/file/image/types/index.d.ts
vendored
26
packages/shared-ux/file/image/types/index.d.ts
vendored
|
@ -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;
|
||||
}
|
134
packages/shared-ux/file/mocks/BUILD.bazel
Normal file
134
packages/shared-ux/file/mocks/BUILD.bazel
Normal file
|
@ -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"],
|
||||
)
|
3
packages/shared-ux/file/mocks/README.md
Normal file
3
packages/shared-ux/file/mocks/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/shared-ux-markdown-mocks
|
||||
|
||||
TODO
|
28
packages/shared-ux/file/mocks/index.ts
Normal file
28
packages/shared-ux/file/mocks/index.ts
Normal file
|
@ -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<BaseFilesClient> => ({
|
||||
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(),
|
||||
});
|
7
packages/shared-ux/file/mocks/kibana.jsonc
Normal file
7
packages/shared-ux/file/mocks/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/shared-ux-file-mocks",
|
||||
"owner": "@elastic/kibana-global-experience",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": [],
|
||||
}
|
9
packages/shared-ux/file/mocks/package.json
Normal file
9
packages/shared-ux/file/mocks/package.json
Normal file
|
@ -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"
|
||||
}
|
12
packages/shared-ux/file/mocks/tsconfig.json
Normal file
12
packages/shared-ux/file/mocks/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"types": [ ]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
]
|
||||
}
|
|
@ -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(
|
||||
[
|
5
packages/shared-ux/file/types/README.md
Normal file
5
packages/shared-ux/file/types/README.md
Normal file
|
@ -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`
|
158
packages/shared-ux/file/types/base_file_client.d.ts
vendored
Normal file
158
packages/shared-ux/file/types/base_file_client.d.ts
vendored
Normal file
|
@ -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<M = unknown> {
|
||||
/**
|
||||
* 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<FileJSON<unknown>>; 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<M> }>;
|
||||
/**
|
||||
* 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<M> }>;
|
||||
/**
|
||||
* 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<FileJSON<M>>; 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<M> }>;
|
||||
/**
|
||||
* 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<any>;
|
||||
/**
|
||||
* 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<FileJSON<unknown>, '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<FileShareJSONWithToken>;
|
||||
/**
|
||||
* 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;
|
||||
}
|
319
packages/shared-ux/file/types/index.d.ts
vendored
Normal file
319
packages/shared-ux/file/types/index.d.ts
vendored
Normal file
|
@ -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<Meta = unknown> = Required<
|
||||
Pick<BaseFileMetadata, 'created' | 'name' | 'Status' | 'Updated'>
|
||||
> &
|
||||
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<Meta = unknown> {
|
||||
/**
|
||||
* 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>['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;
|
||||
}
|
7
packages/shared-ux/file/types/kibana.jsonc
Normal file
7
packages/shared-ux/file/types/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/shared-ux-file-types",
|
||||
"owner": "@elastic/kibana-global-experience",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": []
|
||||
}
|
|
@ -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"
|
14
packages/shared-ux/file/types/tsconfig.json
Normal file
14
packages/shared-ux/file/types/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
"*.d.ts"
|
||||
]
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -12,4 +12,5 @@ export {
|
|||
getImageMetadata,
|
||||
isImage,
|
||||
type ImageMetadataFactory,
|
||||
useBehaviorSubject,
|
||||
} from './src';
|
||||
|
|
|
@ -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/'));
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
|
||||
export { getImageMetadata, isImage, fitToBox, getBlurhashSrc } from './image_metadata';
|
||||
export type { ImageMetadataFactory } from './image_metadata';
|
||||
export { useBehaviorSubject } from './use_behavior_subject';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -27,49 +27,46 @@ export interface EndpointInputs<
|
|||
body?: B;
|
||||
}
|
||||
|
||||
export interface CreateRouteDefinition<Inputs extends EndpointInputs, R> {
|
||||
inputs: {
|
||||
params: TypeOf<NonNullable<Inputs['params']>>;
|
||||
query: TypeOf<NonNullable<Inputs['query']>>;
|
||||
body: TypeOf<NonNullable<Inputs['body']>>;
|
||||
};
|
||||
output: R;
|
||||
}
|
||||
|
||||
export type AnyEndpoint = CreateRouteDefinition<EndpointInputs, unknown>;
|
||||
type Extends<X, Y> = 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<M = unknown> = CreateRouteDefinition<
|
||||
* typeof rt, // We pass in our runtime types
|
||||
* { file: FileJSON<M> }, // 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<any> = () => Promise<unknown>
|
||||
> {
|
||||
inputs: {
|
||||
params: P;
|
||||
query: Q;
|
||||
body: B;
|
||||
params: Extends<Parameters<ClientMethod>[0], TypeOf<NonNullable<Inputs['params']>>>;
|
||||
query: Extends<Parameters<ClientMethod>[0], TypeOf<NonNullable<Inputs['query']>>>;
|
||||
body: Extends<Parameters<ClientMethod>[0], TypeOf<NonNullable<Inputs['body']>>>;
|
||||
};
|
||||
output: R;
|
||||
output: Extends<R, Awaited<ReturnType<ClientMethod>>>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
61
src/plugins/files/common/files_client.ts
Normal file
61
src/plugins/files/common/files_client.ts
Normal file
|
@ -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<M = unknown> extends BaseFilesClient<M> {
|
||||
/**
|
||||
* Get metrics of file system, like storage usage.
|
||||
*
|
||||
* @param args - Get metrics arguments
|
||||
*/
|
||||
getMetrics: () => Promise<FilesMetrics>;
|
||||
/**
|
||||
* 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<M = unknown> = {
|
||||
[K in keyof FilesClient]: Awaited<ReturnType<FilesClient<M>[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<M = unknown> = {
|
||||
[K in keyof FilesClient]: K extends 'list'
|
||||
? (arg?: Omit<Parameters<FilesClient<M>[K]>[0], 'kind'>) => ReturnType<FilesClient<M>[K]>
|
||||
: (arg: Omit<Parameters<FilesClient<M>[K]>[0], 'kind'>) => ReturnType<FilesClient<M>[K]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A factory for creating a {@link ScopedFilesClient}
|
||||
*/
|
||||
export interface FilesClientFactory {
|
||||
/**
|
||||
* Create a files client.
|
||||
*/
|
||||
asUnscoped<M = unknown>(): FilesClient<M>;
|
||||
/**
|
||||
* Create a {@link ScopedFileClient} for a given {@link FileKind}.
|
||||
*
|
||||
* @param fileKind - The {@link FileKind} to create a client for.
|
||||
*/
|
||||
asScoped<M = unknown>(fileKind: string): ScopedFilesClient<M>;
|
||||
}
|
|
@ -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<Meta = unknown> = Required<
|
||||
Pick<BaseFileMetadata, 'created' | 'name' | 'Status' | 'Updated'>
|
||||
> &
|
||||
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<Meta = unknown> {
|
||||
/**
|
||||
* 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>['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.
|
||||
*/
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<HTMLImageElement> {
|
||||
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<undefined | FileImageMetadata> {
|
||||
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 `<img>` 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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<FilesClient> => ({
|
||||
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(),
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue