From 13ec04c7c882e25dfef395530bf5604501de3870 Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Mon, 25 Nov 2024 13:27:51 -0500 Subject: [PATCH 01/16] Fethcing logs, needs to show modal --- .github/workflows/static_code_checks.yaml | 6 -- florist/app/assets/css/florist.css | 2 +- florist/app/jobs/details/page.tsx | 88 ++++++++++++++++++++--- florist/app/jobs/hooks.tsx | 8 +++ 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/.github/workflows/static_code_checks.yaml b/.github/workflows/static_code_checks.yaml index 9787294..dd7e922 100644 --- a/.github/workflows/static_code_checks.yaml +++ b/.github/workflows/static_code_checks.yaml @@ -62,12 +62,6 @@ jobs: uses: pypa/gh-action-pip-audit@v1.1.0 with: virtual-environment: .venv/ - # Skipping 3 cryptography issues that can't be patched because of FL4Health # Skipping 1 cryptography issue that can't be patched because of Flower - # Skipping 1 torch issue that can't be ugraded because of torchvision ignore-vulns: | - GHSA-3ww4-gg4f-jr7f - GHSA-9v9h-cgj8-h64p - GHSA-6vqw-3v5j-54x4 GHSA-h4gh-qq45-vh27 - GHSA-pg7h-5qx3-wjr3 diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css index 5a5e58d..78c110a 100644 --- a/florist/app/assets/css/florist.css +++ b/florist/app/assets/css/florist.css @@ -76,7 +76,7 @@ background-color: lightgray !important; } -.job-expand-button a.btn { +.job-details-button a.btn { padding: 0; margin: 0; } diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index a8eca5f..b6b7672 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; import { useState } from "react"; import { ReactElement } from "react/React"; -import { useGetJob } from "../hooks"; +import { useGetJob, useGetServerLogs, useGetClientLogs } from "../hooks"; import { validStatuses, ClientInfo } from "../definitions"; import loading_gif from "../../assets/img/loading.gif"; @@ -113,7 +113,13 @@ export function JobDetailsBody(): ReactElement { - + ); @@ -177,10 +183,14 @@ export function JobProgressBar({ metrics, totalEpochs, status, + jobId, + clientIndex, }: { metrics: string; totalEpochs: number; status: status; + jobId: string, + clientIndex: number, }): ReactElement { const [collapsed, setCollapsed] = useState(true); @@ -257,7 +267,7 @@ export function JobProgressBar({ {Math.floor(progressPercent)}% -
+
-
{!collapsed ? : null}
+
+ {!collapsed ? + + : null} +
); } -export function JobProgressDetails({ metrics }: { metrics: Object }): ReactElement { +export function JobProgressDetails({ + metrics, + jobId, + clientIndex, +}: { + metrics: Object, + jobId: string, + clientIndex: number, +}): ReactElement { + if (!metrics) { return null; } + const [showLogs, setShowLogs] = useState(false); + let fitStartKey; let fitEndKey; if (metrics.host_type === "server") { @@ -339,6 +364,24 @@ export function JobProgressDetails({ metrics }: { metrics: Object }): ReactEleme {roundMetricsArray.map((roundMetrics, i) => ( ))} + +
+
+ Logs: +
+ +
+ + {showLogs ? + + : null} ); } @@ -356,7 +399,7 @@ export function JobProgressRound({ roundMetrics, index }: { roundMetrics: Object
Round {index + 1}
-
+ @@ -635,6 +680,31 @@ export function JobDetailsClientsInfoTable({ ); } +export function JobLogsModal({ + hostType, + jobId, + clientIndex, + setShowLogs, +}: { + type: string, + jobId: string, + clientIndex: number, + setShowLogs: Callable, +}): ReactElement { + + let data, error, isLoading; + if (hostType === "server") { + ({ data, error, isLoading } = useGetServerLogs(jobId)); + } + if (hostType === "client") { + ({ data, error, isLoading } = useGetClientLogs(jobId, clientIndex)); + } + + return ( +
modal
+ ) +} + export function getTimeString(timeInMiliseconds: number): string { const hours = Math.floor(timeInMiliseconds / 1000 / 60 / 60); const minutes = Math.floor((timeInMiliseconds / 1000 / 60 / 60 - hours) * 60); diff --git a/florist/app/jobs/hooks.tsx b/florist/app/jobs/hooks.tsx index 15bd5de..731ce5a 100644 --- a/florist/app/jobs/hooks.tsx +++ b/florist/app/jobs/hooks.tsx @@ -23,6 +23,14 @@ export function useGetClients() { return useSWR("/api/server/clients", fetcher); } +export function useGetServerLogs(jobId: string) { + return useSWR(`/api/server/job/get_server_log/${jobId}`, fetcher); +} + +export function useGetClientLogs(jobId: string, clientIndex: number) { + return useSWR(`/api/server/job/get_client_log/${jobId}/${clientIndex}`, fetcher); +} + export const usePost = () => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(null); From 515bdb6b9154a9cb68146f226631589423bad04b Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Mon, 25 Nov 2024 15:37:11 -0500 Subject: [PATCH 02/16] Modal fully working --- florist/app/assets/css/florist.css | 23 +++++++++++++++++++++++ florist/app/jobs/details/page.tsx | 23 ++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css index 78c110a..152ae28 100644 --- a/florist/app/assets/css/florist.css +++ b/florist/app/assets/css/florist.css @@ -148,3 +148,26 @@ .job-client-progress .job-progress-bar .progress .progress-bar { height: max-content; } + +.log-viewer.show { + display: block; + z-index: 10000; + background-color: rgba(0, 0, 0, 0.5); +} + +.log-viewer .modal-dialog { + max-width: 90%; +} + +.log-viewer .btn-close { + color: black; + font-size: 30px; + margin-top: -25px; +} + +.log-viewer .modal-body { + white-space: pre; + font-family: monospace; + font-size: 14px; +} + diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index b6b7672..725d50b 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -684,6 +684,7 @@ export function JobLogsModal({ hostType, jobId, clientIndex, + showLogs, setShowLogs, }: { type: string, @@ -701,7 +702,27 @@ export function JobLogsModal({ } return ( -
modal
+
+
+
+
+

Log Viewer

+ +
+ +
+ {isLoading? + Loading Logs + : error ? + "Error loading logs" + : data + } +
+
+
+
) } From fca4df1e3a99ee236206721bcbc1f4452394235c Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Mon, 25 Nov 2024 15:54:23 -0500 Subject: [PATCH 03/16] Download button working, needs tests --- florist/app/assets/css/florist.css | 5 +++++ florist/app/jobs/details/page.tsx | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css index 152ae28..2892477 100644 --- a/florist/app/assets/css/florist.css +++ b/florist/app/assets/css/florist.css @@ -165,6 +165,11 @@ margin-top: -25px; } +.log-viewer a.download-button { + margin: 0 0 0 20px; + padding: 0; +} + .log-viewer .modal-body { white-space: pre; font-family: monospace; diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index 725d50b..fd406ac 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -692,21 +692,32 @@ export function JobLogsModal({ clientIndex: number, setShowLogs: Callable, }): ReactElement { - - let data, error, isLoading; + let data, error, isLoading, fileName; if (hostType === "server") { ({ data, error, isLoading } = useGetServerLogs(jobId)); + fileName = "server.log"; } if (hostType === "client") { ({ data, error, isLoading } = useGetClientLogs(jobId, clientIndex)); + fileName = `client-${clientIndex}.log`; + } + + let dataURL = null; + if (data) { + dataURL = window.URL.createObjectURL( + new Blob([data]), + ); } return ( -
+

Log Viewer

+
+ Download + From 030511c217c9e859f18830d430670e981237f00e Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Tue, 26 Nov 2024 12:43:49 -0500 Subject: [PATCH 04/16] Adding refresh, minor fixes, still needs tests --- florist/app/assets/css/florist.css | 19 +++++- florist/app/assets/js/material-dashboard.js | 11 ++-- florist/app/jobs/details/page.tsx | 73 +++++++++++---------- florist/app/jobs/hooks.tsx | 12 ++-- 4 files changed, 72 insertions(+), 43 deletions(-) diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css index 2892477..80d7d4d 100644 --- a/florist/app/assets/css/florist.css +++ b/florist/app/assets/css/florist.css @@ -159,14 +159,24 @@ max-width: 90%; } +.log-viewer .modal-content { + height: 100%; +} + .log-viewer .btn-close { color: black; font-size: 30px; margin-top: -25px; } +.log-viewer a.refresh-button { + cursor: pointer; + margin-left: 15px; + margin-top: 10px; +} + .log-viewer a.download-button { - margin: 0 0 0 20px; + margin: 3px 0 0 15px; padding: 0; } @@ -176,3 +186,10 @@ font-size: 14px; } +.log-viewer .loading-container { + height: 100%; + width: 100%; + display: inline-grid; + align-content: center; + justify-content: center; +} diff --git a/florist/app/assets/js/material-dashboard.js b/florist/app/assets/js/material-dashboard.js index f201147..2d75bfe 100644 --- a/florist/app/assets/js/material-dashboard.js +++ b/florist/app/assets/js/material-dashboard.js @@ -617,11 +617,12 @@ const observer = new MutationObserver((mutationList) => { if (mutationList[i].type === "childList") { for (var j = 0; j < mutationList[i].addedNodes.length; j++) { const addedNode = mutationList[i].addedNodes[j]; - const inputs = addedNode.querySelectorAll("select"); - const selects = addedNode.querySelectorAll("input"); - const btns = addedNode.querySelectorAll(".btn"); - - hasTargetElementTypes = inputs.length > 0 || selects.length > 0 || btns.length > 0; + if (addedNode.querySelectorAll) { + const inputs = addedNode.querySelectorAll("select"); + const selects = addedNode.querySelectorAll("input"); + const btns = addedNode.querySelectorAll(".btn"); + hasTargetElementTypes = inputs.length > 0 || selects.length > 0 || btns.length > 0; + } } } } diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index fd406ac..eb2b9ab 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; import { useState } from "react"; import { ReactElement } from "react/React"; -import { useGetJob, useGetServerLogs, useGetClientLogs } from "../hooks"; +import { useGetJob, getServerLogsKey, getClientLogsKey, useSWRWithKey } from "../hooks"; import { validStatuses, ClientInfo } from "../definitions"; import loading_gif from "../../assets/img/loading.gif"; @@ -189,8 +189,8 @@ export function JobProgressBar({ metrics: string; totalEpochs: number; status: status; - jobId: string, - clientIndex: number, + jobId: string; + clientIndex: number; }): ReactElement { const [collapsed, setCollapsed] = useState(true); @@ -284,9 +284,9 @@ export function JobProgressBar({
- {!collapsed ? - - : null} + {!collapsed ? ( + + ) : null}
@@ -299,17 +299,16 @@ export function JobProgressDetails({ jobId, clientIndex, }: { - metrics: Object, - jobId: string, - clientIndex: number, + metrics: Object; + jobId: string; + clientIndex: number; }): ReactElement { + const [showLogs, setShowLogs] = useState(false); if (!metrics) { return null; } - const [showLogs, setShowLogs] = useState(false); - let fitStartKey; let fitEndKey; if (metrics.host_type === "server") { @@ -370,18 +369,20 @@ export function JobProgressDetails({ Logs:
- {showLogs ? + {showLogs ? ( - : null} + ) : null} ); } @@ -601,8 +602,8 @@ export function JobDetailsClientsInfoTable({ data, properties, }: { - data: Array, - properties: Object, + data: Array; + properties: Object; }): ReactElement { const [collapsed, setCollapsed] = useState(true); @@ -687,26 +688,26 @@ export function JobLogsModal({ showLogs, setShowLogs, }: { - type: string, - jobId: string, - clientIndex: number, - setShowLogs: Callable, + type: string; + jobId: string; + clientIndex: number; + setShowLogs: Callable; }): ReactElement { - let data, error, isLoading, fileName; + let apiKey, fileName; if (hostType === "server") { - ({ data, error, isLoading } = useGetServerLogs(jobId)); + apiKey = getServerLogsKey(jobId); fileName = "server.log"; } if (hostType === "client") { - ({ data, error, isLoading } = useGetClientLogs(jobId, clientIndex)); + apiKey = getClientLogsKey(jobId, clientIndex); fileName = `client-${clientIndex}.log`; } + const { data, error, isLoading, isValidating, mutate } = useSWRWithKey(apiKey); + let dataURL = null; if (data) { - dataURL = window.URL.createObjectURL( - new Blob([data]), - ); + dataURL = window.URL.createObjectURL(new Blob([data])); } return ( @@ -715,6 +716,9 @@ export function JobLogsModal({

Log Viewer

+ mutate(apiKey)}> + refresh + Download @@ -724,17 +728,20 @@ export function JobLogsModal({
- {isLoading? - Loading Logs - : error ? - "Error loading logs" - : data - } + {isLoading || isValidating ? ( +
+ Loading Logs +
+ ) : error ? ( + "Error loading logs" + ) : ( + data + )}
- ) + ); } export function getTimeString(timeInMiliseconds: number): string { diff --git a/florist/app/jobs/hooks.tsx b/florist/app/jobs/hooks.tsx index 731ce5a..a525f00 100644 --- a/florist/app/jobs/hooks.tsx +++ b/florist/app/jobs/hooks.tsx @@ -23,12 +23,16 @@ export function useGetClients() { return useSWR("/api/server/clients", fetcher); } -export function useGetServerLogs(jobId: string) { - return useSWR(`/api/server/job/get_server_log/${jobId}`, fetcher); +export function getServerLogsKey(jobId: string) { + return `/api/server/job/get_server_log/${jobId}`; } -export function useGetClientLogs(jobId: string, clientIndex: number) { - return useSWR(`/api/server/job/get_client_log/${jobId}/${clientIndex}`, fetcher); +export function getClientLogsKey(jobId: string, clientIndex: number) { + return `/api/server/job/get_client_log/${jobId}/${clientIndex}`; +} + +export function useSWRWithKey(key: string) { + return useSWR(key, fetcher); } export const usePost = () => { From 364be93d97b640e01d696d1df2d78722aa1b3b8c Mon Sep 17 00:00:00 2001 From: Marcelo Lotif Date: Tue, 26 Nov 2024 17:12:37 -0500 Subject: [PATCH 05/16] Needs a wee bit more tests --- florist/app/assets/css/florist.css | 7 +- florist/app/jobs/details/page.tsx | 6 +- .../tests/unit/app/jobs/details/page.test.tsx | 144 +++++++++++++++++- 3 files changed, 147 insertions(+), 10 deletions(-) diff --git a/florist/app/assets/css/florist.css b/florist/app/assets/css/florist.css index 80d7d4d..ed3cb84 100644 --- a/florist/app/assets/css/florist.css +++ b/florist/app/assets/css/florist.css @@ -169,17 +169,12 @@ margin-top: -25px; } -.log-viewer a.refresh-button { +.log-viewer a.refresh-button, .log-viewer a.download-button { cursor: pointer; margin-left: 15px; margin-top: 10px; } -.log-viewer a.download-button { - margin: 3px 0 0 15px; - padding: 0; -} - .log-viewer .modal-body { white-space: pre; font-family: monospace; diff --git a/florist/app/jobs/details/page.tsx b/florist/app/jobs/details/page.tsx index eb2b9ab..415e190 100644 --- a/florist/app/jobs/details/page.tsx +++ b/florist/app/jobs/details/page.tsx @@ -369,7 +369,7 @@ export function JobProgressDetails({ Logs: @@ -719,8 +719,8 @@ export function JobLogsModal({ mutate(apiKey)}> refresh - - Download + + download