Skip to content

Commit

Permalink
support media preview if the data is an image (#685)
Browse files Browse the repository at this point in the history
Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
  • Loading branch information
shanghaikid authored Nov 10, 2024
1 parent 09c15cb commit 86b38b3
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 1 deletion.
110 changes: 110 additions & 0 deletions client/src/components/MediaPreview/MediaPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useState, useEffect } from 'react';
import icons from '../icons/Icons';

const MediaPreview = (props: { value: string }) => {
const { value } = props;
const [showImage, setShowImage] = useState(false);
const [image, setImage] = useState('');
const [showImageStyle, setShowImageStyle] = useState({});
const [imageDimensions, setImageDimensions] = useState({
width: 0,
height: 0,
});

useEffect(() => {
if (isImageSource(value)) {
setImage(value);

// Create an Image object to get natural dimensions
const img = new Image();
img.src = value;
img.onload = () => {
setImageDimensions({
width: img.naturalWidth,
height: img.naturalHeight,
});
};
}
}, [value]);

const handleMouseOver = (e: React.MouseEvent) => {
// Use dynamic image dimensions instead of fixed values
const imageWidth =
imageDimensions.width > 200 ? 200 : imageDimensions.width;
const imageHeight =
imageDimensions.height > 200
? imageDimensions.height * (200 / imageDimensions.width)
: imageDimensions.height;
const offset = 10; // Small offset to position the image beside the cursor

console.log('imageHeight', imageHeight);

// Calculate preliminary position
let left = e.clientX + offset - imageWidth / 2;
let top = e.clientY - imageHeight - 20;

console.log(top);

// Ensure the image stays within viewport boundaries
if (left + imageWidth > window.innerWidth) {
left = e.clientX - imageWidth - offset; // Move to the left side of the cursor if it exceeds the right boundary
}
if (left < 0) {
left = offset; // Move right if it goes off the left edge
}
if (top + imageHeight > window.innerHeight) {
top = window.innerHeight - imageHeight - offset; // Adjust to stay within the bottom boundary
}
if (top < 0) {
top = offset; // Adjust to stay within the top boundary
}

if (image) {
setShowImage(true);
setShowImageStyle({
position: 'fixed',
top: `${top}px`,
left: `${left}px`,
zIndex: 1000,
pointerEvents: 'none',
});
}
};

const handleMouseOut = () => {
setShowImage(false);
};

const isImg = isImageSource(value);

return (
<div style={{ position: 'relative', display: 'inline-block' }}>
<div
onMouseMove={handleMouseOver}
onMouseOut={handleMouseOut}
style={{ cursor: 'pointer' }}
>
{isImg && <icons.img />} {value}
</div>
{showImage && (
<div style={showImageStyle}>
<img
src={image}
alt="preview"
style={{ width: 'auto', maxWidth: '200px', borderRadius: '4px' }}
/>
</div>
)}
</div>
);
};

// Helper function to detect if the value is a URL or Base64-encoded image
function isImageSource(value: string): boolean {
const urlPattern = /\.(jpeg|jpg|gif|png|bmp|webp|svg)$/i;
const base64Pattern =
/^data:image\/(png|jpeg|jpg|gif|bmp|webp|svg\+xml);base64,/i;
return urlPattern.test(value) || base64Pattern.test(value);
}

export default MediaPreview;
17 changes: 17 additions & 0 deletions client/src/components/icons/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,23 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
></path>
</SvgIcon>
),
img: (props = {}) => (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5C1 1.67157 1.67157 1 2.5 1ZM2.5 2C2.22386 2 2 2.22386 2 2.5V8.3636L3.6818 6.6818C3.76809 6.59551 3.88572 6.54797 4.00774 6.55007C4.12975 6.55216 4.24568 6.60372 4.32895 6.69293L7.87355 10.4901L10.6818 7.6818C10.8575 7.50607 11.1425 7.50607 11.3182 7.6818L13 9.3636V2.5C13 2.22386 12.7761 2 12.5 2H2.5ZM2 12.5V9.6364L3.98887 7.64753L7.5311 11.4421L8.94113 13H2.5C2.22386 13 2 12.7761 2 12.5ZM12.5 13H10.155L8.48336 11.153L11 8.6364L13 10.6364V12.5C13 12.7761 12.7761 13 12.5 13ZM6.64922 5.5C6.64922 5.03013 7.03013 4.64922 7.5 4.64922C7.96987 4.64922 8.35078 5.03013 8.35078 5.5C8.35078 5.96987 7.96987 6.35078 7.5 6.35078C7.03013 6.35078 6.64922 5.96987 6.64922 5.5ZM7.5 3.74922C6.53307 3.74922 5.74922 4.53307 5.74922 5.5C5.74922 6.46693 6.53307 7.25078 7.5 7.25078C8.46693 7.25078 9.25078 6.46693 9.25078 5.5C9.25078 4.53307 8.46693 3.74922 7.5 3.74922Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
),
};

export default icons;
3 changes: 2 additions & 1 deletion client/src/components/icons/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ export type IconsType =
| 'link'
| 'cross'
| 'day'
| 'night';
| 'night'
| 'img';
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import StatusIcon, { LoadingType } from '@/components/status/StatusIcon';
import CustomInput from '@/components/customInput/CustomInput';
import CustomMultiSelector from '@/components/customSelector/CustomMultiSelector';
import CollectionColHeader from '../CollectionColHeader';
import MediaPreview from '@/components/MediaPreview/MediaPreview';

export interface CollectionDataProps {
collectionName: string;
Expand Down Expand Up @@ -511,6 +512,8 @@ const CollectionData = (props: CollectionDataProps) => {
case 'bool':
const res = JSON.stringify(cellData);
return <Typography title={res}>{res}</Typography>;
case 'string':
return <MediaPreview value={cellData} />;
default:
return cellData;
}
Expand Down
16 changes: 16 additions & 0 deletions client/src/pages/databases/collections/search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
buildSearchParams,
buildSearchCode,
getColumnWidth,
detectItemType,
} from '@/utils';
import SearchParams from '../../../search/SearchParams';
import DataExplorer, { formatMilvusData } from './DataExplorer';
Expand All @@ -45,6 +46,7 @@ import { ColDefinitionsType } from '@/components/grid/Types';
import { CollectionObject, CollectionFullObject } from '@server/types';
import CodeDialog from '@/pages/dialogs/CodeDialog';
import CollectionColHeader from '../CollectionColHeader';
import MediaPreview from '@/components/MediaPreview/MediaPreview';

export interface CollectionDataProps {
collectionName: string;
Expand Down Expand Up @@ -326,6 +328,20 @@ const Search = (props: CollectionDataProps) => {
headerFormatter: v => {
return <CollectionColHeader def={v} collection={collection} />;
},
formatter(_: any, cellData: any) {
const itemType = detectItemType(cellData);
switch (itemType) {
case 'json':
case 'array':
case 'bool':
const res = JSON.stringify(cellData);
return <Typography title={res}>{res}</Typography>;
case 'string':
return <MediaPreview value={cellData} />;
default:
return cellData;
}
},
getStyle: d => {
const field = collection.schema.fields.find(
f => f.name === key
Expand Down

0 comments on commit 86b38b3

Please sign in to comment.