From ec3a5911267a6cee8499f071fd4f787bd5a4536b Mon Sep 17 00:00:00 2001 From: rdbsolutions Date: Wed, 13 Aug 2025 15:57:18 +0100 Subject: [PATCH] updated storage admin --- actions/azurestorage.js | 81 +++++- pages/storageAdmin.js | 627 +++++++++++++++++++++++----------------- 2 files changed, 434 insertions(+), 274 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index a3c02dc0..2ecad913 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -1693,14 +1693,91 @@ export const listBlobHierarchical = async ( } } }; + +function formatTimeSince(date) { + if (!date) return "Unknown date"; + + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays < 7) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } else { + const diffWeeks = Math.floor(diffDays / 7); + return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; + } +} + +function formatDateDisplay(date) { + return date + ? `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})` + : "Unknown"; +} + +function formatBlobDates(blob) { + const created = blob.createdOn ? new Date(blob.createdOn) : null; + const modified = blob.lastModified ? new Date(blob.lastModified) : null; + + if (!created && !modified) return "Unknown"; + + // If both exist and are the same + if (created && modified && created.getTime() === modified.getTime()) { + return `${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )})`; + } + + // If modified exists and is newer than created + if (created && modified && modified.getTime() > created.getTime()) { + return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince( + modified + )})`; + } + + // Only one date exists + const date = modified || created; + return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`; +} + export async function listBlobHierarchicalForUser(containerClient) { const blobNames = []; for await (const blob of containerClient.listBlobsFlat()) { - // blobNames.push(blob.name); + const lastModifiedDate = blob.properties?.lastModified || null; + const createdOnDate = blob.properties?.createdOn || null; + + let displayDate = ""; + + if (createdOnDate && lastModifiedDate) { + if ( + createdOnDate.toDateString() === lastModifiedDate.toDateString() + ) { + // Same day — show just one + displayDate = `Date: ${formatDateDisplay(lastModifiedDate)}`; + } else { + // Different days — show both + displayDate = `Created: ${formatDateDisplay( + createdOnDate + )}, Modified: ${formatDateDisplay(lastModifiedDate)}`; + } + } else if (lastModifiedDate) { + displayDate = `Modified: ${formatDateDisplay(lastModifiedDate)}`; + } else if (createdOnDate) { + displayDate = `Created: ${formatDateDisplay(createdOnDate)}`; + } else { + displayDate = "Unknown date"; + } blobNames.push({ name: blob.name, - size: blob.properties.contentLength || 0, // in bytes + size: blob.properties.contentLength || 0, + lastModified: lastModifiedDate + ? lastModifiedDate.toISOString() + : null, + createdOn: createdOnDate ? createdOnDate.toISOString() : null, + displayDate, }); } return blobNames; diff --git a/pages/storageAdmin.js b/pages/storageAdmin.js index 09eda695..c65167d9 100644 --- a/pages/storageAdmin.js +++ b/pages/storageAdmin.js @@ -1,3 +1,6 @@ +import CookieBanner from "../components/cookieBanner"; +import Footer from "../components/footer"; +import Header from "../components/header"; import { DefaultAzureCredential } from "@azure/identity"; import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; import { v4 as uuidv4 } from "uuid"; @@ -7,6 +10,7 @@ import { listBlobHierarchical, listContainersForUser, } from "../actions/azurestorage"; +import Head from "next/head"; import Link from "next/link"; import { PrismaClient } from "@prisma/client"; import { @@ -19,8 +23,10 @@ import { getIP, sendRepCompleteMessage, deleteMyRepresentationsFromBlob, + deleteAwaitingSubmissionsFromBlob, } from "../actions"; import CryptoJS from "crypto-js"; +import { useState } from "react"; function getUpToLastSlash(url) { let lastSlash = url.lastIndexOf("/"); @@ -47,6 +53,48 @@ async function streamToString(readableStream) { }); } +function formatTimeSince(date) { + if (!date) return "Unknown date"; + + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays < 7) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } else { + const diffWeeks = Math.floor(diffDays / 7); + return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; + } +} + +function formatBlobDates(blob) { + const created = blob.createdOn ? new Date(blob.createdOn) : null; + const modified = blob.lastModified ? new Date(blob.lastModified) : null; + + if (!created && !modified) return "Unknown"; + + // If both exist and are the same + if (created && modified && created.getTime() === modified.getTime()) { + return `${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )})`; + } + + // If modified exists and is newer than created + if (created && modified && modified.getTime() > created.getTime()) { + return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince( + modified + )})`; + } + + // Only one date exists + const date = modified || created; + return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`; +} + const deleteRepItem = (containerID, caseID, repfile) => { deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then( (data) => { @@ -56,185 +104,312 @@ const deleteRepItem = (containerID, caseID, repfile) => { ); }; +const deleteCaseItem = (containerID, caseID) => { + deleteAwaitingSubmissionsFromBlob(containerID, caseID).then((data) => { + return data; + }); +}; + export default function StoragePage({ data }) { console.log(data); + + const [showRepJson, setShowRepJson] = useState(true); + const [showTmpFile, setShowTmpFile] = useState(true); + + const toggleAll = () => { + const newState = !(showRepJson || showTmpFile); + setShowRepJson(newState); + setShowTmpFile(newState); + }; return ( -
-
-

Storage Account Contents

- {data.map((user) => ( -
- {user.containers.map((c) => ( -
-
-

- {c.container} - {user.email} -

- {c.folders.map((folder) => ( -
-

- {folder.folderPath || "Root"} -

- {/* {folder.hasRepJson && ( +
+ + Storage Account Browser + +
+ +
+
+
+

Storage Account Contents

+ + {/* Toggle buttons */} +
+ + + +
+ + {data.map((user) => ( +
+ {user.containers.map((c) => ( +
+
+

+ {c.container} - {user.email} +

+ {c.folders.map((folder) => ( +
+

+ {folder.folderPath || + "Root"} +

+ {/* {folder.hasRepJson && (

rep.json file found:{" "} {folder.repJsonBlob.name}

)} */} - {folder.hasRepJson && ( -
- {" "} - - {folder.repComplete && ( - + + )} + {folder.hasRepJson && ( +
+ {" "} + - )} - {folder.repComplete && ( - - )} -
- )} - -
    - {folder.blobs.map((blob) => ( -
  • - - {blob.name} ( - {bytesToSize( - blob.contentLength || - blob.size + className=" cardModuleRemoveCase govuk-link govuk-link--no-underline govuk-link--inverse " + onClick={() => { + confirm( + "Do you want tyo delete rep " + + folder.repJsonBlob.name.split( + "/" + )[0] + + "?\n" + ) && + (deleteRepItem( + c.container, + folder.repJsonBlob.name.split( + "/" + )[0], + folder.repJsonBlob.name.split( + "/" + )[1] + ), + alert( + "Rep Deleted!" + ), + window.location.reload()); + }} + > + Delete Rep + + {folder.repComplete && ( + )} + {folder.repComplete && ( + + )} +
+ )} + +
    + {folder.blobs.map( + (blob) => ( +
  • + + { + blob.name + }{" "} + ( + {bytesToSize( + blob.contentLength || + blob.size + )} + ) + {" "} + {blob.lastModified && ( + + {/* { + blob.displayDate + } */} + {formatBlobDates( + blob + )} + + )} +
  • ) - - - ))} -
+ )} + +
+ ))}
- ))} -
+
+ ))}
))}
- ))} +
+
); @@ -299,112 +474,6 @@ export async function getServerSideProps({ req }) { user.email ); - // Add hashedName for each blob - // const containersWithHashes = containers.map((container) => { - // console.log("eee "); - - // const groupedByFolder = container.blobs.reduce((acc, blob) => { - // // Get folder path - everything before last slash, or "" if no slash - // const folderPath = getUpToLastSlash(blob.name.trim()) || ""; - - // const hashedfilepath = hashAPIPath( - // "/api/file/downloadblob?container=" + - // container.container + - // "&casefolderID=" + - // folderPath + - // "&blobname=" + - // blob.name - // .substring(blob.name.lastIndexOf("/") + 1) - // .trim(), - // process.env.HASHKEY - // ); - - // const blobWithHash = { ...blob, hashedfilepath }; - - // // Find or create folder group - // let folderGroup = acc.find( - // (f) => f.folderPath === folderPath - // ); - // if (!folderGroup) { - // folderGroup = { folderPath, blobs: [] }; - // acc.push(folderGroup); - // } - // folderGroup.blobs.push(blobWithHash); - - // return acc; - // }, []); - - // console.log(groupedByFolder); - // return { - // ...container, - // blobs: container.blobs.map((blob) => ({ - // ...blob, - // hashedfilepath: hashAPIPath( - // "/api/file/downloadblob?container=" + - // container.container + - // "&casefolderID=" + - // getUpToLastSlash(blob.name.trim()) + - // "&blobname=" + - // blob.name - // .substring(blob.name.lastIndexOf("/") + 1) - // .trim(), - // process.env.HASHKEY - // ), - // })), - // }; - // }); - - // const containersWithHashes = containers.map((container) => { - // const groupedByFolder = container.blobs.reduce((acc, blob) => { - // const folderPath = getUpToLastSlash(blob.name.trim()) || ""; - - // const hashedfilepath = hashAPIPath( - // "/api/file/downloadblob?container=" + - // container.container + - // "&casefolderID=" + - // folderPath + - // "&blobname=" + - // blob.name - // .substring(blob.name.lastIndexOf("/") + 1) - // .trim(), - // process.env.HASHKEY - // ); - - // const blobWithHash = { ...blob, hashedfilepath }; - - // let folderGroup = acc.find( - // (f) => f.folderPath === folderPath - // ); - // if (!folderGroup) { - // folderGroup = { - // folderPath, - // blobs: [], - // hasRepJson: false, // flag - // repJsonBlob: null, // save the rep.json blob if found - // }; - // acc.push(folderGroup); - // } - - // folderGroup.blobs.push(blobWithHash); - - // // Check if this blob ends with "rep.json" - // if (blob.name.toLowerCase().endsWith("rep.json")) { - // folderGroup.hasRepJson = true; - // folderGroup.repJsonBlob = blobWithHash; - // // You can also do some processing here if you want - // } - - // return acc; - // }, []); - - // return { - // ...container, - // folders: groupedByFolder, - // // Optionally remove flat blobs if you don't want them - // // blobs: undefined, - // }; - // }); - const containersWithHashes = await Promise.all( containers.map(async (container) => { const groupedByFolder = await container.blobs.reduce( @@ -429,7 +498,16 @@ export async function getServerSideProps({ req }) { process.env.HASHKEY ); - const blobWithHash = { ...blob, hashedfilepath }; + const fileNameOnly = blob.name.substring( + blob.name.lastIndexOf("/") + 1 + ); + const isTmpFile = fileNameOnly.startsWith("TMP-"); + + const blobWithHash = { + ...blob, + hashedfilepath, + isTmpFile, + }; let folderGroup = acc.find( (f) => f.folderPath === folderPath @@ -440,13 +518,18 @@ export async function getServerSideProps({ req }) { blobs: [], hasRepJson: false, repJsonBlob: null, - repComplete: false, // new flag + repComplete: false, + hasTmpFile: false, }; acc.push(folderGroup); } folderGroup.blobs.push(blobWithHash); + if (isTmpFile) { + folderGroup.hasTmpFile = true; + } + // If this blob ends with rep.json, fetch and parse JSON if (blob.name.toLowerCase().endsWith("rep.json")) { folderGroup.hasRepJson = true;