diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f2bb1312c4365..253dd86879dda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1026,6 +1026,10 @@ 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/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/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 packages/shared-ux/link/redirect_app/types @elastic/kibana-global-experience diff --git a/examples/files_example/common/index.ts b/examples/files_example/common/index.ts index 3ede97859f9df..29b2c97c9532e 100644 --- a/examples/files_example/common/index.ts +++ b/examples/files_example/common/index.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import type { FileKind, FileImageMetadata } from '@kbn/files-plugin/common'; +import type { FileKind } from '@kbn/files-plugin/common'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; export const PLUGIN_ID = 'filesExample'; export const PLUGIN_NAME = 'Files example'; diff --git a/examples/files_example/public/imports.ts b/examples/files_example/public/imports.ts index f21a30a390e60..797dbd6e3a608 100644 --- a/examples/files_example/public/imports.ts +++ b/examples/files_example/public/imports.ts @@ -14,7 +14,8 @@ export { FilesContext, type ScopedFilesClient, FilePicker, - Image, } from '@kbn/files-plugin/public'; +export { FileImage as Image } from '@kbn/shared-ux-file-image'; + export type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; diff --git a/package.json b/package.json index ba63c4edf0400..724ec9b99ed92 100644 --- a/package.json +++ b/package.json @@ -385,6 +385,10 @@ "@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-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-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", "@kbn/shared-ux-link-redirect-app-types": "link:bazel-bin/packages/shared-ux/link/redirect_app/types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 23b01b114cce2..17a515a5b04a0 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -333,6 +333,10 @@ 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/image/impl:build", + "//packages/shared-ux/file/image/mocks:build", + "//packages/shared-ux/file/image/types:build", + "//packages/shared-ux/file/util:build", "//packages/shared-ux/link/redirect_app/impl:build", "//packages/shared-ux/link/redirect_app/mocks:build", "//packages/shared-ux/link/redirect_app/types:build", @@ -681,6 +685,9 @@ 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/image/impl:build_types", + "//packages/shared-ux/file/image/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", "//packages/shared-ux/markdown/impl:build_types", diff --git a/packages/shared-ux/card/no_data/mocks/tsconfig.json b/packages/shared-ux/card/no_data/mocks/tsconfig.json index 4703a8ebf5e35..8ed0253743ff7 100644 --- a/packages/shared-ux/card/no_data/mocks/tsconfig.json +++ b/packages/shared-ux/card/no_data/mocks/tsconfig.json @@ -5,9 +5,9 @@ "emitDeclarationOnly": true, "outDir": "target_types", "types": [ - "jest", "node", - "react" + "react", + "jest" ] }, "include": [ diff --git a/packages/shared-ux/file/image/impl/BUILD.bazel b/packages/shared-ux/file/image/impl/BUILD.bazel new file mode 100644 index 0000000000000..95f8b36032d86 --- /dev/null +++ b/packages/shared-ux/file/image/impl/BUILD.bazel @@ -0,0 +1,148 @@ +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-image" + +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//classnames", + "@npm//@emotion/react", + "@npm//@emotion/css", + "//packages/shared-ux/file/util", + "//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/classnames", + "//packages/kbn-ambient-ui-types", + "//packages/shared-ux/file/util:npm_module_types", + "//packages/shared-ux/file/image/types:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/image/impl/README.mdx b/packages/shared-ux/file/image/impl/README.mdx new file mode 100644 index 0000000000000..fac2086a4e51c --- /dev/null +++ b/packages/shared-ux/file/image/impl/README.mdx @@ -0,0 +1,18 @@ +--- +id: sharedUX/Components/FileImage +slug: /shared-ux/components/file-image +title: File image +description: Display images stored in Kibana files. +tags: ['shared-ux', 'component', 'files'] +date: 2022-11-22 +--- + +## Description + +A light wrapper around that introduces some "file-aware" functionality like displaying a [blurhash](https://blurha.sh/) before the final image loads. + +## Lazy loading + +It is a good practice to leverage the browser-native lazy loading of images. Depending on your use case, make sure that content can be loaded lazily where possible. + +For more information see the [loading attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) for more information. \ No newline at end of file diff --git a/packages/shared-ux/file/image/impl/index.tsx b/packages/shared-ux/file/image/impl/index.tsx new file mode 100644 index 0000000000000..957ba6b990590 --- /dev/null +++ b/packages/shared-ux/file/image/impl/index.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { Image as FileImage } from './src/image'; +export type { Props as FileImageProps } from './src/image'; diff --git a/packages/shared-ux/file/image/impl/kibana.jsonc b/packages/shared-ux/file/image/impl/kibana.jsonc new file mode 100644 index 0000000000000..93a4709c14bca --- /dev/null +++ b/packages/shared-ux/file/image/impl/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-image", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/image/impl/package.json b/packages/shared-ux/file/image/impl/package.json new file mode 100644 index 0000000000000..9b45e313c4194 --- /dev/null +++ b/packages/shared-ux/file/image/impl/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-image", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/src/plugins/files/public/components/image/image.stories.tsx b/packages/shared-ux/file/image/impl/src/image.stories.tsx similarity index 92% rename from src/plugins/files/public/components/image/image.stories.tsx rename to packages/shared-ux/file/image/impl/src/image.stories.tsx index 15967437d0849..762d5407b441a 100644 --- a/src/plugins/files/public/components/image/image.stories.tsx +++ b/packages/shared-ux/file/image/impl/src/image.stories.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { getImageMetadata } from '../util'; +import { getImageMetadata } from '@kbn/shared-ux-file-util'; +import { getImageData as getBlob, base64dLogo } from '@kbn/shared-ux-file-image-mocks'; import { Image, Props } from './image'; -import { getImageData as getBlob, base64dLogo } from './image.constants.stories'; const defaultArgs: Props = { alt: 'test', src: `data:image/png;base64,${base64dLogo}` }; export default { - title: 'components/Image', + title: 'files/Image', component: Image, args: defaultArgs, decorators: [ diff --git a/src/plugins/files/public/components/image/image.tsx b/packages/shared-ux/file/image/impl/src/image.tsx similarity index 96% rename from src/plugins/files/public/components/image/image.tsx rename to packages/shared-ux/file/image/impl/src/image.tsx index d538ad2308040..59dc56d4ddc15 100644 --- a/src/plugins/files/public/components/image/image.tsx +++ b/packages/shared-ux/file/image/impl/src/image.tsx @@ -9,10 +9,10 @@ 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 { getBlurhashSrc } from '@kbn/shared-ux-file-util'; import classNames from 'classnames'; import { css } from '@emotion/react'; -import type { FileImageMetadata } from '../../../common'; -import { getBlurhashSrc } from '../util'; export type Props = { meta?: FileImageMetadata } & EuiImageProps; diff --git a/src/plugins/files/public/components/image/index.ts b/packages/shared-ux/file/image/impl/src/index.ts similarity index 100% rename from src/plugins/files/public/components/image/index.ts rename to packages/shared-ux/file/image/impl/src/index.ts diff --git a/packages/shared-ux/file/image/impl/tsconfig.json b/packages/shared-ux/file/image/impl/tsconfig.json new file mode 100644 index 0000000000000..dad7279f0e301 --- /dev/null +++ b/packages/shared-ux/file/image/impl/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "node", + "jest", + "react", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/shared-ux/file/image/mocks/BUILD.bazel b/packages/shared-ux/file/image/mocks/BUILD.bazel new file mode 100644 index 0000000000000..0c25ef25839ae --- /dev/null +++ b/packages/shared-ux/file/image/mocks/BUILD.bazel @@ -0,0 +1,127 @@ +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-image-mocks" + +SOURCE_FILES = glob( + [ + "**/*.ts", + ], + 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 = [ ] + +# 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/node", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/image/mocks/README.md b/packages/shared-ux/file/image/mocks/README.md new file mode 100644 index 0000000000000..c05a289f6aee5 --- /dev/null +++ b/packages/shared-ux/file/image/mocks/README.md @@ -0,0 +1,3 @@ +# @kbn/shared-ux-card-no-data-mocks + +TODO diff --git a/packages/shared-ux/file/image/mocks/index.ts b/packages/shared-ux/file/image/mocks/index.ts new file mode 100644 index 0000000000000..6337cc0044596 --- /dev/null +++ b/packages/shared-ux/file/image/mocks/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { base64dLogo, getImageData } from './src'; diff --git a/packages/shared-ux/file/image/mocks/kibana.jsonc b/packages/shared-ux/file/image/mocks/kibana.jsonc new file mode 100644 index 0000000000000..bb4d6a6acca4e --- /dev/null +++ b/packages/shared-ux/file/image/mocks/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-image-mocks", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/image/mocks/package.json b/packages/shared-ux/file/image/mocks/package.json new file mode 100644 index 0000000000000..02f631e8257ea --- /dev/null +++ b/packages/shared-ux/file/image/mocks/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-image-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" +} \ No newline at end of file diff --git a/src/plugins/files/public/components/image/image.constants.stories.tsx b/packages/shared-ux/file/image/mocks/src/index.ts similarity index 100% rename from src/plugins/files/public/components/image/image.constants.stories.tsx rename to packages/shared-ux/file/image/mocks/src/index.ts diff --git a/packages/shared-ux/file/image/mocks/tsconfig.json b/packages/shared-ux/file/image/mocks/tsconfig.json new file mode 100644 index 0000000000000..88aabf570de07 --- /dev/null +++ b/packages/shared-ux/file/image/mocks/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "node", + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/shared-ux/file/image/types/BUILD.bazel b/packages/shared-ux/file/image/types/BUILD.bazel new file mode 100644 index 0000000000000..d328c3c4fc76a --- /dev/null +++ b/packages/shared-ux/file/image/types/BUILD.bazel @@ -0,0 +1,59 @@ +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 = "types" +PKG_REQUIRE_NAME = "@kbn/shared-ux-file-image-types" + +SRCS = glob( + [ + "*.d.ts", + ] +) + +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 = [ +] + +js_library( + name = PKG_DIRNAME, + srcs = SRCS + NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS, + 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"], +) + +alias( + name = "npm_module_types", + actual = ":" + PKG_DIRNAME, + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/image/types/README.md b/packages/shared-ux/file/image/types/README.md new file mode 100644 index 0000000000000..6bef9e4caac23 --- /dev/null +++ b/packages/shared-ux/file/image/types/README.md @@ -0,0 +1,3 @@ +# @kbn/shared-ux-link-redirect-app-types + +TODO diff --git a/packages/shared-ux/file/image/types/index.d.ts b/packages/shared-ux/file/image/types/index.d.ts new file mode 100644 index 0000000000000..0635982d12a75 --- /dev/null +++ b/packages/shared-ux/file/image/types/index.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Set of metadata captured for every image uploaded via the file services' + * public components. + */ +export interface FileImageMetadata { + /** + * The blurhash that can be displayed while the image is loading + */ + blurhash?: string; + /** + * Width, in px, of the original image + */ + width: number; + /** + * Height, in px, of the original image + */ + height: number; +} diff --git a/packages/shared-ux/file/image/types/kibana.jsonc b/packages/shared-ux/file/image/types/kibana.jsonc new file mode 100644 index 0000000000000..c337cfd460355 --- /dev/null +++ b/packages/shared-ux/file/image/types/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-link-redirect-app-types", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/image/types/package.json b/packages/shared-ux/file/image/types/package.json new file mode 100644 index 0000000000000..66726e55b51fe --- /dev/null +++ b/packages/shared-ux/file/image/types/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/shared-ux-link-redirect-app-types", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/shared-ux/file/image/types/tsconfig.json b/packages/shared-ux/file/image/types/tsconfig.json new file mode 100644 index 0000000000000..f566d00dd2704 --- /dev/null +++ b/packages/shared-ux/file/image/types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "rxjs", + "@types/react", + ] + }, + "include": [ + "*.d.ts" + ] +} diff --git a/packages/shared-ux/file/util/BUILD.bazel b/packages/shared-ux/file/util/BUILD.bazel new file mode 100644 index 0000000000000..e7aeb058410b9 --- /dev/null +++ b/packages/shared-ux/file/util/BUILD.bazel @@ -0,0 +1,138 @@ +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 = "util" +PKG_REQUIRE_NAME = "@kbn/shared-ux-file-util" + +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//jest", + "@npm//blurhash", +] + +# 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//blurhash", + "//packages/shared-ux/file/image/types:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +js_library( + name = "npm_module_types", + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "build_types", + deps = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/file/util/README.mdx b/packages/shared-ux/file/util/README.mdx new file mode 100644 index 0000000000000..6471ddec90d6c --- /dev/null +++ b/packages/shared-ux/file/util/README.mdx @@ -0,0 +1,12 @@ +--- +id: sharedUX/Components/FileUtilities +slug: /shared-ux/components/util +title: File component utilities +description: Common utilities used by shared UX file components. +tags: ['shared-ux', 'utilities', 'files'] +date: 2022-11-22 +--- + +## Description + +A set of utilities used by shared UX file components for working with files. diff --git a/packages/shared-ux/file/util/index.ts b/packages/shared-ux/file/util/index.ts new file mode 100644 index 0000000000000..25ce4d9f13cbb --- /dev/null +++ b/packages/shared-ux/file/util/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { + fitToBox, + getBlurhashSrc, + getImageMetadata, + isImage, + type ImageMetadataFactory, +} from './src'; diff --git a/packages/shared-ux/file/util/jest.config.js b/packages/shared-ux/file/util/jest.config.js new file mode 100644 index 0000000000000..6ca3cc46f0eb6 --- /dev/null +++ b/packages/shared-ux/file/util/jest.config.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/file/util'], + verbose: true, +}; diff --git a/packages/shared-ux/file/util/kibana.jsonc b/packages/shared-ux/file/util/kibana.jsonc new file mode 100644 index 0000000000000..ef30839a63640 --- /dev/null +++ b/packages/shared-ux/file/util/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-file-util", + "owner": "@elastic/kibana-global-experience", + "runtimeDeps": [], + "typeDeps": [] +} diff --git a/packages/shared-ux/file/util/package.json b/packages/shared-ux/file/util/package.json new file mode 100644 index 0000000000000..91b5fbb765173 --- /dev/null +++ b/packages/shared-ux/file/util/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/shared-ux-file-util", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "types": "./target_types/index.d.ts" +} diff --git a/packages/shared-ux/file/util/src/image_metadata.test.ts b/packages/shared-ux/file/util/src/image_metadata.test.ts new file mode 100644 index 0000000000000..d91b9be36f6bb --- /dev/null +++ b/packages/shared-ux/file/util/src/image_metadata.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fitToBox } from './image_metadata'; +describe('util', () => { + describe('fitToBox', () => { + test('300x300', () => { + expect(fitToBox(300, 300)).toMatchInlineSnapshot(` + Object { + "height": 120, + "width": 120, + } + `); + }); + + test('300x150', () => { + expect(fitToBox(300, 150)).toMatchInlineSnapshot(` + Object { + "height": 60, + "width": 120, + } + `); + }); + + test('4500x9000', () => { + expect(fitToBox(4500, 9000)).toMatchInlineSnapshot(` + Object { + "height": 120, + "width": 60, + } + `); + }); + + test('1000x300', () => { + expect(fitToBox(1000, 300)).toMatchInlineSnapshot(` + Object { + "height": 36, + "width": 120, + } + `); + }); + + test('0x0', () => { + expect(fitToBox(0, 0)).toMatchInlineSnapshot(` + Object { + "height": 0, + "width": 0, + } + `); + }); + }); +}); diff --git a/packages/shared-ux/file/util/src/image_metadata.ts b/packages/shared-ux/file/util/src/image_metadata.ts new file mode 100644 index 0000000000000..f19abcf14c053 --- /dev/null +++ b/packages/shared-ux/file/util/src/image_metadata.ts @@ -0,0 +1,111 @@ +/* + * 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 bh from 'blurhash'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; + +export function isImage(file: { type?: string }): boolean { + return Boolean(file.type?.startsWith('image/')); +} + +export const boxDimensions = { + width: 120, + height: 120, +}; + +/** + * Calculate the size of an image, fitting to our limits see {@link boxDimensions}, + * while preserving the aspect ratio. + */ +export function fitToBox(width: number, height: number): { width: number; height: number } { + const offsetRatio = Math.abs( + Math.min( + // Find the aspect at which our box is smallest, if less than 1, it means we exceed the limits + Math.min(boxDimensions.width / width, boxDimensions.height / height), + // Values greater than 1 are within our limits + 1 + ) - 1 // Get the percentage we are exceeding. E.g., 0.3 - 1 = -0.7 means the image needs to shrink by 70% to fit + ); + return { + width: Math.floor(width - offsetRatio * width), + height: Math.floor(height - offsetRatio * height), + }; +} + +/** + * Get the native size of the image + */ +function loadImage(src: string): Promise { + return new Promise((res, rej) => { + const image = new window.Image(); + image.src = src; + image.onload = () => res(image); + image.onerror = rej; + }); +} + +/** + * Extract image metadata, assumes that file or blob as an image! + */ +export async function getImageMetadata(file: File | Blob): Promise { + const imgUrl = window.URL.createObjectURL(file); + try { + const image = await loadImage(imgUrl); + const canvas = document.createElement('canvas'); + // blurhash encoding and decoding is an expensive algorithm, + // so we have to shrink the image to speed up the calculation + const { width, height } = fitToBox(image.width, image.height); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Could not get 2d canvas context!'); + ctx.drawImage(image, 0, 0, width, height); + const imgData = ctx.getImageData(0, 0, width, height); + return { + blurhash: bh.encode(imgData.data, imgData.width, imgData.height, 4, 4), + width: image.width, + height: image.height, + }; + } catch (e) { + // Don't error out if we cannot generate the blurhash + return undefined; + } finally { + window.URL.revokeObjectURL(imgUrl); + } +} + +export type ImageMetadataFactory = typeof getImageMetadata; + +export function getBlurhashSrc({ + width, + height, + hash, +}: { + width: number; + height: number; + hash: string; +}): string { + const smallSizeImageCanvas = document.createElement('canvas'); + const { width: blurWidth, height: blurHeight } = fitToBox(width, height); + smallSizeImageCanvas.width = blurWidth; + smallSizeImageCanvas.height = blurHeight; + + const smallSizeImageCtx = smallSizeImageCanvas.getContext('2d')!; + const imageData = smallSizeImageCtx.createImageData(blurWidth, blurHeight); + imageData.data.set(bh.decode(hash, blurWidth, blurHeight)); + smallSizeImageCtx.putImageData(imageData, 0, 0); + + // scale back the blurred image to the size of the original image, + // so it is sized and positioned the same as the original image when used with an `` tag + const originalSizeImageCanvas = document.createElement('canvas'); + originalSizeImageCanvas.width = width; + originalSizeImageCanvas.height = height; + const originalSizeImageCtx = originalSizeImageCanvas.getContext('2d')!; + originalSizeImageCtx.drawImage(smallSizeImageCanvas, 0, 0, width, height); + return originalSizeImageCanvas.toDataURL(); +} diff --git a/packages/shared-ux/file/util/src/index.ts b/packages/shared-ux/file/util/src/index.ts new file mode 100644 index 0000000000000..9e0dccc6c3365 --- /dev/null +++ b/packages/shared-ux/file/util/src/index.ts @@ -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 { getImageMetadata, isImage, fitToBox, getBlurhashSrc } from './image_metadata'; +export type { ImageMetadataFactory } from './image_metadata'; diff --git a/packages/shared-ux/file/util/tsconfig.json b/packages/shared-ux/file/util/tsconfig.json new file mode 100644 index 0000000000000..47ad657279cbb --- /dev/null +++ b/packages/shared-ux/file/util/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "jest", + "node", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ] +} diff --git a/packages/shared-ux/page/no_data/mocks/BUILD.bazel b/packages/shared-ux/page/no_data/mocks/BUILD.bazel index eaeec70938a64..52b1806936a2d 100644 --- a/packages/shared-ux/page/no_data/mocks/BUILD.bazel +++ b/packages/shared-ux/page/no_data/mocks/BUILD.bazel @@ -47,7 +47,7 @@ NPM_MODULE_EXTRA_FILES = [ # eg. "@npm//lodash" RUNTIME_DEPS = [ "@npm//react", - "@npm//@storybook/addon-actions", + "@npm//@storybook/addon-actions", "//packages/shared-ux/card/no_data/mocks", "//packages/shared-ux/storybook/mock", ] diff --git a/src/plugins/files/common/index.ts b/src/plugins/files/common/index.ts index 16a7d03775e86..05d32a0645a0d 100755 --- a/src/plugins/files/common/index.ts +++ b/src/plugins/files/common/index.ts @@ -23,7 +23,6 @@ export type { FileSavedObject, BaseFileMetadata, FileShareOptions, - FileImageMetadata, FileUnshareOptions, BlobStorageSettings, UpdatableFileMetadata, diff --git a/src/plugins/files/common/types.ts b/src/plugins/files/common/types.ts index 247340c583ea6..370be9028317c 100644 --- a/src/plugins/files/common/types.ts +++ b/src/plugins/files/common/types.ts @@ -559,22 +559,3 @@ export interface FilesMetrics { */ countByExtension: Record; } - -/** - * Set of metadata captured for every image uploaded via the file services' - * public components. - */ -export interface FileImageMetadata { - /** - * The blurhash that can be displayed while the image is loading - */ - blurhash?: string; - /** - * Width, in px, of the original image - */ - width: number; - /** - * Height, in px, of the original image - */ - height: number; -} diff --git a/src/plugins/files/public/components/file_picker/components/file_card.tsx b/src/plugins/files/public/components/file_picker/components/file_card.tsx index 8e5730a9745ac..148f77736bfe1 100644 --- a/src/plugins/files/public/components/file_picker/components/file_card.tsx +++ b/src/plugins/files/public/components/file_picker/components/file_card.tsx @@ -12,8 +12,9 @@ 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 { FileImageMetadata, FileJSON } from '../../../../common'; -import { Image } from '../../image'; +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 { useFilePickerContext } from '../context'; diff --git a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx b/src/plugins/files/public/components/file_picker/file_picker.stories.tsx index 573f321ebcf4c..e0de316d60b24 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx +++ b/src/plugins/files/public/components/file_picker/file_picker.stories.tsx @@ -9,10 +9,11 @@ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import type { FileJSON, FileImageMetadata } from '../../../common'; +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 { base64dLogo } from '../image/image.constants.stories'; import { FilesContext } from '../context'; import { FilePicker, Props as FilePickerProps } from './file_picker'; diff --git a/src/plugins/files/public/components/index.ts b/src/plugins/files/public/components/index.ts index e4470d4a58be7..87354bd934f32 100644 --- a/src/plugins/files/public/components/index.ts +++ b/src/plugins/files/public/components/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export { Image, type ImageProps } from './image'; export { UploadFile, type UploadFileProps } from './upload_file'; export { FilePicker, type FilePickerProps } from './file_picker'; export { FilesContext } from './context'; diff --git a/src/plugins/files/public/components/util/image_metadata.ts b/src/plugins/files/public/components/util/image_metadata.ts index b1faed7b294b8..891448b99bbbe 100644 --- a/src/plugins/files/public/components/util/image_metadata.ts +++ b/src/plugins/files/public/components/util/image_metadata.ts @@ -7,7 +7,7 @@ */ import * as bh from 'blurhash'; -import type { FileImageMetadata } from '../../../common'; +import type { FileImageMetadata } from '@kbn/shared-ux-file-image-types'; export function isImage(file: { type?: string }): boolean { return Boolean(file.type?.startsWith('image/')); diff --git a/src/plugins/files/public/index.ts b/src/plugins/files/public/index.ts index 2d07fa06b8a4e..dc36b4d29d25a 100644 --- a/src/plugins/files/public/index.ts +++ b/src/plugins/files/public/index.ts @@ -17,8 +17,6 @@ export type { } from './types'; export { FilesContext, - Image, - type ImageProps, UploadFile, type UploadFileProps, FilePicker, diff --git a/src/plugins/files_management/public/components/file_flyout.tsx b/src/plugins/files_management/public/components/file_flyout.tsx index 93339edae5000..05f77ea69af41 100644 --- a/src/plugins/files_management/public/components/file_flyout.tsx +++ b/src/plugins/files_management/public/components/file_flyout.tsx @@ -23,7 +23,7 @@ import { import numeral from '@elastic/numeral'; import type { FileJSON } from '@kbn/files-plugin/common'; import type { FunctionComponent } from 'react'; -import { Image } from '@kbn/files-plugin/public'; +import { FileImage as Image } from '@kbn/shared-ux-file-image'; import React from 'react'; import { i18nTexts } from '../i18n_texts'; import { useFilesManagementContext } from '../context'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 8d2fb8125c1cb..a1d756709ee16 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -654,6 +654,14 @@ "@kbn/shared-ux-card-no-data-mocks/*": ["packages/shared-ux/card/no_data/mocks/*"], "@kbn/shared-ux-card-no-data-types": ["packages/shared-ux/card/no_data/types"], "@kbn/shared-ux-card-no-data-types/*": ["packages/shared-ux/card/no_data/types/*"], + "@kbn/shared-ux-file-image": ["packages/shared-ux/file/image/impl"], + "@kbn/shared-ux-file-image/*": ["packages/shared-ux/file/image/impl/*"], + "@kbn/shared-ux-file-image-mocks": ["packages/shared-ux/file/image/mocks"], + "@kbn/shared-ux-file-image-mocks/*": ["packages/shared-ux/file/image/mocks/*"], + "@kbn/shared-ux-link-redirect-app-types": ["packages/shared-ux/file/image/types"], + "@kbn/shared-ux-link-redirect-app-types/*": ["packages/shared-ux/file/image/types/*"], + "@kbn/shared-ux-file-util": ["packages/shared-ux/file/util"], + "@kbn/shared-ux-file-util/*": ["packages/shared-ux/file/util/*"], "@kbn/shared-ux-link-redirect-app": ["packages/shared-ux/link/redirect_app/impl"], "@kbn/shared-ux-link-redirect-app/*": ["packages/shared-ux/link/redirect_app/impl/*"], "@kbn/shared-ux-link-redirect-app-mocks": ["packages/shared-ux/link/redirect_app/mocks"], diff --git a/yarn.lock b/yarn.lock index d0192a4719a7b..a1e85461a3915 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3901,6 +3901,22 @@ version "0.0.0" uid "" +"@kbn/shared-ux-file-image-mocks@link:bazel-bin/packages/shared-ux/file/image/mocks": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-image-types@link:bazel-bin/packages/shared-ux/file/image/types": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-image@link:bazel-bin/packages/shared-ux/file/image/impl": + version "0.0.0" + uid "" + +"@kbn/shared-ux-file-util@link:bazel-bin/packages/shared-ux/file/util": + version "0.0.0" + uid "" + "@kbn/shared-ux-link-redirect-app-mocks@link:bazel-bin/packages/shared-ux/link/redirect_app/mocks": version "0.0.0" uid ""