Make upload list friendlier

This commit is contained in:
Sam Becker 2025-03-14 22:10:26 -05:00
parent a494a230b5
commit 1ff7404f6e
9 changed files with 42 additions and 20 deletions

View File

@ -1,6 +1,6 @@
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache'; import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import { getUniqueTagsCached } from '@/photo/cache'; import { getUniqueTagsCached, getUniqueRecipesCached } from '@/photo/cache';
import AdminUploadsClient from '@/admin/AdminUploadsClient'; import AdminUploadsClient from '@/admin/AdminUploadsClient';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { PATH_ADMIN_PHOTOS } from '@/app/paths'; import { PATH_ADMIN_PHOTOS } from '@/app/paths';
@ -10,6 +10,7 @@ export const maxDuration = 60;
export default async function AdminUploadsPage() { export default async function AdminUploadsPage() {
const urls = await getStorageUploadUrlsNoStore(); const urls = await getStorageUploadUrlsNoStore();
const uniqueTags = await getUniqueTagsCached(); const uniqueTags = await getUniqueTagsCached();
const uniqueRecipes = await getUniqueRecipesCached();
if (urls.length === 0) { if (urls.length === 0) {
redirect(PATH_ADMIN_PHOTOS); redirect(PATH_ADMIN_PHOTOS);
@ -20,6 +21,7 @@ export default async function AdminUploadsPage() {
<AdminUploadsClient {...{ <AdminUploadsClient {...{
urls, urls,
uniqueTags, uniqueTags,
uniqueRecipes,
}} />} }} />}
/> />
); );

View File

@ -7,7 +7,10 @@ export default function AddButton(
return ( return (
<PathLoaderButton <PathLoaderButton
{...props} {...props}
icon={<BiImageAdd size={18} className="translate-x-[1px]" />} icon={<BiImageAdd
size={18}
className="translate-x-[1px] translate-y-[1px]"
/>}
> >
Add Add
</PathLoaderButton> </PathLoaderButton>

View File

@ -1,14 +1,13 @@
'use client'; 'use client';
import { StorageListResponse } from '@/platforms/storage'; import { StorageListItem, StorageListResponse } from '@/platforms/storage';
import AdminBatchUploadActions from './AdminBatchUploadActions'; import AdminBatchUploadActions from './AdminBatchUploadActions';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Tags } from '@/tag'; import { Tags } from '@/tag';
import AdminUploadsTable from './AdminUploadsTable'; import AdminUploadsTable from './AdminUploadsTable';
import { Recipes } from '@/recipe';
export type UrlAddStatus = { export type UrlAddStatus = StorageListItem & {
url: string
uploadedAt?: Date
status?: 'waiting' | 'adding' | 'added' status?: 'waiting' | 'adding' | 'added'
statusMessage?: string statusMessage?: string
progress?: number progress?: number
@ -20,6 +19,7 @@ export default function AdminUploadsClient({
}: { }: {
urls: StorageListResponse urls: StorageListResponse
uniqueTags?: Tags uniqueTags?: Tags
uniqueRecipes?: Recipes
}) { }) {
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [urlAddStatuses, setUrlAddStatuses] = useState<UrlAddStatus[]>(urls); const [urlAddStatuses, setUrlAddStatuses] = useState<UrlAddStatus[]>(urls);

View File

@ -2,7 +2,7 @@
import ImageSmall from '@/components/image/ImageSmall'; import ImageSmall from '@/components/image/ImageSmall';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
import { getIdFromStorageUrl } from '@/platforms/storage'; import { getExtensionFromStorageUrl } from '@/platforms/storage';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { FaRegCircleCheck } from 'react-icons/fa6'; import { FaRegCircleCheck } from 'react-icons/fa6';
import { pathForAdminUploadUrl } from '@/app/paths'; import { pathForAdminUploadUrl } from '@/app/paths';
@ -26,7 +26,7 @@ export default function AdminUploadsTable({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{urlAddStatuses.map(({ url, status, statusMessage, uploadedAt }) => {urlAddStatuses.map(({ url, status, statusMessage, uploadedAt, size }) =>
<div key={url}> <div key={url}>
<div className={clsx( <div className={clsx(
'flex items-center gap-2 w-full min-h-8', 'flex items-center gap-2 w-full min-h-8',
@ -56,8 +56,10 @@ export default function AdminUploadsTable({
/> />
</div> </div>
<span className="grow min-w-0"> <span className="grow min-w-0">
<div className="overflow-hidden text-ellipsis"> <div className="truncate">
{getIdFromStorageUrl(url)} {uploadedAt
? <ResponsiveDate date={uploadedAt} />
: '—'}
</div> </div>
<div className="text-dim overflow-hidden text-ellipsis"> <div className="text-dim overflow-hidden text-ellipsis">
{isAdding || isComplete {isAdding || isComplete
@ -66,9 +68,10 @@ export default function AdminUploadsTable({
: status === 'adding' : status === 'adding'
? statusMessage ?? 'Adding ...' ? statusMessage ?? 'Adding ...'
: 'Waiting' : 'Waiting'
: uploadedAt : size
? <ResponsiveDate date={uploadedAt} /> // eslint-disable-next-line max-len
: '—'} ? `${size} ${getExtensionFromStorageUrl(url)?.toUpperCase()}`
: getExtensionFromStorageUrl(url)?.toUpperCase()}
</div> </div>
</span> </span>
</div> </div>

View File

@ -6,6 +6,7 @@ import {
PutObjectCommand, PutObjectCommand,
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
import { StorageListResponse, generateStorageId } from '.'; import { StorageListResponse, generateStorageId } from '.';
import { formatBytesToMB } from '@/utility/number';
const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? ''; const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? ''; const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
@ -70,10 +71,11 @@ export const awsS3List = async (
Bucket: AWS_S3_BUCKET, Bucket: AWS_S3_BUCKET,
Prefix, Prefix,
})) }))
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({ .then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
url: urlForKey(Key), url: urlForKey(Key),
fileName: Key ?? '', fileName: Key ?? '',
uploadedAt: LastModified, uploadedAt: LastModified,
size: Size ? formatBytesToMB(Size) : undefined,
})) ?? []); })) ?? []);
export const awsS3Delete = async (Key: string) => { export const awsS3Delete = async (Key: string) => {

View File

@ -7,6 +7,7 @@ import {
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
import { StorageListResponse, generateStorageId } from '.'; import { StorageListResponse, generateStorageId } from '.';
import { removeUrlProtocol } from '@/utility/url'; import { removeUrlProtocol } from '@/utility/url';
import { formatBytesToMB } from '@/utility/number';
const CLOUDFLARE_R2_BUCKET = const CLOUDFLARE_R2_BUCKET =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? ''; process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
@ -90,10 +91,11 @@ export const cloudflareR2List = async (
Bucket: CLOUDFLARE_R2_BUCKET, Bucket: CLOUDFLARE_R2_BUCKET,
Prefix, Prefix,
})) }))
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({ .then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
url: urlForKey(Key), url: urlForKey(Key),
fileName: Key ?? '', fileName: Key ?? '',
uploadedAt: LastModified, uploadedAt: LastModified,
size: Size ? formatBytesToMB(Size) : undefined,
})) ?? []); })) ?? []);
export const cloudflareR2Delete = async (Key: string) => { export const cloudflareR2Delete = async (Key: string) => {

View File

@ -33,11 +33,14 @@ import { PATH_API_PRESIGNED_URL } from '@/app/paths';
export const generateStorageId = () => generateNanoid(16); export const generateStorageId = () => generateNanoid(16);
export type StorageListResponse = { export type StorageListItem = {
url: string url: string
fileName: string fileName: string
uploadedAt?: Date uploadedAt?: Date
}[]; size?: string
};
export type StorageListResponse = StorageListItem[];
export type StorageType = export type StorageType =
'vercel-blob' | 'vercel-blob' |

View File

@ -1,7 +1,8 @@
import { PATH_API_VERCEL_BLOB_UPLOAD } from '@/app/paths'; import { PATH_API_VERCEL_BLOB_UPLOAD } from '@/app/paths';
import { copy, del, list, put } from '@vercel/blob'; import { copy, del, list, put } from '@vercel/blob';
import { upload } from '@vercel/blob/client'; import { upload } from '@vercel/blob/client';
import { fileNameForStorageUrl } from '.'; import { fileNameForStorageUrl, StorageListResponse } from '.';
import { formatBytesToMB } from '@/utility/number';
const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match( const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i, /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
@ -56,9 +57,12 @@ export const vercelBlobCopy = (
export const vercelBlobDelete = (fileName: string) => del(fileName); export const vercelBlobDelete = (fileName: string) => del(fileName);
export const vercelBlobList = (prefix: string) => list({ prefix }) export const vercelBlobList = (
.then(({ blobs }) => blobs.map(({ url, uploadedAt }) => ({ prefix: string,
): Promise<StorageListResponse> => list({ prefix })
.then(({ blobs }) => blobs.map(({ url, uploadedAt, size }) => ({
url, url,
fileName: fileNameForStorageUrl(url), fileName: fileNameForStorageUrl(url),
uploadedAt, uploadedAt,
size: formatBytesToMB(size),
}))); })));

View File

@ -84,3 +84,6 @@ export const formatNumberToFraction = (number: number) => {
return `${sign}${integer}${decimalFormatted}`; return `${sign}${integer}${decimalFormatted}`;
} }
}; };
export const formatBytesToMB = (bytes: number) =>
`${(bytes / 1024 / 1024).toFixed(2)}MB`;