From ec6c703c60ead5f362606d76badb782c157db0d8 Mon Sep 17 00:00:00 2001 From: Yousif Yassi Date: Fri, 1 Dec 2023 12:21:33 -0500 Subject: [PATCH 1/3] fix: DIA-743: [FE] Blank space is displayed instead of the records on scroll up/down --- src/components/CellViews/ImageCell.js | 13 +++- src/components/DataManager/DataManager.js | 27 ++++---- src/components/Label/Label.js | 77 ++++++++++----------- src/providers/ImageProvider.js | 81 +++++++++++++++++++++++ 4 files changed, 148 insertions(+), 50 deletions(-) create mode 100644 src/providers/ImageProvider.js diff --git a/src/components/CellViews/ImageCell.js b/src/components/CellViews/ImageCell.js index 1c9b498d..2f8db7dc 100644 --- a/src/components/CellViews/ImageCell.js +++ b/src/components/CellViews/ImageCell.js @@ -1,6 +1,8 @@ import { getRoot } from "mobx-state-tree"; import { FF_LSDV_4711, isFF } from "../../utils/feature-flags"; import { AnnotationPreview } from "../Common/AnnotationPreview/AnnotationPreview"; +import { useRef } from "react"; +import { useImageProvider } from "../../providers/ImageProvider"; const imgDefaultProps = {}; @@ -13,23 +15,32 @@ export const ImageCell = (column) => { column: { alias }, } = column; const root = getRoot(original); + const imgRef = useRef(); + const { getImage } = useImageProvider(); const renderImagePreview = original.total_annotations === 0 || !root.showPreviews; const imgSrc = Array.isArray(value) ? value[0] : value; if (!imgSrc) return null; + getImage(imgSrc).then((loadedImage) => { + if (imgRef.current && loadedImage.loaded && loadedImage.url) { + imgRef.current.setAttribute("src", loadedImage.url); + imgRef.current.style.display = ""; + } + }); return renderImagePreview ? ( Data ) : ( diff --git a/src/components/DataManager/DataManager.js b/src/components/DataManager/DataManager.js index 71899ab3..90911bc5 100644 --- a/src/components/DataManager/DataManager.js +++ b/src/components/DataManager/DataManager.js @@ -11,6 +11,7 @@ import { FiltersSidebar } from "../Filters/FiltersSidebar/FilterSidebar"; import { DataView } from "../MainView"; import "./DataManager.styl"; import { Toolbar } from "./Toolbar/Toolbar"; +import { ImageProvider } from "../../providers/ImageProvider"; const injector = inject(({ store }) => { const { sidebarEnabled, sidebarVisible } = store.viewsStore ?? {}; @@ -127,19 +128,21 @@ const TabsSwitch = switchInjector(observer(({ sdk, views, tabs, selectedKey }) = export const DataManager = injector(({ shrinkWidth }) => { return ( - - - - - + + + + + + - - - + + + - - - - + + + + + ); }); diff --git a/src/components/Label/Label.js b/src/components/Label/Label.js index 942769f5..aeca3ceb 100644 --- a/src/components/Label/Label.js +++ b/src/components/Label/Label.js @@ -11,6 +11,7 @@ import { Resizer } from "../Common/Resizer/Resizer"; import { Space } from "../Common/Space/Space"; import { DataView } from "../MainView"; import "./Label.styl"; +import { ImageProvider } from "../../providers/ImageProvider"; const LabelingHeader = ({ SDK, onClick, isExplorerMode }) => { return ( @@ -99,45 +100,47 @@ export const Labeling = injector(observer(({ const outlinerEnabled = isFF(FF_DEV_1170); return ( - - {SDK.interfaceEnabled("labelingHeader") && ( - - )} - - - {isExplorerMode && ( - - - - - + + + {SDK.interfaceEnabled("labelingHeader") && ( + )} - - {loading && } - + + {isExplorerMode && ( + + + + + + )} + + + {loading && } + + - - + + ); })); diff --git a/src/providers/ImageProvider.js b/src/providers/ImageProvider.js new file mode 100644 index 00000000..d345faa1 --- /dev/null +++ b/src/providers/ImageProvider.js @@ -0,0 +1,81 @@ +import React, { useCallback, useContext, useMemo, useRef } from "react"; + +export const ImageContext = React.createContext({}); +ImageContext.displayName = "ImageContext"; + +export const ImageProvider = ({ children }) => { + const loadedImagesRef = useRef(new Map()); + const getImage = useCallback((imgSrc) => { + const loadedImages = loadedImagesRef.current; + + if (loadedImages.has(imgSrc)) { + const loadedImage = loadedImages.get(imgSrc); + + return loadedImage.promise; + } + const imageFetchPromise = fetch(imgSrc) + .then((response) => { + const reader = response.body.getReader(); + + return new ReadableStream({ + start(controller) { + return pump(); + function pump() { + return reader.read().then(({ done, value }) => { + // When no more data needs to be consumed, close the stream + if (done) { + controller.close(); + return; + } + // Enqueue the next data chunk into our target stream + controller.enqueue(value); + return pump(); + }); + } + }, + }); + }) + // Create a new response out of the stream + .then((stream) => new Response(stream)) + // Create an object URL for the response + .then((response) => response.blob()) + .then((blob) => URL.createObjectURL(blob)) + // Update image + .then((url) => { + loadedImages.set(imgSrc, { + url, + promise: imageFetchPromise, + loaded: true, + }); + return loadedImages.get(imgSrc); + }) + .catch((err) => { + console.error(err); + return loadedImages.get(imgSrc); + }); + + loadedImages.set(imgSrc, { + url: imgSrc, + promise: imageFetchPromise, + loaded: false, + }); + + return imageFetchPromise; + }, []); + const contextValue = useMemo(() => { + return { + loadedImages: loadedImagesRef.current, + getImage, + }; + }, [getImage]); + + return ( + + {children} + + ); +}; + +export const useImageProvider = () => { + return useContext(ImageContext) ?? {}; +}; \ No newline at end of file From a4ea50087139b8fb2445ebc46bd8c8f81b5fd670 Mon Sep 17 00:00:00 2001 From: Yousif Yassi Date: Thu, 7 Dec 2023 21:08:45 -0500 Subject: [PATCH 2/3] image loading with sw --- src/components/CellViews/ImageCell.js | 17 ++--- src/components/DataManager/DataManager.js | 27 ++++---- src/components/Label/Label.js | 77 +++++++++++------------ src/providers/ImageProvider.js | 37 +++++++++++ 4 files changed, 91 insertions(+), 67 deletions(-) diff --git a/src/components/CellViews/ImageCell.js b/src/components/CellViews/ImageCell.js index 2f8db7dc..73da002d 100644 --- a/src/components/CellViews/ImageCell.js +++ b/src/components/CellViews/ImageCell.js @@ -1,8 +1,6 @@ import { getRoot } from "mobx-state-tree"; import { FF_LSDV_4711, isFF } from "../../utils/feature-flags"; import { AnnotationPreview } from "../Common/AnnotationPreview/AnnotationPreview"; -import { useRef } from "react"; -import { useImageProvider } from "../../providers/ImageProvider"; const imgDefaultProps = {}; @@ -15,32 +13,27 @@ export const ImageCell = (column) => { column: { alias }, } = column; const root = getRoot(original); - const imgRef = useRef(); - const { getImage } = useImageProvider(); + const isDE = root.SDK.type === 'DE'; const renderImagePreview = original.total_annotations === 0 || !root.showPreviews; const imgSrc = Array.isArray(value) ? value[0] : value; if (!imgSrc) return null; - getImage(imgSrc).then((loadedImage) => { - if (imgRef.current && loadedImage.loaded && loadedImage.url) { - imgRef.current.setAttribute("src", loadedImage.url); - imgRef.current.style.display = ""; - } - }); + if (isDE) { + imgDefaultProps.crossOrigin = 'anonymous'; + } return renderImagePreview ? ( Data ) : ( diff --git a/src/components/DataManager/DataManager.js b/src/components/DataManager/DataManager.js index 90911bc5..71899ab3 100644 --- a/src/components/DataManager/DataManager.js +++ b/src/components/DataManager/DataManager.js @@ -11,7 +11,6 @@ import { FiltersSidebar } from "../Filters/FiltersSidebar/FilterSidebar"; import { DataView } from "../MainView"; import "./DataManager.styl"; import { Toolbar } from "./Toolbar/Toolbar"; -import { ImageProvider } from "../../providers/ImageProvider"; const injector = inject(({ store }) => { const { sidebarEnabled, sidebarVisible } = store.viewsStore ?? {}; @@ -128,21 +127,19 @@ const TabsSwitch = switchInjector(observer(({ sdk, views, tabs, selectedKey }) = export const DataManager = injector(({ shrinkWidth }) => { return ( - - - - - - + + + + + - - - + + + - - - - - + + + + ); }); diff --git a/src/components/Label/Label.js b/src/components/Label/Label.js index aeca3ceb..942769f5 100644 --- a/src/components/Label/Label.js +++ b/src/components/Label/Label.js @@ -11,7 +11,6 @@ import { Resizer } from "../Common/Resizer/Resizer"; import { Space } from "../Common/Space/Space"; import { DataView } from "../MainView"; import "./Label.styl"; -import { ImageProvider } from "../../providers/ImageProvider"; const LabelingHeader = ({ SDK, onClick, isExplorerMode }) => { return ( @@ -100,47 +99,45 @@ export const Labeling = injector(observer(({ const outlinerEnabled = isFF(FF_DEV_1170); return ( - - - {SDK.interfaceEnabled("labelingHeader") && ( - - )} - - - {isExplorerMode && ( - - - - - - )} - - - {loading && } + + {SDK.interfaceEnabled("labelingHeader") && ( + + )} + + + {isExplorerMode && ( + + tag={Resizer} + name="dataview" + minWidth={200} + showResizerLine={false} + type={'quickview'} + maxWidth={window.innerWidth * 0.35} + initialWidth={view.labelingTableWidth} // hardcoded as in main-menu-trigger + onResizeFinished={onResize} + style={{ display: "flex", flex: 1, width: '100%' }} + > + + + )} + + + {loading && } + - - + + ); })); diff --git a/src/providers/ImageProvider.js b/src/providers/ImageProvider.js index d345faa1..f0dfcc62 100644 --- a/src/providers/ImageProvider.js +++ b/src/providers/ImageProvider.js @@ -3,6 +3,43 @@ import React, { useCallback, useContext, useMemo, useRef } from "react"; export const ImageContext = React.createContext({}); ImageContext.displayName = "ImageContext"; +function registerImageProviderServiceWorker(serviceWorkerFileName) { + if ("serviceWorker" in navigator) { + navigator.serviceWorker + .register(serviceWorkerFileName) + .then(function(registration) { + console.log( + "Service Worker registered with scope:", + registration.scope, + ); + }) + .catch(function(error) { + console.log("Service Worker registration failed:", error); + }); + } +} + +function awakenServiceWorker() { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready.then((registration) => { + registration.active.postMessage({ type: "awaken" }); + }); + } +} + +// sw.js and sw-fallback.js are part of the opensource project label-studio/label_studio/core/static/js/ directory +// and are copied with the rest of the static files when running `python manage.py collectstatic` +registerImageProviderServiceWorker("/image-provider-sw.js"); + +// Wake up the service worker when the page becomes visible +// This is needed to ensure we are cleaning up cache when the user is using the application +// and not only when the user is closing the tab. +document.addEventListener("visibilitychange", () => { + if (!document.hidden) { + awakenServiceWorker(); + } +}); + export const ImageProvider = ({ children }) => { const loadedImagesRef = useRef(new Map()); const getImage = useCallback((imgSrc) => { From d1ef444a1c4283859bc4006b8556e5689ef6c24f Mon Sep 17 00:00:00 2001 From: Yousif Yassi Date: Thu, 7 Dec 2023 21:11:15 -0500 Subject: [PATCH 3/3] we dont need this anymore --- src/providers/ImageProvider.js | 118 --------------------------------- 1 file changed, 118 deletions(-) delete mode 100644 src/providers/ImageProvider.js diff --git a/src/providers/ImageProvider.js b/src/providers/ImageProvider.js deleted file mode 100644 index f0dfcc62..00000000 --- a/src/providers/ImageProvider.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useCallback, useContext, useMemo, useRef } from "react"; - -export const ImageContext = React.createContext({}); -ImageContext.displayName = "ImageContext"; - -function registerImageProviderServiceWorker(serviceWorkerFileName) { - if ("serviceWorker" in navigator) { - navigator.serviceWorker - .register(serviceWorkerFileName) - .then(function(registration) { - console.log( - "Service Worker registered with scope:", - registration.scope, - ); - }) - .catch(function(error) { - console.log("Service Worker registration failed:", error); - }); - } -} - -function awakenServiceWorker() { - if ("serviceWorker" in navigator) { - navigator.serviceWorker.ready.then((registration) => { - registration.active.postMessage({ type: "awaken" }); - }); - } -} - -// sw.js and sw-fallback.js are part of the opensource project label-studio/label_studio/core/static/js/ directory -// and are copied with the rest of the static files when running `python manage.py collectstatic` -registerImageProviderServiceWorker("/image-provider-sw.js"); - -// Wake up the service worker when the page becomes visible -// This is needed to ensure we are cleaning up cache when the user is using the application -// and not only when the user is closing the tab. -document.addEventListener("visibilitychange", () => { - if (!document.hidden) { - awakenServiceWorker(); - } -}); - -export const ImageProvider = ({ children }) => { - const loadedImagesRef = useRef(new Map()); - const getImage = useCallback((imgSrc) => { - const loadedImages = loadedImagesRef.current; - - if (loadedImages.has(imgSrc)) { - const loadedImage = loadedImages.get(imgSrc); - - return loadedImage.promise; - } - const imageFetchPromise = fetch(imgSrc) - .then((response) => { - const reader = response.body.getReader(); - - return new ReadableStream({ - start(controller) { - return pump(); - function pump() { - return reader.read().then(({ done, value }) => { - // When no more data needs to be consumed, close the stream - if (done) { - controller.close(); - return; - } - // Enqueue the next data chunk into our target stream - controller.enqueue(value); - return pump(); - }); - } - }, - }); - }) - // Create a new response out of the stream - .then((stream) => new Response(stream)) - // Create an object URL for the response - .then((response) => response.blob()) - .then((blob) => URL.createObjectURL(blob)) - // Update image - .then((url) => { - loadedImages.set(imgSrc, { - url, - promise: imageFetchPromise, - loaded: true, - }); - return loadedImages.get(imgSrc); - }) - .catch((err) => { - console.error(err); - return loadedImages.get(imgSrc); - }); - - loadedImages.set(imgSrc, { - url: imgSrc, - promise: imageFetchPromise, - loaded: false, - }); - - return imageFetchPromise; - }, []); - const contextValue = useMemo(() => { - return { - loadedImages: loadedImagesRef.current, - getImage, - }; - }, [getImage]); - - return ( - - {children} - - ); -}; - -export const useImageProvider = () => { - return useContext(ImageContext) ?? {}; -}; \ No newline at end of file