diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css
index 5a5e58d..ba23331 100644
--- a/florist/app/assets/css/florist.css
+++ b/florist/app/assets/css/florist.css
@@ -81,6 +81,11 @@
margin: 0;
}
+.job-details-download-button a.btn {
+ padding: 0;
+ margin: 15px 0 0 0;
+}
+
.job-round-details {
padding-left: 40px;
}
diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx
index a8eca5f..6ab9761 100644
--- a/florist/app/jobs/details/page.tsx
+++ b/florist/app/jobs/details/page.tsx
@@ -177,10 +177,12 @@ export function JobProgressBar({
metrics,
totalEpochs,
status,
+ clientIndex,
}: {
- metrics: string;
- totalEpochs: number;
- status: status;
+ metrics: string,
+ totalEpochs: number,
+ status: status,
+ clientIndex: number,
}): ReactElement {
const [collapsed, setCollapsed] = useState(true);
@@ -273,14 +275,18 @@ export function JobProgressBar({
-
{!collapsed ? : null}
+
+ {!collapsed ? (
+
+ ) : null}
+
);
}
-export function JobProgressDetails({ metrics }: { metrics: Object }): ReactElement {
+export function JobProgressDetails({ metrics, clientIndex }: { metrics: Object, clientIndex: number }): ReactElement {
if (!metrics) {
return null;
}
@@ -311,6 +317,9 @@ export function JobProgressDetails({ metrics }: { metrics: Object }): ReactEleme
}
}
+ let metricsFileName = metrics.host_type === "server" ? "server-metrics.json" : `client-metrics-${clientIndex}.json`;
+ let metricsFileURL = window.URL.createObjectURL(new Blob([JSON.stringify(metrics, null, 4)]));
+
return (
@@ -339,6 +348,21 @@ export function JobProgressDetails({ metrics }: { metrics: Object }): ReactEleme
{roundMetricsArray.map((roundMetrics, i) => (
))}
+
+
+
);
}
@@ -623,6 +647,7 @@ export function JobDetailsClientsInfoTable({
diff --git a/florist/tests/unit/app/jobs/details/page.test.tsx b/florist/tests/unit/app/jobs/details/page.test.tsx
index 5bb9a39..548f96e 100644
--- a/florist/tests/unit/app/jobs/details/page.test.tsx
+++ b/florist/tests/unit/app/jobs/details/page.test.tsx
@@ -27,6 +27,12 @@ function setupGetJobMock(data: JobData, isLoading: boolean = false, error = null
});
}
+function setupURLSpyMock(urlSpy, testURL: string = "foo") {
+ urlSpy = jest.spyOn(window, "URL");
+ urlSpy.createObjectURL = jest.fn((_) => testURL);
+ return urlSpy;
+}
+
function makeTestJob(): JobData {
return {
_id: testJobId,
@@ -292,6 +298,14 @@ describe("Job Details Page", () => {
expect(progressBar).toHaveClass("bg-danger");
});
describe("Details", () => {
+ let urlSpy;
+ afterEach(() => {
+ if (urlSpy) {
+ // making sure the mock is clear even on error,
+ // otherwise some weird errors start popping up
+ urlSpy.mockRestore();
+ }
+ });
it("Should be collapsed by default", () => {
setupGetJobMock(makeTestJob());
const { container } = render();
@@ -300,6 +314,7 @@ describe("Job Details Page", () => {
});
it("Should open when the toggle button is clicked", () => {
setupGetJobMock(makeTestJob());
+ setupURLSpyMock(urlSpy);
const { container } = render();
const toggleButton = container.querySelector(".job-details-toggle a");
expect(toggleButton).toHaveTextContent("Expand");
@@ -315,6 +330,7 @@ describe("Job Details Page", () => {
const testJob = makeTestJob();
const serverMetrics = JSON.parse(testJob.server_metrics);
setupGetJobMock(testJob);
+ setupURLSpyMock(urlSpy);
const { container } = render();
const toggleButton = container.querySelector(".job-details-toggle a");
act(() => toggleButton.click());
@@ -365,6 +381,7 @@ describe("Job Details Page", () => {
const testJob = makeTestJob();
const serverMetrics = JSON.parse(testJob.server_metrics);
setupGetJobMock(testJob);
+ setupURLSpyMock(urlSpy);
const { container } = render();
const progressToggleButton = container.querySelector(".job-details-toggle a");
act(() => progressToggleButton.click());
@@ -378,6 +395,7 @@ describe("Job Details Page", () => {
const testJob = makeTestJob();
const serverMetrics = JSON.parse(testJob.server_metrics);
setupGetJobMock(testJob);
+ setupURLSpyMock(urlSpy);
const { container } = render();
const progressToggleButton = container.querySelector(".job-details-toggle a");
act(() => progressToggleButton.click());
@@ -398,6 +416,7 @@ describe("Job Details Page", () => {
const testJob = makeTestJob();
const serverMetrics = JSON.parse(testJob.server_metrics);
setupGetJobMock(testJob);
+ setupURLSpyMock(urlSpy);
const { container } = render();
const progressToggleButton = container.querySelector(".job-details-toggle a");
act(() => progressToggleButton.click());
@@ -461,6 +480,52 @@ describe("Job Details Page", () => {
);
});
});
+ describe("Download metrics", () => {
+ it("Should render the download server metrics button correctly", async () => {
+ const testJob = makeTestJob();
+ setupGetJobMock(testJob);
+ const { container } = render();
+
+ let blobSpy = jest.spyOn(Blob.prototype, "constructor");
+ const testURL = "test url";
+ urlSpy = setupURLSpyMock(urlSpy, testURL);
+
+ const progressToggleButton = container.querySelector(".job-details-toggle a");
+ act(() => progressToggleButton.click());
+
+ const expectedServerMetrics = JSON.stringify(JSON.parse(testJob.server_metrics), null, 4);
+ expect(urlSpy.createObjectURL).toHaveBeenCalledWith(new Blob([expectedServerMetrics]));
+
+ const jobProgressDetailsComponent = container.querySelector(".job-progress-detail");
+ const downloadMetricsButton = jobProgressDetailsComponent.querySelector(".download-metrics-button");
+ expect(downloadMetricsButton.getAttribute("href")).toBe(testURL);
+ expect(downloadMetricsButton.getAttribute("download")).toBe("server-metrics.json");
+ });
+ it("Should render the download client metrics button correctly", () => {
+ const testJob = makeTestJob();
+ setupGetJobMock(testJob);
+ const { container } = render();
+
+ const testURL = "test url";
+ urlSpy = setupURLSpyMock(urlSpy, testURL);
+
+ const testClientIndex = 1;
+ let toggleButton = container.querySelectorAll(".job-client-progress .job-details-toggle a")[
+ testClientIndex
+ ];
+ act(() => toggleButton.click());
+
+ const expectedClientMetrics = JSON.stringify(JSON.parse(testJob.clients_info[testClientIndex].metrics), null, 4);
+ expect(urlSpy.createObjectURL).toHaveBeenCalledWith(new Blob([expectedClientMetrics]));
+
+ const clientProgressDetailsComponent = container.querySelector(
+ `#job-details-client-config-progress-${testClientIndex} .job-progress-detail`,
+ );
+ const downloadMetricsButton = clientProgressDetailsComponent.querySelector(".download-metrics-button");
+ expect(downloadMetricsButton.getAttribute("href")).toBe(testURL);
+ expect(downloadMetricsButton.getAttribute("download")).toBe(`client-metrics-${testClientIndex}.json`);
+ });
+ });
describe("Clients", () => {
it("Renders their progress bars correctly", () => {
const testJob = makeTestJob();
@@ -479,6 +544,7 @@ describe("Job Details Page", () => {
it("Renders the progress details correctly", () => {
const testJob = makeTestJob();
setupGetJobMock(testJob);
+ setupURLSpyMock(urlSpy);
const { container } = render();
let toggleButton = container.querySelectorAll(".job-client-progress .job-details-toggle a")[0];