Merge branch 'main' from workbench2.git
[arvados.git] / services / workbench2 / src / store / process-logs-panel / process-logs-panel-actions.ts
index 87a2fa12aaddc1d6d980908583dfda366c0f1ab4..4e52431eebadc9a05b240ee530cd29e3390569e4 100644 (file)
@@ -13,7 +13,7 @@ import { Process, getProcess } from 'store/processes/process';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { CollectionFile, CollectionFileType } from "models/collection-file";
-import { ContainerRequestResource } from "models/container-request";
+import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
 
 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
 const LOG_TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9}Z/;
@@ -40,15 +40,16 @@ export const setProcessLogsPanelFilter = (filter: string) =>
 
 export const initProcessLogsPanel = (processUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
+        let process: Process | undefined;
         try {
             dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
-            const process = getProcess(processUuid)(getState().resources);
+            process = getProcess(processUuid)(getState().resources);
             if (process?.containerRequest?.uuid) {
                 // Get log file size info
                 const logFiles = await loadContainerLogFileList(process.containerRequest, logService);
 
                 // Populate lastbyte 0 for each file
-                const filesWithProgress = logFiles.map((file) => ({file, lastByte: 0}));
+                const filesWithProgress = logFiles.map((file) => ({ file, lastByte: 0 }));
 
                 // Fetch array of LogFragments
                 const logLines = await loadContainerLogFileContents(filesWithProgress, logService, process);
@@ -57,13 +58,16 @@ export const initProcessLogsPanel = (processUuid: string) =>
                 const initialState = createInitialLogPanelState(logFiles, logLines);
                 dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState));
             }
-        } catch(e) {
+        } catch (e) {
             // On error, populate empty state to allow polling to start
             const initialState = createInitialLogPanelState([], []);
             dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState));
             // Only show toast on errors other than 404 since 404 is expected when logs do not exist yet
             if (e.status !== 404) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not load process logs', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Error loading process logs', hideDuration: 4000, kind: SnackbarKind.ERROR }));
+            }
+            if (e.status === 404 && process?.containerRequest.state === ContainerRequestState.FINAL) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Log collection was trashed or deleted.', hideDuration: 4000, kind: SnackbarKind.WARNING }));
             }
         }
     };
@@ -88,7 +92,7 @@ export const pollProcessLogs = (processUuid: string) =>
                     const isChanged = !isNew && currentStateLogLastByte < updatedFile.size;
 
                     if (isNew || isChanged) {
-                        return acc.concat({file: updatedFile, lastByte: currentStateLogLastByte});
+                        return acc.concat({ file: updatedFile, lastByte: currentStateLogLastByte });
                     } else {
                         return acc;
                     }
@@ -132,17 +136,17 @@ const loadContainerLogFileList = async (containerRequest: ContainerRequestResour
  * @returns LogFragment[] containing a single LogFragment corresponding to each input file
  */
 const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgress[], logService: LogService, process: Process) => (
-    (await Promise.allSettled(logFilesWithProgress.filter(({file}) => file.size > 0).map(({file, lastByte}) => {
+    (await Promise.allSettled(logFilesWithProgress.filter(({ file }) => file.size > 0).map(({ file, lastByte }) => {
         const requestSize = file.size - lastByte;
         if (requestSize > maxLogFetchSize) {
             const chunkSize = Math.floor(maxLogFetchSize / 2);
-            const firstChunkEnd = lastByte+chunkSize-1;
+            const firstChunkEnd = lastByte + chunkSize - 1;
             return Promise.all([
                 logService.getLogFileContents(process.containerRequest, file, lastByte, firstChunkEnd),
-                logService.getLogFileContents(process.containerRequest, file, file.size-chunkSize, file.size-1)
+                logService.getLogFileContents(process.containerRequest, file, file.size - chunkSize, file.size - 1)
             ] as Promise<(LogFragment)>[]);
         } else {
-            return Promise.all([logService.getLogFileContents(process.containerRequest, file, lastByte, file.size-1)]);
+            return Promise.all([logService.getLogFileContents(process.containerRequest, file, lastByte, file.size - 1)]);
         }
     })).then((res) => {
         if (res.length && res.every(promiseResult => (promiseResult.status === 'rejected'))) {
@@ -150,7 +154,7 @@ const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgre
             //   error if every request failed
             const error = res.find(
                 (promiseResult): promiseResult is PromiseRejectedResult => promiseResult.status === 'rejected'
-              )?.reason;
+            )?.reason;
             return Promise.reject(error);
         }
         return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<LogFragment[]> => (
@@ -161,16 +165,16 @@ const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgre
             //   (prevent incorrect snipline generation or an un-resumable situation)
             !!promiseResult.value.every(logFragment => logFragment.contents.length)
         )).map(one => one.value)
-    })).map((logResponseSet)=> {
+    })).map((logResponseSet) => {
         // For any multi fragment response set, modify the last line of non-final chunks to include a line break and snip line
         //   Don't add snip line as a separate line so that sorting won't reorder it
         for (let i = 1; i < logResponseSet.length; i++) {
-            const fragment = logResponseSet[i-1];
-            const lastLineIndex = fragment.contents.length-1;
+            const fragment = logResponseSet[i - 1];
+            const lastLineIndex = fragment.contents.length - 1;
             const lastLineContents = fragment.contents[lastLineIndex];
             const newLastLine = `${lastLineContents}\n${SNIPLINE}`;
 
-            logResponseSet[i-1].contents[lastLineIndex] = newLastLine;
+            logResponseSet[i - 1].contents[lastLineIndex] = newLastLine;
         }
 
         // Merge LogFragment Array (representing multiple log line arrays) into single LogLine[] / LogFragment
@@ -181,7 +185,7 @@ const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgre
     })
 );
 
-const createInitialLogPanelState = (logFiles: CollectionFile[], logFragments: LogFragment[]): {filters: string[], logs: ProcessLogs} => {
+const createInitialLogPanelState = (logFiles: CollectionFile[], logFragments: LogFragment[]): { filters: string[], logs: ProcessLogs } => {
     const logs = groupLogs(logFiles, logFragments);
     const filters = Object.keys(logs);
     return { filters, logs };
@@ -201,12 +205,12 @@ const groupLogs = (logFiles: CollectionFile[], logFragments: LogFragment[]): Pro
 
     const groupedLogs = logFragments.reduce((grouped, fragment) => ({
         ...grouped,
-        [fragment.logType as string]: {lastByte: fetchLastByteNumber(logFiles, fragment.logType), contents: fragment.contents}
+        [fragment.logType as string]: { lastByte: fetchLastByteNumber(logFiles, fragment.logType), contents: fragment.contents }
     }), {});
 
     return {
-        [MAIN_FILTER_TYPE]: {lastByte: undefined, contents: mainLogs},
-        [ALL_FILTER_TYPE]: {lastByte: undefined, contents: allLogs},
+        [MAIN_FILTER_TYPE]: { lastByte: undefined, contents: mainLogs },
+        [ALL_FILTER_TYPE]: { lastByte: undefined, contents: allLogs },
         ...groupedLogs,
     }
 };
@@ -233,9 +237,9 @@ const mergeMultilineLoglines = (logFragments: LogFragment[]) => (
                     // Partial line without timestamp detected
                     if (i > 0) {
                         // If not first line, copy line to previous line
-                        const previousLineContents = fragmentCopy.contents[i-1];
+                        const previousLineContents = fragmentCopy.contents[i - 1];
                         const newPreviousLineContents = `${previousLineContents}\n${lineContents}`;
-                        fragmentCopy.contents[i-1] = newPreviousLineContents;
+                        fragmentCopy.contents[i - 1] = newPreviousLineContents;
                     }
                     // Delete the current line and prevent iterating
                     fragmentCopy.contents.splice(i, 1);
@@ -283,7 +287,7 @@ export const navigateToLogCollection = (uuid: string) =>
             await services.collectionService.get(uuid);
             dispatch<any>(navigateTo(uuid));
         } catch {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not request collection', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Log collection was trashed or deleted.', hideDuration: 4000, kind: SnackbarKind.WARNING }));
         }
     };