Make upload list friendlier
This commit is contained in:
parent
a494a230b5
commit
1ff7404f6e
@ -1,6 +1,6 @@
|
||||
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { getUniqueTagsCached } from '@/photo/cache';
|
||||
import { getUniqueTagsCached, getUniqueRecipesCached } from '@/photo/cache';
|
||||
import AdminUploadsClient from '@/admin/AdminUploadsClient';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
||||
@ -10,6 +10,7 @@ export const maxDuration = 60;
|
||||
export default async function AdminUploadsPage() {
|
||||
const urls = await getStorageUploadUrlsNoStore();
|
||||
const uniqueTags = await getUniqueTagsCached();
|
||||
const uniqueRecipes = await getUniqueRecipesCached();
|
||||
|
||||
if (urls.length === 0) {
|
||||
redirect(PATH_ADMIN_PHOTOS);
|
||||
@ -20,6 +21,7 @@ export default async function AdminUploadsPage() {
|
||||
<AdminUploadsClient {...{
|
||||
urls,
|
||||
uniqueTags,
|
||||
uniqueRecipes,
|
||||
}} />}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -7,7 +7,10 @@ export default function AddButton(
|
||||
return (
|
||||
<PathLoaderButton
|
||||
{...props}
|
||||
icon={<BiImageAdd size={18} className="translate-x-[1px]" />}
|
||||
icon={<BiImageAdd
|
||||
size={18}
|
||||
className="translate-x-[1px] translate-y-[1px]"
|
||||
/>}
|
||||
>
|
||||
Add
|
||||
</PathLoaderButton>
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { StorageListResponse } from '@/platforms/storage';
|
||||
import { StorageListItem, StorageListResponse } from '@/platforms/storage';
|
||||
import AdminBatchUploadActions from './AdminBatchUploadActions';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Tags } from '@/tag';
|
||||
import AdminUploadsTable from './AdminUploadsTable';
|
||||
import { Recipes } from '@/recipe';
|
||||
|
||||
export type UrlAddStatus = {
|
||||
url: string
|
||||
uploadedAt?: Date
|
||||
export type UrlAddStatus = StorageListItem & {
|
||||
status?: 'waiting' | 'adding' | 'added'
|
||||
statusMessage?: string
|
||||
progress?: number
|
||||
@ -20,6 +19,7 @@ export default function AdminUploadsClient({
|
||||
}: {
|
||||
urls: StorageListResponse
|
||||
uniqueTags?: Tags
|
||||
uniqueRecipes?: Recipes
|
||||
}) {
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [urlAddStatuses, setUrlAddStatuses] = useState<UrlAddStatus[]>(urls);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import ImageSmall from '@/components/image/ImageSmall';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import { getIdFromStorageUrl } from '@/platforms/storage';
|
||||
import { getExtensionFromStorageUrl } from '@/platforms/storage';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { FaRegCircleCheck } from 'react-icons/fa6';
|
||||
import { pathForAdminUploadUrl } from '@/app/paths';
|
||||
@ -26,7 +26,7 @@ export default function AdminUploadsTable({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{urlAddStatuses.map(({ url, status, statusMessage, uploadedAt }) =>
|
||||
{urlAddStatuses.map(({ url, status, statusMessage, uploadedAt, size }) =>
|
||||
<div key={url}>
|
||||
<div className={clsx(
|
||||
'flex items-center gap-2 w-full min-h-8',
|
||||
@ -56,8 +56,10 @@ export default function AdminUploadsTable({
|
||||
/>
|
||||
</div>
|
||||
<span className="grow min-w-0">
|
||||
<div className="overflow-hidden text-ellipsis">
|
||||
{getIdFromStorageUrl(url)}
|
||||
<div className="truncate">
|
||||
{uploadedAt
|
||||
? <ResponsiveDate date={uploadedAt} />
|
||||
: '—'}
|
||||
</div>
|
||||
<div className="text-dim overflow-hidden text-ellipsis">
|
||||
{isAdding || isComplete
|
||||
@ -66,9 +68,10 @@ export default function AdminUploadsTable({
|
||||
: status === 'adding'
|
||||
? statusMessage ?? 'Adding ...'
|
||||
: 'Waiting'
|
||||
: uploadedAt
|
||||
? <ResponsiveDate date={uploadedAt} />
|
||||
: '—'}
|
||||
: size
|
||||
// eslint-disable-next-line max-len
|
||||
? `${size} ${getExtensionFromStorageUrl(url)?.toUpperCase()}`
|
||||
: getExtensionFromStorageUrl(url)?.toUpperCase()}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
PutObjectCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { StorageListResponse, generateStorageId } from '.';
|
||||
import { formatBytesToMB } from '@/utility/number';
|
||||
|
||||
const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
|
||||
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
|
||||
@ -70,10 +71,11 @@ export const awsS3List = async (
|
||||
Bucket: AWS_S3_BUCKET,
|
||||
Prefix,
|
||||
}))
|
||||
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({
|
||||
.then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
|
||||
url: urlForKey(Key),
|
||||
fileName: Key ?? '',
|
||||
uploadedAt: LastModified,
|
||||
size: Size ? formatBytesToMB(Size) : undefined,
|
||||
})) ?? []);
|
||||
|
||||
export const awsS3Delete = async (Key: string) => {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { StorageListResponse, generateStorageId } from '.';
|
||||
import { removeUrlProtocol } from '@/utility/url';
|
||||
import { formatBytesToMB } from '@/utility/number';
|
||||
|
||||
const CLOUDFLARE_R2_BUCKET =
|
||||
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
|
||||
@ -90,10 +91,11 @@ export const cloudflareR2List = async (
|
||||
Bucket: CLOUDFLARE_R2_BUCKET,
|
||||
Prefix,
|
||||
}))
|
||||
.then((data) => data.Contents?.map(({ Key, LastModified }) => ({
|
||||
.then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
|
||||
url: urlForKey(Key),
|
||||
fileName: Key ?? '',
|
||||
uploadedAt: LastModified,
|
||||
size: Size ? formatBytesToMB(Size) : undefined,
|
||||
})) ?? []);
|
||||
|
||||
export const cloudflareR2Delete = async (Key: string) => {
|
||||
|
||||
@ -33,11 +33,14 @@ import { PATH_API_PRESIGNED_URL } from '@/app/paths';
|
||||
|
||||
export const generateStorageId = () => generateNanoid(16);
|
||||
|
||||
export type StorageListResponse = {
|
||||
export type StorageListItem = {
|
||||
url: string
|
||||
fileName: string
|
||||
uploadedAt?: Date
|
||||
}[];
|
||||
size?: string
|
||||
};
|
||||
|
||||
export type StorageListResponse = StorageListItem[];
|
||||
|
||||
export type StorageType =
|
||||
'vercel-blob' |
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { PATH_API_VERCEL_BLOB_UPLOAD } from '@/app/paths';
|
||||
import { copy, del, list, put } from '@vercel/blob';
|
||||
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(
|
||||
/^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 vercelBlobList = (prefix: string) => list({ prefix })
|
||||
.then(({ blobs }) => blobs.map(({ url, uploadedAt }) => ({
|
||||
export const vercelBlobList = (
|
||||
prefix: string,
|
||||
): Promise<StorageListResponse> => list({ prefix })
|
||||
.then(({ blobs }) => blobs.map(({ url, uploadedAt, size }) => ({
|
||||
url,
|
||||
fileName: fileNameForStorageUrl(url),
|
||||
uploadedAt,
|
||||
size: formatBytesToMB(size),
|
||||
})));
|
||||
|
||||
@ -84,3 +84,6 @@ export const formatNumberToFraction = (number: number) => {
|
||||
return `${sign}${integer}${decimalFormatted}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatBytesToMB = (bytes: number) =>
|
||||
`${(bytes / 1024 / 1024).toFixed(2)}MB`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user