Merged PR 1905: Download throttling

Related work items: #19431
This commit is contained in:
Robert Bond
2025-10-01 08:28:16 +00:00
13 changed files with 959 additions and 225 deletions
+3
View File
@@ -45,3 +45,6 @@ public/swagger.json
certificates/cert-key.pem certificates/cert-key.pem
certificates/cert.pem certificates/cert.pem
gitclean.sh gitclean.sh
pages/admin/logcheck.js
pages/admin/logchecker.js
pages/baracuda.html
+20 -1
View File
@@ -2144,8 +2144,25 @@ export const getNewDocumentsPaged = async (
fieldSort, fieldSort,
showNumberOfRecords, showNumberOfRecords,
documentType, documentType,
documentOrigin,
selectedWeeks selectedWeeks
) => { ) => {
console.log(
"/api/admin/getlatestdocuments_api?pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords +
"&documentType=" +
documentType +
"&numberWeeks=" +
selectedWeeks +
"&documentOrigin=" +
documentOrigin
);
return axios return axios
.get( .get(
"/api/admin/getlatestdocuments_api?pageNumber=" + "/api/admin/getlatestdocuments_api?pageNumber=" +
@@ -2159,7 +2176,9 @@ export const getNewDocumentsPaged = async (
"&documentType=" + "&documentType=" +
documentType + documentType +
"&numberWeeks=" + "&numberWeeks=" +
selectedWeeks selectedWeeks +
"&documentOrigin=" +
documentOrigin
) )
.then((res) => { .then((res) => {
return res.data; return res.data;
+2
View File
@@ -26,6 +26,8 @@ export default function AccountsTab({ userData }) {
> >
{user.email} {user.email}
</Link> </Link>
<br />
{user.id}
</dd> </dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16 "> <dd className="govuk-summary-list__value govuk-!-font-size-16 ">
Last accessed: {user.created} Last accessed: {user.created}
+216 -49
View File
@@ -7,6 +7,9 @@ import { connect } from "react-redux";
import transLookup from "../../../data/lookuptranslations.json"; import transLookup from "../../../data/lookuptranslations.json";
import { formatDates } from "../../utils"; import { formatDates } from "../../utils";
import PaginationControl from "../../../components/case/pagination"; import PaginationControl from "../../../components/case/pagination";
import DocumentLink from "../../utils/downloads";
import { useDownloadQueue } from "../../utils/downloadmanager";
import { sendGAEvent } from "@next/third-parties/google"; import { sendGAEvent } from "@next/third-parties/google";
import { import {
@@ -170,6 +173,21 @@ const getDocumentTypes = [
}, },
]; ];
const getOrigin = [
{
"Value": 846040000,
"Label": "MDU",
},
{
"Value": 846040001,
"Label": "Portal",
},
{
"Value": 846040002,
"Label": "Manual",
},
];
const DocumentsTab = (props) => { const DocumentsTab = (props) => {
let { t, lang } = useTranslation(); let { t, lang } = useTranslation();
const firstRender = useRef(false); const firstRender = useRef(false);
@@ -189,12 +207,15 @@ const DocumentsTab = (props) => {
const { locale } = router; const { locale } = router;
var documentDetailsArr = documentDetailsObj || []; var documentDetailsArr = documentDetailsObj || [];
const { startDownload, getStatus } = useDownloadQueue(3);
const [selectedOption, setSelectedOption] = useState(10); const [selectedOption, setSelectedOption] = useState(10);
const [selectedWeeks, setSelectedWeeks] = useState(1); const [selectedWeeks, setSelectedWeeks] = useState(1);
const [documentTypes, setDocumentTypes] = useState(getDocumentTypes); const [documentTypes, setDocumentTypes] = useState(getDocumentTypes);
const [documentOrigin, setDocumentOrigin] = useState(getOrigin);
const [selectedDocumentType, setSelectedDocumentType] = useState("all"); const [selectedDocumentType, setSelectedDocumentType] = useState("all");
const [selectedDocumentOrigin, setSelectedDocumentOrigin] = useState("all");
const [selectAll, setSelectAll] = useState(false); const [selectAll, setSelectAll] = useState(false);
const [selectOriginAll, setSelectOriginAll] = useState(false);
const [checkedItems, setCheckedItems] = useState( const [checkedItems, setCheckedItems] = useState(
documentTypes.reduce((acc, item) => { documentTypes.reduce((acc, item) => {
@@ -203,6 +224,13 @@ const DocumentsTab = (props) => {
}, {}) }, {})
); );
const [checkedOriginItems, setCheckedOriginItems] = useState(
documentOrigin.reduce((acc, item) => {
acc[item.pinswg_origin] = false;
return acc;
}, {})
);
const handleCheckboxChange = (e) => { const handleCheckboxChange = (e) => {
const { name, checked } = e.target; const { name, checked } = e.target;
@@ -213,6 +241,16 @@ const DocumentsTab = (props) => {
setSelectAll(false); setSelectAll(false);
}; };
const handleCheckboxOriginChange = (e) => {
const { name, checked } = e.target;
setCheckedOriginItems((prev) => ({
...prev,
[name]: checked,
}));
setSelectOriginAll(false);
};
const handleSelectAllChange = (e) => { const handleSelectAllChange = (e) => {
const { checked } = e.target; const { checked } = e.target;
setSelectAll(checked); // Set the 'Select All' state setSelectAll(checked); // Set the 'Select All' state
@@ -230,16 +268,42 @@ const DocumentsTab = (props) => {
} }
}; };
const handleSelectOriginAllChange = (e) => {
const { checked } = e.target;
setSelectOriginAll(checked); // Set the 'Select All' state
if (checked) {
// If 'Select All' is checked, check all the other checkboxes
setCheckedOriginItems(
documentOrigin.reduce((acc, item) => {
acc[item.pinswg_origin] = true;
return acc;
}, {})
);
} else {
clearCheckboxes();
}
};
const clearCheckboxes = () => { const clearCheckboxes = () => {
setSelectAll(false); setSelectAll(false);
setSelectOriginAll(false);
setCheckedItems( setCheckedItems(
documentTypes.reduce((acc, item) => { documentTypes.reduce((acc, item) => {
acc[item.pinswg_isharedocumentlocations] = false; acc[item.pinswg_isharedocumentlocations] = false;
return acc; return acc;
}, {}), }, {}),
setSelectedDocumentType("all"), setSelectedDocumentType("all"),
setCurrentPage(1) setCurrentPage(1)
); );
setCheckedOriginItems(
documentOrigin.reduce((acc, item) => {
acc["origin_" + item.pinswg_origin] = false;
return acc;
}, {}),
setSelectedDocumentOrigin("all")
);
}; };
let ShowDocLinks = false; let ShowDocLinks = false;
let checkShowDocLinks = docsOffline != false ? true : false; let checkShowDocLinks = docsOffline != false ? true : false;
@@ -314,6 +378,9 @@ const DocumentsTab = (props) => {
{t("search:clear-filter-button-label")} {t("search:clear-filter-button-label")}
</button> </button>
</div> </div>
<h4 className="govuk-body govuk-!-font-size-14 govuk-!-margin-bottom-0">
Document Type
</h4>
<div className="govuk-checkboxes govuk-checkboxes--small"> <div className="govuk-checkboxes govuk-checkboxes--small">
{" "} {" "}
<> <>
@@ -335,17 +402,6 @@ const DocumentsTab = (props) => {
{router.locale == "cy" {router.locale == "cy"
? "Pawb" ? "Pawb"
: "All"}{" "} : "All"}{" "}
- (
{documentTypes.reduce(
(accumulator, currentItem) => {
return (
accumulator +
currentItem.count
);
},
0
)}
)
</label> </label>
</div> </div>
</> </>
@@ -389,17 +445,105 @@ const DocumentsTab = (props) => {
); );
})} })}
</div>{" "} </div>{" "}
<h4 className="govuk-body govuk-!-font-size-14 govuk-!-margin-bottom-0">
Document Origin
</h4>
<div className="govuk-checkboxes govuk-checkboxes_short govuk-checkboxes--small">
{" "}
<>
<div className="govuk-checkboxes__item">
<input
className="govuk-checkboxes__input"
type="checkbox"
id={"all_" + "_" + 0}
key={0}
name={"all_" + "_" + 0}
checked={selectOriginAll}
value={"all"}
onChange={
handleSelectOriginAllChange
}
/>
<label
className="govuk-label-s govuk-checkboxes__label"
htmlFor={"all" + "__" + 0}
>
{router.locale == "cy"
? "Pawb"
: "All"}{" "}
</label>
</div>
</>
{documentOrigin.map((item, key) => {
return (
<div
className="govuk-checkboxes__item"
key={key}
>
<input
className="govuk-checkboxes__input"
type="checkbox"
id={"origin_" + item.Value}
key={key}
value={"origin_" + item.Value}
name={"origin_" + item.Value}
checked={
checkedOriginItems[
"origin_" + item.Value
]
}
onChange={
handleCheckboxOriginChange
}
/>
<label
className="govuk-label-s govuk-checkboxes__label"
htmlFor={"origin_" + item.Value}
>
{router.locale == "cy"
? jsonpath({
path:
'$..[?(@ && @.value=="' +
item.Label +
'")].value_cy',
json: transLookup,
eval: true,
})
: item.Label ||
t(
"case:summary-no-date-entered-label"
)}{" "}
</label>
</div>
);
})}
</div>{" "}
<div> <div>
<button <button
className="govuk-button " className="govuk-button "
type="button" type="button"
onClick={(e) => { onClick={(e) => {
const data = [ const checkedValues = [
...document.querySelectorAll( ...document.querySelectorAll(
".govuk-checkboxes__input:checked" ".govuk-checkboxes__input:checked"
), ),
].map((e) => e.value); ].map((e) => e.value);
setSelectedDocumentType(data);
const origins = checkedValues
.filter((val) =>
val.startsWith("origin_")
)
.map((val) =>
val.replace(/^origin_/, "")
); // strip prefix
const types = checkedValues.filter(
(val) => !val.startsWith("origin_")
);
setSelectedDocumentType(types);
setSelectedDocumentOrigin(origins);
setCurrentPage(1); setCurrentPage(1);
}} }}
> >
@@ -610,43 +754,63 @@ const DocumentsTab = (props) => {
{!ShowDocLinks ? ( {!ShowDocLinks ? (
detailsObj.pinswg_publishtoweb ? ( detailsObj.pinswg_publishtoweb ? (
<Link // <Link
scroll={ // scroll={
false // false
// }
// href={
// detailsObj.pinswg_hashlink
// }
// key={key}
// onClick={() => {
// toggleDownLoadLink(
// key
// ),
// sendGAEvent(
// "event",
// "DownloadedFile",
// {
// caseReference:
// props.caseReference,
// filename:
// detailsObj.pinswg_name,
// }
// );
// }}
// >
// <span className="results-visually-hidden">
// {t(
// "case:documents-name-label"
// )}
// :
// </span>
// {_.has(
// detailsObj,
// "pinswg_name"
// )
// ? detailsObj.pinswg_name
// : ""}
// </Link>
<DocumentLink
key={
detailsObj.pinswg_documentid
} }
href={ id={
detailsObj.pinswg_hashlink detailsObj.pinswg_documentid
} }
key={key} detailsObj={
onClick={() => { detailsObj
toggleDownLoadLink( }
key caseReference={
), caseReference
sendGAEvent( }
"event", startDownload={
"DownloadedFile", startDownload
{ }
caseReference: getStatus={
props.caseReference, getStatus
filename: }
detailsObj.pinswg_name, />
}
);
}}
>
<span className="results-visually-hidden">
{t(
"case:documents-name-label"
)}
:
</span>
{_.has(
detailsObj,
"pinswg_name"
)
? detailsObj.pinswg_name
: ""}
</Link>
) : ( ) : (
<> <>
<span className="results-visually-hidden"> <span className="results-visually-hidden">
@@ -797,6 +961,7 @@ const DocumentsTab = (props) => {
fieldSort, fieldSort,
selectedOption, selectedOption,
selectedDocumentType, selectedDocumentType,
selectedDocumentOrigin,
selectedWeeks selectedWeeks
) )
.then((data) => data) .then((data) => data)
@@ -810,6 +975,7 @@ const DocumentsTab = (props) => {
selectedOption, selectedOption,
selectedDocumentType, selectedDocumentType,
selectedWeeks, selectedWeeks,
selectedDocumentOrigin,
props.setDocumentDetails, props.setDocumentDetails,
] ]
); );
@@ -942,6 +1108,7 @@ const DocumentsTab = (props) => {
fieldSortState, fieldSortState,
orderByState, orderByState,
getDocumentTypes, getDocumentTypes,
getOrigin,
getDocumentResults, getDocumentResults,
lang, lang,
selectedWeeks, selectedWeeks,
+60 -36
View File
@@ -17,6 +17,8 @@ import {
import { setCurrentPage } from "../../store/currentView/action"; import { setCurrentPage } from "../../store/currentView/action";
import { setDocumentDetails } from "../../store/searchOutput/action"; import { setDocumentDetails } from "../../store/searchOutput/action";
import DocumentLink from "../utils/downloads";
import { useDownloadQueue } from "../utils/downloadmanager";
const DocumentDetails = (props) => { const DocumentDetails = (props) => {
let { t, lang } = useTranslation(); let { t, lang } = useTranslation();
@@ -37,7 +39,7 @@ const DocumentDetails = (props) => {
const { locale } = router; const { locale } = router;
var documentDetailsArr = documentDetailsObj || []; var documentDetailsArr = documentDetailsObj || [];
const { startDownload, getStatus } = useDownloadQueue(3);
const [selectedOption, setSelectedOption] = useState(10); const [selectedOption, setSelectedOption] = useState(10);
const [documentTypes, setDocumentTypes] = useState([{}]); const [documentTypes, setDocumentTypes] = useState([{}]);
const [selectedDocumentType, setSelectedDocumentType] = useState("all"); const [selectedDocumentType, setSelectedDocumentType] = useState("all");
@@ -764,41 +766,63 @@ const DocumentDetails = (props) => {
)} )}
{!ShowDocLinks ? ( {!ShowDocLinks ? (
<Link <>
scroll={false} <DocumentLink
href={ key={
detailsObj.pinswg_hashlink detailsObj.pinswg_documentid
} }
key={key} id={
onClick={() => { detailsObj.pinswg_documentid
toggleDownLoadLink( }
key detailsObj={
), detailsObj
sendGAEvent( }
"event", caseReference={
"DownloadedFile", caseReference
{ }
caseReference: startDownload={
props.caseReference, startDownload
filename: }
detailsObj.pinswg_name, getStatus={
} getStatus
); }
}} />
> {/* <Link
<span className="results-visually-hidden"> scroll={false}
{t( href={
"case:documents-name-label" detailsObj.pinswg_hashlink
)} }
: key={key}
</span> onClick={() => {
{_.has( toggleDownLoadLink(
detailsObj, key
"pinswg_name" ),
) sendGAEvent(
? detailsObj.pinswg_name "event",
: ""} "DownloadedFile",
</Link> {
caseReference:
props.caseReference,
filename:
detailsObj.pinswg_name,
}
);
}}
>
<span className="results-visually-hidden">
{t(
"case:documents-name-label"
)}
:
</span>
{_.has(
detailsObj,
"pinswg_name"
)
? detailsObj.pinswg_name
: ""}
</Link> */}
</>
) : ( ) : (
<> <>
<span className="results-visually-hidden"> <span className="results-visually-hidden">
+48
View File
@@ -0,0 +1,48 @@
import { useState, useCallback } from "react";
export function useDownloadQueue(maxConcurrent = 3) {
const [queue, setQueue] = useState([]); // queued tasks
const [activeCount, setActiveCount] = useState(0);
const [statuses, setStatuses] = useState({}); // { id: "idle"|"queued"|"downloading"|"done"|"failed" }
const scheduleNext = useCallback(() => {
setQueue((queueCurr) => {
if (activeCount >= maxConcurrent || queueCurr.length === 0)
return queueCurr;
const [task, ...rest] = queueCurr;
setStatuses((s) => ({ ...s, [task.id]: "downloading" }));
setActiveCount((c) => c + 1);
// Run the download fully in async IIFE
(async () => {
try {
await task.downloadFn(); // triggers fetch + blob + <a> click
setStatuses((s) => ({ ...s, [task.id]: "done" }));
} catch (err) {
console.error(err);
setStatuses((s) => ({ ...s, [task.id]: "failed" }));
} finally {
setActiveCount((c) => c - 1);
scheduleNext(); // promote next in queue
}
})();
return rest;
});
}, [activeCount, maxConcurrent]);
const startDownload = useCallback(
(id, downloadFn) => {
setStatuses((s) => ({ ...s, [id]: "queued" }));
setQueue((curr) => [...curr, { id, downloadFn }]);
scheduleNext();
},
[scheduleNext]
);
const getStatus = useCallback((id) => statuses[id] || "idle", [statuses]);
return { startDownload, getStatus };
}
+325
View File
@@ -0,0 +1,325 @@
// // import { useState } from "react";
// // export default function DocumentLink({ detailsObj, caseReference }) {
// // const [downloadStatus, setDownloadStatus] = useState("");
// // const [progress, setProgress] = useState(null);
// // const handleDownload = async () => {
// // setDownloadStatus("Downloading");
// // setProgress(0);
// // try {
// // const res = await fetch(detailsObj.pinswg_hashlink);
// // if (!res.ok) throw new Error("Download failed");
// // const contentDisposition = res.headers.get("content-disposition");
// // const filename = contentDisposition
// // ? contentDisposition.split("filename=")[1]
// // : detailsObj.pinswg_name || "document";
// // const contentLength = res.headers.get("content-length");
// // const totalBytes = contentLength
// // ? parseInt(contentLength, 10)
// // : null;
// // const reader = res.body.getReader();
// // let receivedBytes = 0;
// // const chunks = [];
// // while (true) {
// // const { done, value } = await reader.read();
// // if (done) break;
// // chunks.push(value);
// // receivedBytes += value.length;
// // if (totalBytes) {
// // const percent = Math.round(
// // (receivedBytes / totalBytes) * 100
// // );
// // setProgress(percent);
// // }
// // }
// // // Combine chunks into one blob
// // const blob = new Blob(chunks);
// // const url = window.URL.createObjectURL(blob);
// // const link = document.createElement("a");
// // link.href = url;
// // link.download = filename;
// // document.body.appendChild(link);
// // link.click();
// // document.body.removeChild(link);
// // setDownloadStatus("Download complete!");
// // setProgress(100);
// // // Fire GA event
// // window.gtag?.("event", "DownloadedFile", {
// // caseReference,
// // filename,
// // });
// // } catch (err) {
// // console.error(err);
// // setDownloadStatus("Download failed");
// // setProgress(null);
// // }
// // };
// // return (
// // <>
// // {detailsObj.pinswg_publishtoweb ? (
// // <button className="documentLink" onClick={handleDownload}>
// // <span className="results-visually-hidden">
// // Document name:
// // </span>
// // {detailsObj.pinswg_name || ""}
// // </button>
// // ) : (
// // <>
// // <span className="results-visually-hidden">
// // Document name:
// // </span>
// // {detailsObj.pinswg_name || ""}
// // </>
// // )}
// // {downloadStatus && (
// // <p
// // className={
// // downloadStatus == "Downloading"
// // ? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
// // : "govuk-!-font-size-14 govuk-!-margin-left-0 "
// // }
// // >
// // {downloadStatus}
// // {/* {progress !== null && ` (${progress}%)`} */}
// // </p>
// // )}
// // </>
// // );
// // }
// import { useState } from "react";
// export default function DocumentLink({
// id,
// detailsObj,
// caseReference,
// startDownload,
// getStatus,
// }) {
// const [downloadStatus, setDownloadStatus] = useState("");
// const [progress, setProgress] = useState(null);
// const downloadFile = async () => {
// setDownloadStatus("Downloading");
// setProgress(0);
// try {
// const res = await fetch(detailsObj.pinswg_hashlink);
// if (!res.ok) throw new Error("Download failed");
// const contentDisposition = res.headers.get("content-disposition");
// const filename = contentDisposition
// ? contentDisposition.split("filename=")[1]
// : detailsObj.pinswg_name || "document";
// const contentLength = res.headers.get("content-length");
// const totalBytes = contentLength
// ? parseInt(contentLength, 10)
// : null;
// const reader = res.body.getReader();
// let receivedBytes = 0;
// const chunks = [];
// while (true) {
// const { done, value } = await reader.read();
// if (done) break;
// chunks.push(value);
// receivedBytes += value.length;
// if (totalBytes) {
// const percent = Math.round(
// (receivedBytes / totalBytes) * 100
// );
// setProgress(percent);
// }
// }
// const blob = new Blob(chunks);
// const url = window.URL.createObjectURL(blob);
// const link = document.createElement("a");
// link.href = url;
// link.download = filename;
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// setDownloadStatus("Download complete!");
// setProgress(100);
// window.gtag?.("event", "DownloadedFile", {
// caseReference,
// filename,
// });
// } catch (err) {
// console.error(err);
// setDownloadStatus("Download failed");
// setProgress(null);
// }
// };
// const handleClick = () => {
// if (getStatus(id) !== "idle") return; // don't double enqueue
// startDownload(id, downloadFile);
// };
// const status = getStatus(id);
// return (
// <>
// {detailsObj.pinswg_publishtoweb ? (
// <button className="documentLink" onClick={handleClick}>
// <span className="results-visually-hidden">
// Document name:
// </span>
// {detailsObj.pinswg_name || ""}
// </button>
// ) : (
// <>
// <span className="results-visually-hidden">
// Document name:
// </span>
// {detailsObj.pinswg_name || ""}
// </>
// )}
// {status !== "idle" && (
// <p
// className={
// status === "downloading"
// ? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
// : "govuk-!-font-size-14 govuk-!-margin-left-0"
// }
// >
// {status === "downloading" && progress !== null
// ? `Downloading (${progress}%)`
// : status === "queued"
// ? "Queued"
// : status === "done"
// ? "Download complete!"
// : status === "failed"
// ? "Download failed"
// : ""}
// </p>
// )}
// </>
// );
// }
import { useState } from "react";
import useTranslation from "next-translate/useTranslation";
export default function DocumentLink({
id,
detailsObj,
caseReference,
startDownload,
getStatus,
}) {
const [progress, setProgress] = useState(null);
let { t } = useTranslation();
const downloadFile = async () => {
setProgress(0);
const res = await fetch(detailsObj.pinswg_hashlink);
if (!res.ok) throw new Error("Download failed");
const contentDisposition = res.headers.get("content-disposition");
const filename = contentDisposition
? contentDisposition.split("filename=")[1]
: detailsObj.pinswg_name || "document";
const contentLength = res.headers.get("content-length");
const totalBytes = contentLength ? parseInt(contentLength, 10) : null;
const reader = res.body.getReader();
let receivedBytes = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedBytes += value.length;
if (totalBytes) {
const percent = Math.round((receivedBytes / totalBytes) * 100);
setProgress(percent);
}
}
const blob = new Blob(chunks);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
setProgress(100);
window.gtag?.("event", "DownloadedFile", { caseReference, filename });
};
const handleClick = () => {
if (getStatus(id) !== "idle") return; // avoid double enqueue
startDownload(id, downloadFile);
};
const status = getStatus(id);
return (
<div>
{detailsObj.pinswg_publishtoweb ? (
<button className="documentLink" onClick={handleClick}>
<span className="results-visually-hidden">
Document name:
</span>
{detailsObj.pinswg_name || ""}
</button>
) : (
<span className="results-visually-hidden">
{detailsObj.pinswg_name || ""}
</span>
)}
{status !== "idle" && (
<p
className={
status === "downloading"
? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
: "govuk-!-font-size-14 govuk-!-margin-left-0"
}
>
{status === "downloading" && progress !== null
? t("case:downloading-label")
: status === "queued"
? t("case:download-queued-label")
: status === "done"
? t("case:download-complete-label")
: status === "failed"
? t("case:download-failed-label")
: ""}
</p>
)}
</div>
);
}
+5 -1
View File
@@ -100,5 +100,9 @@
"do-you-want-to-delete-case-disclaimer-label": "Ar ôl dileu, bydd yr holl wybodaeth a gyflwynwyd gennych yn cael ei cholli.", "do-you-want-to-delete-case-disclaimer-label": "Ar ôl dileu, bydd yr holl wybodaeth a gyflwynwyd gennych yn cael ei cholli.",
"representation-date-passed-label": "Mae'r cyfnod cyflwyno sylwadau ar gyfer yr apêl hon rhwng {{startDate}} a {{endDate}}", "representation-date-passed-label": "Mae'r cyfnod cyflwyno sylwadau ar gyfer yr apêl hon rhwng {{startDate}} a {{endDate}}",
"representation-date-passed-additional-label": "Gallwch wneud sylw drwy e-bostio,", "representation-date-passed-additional-label": "Gallwch wneud sylw drwy e-bostio,",
"date-raised": "Dyddiad codi" "date-raised": "Dyddiad codi",
"download-complete-label": "Lawrlwytho wedi'i gwblhau!",
"downloading-label": "Wrthi'n llwytho i lawr",
"download-failed-label": "Methodd y lawrlwythiad",
"download-queued-label": "Wedi'i giwio"
} }
+5 -1
View File
@@ -100,5 +100,9 @@
"do-you-want-to-delete-case-disclaimer-label": "Once deleted all information you have submited, will be lost.", "do-you-want-to-delete-case-disclaimer-label": "Once deleted all information you have submited, will be lost.",
"representation-date-passed-label": "The representation period for this appeal is between {{startDate}} and {{endDate}}", "representation-date-passed-label": "The representation period for this appeal is between {{startDate}} and {{endDate}}",
"representation-date-passed-additional-label": "You can make a representation by emailing,", "representation-date-passed-additional-label": "You can make a representation by emailing,",
"date-raised": "Date raised" "date-raised": "Date raised",
"download-complete-label": "Download complete!",
"downloading-label": "Downloading",
"download-failed-label": "Download failed",
"download-queued-label": "Queued"
} }
+36 -35
View File
@@ -54,10 +54,10 @@ const StoragePage = (props) => {
{/* Tabs */} {/* Tabs */}
<ul className="govuk-tabs__list"> <ul className="govuk-tabs__list">
{[ {[
"documents",
"appeals",
"storage", "storage",
"accounts", "accounts",
"appeals",
"documents",
].map((tab) => ( ].map((tab) => (
<li <li
key={tab} key={tab}
@@ -76,20 +76,50 @@ const StoragePage = (props) => {
props.setCurrentPage(1); props.setCurrentPage(1);
}} }}
> >
{" "}
{tab === "documents" &&
"Latest Documents"}
{tab === "appeals" &&
"Latest Appeals"}
{tab === "storage" && {tab === "storage" &&
"Storage Account"} "Storage Account"}
{tab === "accounts" && {tab === "accounts" &&
"User Accounts"} "User Accounts"}
{tab === "appeals" &&
"Latest Appeals"}
{tab === "documents" &&
"Latest Documents"}
</a> </a>
</li> </li>
))} ))}
</ul> </ul>
{/* Panels */} {/* Panels */}
<div
className={
whichTab === "documents"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<DocumentsTab
documentDetailsObj={
props.searchResultsObj
.documentDetailsObj
}
docsOffline={docsOffline}
showFilteredDocs={props.showFilteredDocs}
/>
</div>
<div
className={
whichTab === "appeals"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<AppealsTab
searchResultsObj={searchResultsObj}
/>
</div>
<div <div
className={ className={
whichTab === "storage" whichTab === "storage"
@@ -112,35 +142,6 @@ const StoragePage = (props) => {
> >
<AccountsTab userData={userData} /> <AccountsTab userData={userData} />
</div> </div>
<div
className={
whichTab === "appeals"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<AppealsTab
searchResultsObj={searchResultsObj}
/>
</div>
<div
className={
whichTab === "documents"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<DocumentsTab
documentDetailsObj={
props.searchResultsObj
.documentDetailsObj
}
docsOffline={docsOffline}
showFilteredDocs={props.showFilteredDocs}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
+25 -1
View File
@@ -64,9 +64,11 @@ export default async function ApiProxy(req, res) {
var fieldSort = req.query.fieldSort || "desc"; var fieldSort = req.query.fieldSort || "desc";
var showNumberOfRecords = req.query.showNumberOfRecords || 10; var showNumberOfRecords = req.query.showNumberOfRecords || 10;
var documentType = req.query.documentType || "all"; var documentType = req.query.documentType || "all";
var documentOrigin = req.query.documentOrigin || "all";
var numberOfWeeks = req.query.numberWeeks || 1; var numberOfWeeks = req.query.numberWeeks || 1;
var docTypeQueryString = ""; var docTypeQueryString = "";
var docOriginQueryString = "";
if (documentType != "all") { if (documentType != "all") {
if (documentType.indexOf(",") >= 0) { if (documentType.indexOf(",") >= 0) {
@@ -91,6 +93,27 @@ export default async function ApiProxy(req, res) {
} }
} }
if (documentOrigin != "all") {
if (documentOrigin.indexOf(",") >= 0) {
const documentOriginArr = documentOrigin.split(",");
docOriginQueryString = " and (";
documentOriginArr.forEach(function (item, index) {
console.log(item, index);
if (item != "all") {
docOriginQueryString += " pinswg_origin eq " + item;
docOriginQueryString +=
index < documentOriginArr.length - 1 ? " or " : "";
}
});
docOriginQueryString += " )";
} else {
docOriginQueryString += " and pinswg_origin eq " + documentOrigin;
}
}
function daysAgoISO(days) { function daysAgoISO(days) {
const today = new Date(); const today = new Date();
const resultDate = new Date(today); const resultDate = new Date(today);
@@ -103,6 +126,7 @@ export default async function ApiProxy(req, res) {
"pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " + "pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " +
daysAgoISO(numberOfWeeks) + daysAgoISO(numberOfWeeks) +
docTypeQueryString + docTypeQueryString +
docOriginQueryString +
"&$orderby=" + "&$orderby=" +
orderby + orderby +
" " + " " +
@@ -115,7 +139,7 @@ export default async function ApiProxy(req, res) {
console.log( console.log(
"\n==========================================\n", "\n==========================================\n",
"\nnew docs search ", "\nnew docs search---- ",
"\n\nQuery url: " + queryUrl, "\n\nQuery url: " + queryUrl,
"\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), "\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
"\n==========================================\n" "\n==========================================\n"
+194 -101
View File
@@ -1,110 +1,203 @@
/** // /**
* @swagger // * @swagger
* /api/documents/download/{id}: // * /api/documents/download/{id}:
* get: // * get:
* tags: // * tags:
* - Documents // * - Documents
* description: Get document // * description: Get document
* parameters: // * parameters:
* - name: id // * - name: id
* in: path // * in: path
* description: iShare ID // * description: iShare ID
* type: string // * type: string
* required: true // * required: true
* default: A41567436 // * default: A41567436
* - name: hash // * - name: hash
* in: query // * in: query
* description: Hash string // * description: Hash string
* default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71 // * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71
* responses: // * responses:
* 200: // * 200:
* description: Success // * description: Success
*/ // */
// import axios from "axios";
// import CryptoJS from "crypto-js";
// import { getToken, consoleLogger } from "../../../../actions";
// const WORDKEY = process.env.HASHKEY;
// const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT;
// const tenantId = process.env.TENANT;
// const WEBAPI_URL =
// process.env.RELAY_ROOT ||
// "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
// const hashAPIPath = (queryPath) => {
// var hashlink = CryptoJS.HmacSHA256(
// queryPath,
// CryptoJS.enc.Hex.parse(WORDKEY)
// );
// hashlink = hashlink.toString(CryptoJS.enc.Hex);
// //return "&hash=" + hashlink;
// return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink;
// };
// const azureHeadersPaged = (access_token) => {
// return {
// headers: {
// "OData-MaxVersion": "4.0",
// "OData-Version": "4.0",
// "Accept": "application/json;odata.metadata=none",
// "Prefer":
// 'odata.include-annotations="*",return=representation, odata.maxpagesize=10',
// "Content-Type": "application/json",
// "Authorization": "Bearer " + access_token,
// },
// };
// };
// export default async function ApiProxy(req, res) {
// var token = await getToken();
// var docRef = req.query;
// var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash;
// const configDocument = (access_token) => {
// return {
// headers: {
// "OData-MaxVersion": "4.0",
// "OData-Version": "4.0",
// "Accept": "application/json",
// "Prefer": 'odata.include-annotations="*",return=representation',
// "Content-Type": "application/json",
// "Authorization": "Bearer " + access_token,
// },
// responseType: "arraybuffer",
// };
// };
// return axios
// .get(
// WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl),
// configDocument(token.access_token)
// )
// .then((response) => {
// const bytes = response.data.byteLength;
// console.log(bytes);
// res.setHeader(
// "content-disposition",
// "attachment; filename=" +
// response.headers["content-disposition"].split(
// "filename="
// )[1]
// );
// return res.status(200).send(response.data);
// })
// .then(() => {
// console.log(docRef.id + "has downloaded");
// return "downloadComplete";
// })
// .catch((error) => {
// consoleLogger(error);
// res.redirect("/filenotavailable");
// });
// }
import axios from "axios"; import axios from "axios";
import CryptoJS from "crypto-js";
import { getToken, consoleLogger } from "../../../../actions"; import { getToken, consoleLogger } from "../../../../actions";
const WORDKEY = process.env.HASHKEY;
const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT;
const tenantId = process.env.TENANT;
const WEBAPI_URL = const WEBAPI_URL =
process.env.RELAY_ROOT || process.env.RELAY_ROOT ||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
const hashAPIPath = (queryPath) => { // Retry utility with logging
var hashlink = CryptoJS.HmacSHA256( async function retry(fn, retries = 3, delay = 1000) {
queryPath, for (let attempt = 1; attempt <= retries; attempt++) {
CryptoJS.enc.Hex.parse(WORDKEY) try {
); return { response: await fn(), attempts: attempt };
hashlink = hashlink.toString(CryptoJS.enc.Hex); } catch (err) {
if (attempt === retries) throw err;
//return "&hash=" + hashlink; console.log(`Retry ${attempt} failed, retrying in ${delay}ms...`);
await new Promise((res) => setTimeout(res, delay));
return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; delay *= 2; // exponential backoff
}; }
}
const azureHeadersPaged = (access_token) => {
return {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json;odata.metadata=none",
"Prefer":
'odata.include-annotations="*",return=representation, odata.maxpagesize=10',
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token,
},
};
};
export default async function ApiProxy(req, res) {
var token = await getToken();
var docRef = req.query;
var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash;
const configDocument = (access_token) => {
return {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Prefer": 'odata.include-annotations="*",return=representation',
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token,
},
responseType: "arraybuffer",
};
};
return axios
.get(
WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl),
configDocument(token.access_token)
)
.then((response) => {
const bytes = response.data.byteLength;
console.log(bytes);
res.setHeader(
"content-disposition",
"attachment; filename=" +
response.headers["content-disposition"].split(
"filename="
)[1]
);
return res.status(200).send(response.data);
})
.then(() => {
console.log(docRef.id + "has downloaded");
return "downloadComplete";
})
.catch((error) => {
consoleLogger(error);
res.redirect("/filenotavailable");
});
} }
const ApiProxy = async (req, res) => {
const docRef = req.query;
try {
const token = await getToken();
const startTime = Date.now();
const fetchStream = async () => {
const queryUrl =
"documents/download/" + docRef.id + "?hash=" + docRef.hash;
return axios.get(WEBAPI_URL + queryUrl, {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Prefer":
'odata.include-annotations="*",return=representation',
"Content-Type": "application/json",
"Authorization": "Bearer " + token.access_token,
},
responseType: "stream",
});
};
const { response, attempts } = await retry(fetchStream, 3, 1000);
// Extract filename
const contentDisposition = response.headers["content-disposition"];
const filename = contentDisposition
? contentDisposition.split("filename=")[1]
: docRef.id;
res.setHeader(
"Content-Disposition",
`attachment; filename=${filename}`
);
res.setHeader("Content-Type", "application/octet-stream");
let totalBytes = 0;
response.data.on("data", (chunk) => {
totalBytes += chunk.length;
});
response.data.pipe(res);
response.data.on("end", () => {
const durationMs = Date.now() - startTime;
console.log(
`[Download Complete] Document: ${filename}, ID: ${docRef.id}, Size: ${totalBytes} bytes, Duration: ${durationMs}ms, Attempts: ${attempts}`
);
});
response.data.on("error", (err) => {
consoleLogger(err);
if (!res.headersSent) res.redirect("/filenotavailable");
});
} catch (error) {
consoleLogger(error);
if (!res.headersSent) res.redirect("/filenotavailable");
}
};
export const config = {
api: {
responseLimit: false,
},
};
export default ApiProxy;
+20
View File
@@ -1469,6 +1469,22 @@ textarea,
top: calc(50% - 10px); top: calc(50% - 10px);
} }
} }
.documentLink {
border: none;
text-align: left;
cursor: pointer;
color: #0360a6;
display: block;
padding-left: 0px;
&:hover {
background-color: $white;
color: #3b7dc5;
border: none;
padding-left: 0px
}
}
} }
.appealModule { .appealModule {
@@ -2932,6 +2948,10 @@ fade {
.govuk-label-s { .govuk-label-s {
font-size: 14px; font-size: 14px;
} }
&.govuk-checkboxes_short {
height: 180px;
}
} }