Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Files] Simplify image component implementation and usage #145347

Merged
merged 8 commits into from
Nov 18, 2022

Conversation

Dosant
Copy link
Contributor

@Dosant Dosant commented Nov 16, 2022

Needed for #81345

Summary

While working on the Image Embeddable I've struggle with using the image component, especially making the blur hash part work as I wanted. The main problems were caused by a custom container around the image which had custom styling - I had to properly style it separately to fit my use-case. The other problem was the BlurHash as since it was implemented as a separate image with custom positioning, I couldn't easily position it exactly the same as I positioned the original image.

To make Image component easier to use I suggest the following changes. I also wanted to make the component thinner.

  • Use a single tag for the underlying image - both for the src and for blurhash. This way we can be sure the image and the placeholder are always position in the same way. To make it work we first set src=blurhash, and then when it is ready we set a real source. The trick here is that the browser loads the new src without removing the previous image.
    • The downside is that we lost the logic with blurDelayExpired. This isn't great, but I think we can live with that considering the simplicity of the new approach. I also reduced the blur box size so that it calculates a bit more faster, as it would have to do this for every image now.
    • Also, to solve all the edge cases, I had to make the blurred image the same size as the original image by stretching it back to the original size. The problem is that I had to make that part of the code async [Files] Simplify image component implementation and usage #145347 (comment)
  • The image partially implemented some features from EuiImage (like predefined set of sizes). Instead of keeping the partial implementation with some code copy paste, I simply suggest we use EuiImage underneath. This make our image a tiny wrapper around EuiImage which also supports blurhash.
  • Added a storybook that tests custom sizing with css.
  • We discussed with @jloleysens that maybe we don't needs custom viewport visibility logic and just use a native browser attribute. The browsers which don't support it will simply load the image eagerly (only Safari atm). I think the native approach is fine here and we should remove the custom code to reduce bug surface.

ctx.putImageData(imageData, 0, 0);

return new Promise((resolve) => {
// On this point canvas contains a downsized blurred image
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part I don't like but couldn't find the workaround.
To make the blurred image sizing behave just like the original image we must have the blurred image stretched back to the original size. The problem is that I couldn't find a way to do this synchronously, so now there is a moment in time where image component doesn't render nor the blurred version, nor the src version

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add this as a comment in the code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like below. image param can be another canvas.

ctx2.drawImage(ctx, 0, 0, blurWidth, blurHeight, 0, 0, width, height);

Copy link
Contributor

@jloleysens jloleysens Nov 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, can we just return canvas.toDataURL() once we have the first canvas set up and skip the async image loading step? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vadimkibana, the idea with the second canvas works. thanks

I was trying it before like this:

ctx2.drawImage(ctx1, 0, 0, width, height);

But it actually should be like this (drawImage can receive canvas element, not ctx):

ctx2.drawImage(canvas1, 0, 0, width, height);

Actually, can we just return canvas.toDataURL() once we have the first canvas set up and skip the async image loading step?

I needed the second step (the image loading or the second canvas) to resize the blurred image back to the original size, so it behaves the same as the original image

@Dosant Dosant changed the title Simplify image component implementation and usage [Files] Simplify image component implementation and usage Nov 16, 2022
@Dosant Dosant changed the title [Files] Simplify image component implementation and usage [Discuss][Files] Simplify image component implementation and usage Nov 16, 2022
@Dosant Dosant added Team:SharedUX Team label for AppEx-SharedUX (formerly Global Experience) feature:Files labels Nov 16, 2022
@Dosant Dosant marked this pull request as ready for review November 16, 2022 14:03
@Dosant Dosant requested a review from a team as a code owner November 16, 2022 14:03
@Dosant Dosant added the release_note:skip Skip the PR/issue when compiling release notes label Nov 16, 2022
Copy link
Contributor

@vadimkibana vadimkibana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes LGTM.

const { isVisible, ref: observerRef } = useViewportObserver({ onFirstVisible });
export const Image = ({ src, url, alt, onLoad, onError, meta, ...rest }: Props) => {
const imageSrc = (src || url)!; // <EuiImage/> allows to use either `src` or `url`
const [currentImageSrc, onBlurHashLoaded] = useCurrentImageSrc(imageSrc, meta);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea, I'm curious if we could have it in the future something like:

const src = useObservable(file.src$);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. the Content Management Cache could return a content item file object:

const ContentThumbnail = ({ id }) => {
  const { cache } = useContent();
  const file = cache.get(id);

  // but, then what is next: (1) there could be a generic system for content items to return an associated image:
  const { src, width, height } = useObservable(file.img$);

  // or, (2) some content items could register "custom" metadata in the registry
  const { src, width, height } = useObservable(file.custom.img$);

  return <img src={src} ... />
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, any content item could specify an associated image, but also for some content items, like dashboard we could create a screenshot to use as a thumbnail.

ctx.putImageData(imageData, 0, 0);

return new Promise((resolve) => {
// On this point canvas contains a downsized blurred image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like below. image param can be another canvas.

ctx2.drawImage(ctx, 0, 0, blurWidth, blurHeight, 0, 0, width, height);

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @Dosant , I tested this locally and it is working as intended.

It's really cool that we can get rid of the viewport observer. Could we make the default for the Image component that it uses lazy loading (i.e., by default set it, and accept a special value false to set it to undefined?

(will finish my review later today)

(Story) => {
React.useLayoutEffect(() => {
// @ts-ignore
window.__image_stories_simulate_slow_load = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice hack 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally adding hack code into production for stories/tests should be avoided, but I see why you have done it in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that such hacks must be avoided.
I thought of also using process.env.NODE_ENV !== 'production but looks like env is production is storybook :(

src/plugins/files/public/components/image/image.tsx Outdated Show resolved Hide resolved
src/plugins/files/public/components/image/image.tsx Outdated Show resolved Hide resolved
src/plugins/files/public/components/image/image.tsx Outdated Show resolved Hide resolved
src/plugins/files/public/components/image/image.tsx Outdated Show resolved Hide resolved
ctx.putImageData(imageData, 0, 0);

return new Promise((resolve) => {
// On this point canvas contains a downsized blurred image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add this as a comment in the code?

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline and left a few comments in the code. Most notable:

  • Would be nice to address readability comments (up to you if you want to keep hook outside component though)
  • Can we return canvas.getDataUrl() from the first canvas and skip the async Image loading step?

Summary of changes, we are trading the following:

  1. Only show the blurhash when the image is taking a long time to load
  2. Fade the blurhash away when the image is actually ready (UX effect)

For a simplified implementation that still does most of what the initial implementation did. Great work man 👏🏻

ctx.putImageData(imageData, 0, 0);

return new Promise((resolve) => {
// On this point canvas contains a downsized blurred image
Copy link
Contributor

@jloleysens jloleysens Nov 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, can we just return canvas.toDataURL() once we have the first canvas set up and skip the async image loading step? 😄

@Dosant Dosant changed the title [Discuss][Files] Simplify image component implementation and usage [Files] Simplify image component implementation and usage Nov 17, 2022
@Dosant
Copy link
Contributor Author

Dosant commented Nov 17, 2022

@elasticmachine merge upstream

@Dosant
Copy link
Contributor Author

Dosant commented Nov 17, 2022

@elasticmachine merge upstream

@Dosant
Copy link
Contributor Author

Dosant commented Nov 18, 2022

@elasticmachine merge upstream

@Dosant Dosant enabled auto-merge (squash) November 18, 2022 09:49
@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
files 68 62 -6

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
files 18 16 -2

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
files 27.0KB 27.0KB +15.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
files 19.4KB 17.1KB -2.3KB
Unknown metric groups

API count

id before after diff
files 260 254 -6

ESLint disabled in files

id before after diff
osquery 1 2 +1

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
files 3 2 -1
fleet 59 65 +6
osquery 108 113 +5
securitySolution 442 448 +6
total +18

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
files 7 6 -1
fleet 67 73 +6
osquery 109 115 +6
securitySolution 519 525 +6
total +19

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@Dosant Dosant merged commit e3146a0 into elastic:main Nov 18, 2022
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Nov 18, 2022
Dosant added a commit that referenced this pull request Nov 24, 2022
…146159)

## Summary

Follow up to #145347
Needed for #81345
Partially fixes #145567


Two things: 

- [x] Handle errors related to blurhash loading. Make sure that the
original image is loaded if blurhash is failed to generate src or failed
to load
- [x] Make blurhash appear after the delay. This is needed for better UX
when the original image loads fast. Implemented using `css` animation
where first part of the animation is a static state with `opacity: 0,`
and then the image is revealed in the end. If the original image loads
faster, then it appears instantaneously without the delay
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting feature:Files release_note:skip Skip the PR/issue when compiling release notes Team:SharedUX Team label for AppEx-SharedUX (formerly Global Experience) v8.7.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants