From 7577562cc196bf5ce2b76f8fe26df21dc7c7a39e Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Thu, 11 Jun 2026 09:01:37 -0500 Subject: [PATCH 1/9] chore(well-show): Rm unused import --- src/pages/ocotillo/thing/well-show.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ocotillo/thing/well-show.tsx b/src/pages/ocotillo/thing/well-show.tsx index c039d512..42439380 100644 --- a/src/pages/ocotillo/thing/well-show.tsx +++ b/src/pages/ocotillo/thing/well-show.tsx @@ -3,7 +3,6 @@ import { useDataProvider, useList, useResourceParams } from '@refinedev/core' import { captureEvent } from '@/analytics/posthog' import { useQuery } from '@tanstack/react-query' import { Show, useDataGrid } from '@refinedev/mui' -import { AppBreadcrumb } from '@/components/AppBreadcrumb' import { TransducerObservationWithBlockResponse } from '@/generated/types.gen' import { IAsset, From 9837316b1b334ea03b09c8b7a61fa56eb93f00b2 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Thu, 11 Jun 2026 09:20:55 -0500 Subject: [PATCH 2/9] feat(Attachments): Add support for pdf & plain text types --- src/components/WellShow/Attachments.tsx | 120 +++++++++++++++++++++--- 1 file changed, 106 insertions(+), 14 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index bb4414af..12494249 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -8,6 +8,7 @@ import { Stack, Typography, Tooltip, + Button, } from '@mui/material' import { DataGrid, GridColDef } from '@mui/x-data-grid' import { @@ -23,6 +24,15 @@ import type { IAsset } from '@/interfaces/ocotillo' type ImageViewMode = 'grid' | 'slideshow' +const isImage = (asset: IAsset) => asset.mime_type?.startsWith('image/') + +const isPdf = (asset: IAsset) => asset.mime_type === 'application/pdf' + +const isText = (asset: IAsset) => asset.mime_type === 'text/plain' + +const canPreview = (asset: IAsset) => + Boolean(asset.signed_url) && (isImage(asset) || isPdf(asset) || isText(asset)) + export const AttachmentsCard = ({ assets, isLoading, @@ -33,10 +43,7 @@ export const AttachmentsCard = ({ const [imageViewMode, setImageViewMode] = useState('grid') const [slideshowIndex, setSlideshowIndex] = useState(0) - const imageAssets = useMemo( - () => assets.filter((a: { signed_url?: string }) => a?.signed_url), - [assets] - ) + const previewAssets = useMemo(() => assets.filter(canPreview), [assets]) const columns = useMemo[]>( () => [ @@ -74,12 +81,44 @@ export const AttachmentsCard = ({ ) }, }, + { + field: 'actions', + headerName: 'Actions', + width: 180, + sortable: false, + renderCell: ({ row }) => ( + + {row.signed_url && ( + + )} + + {row.signed_url && ( + + )} + + ), + }, ], [] ) - const currentImage = imageAssets[slideshowIndex] - const hasImages = imageAssets.length > 0 + const currentAsset = previewAssets[slideshowIndex] + const hasAssets = previewAssets.length > 0 return ( @@ -108,7 +147,7 @@ export const AttachmentsCard = ({ ) : ( {/* Images section (above table) with view toggle */} - {hasImages && ( + {hasAssets && ( - {imageAssets.map((img, idx) => ( + {previewAssets.map((img, idx) => ( + + - {imageAssets.length > 1 && ( + {previewAssets.length > 1 && ( <> setSlideshowIndex((i) => - i === 0 ? imageAssets.length - 1 : i - 1 + i === 0 ? previewAssets.length - 1 : i - 1 ) } sx={{ @@ -225,7 +266,7 @@ export const AttachmentsCard = ({ aria-label="Next image" onClick={() => setSlideshowIndex((i) => - i === imageAssets.length - 1 ? 0 : i + 1 + i === previewAssets.length - 1 ? 0 : i + 1 ) } sx={{ @@ -253,7 +294,7 @@ export const AttachmentsCard = ({ borderRadius: 1, }} > - {slideshowIndex + 1} / {imageAssets.length} + {slideshowIndex + 1} / {previewAssets.length} )} @@ -279,3 +320,54 @@ export const AttachmentsCard = ({ ) } + +const AssetPreview = ({ asset }: { asset: IAsset }) => { + if (isImage(asset)) { + return ( + + ) + } + + if (isPdf(asset)) { + return ( + + ) + } + + if (isText(asset)) { + return ( + + ) + } + + return Preview not available. +} From 0fc39bfc36c0644dfd886826cb3315fab6cd0a32 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Thu, 11 Jun 2026 09:29:59 -0500 Subject: [PATCH 3/9] fix(Attachments): The height will be set by the previewViewMode --- src/components/WellShow/Attachments.tsx | 91 ++++++++++--------------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index 12494249..c2cbb195 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -22,7 +22,7 @@ import { Masonry } from '@mui/lab' import { settings } from '@/settings' import type { IAsset } from '@/interfaces/ocotillo' -type ImageViewMode = 'grid' | 'slideshow' +type PreviewViewMode = 'grid' | 'slideshow' const isImage = (asset: IAsset) => asset.mime_type?.startsWith('image/') @@ -40,7 +40,8 @@ export const AttachmentsCard = ({ assets: IAsset[] isLoading: boolean }) => { - const [imageViewMode, setImageViewMode] = useState('grid') + const [previewViewMode, setPreviewViewMode] = + useState('grid') const [slideshowIndex, setSlideshowIndex] = useState(0) const previewAssets = useMemo(() => assets.filter(canPreview), [assets]) @@ -159,9 +160,9 @@ export const AttachmentsCard = ({ setImageViewMode('grid')} - aria-pressed={imageViewMode === 'grid'} + color={previewViewMode === 'grid' ? 'primary' : 'default'} + onClick={() => setPreviewViewMode('grid')} + aria-pressed={previewViewMode === 'grid'} > @@ -170,29 +171,29 @@ export const AttachmentsCard = ({ { - setImageViewMode('slideshow') + setPreviewViewMode('slideshow') setSlideshowIndex(0) }} - aria-pressed={imageViewMode === 'slideshow'} + aria-pressed={previewViewMode === 'slideshow'} > - {imageViewMode === 'grid' ? ( + {previewViewMode === 'grid' ? ( - {previewAssets.map((img, idx) => ( + {previewAssets.map((asset, idx) => ( { setSlideshowIndex(idx) - setImageViewMode('slideshow') + setPreviewViewMode('slideshow') }} sx={{ display: 'block', @@ -203,17 +204,7 @@ export const AttachmentsCard = ({ textAlign: 'left', }} > - - + ))} @@ -231,17 +222,9 @@ export const AttachmentsCard = ({ justifyContent: 'center', }} > - - + {currentAsset && ( + + )} {previewAssets.length > 1 && ( <> { +const AssetPreview = ({ + asset, + variant, +}: { + asset: IAsset + variant: 'grid' | 'slideshow' +}) => { + const height = variant === 'grid' ? 220 : 500 + if (isImage(asset)) { return ( { alt={asset.name} sx={{ width: '100%', - maxHeight: 400, + maxHeight: height, objectFit: 'contain', display: 'block', + bgcolor: 'grey.100', }} /> ) } - if (isPdf(asset)) { - return ( - - ) - } - - if (isText(asset)) { + if (isPdf(asset) || isText(asset)) { return ( { title={asset.name} sx={{ width: '100%', - height: 300, + height, border: 0, bgcolor: 'background.paper', }} @@ -369,5 +346,9 @@ const AssetPreview = ({ asset }: { asset: IAsset }) => { ) } - return Preview not available. + return ( + + Preview not available. + + ) } From 5cf8fd2f865013b38622b8add99c36330d478d3a Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 12 Jun 2026 07:46:12 -0500 Subject: [PATCH 4/9] fix(Attachments): Download images now works once clicked --- src/components/WellShow/Attachments.tsx | 113 +++++++++++++----------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index c2cbb195..c0870b05 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -8,7 +8,6 @@ import { Stack, Typography, Tooltip, - Button, } from '@mui/material' import { DataGrid, GridColDef } from '@mui/x-data-grid' import { @@ -54,18 +53,40 @@ export const AttachmentsCard = ({ headerName: 'URL', flex: 1, minWidth: 200, - renderCell: ({ value }) => { - const href = typeof value === 'string' ? value : '' - if (!href) { + renderCell: ({ row, value }) => { + const uri = typeof value === 'string' ? value : '' + const signedUrl = row.signed_url + + if (!uri) { return ( N/A ) } + + // Show the URI as plain text when there is no signed URL. + // Use the signed URL as the actual href when it is available, + if (!signedUrl) { + return ( + + {uri} + + ) + } + return ( - {href} + {uri} ) }, }, - { - field: 'actions', - headerName: 'Actions', - width: 180, - sortable: false, - renderCell: ({ row }) => ( - - {row.signed_url && ( - - )} - - {row.signed_url && ( - - )} - - ), - }, ], [] ) @@ -304,6 +293,37 @@ export const AttachmentsCard = ({ ) } +const previewStyles = { + grid: { + image: { + width: '100%', + display: 'block', + verticalAlign: 'bottom', + }, + frame: { + width: '100%', + height: 220, + border: 0, + bgcolor: 'background.paper', + }, + }, + slideshow: { + image: { + width: '100%', + maxWidth: '100%', + maxHeight: 400, + objectFit: 'contain', + display: 'block', + }, + frame: { + width: '100%', + height: 400, + border: 0, + bgcolor: 'background.paper', + }, + }, +} as const + const AssetPreview = ({ asset, variant, @@ -311,21 +331,13 @@ const AssetPreview = ({ asset: IAsset variant: 'grid' | 'slideshow' }) => { - const height = variant === 'grid' ? 220 : 500 - if (isImage(asset)) { return ( ) } @@ -336,12 +348,7 @@ const AssetPreview = ({ component="iframe" src={asset.signed_url} title={asset.name} - sx={{ - width: '100%', - height, - border: 0, - bgcolor: 'background.paper', - }} + sx={previewStyles[variant].frame} /> ) } From ed70233db635590a6b8f774f3cc57f0719be2d06 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 12 Jun 2026 08:13:29 -0500 Subject: [PATCH 5/9] feat(Attachments): Add download button --- src/components/WellShow/Attachments.tsx | 64 ++++++++++++++----------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index c0870b05..c76c578b 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -3,11 +3,11 @@ import { Box, ButtonBase, IconButton, - Link, Paper, Stack, Typography, Tooltip, + Button, } from '@mui/material' import { DataGrid, GridColDef } from '@mui/x-data-grid' import { @@ -53,9 +53,8 @@ export const AttachmentsCard = ({ headerName: 'URL', flex: 1, minWidth: 200, - renderCell: ({ row, value }) => { + renderCell: ({ value }) => { const uri = typeof value === 'string' ? value : '' - const signedUrl = row.signed_url if (!uri) { return ( @@ -65,31 +64,10 @@ export const AttachmentsCard = ({ ) } - // Show the URI as plain text when there is no signed URL. - // Use the signed URL as the actual href when it is available, - if (!signedUrl) { - return ( - - {uri} - - ) - } - return ( - - {uri} - + {uri || 'N/A'} + + ) + }, + }, + { + field: 'actions', + headerName: 'Actions', + width: 140, + sortable: false, + renderCell: ({ row }) => { + if (!row.signed_url) { + return ( + + N/A + + ) + } + + // Use the signed URL as a download action. + // Files are currently served with a generic binary content type + // (application/octet-stream), which browsers typically handle as + // downloadable content + return ( + ) }, }, From 42b10957d64ae9077319779c08fbaa383c916920 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 12 Jun 2026 14:06:30 -0500 Subject: [PATCH 6/9] feat(Attachments): Add download button to asset previews --- src/components/WellShow/Attachments.tsx | 209 ++++++++++++++---------- 1 file changed, 125 insertions(+), 84 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index c76c578b..6efbd0ef 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -9,7 +9,6 @@ import { Tooltip, Button, } from '@mui/material' -import { DataGrid, GridColDef } from '@mui/x-data-grid' import { ChevronLeft, ChevronRight, @@ -18,7 +17,6 @@ import { ViewCarousel, } from '@mui/icons-material' import { Masonry } from '@mui/lab' -import { settings } from '@/settings' import type { IAsset } from '@/interfaces/ocotillo' type PreviewViewMode = 'grid' | 'slideshow' @@ -45,75 +43,44 @@ export const AttachmentsCard = ({ const previewAssets = useMemo(() => assets.filter(canPreview), [assets]) - const columns = useMemo[]>( - () => [ - { field: 'name', headerName: 'Name', minWidth: 150 }, - { - field: 'uri', - headerName: 'URL', - flex: 1, - minWidth: 200, - renderCell: ({ value }) => { - const uri = typeof value === 'string' ? value : '' + // const columns = useMemo[]>( + // () => [ + // { field: 'name', headerName: 'Name', minWidth: 150 }, + // { + // field: 'actions', + // headerName: 'Actions', + // width: 140, + // sortable: false, + // headerAlign: 'right', + // align: 'right', + // renderCell: ({ row }) => { + // if (!row.signed_url) { + // return ( + // + // N/A + // + // ) + // } - if (!uri) { - return ( - - N/A - - ) - } - - return ( - - {uri || 'N/A'} - - ) - }, - }, - { - field: 'actions', - headerName: 'Actions', - width: 140, - sortable: false, - renderCell: ({ row }) => { - if (!row.signed_url) { - return ( - - N/A - - ) - } - - // Use the signed URL as a download action. - // Files are currently served with a generic binary content type - // (application/octet-stream), which browsers typically handle as - // downloadable content - return ( - - ) - }, - }, - ], - [] - ) + // // Use the signed URL as a download action. + // // Files are currently served with a generic binary content type + // // (application/octet-stream), which browsers typically handle as + // // downloadable content + // return ( + // + // ) + // }, + // }, + // ], + // [] + // ) const currentAsset = previewAssets[slideshowIndex] const hasAssets = previewAssets.length > 0 @@ -201,7 +168,7 @@ export const AttachmentsCard = ({ textAlign: 'left', }} > - + ))} @@ -220,7 +187,10 @@ export const AttachmentsCard = ({ }} > {currentAsset && ( - + )} {previewAssets.length > 1 && ( <> @@ -282,18 +252,6 @@ export const AttachmentsCard = ({ )} )} - - )} @@ -367,3 +325,86 @@ const AssetPreview = ({ ) } + +const AssetPreviewWithOverlay = ({ + asset, + variant, +}: { + asset: IAsset + variant: 'grid' | 'slideshow' +}) => { + const isSlideshow = variant === 'slideshow' + + return ( + + + + + + + {asset.name} + + + {asset.signed_url && ( + + )} + + ) +} From 5195ab8522448f36507e45a59ad6fc88bd58bc0f Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 12 Jun 2026 14:20:13 -0500 Subject: [PATCH 7/9] fix(Attachment): Add signed url refresh --- src/components/WellShow/Attachments.tsx | 87 +++++++++++++------------ src/pages/ocotillo/thing/well-show.tsx | 1 + 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index 6efbd0ef..2ae4bb31 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -33,9 +33,11 @@ const canPreview = (asset: IAsset) => export const AttachmentsCard = ({ assets, isLoading, + refetchAssets, }: { assets: IAsset[] isLoading: boolean + refetchAssets: () => Promise }) => { const [previewViewMode, setPreviewViewMode] = useState('grid') @@ -43,45 +45,6 @@ export const AttachmentsCard = ({ const previewAssets = useMemo(() => assets.filter(canPreview), [assets]) - // const columns = useMemo[]>( - // () => [ - // { field: 'name', headerName: 'Name', minWidth: 150 }, - // { - // field: 'actions', - // headerName: 'Actions', - // width: 140, - // sortable: false, - // headerAlign: 'right', - // align: 'right', - // renderCell: ({ row }) => { - // if (!row.signed_url) { - // return ( - // - // N/A - // - // ) - // } - - // // Use the signed URL as a download action. - // // Files are currently served with a generic binary content type - // // (application/octet-stream), which browsers typically handle as - // // downloadable content - // return ( - // - // ) - // }, - // }, - // ], - // [] - // ) - const currentAsset = previewAssets[slideshowIndex] const hasAssets = previewAssets.length > 0 @@ -168,7 +131,11 @@ export const AttachmentsCard = ({ textAlign: 'left', }} > - + ))} @@ -190,6 +157,7 @@ export const AttachmentsCard = ({ )} {previewAssets.length > 1 && ( @@ -329,12 +297,45 @@ const AssetPreview = ({ const AssetPreviewWithOverlay = ({ asset, variant, + refetchAssets, }: { asset: IAsset variant: 'grid' | 'slideshow' + refetchAssets: () => Promise }) => { const isSlideshow = variant === 'slideshow' + const downloadAsset = async ( + asset: IAsset, + refetchAssets: () => Promise + ) => { + let response = await fetch(asset.signed_url) + + if (response.status === 403) { + await refetchAssets() + + // This refetch updates React state, but this local asset still has + // the old signed URL. User can click again after URLs refresh. + return + } + + if (!response.ok) { + throw new Error('Failed to download asset') + } + + const blob = await response.blob() + const objectUrl = URL.createObjectURL(blob) + + const link = document.createElement('a') + link.href = objectUrl + link.download = asset.name || 'download' + document.body.appendChild(link) + link.click() + link.remove() + + URL.revokeObjectURL(objectUrl) + } + return ( event.stopPropagation()} + onClick={(event) => { + event.stopPropagation() + downloadAsset(asset, refetchAssets) + }} sx={{ position: 'absolute', right: 8, diff --git a/src/pages/ocotillo/thing/well-show.tsx b/src/pages/ocotillo/thing/well-show.tsx index 42439380..d7049681 100644 --- a/src/pages/ocotillo/thing/well-show.tsx +++ b/src/pages/ocotillo/thing/well-show.tsx @@ -401,6 +401,7 @@ export const WellShow = () => { From 8784a174a6bf47e23455e3cd6d6da1288482aa1a Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 12 Jun 2026 15:18:02 -0500 Subject: [PATCH 8/9] fix(Attachments): Add refresh if download took longer than 15 mins --- src/components/WellShow/Attachments.tsx | 52 ++++++++++++++++--------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index 2ae4bb31..eb37836e 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -309,31 +309,47 @@ const AssetPreviewWithOverlay = ({ asset: IAsset, refetchAssets: () => Promise ) => { - let response = await fetch(asset.signed_url) + const downloadFromUrl = async (url: string, fileName: string) => { + const response = await fetch(url) - if (response.status === 403) { - await refetchAssets() + if (!response.ok) { + throw response + } - // This refetch updates React state, but this local asset still has - // the old signed URL. User can click again after URLs refresh. - return - } + const blob = await response.blob() + const objectUrl = URL.createObjectURL(blob) + + const link = document.createElement('a') + link.href = objectUrl + link.download = fileName || 'download' + document.body.appendChild(link) + link.click() + link.remove() - if (!response.ok) { - throw new Error('Failed to download asset') + URL.revokeObjectURL(objectUrl) } - const blob = await response.blob() - const objectUrl = URL.createObjectURL(blob) + try { + await downloadFromUrl(asset.signed_url, asset.name) + } catch (error) { + if (!(error instanceof Response) || error.status !== 403) { + throw error + } + + const refetchResult = await refetchAssets() + const refreshedAssets = + refetchResult.data?.data ?? refetchResult.data ?? [] - const link = document.createElement('a') - link.href = objectUrl - link.download = asset.name || 'download' - document.body.appendChild(link) - link.click() - link.remove() + const refreshedAsset = refreshedAssets.find( + (item: IAsset) => item.id === asset.id + ) - URL.revokeObjectURL(objectUrl) + if (!refreshedAsset?.signed_url) { + throw new Error('Could not refresh signed URL') + } + + await downloadFromUrl(refreshedAsset.signed_url, refreshedAsset.name) + } } return ( From 8607ea45a19a5e8396f749a858d028a5792158c7 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 12 Jun 2026 15:34:16 -0500 Subject: [PATCH 9/9] fix(Attachments): Add types --- src/components/WellShow/Attachments.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index eb37836e..a5fd140f 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -18,6 +18,8 @@ import { } from '@mui/icons-material' import { Masonry } from '@mui/lab' import type { IAsset } from '@/interfaces/ocotillo' +import { QueryObserverResult } from '@tanstack/react-query' +import { GetListResponse, HttpError } from '@refinedev/core' type PreviewViewMode = 'grid' | 'slideshow' @@ -37,7 +39,9 @@ export const AttachmentsCard = ({ }: { assets: IAsset[] isLoading: boolean - refetchAssets: () => Promise + refetchAssets: () => Promise< + QueryObserverResult, HttpError> + > }) => { const [previewViewMode, setPreviewViewMode] = useState('grid') @@ -301,13 +305,17 @@ const AssetPreviewWithOverlay = ({ }: { asset: IAsset variant: 'grid' | 'slideshow' - refetchAssets: () => Promise + refetchAssets: () => Promise< + QueryObserverResult, HttpError> + > }) => { const isSlideshow = variant === 'slideshow' const downloadAsset = async ( asset: IAsset, - refetchAssets: () => Promise + refetchAssets: () => Promise< + QueryObserverResult, HttpError> + > ) => { const downloadFromUrl = async (url: string, fileName: string) => { const response = await fetch(url) @@ -402,8 +410,6 @@ const AssetPreviewWithOverlay = ({ variant="outlined" className="asset-overlay" size="small" - component="a" - href={asset.signed_url} onClick={(event) => { event.stopPropagation() downloadAsset(asset, refetchAssets)