[arvados-workbench2] created: 2.7.0-181-g75b017ae
git repository hosting
git at public.arvados.org
Thu Oct 26 21:09:19 UTC 2023
at 75b017ae9d566d523e448aaeb863c4d89b3548fe (commit)
commit 75b017ae9d566d523e448aaeb863c4d89b3548fe
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Thu Oct 26 16:56:28 2023 -0400
21067: Better handling of missing output/logs on process panel.
A few other important changes:
* Similar to the change in #21077, this removes the default error
snackbar popups any time a 400 error happens. This reduces user
confusion, particularly when "harmless" errors would occur.
* The collection files component will no longer give an "unhandled
rejection" React failure when it can't load the file list (this took
forever to track down.)
* Collections, projects and workflows will now default to a "not
found" panel if they fail to load. Previously, collections and
workflows would show nothing at all, and projects would show an empty
list.
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
index 83de48de..f1e50e0f 100644
--- a/src/components/collection-panel-files/collection-panel-files.tsx
+++ b/src/components/collection-panel-files/collection-panel-files.tsx
@@ -311,6 +311,8 @@ export const CollectionPanelFiles = withStyles(styles)(
return { ...next, ...prev };
}, {});
setPathData(state => ({ ...state, ...newState }));
+ }, () => {
+ // Nothing to do
})
.finally(() => {
setIsLoading(false);
diff --git a/src/index.tsx b/src/index.tsx
index a3f6c1ee..b0bda23b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -153,25 +153,13 @@ fetchConfig().then(({ config, apiHost }) => {
const services = createServices(config, {
progressFn: (id, working) => {
- //store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
},
errorFn: (id, error, showSnackBar: boolean) => {
if (showSnackBar) {
console.error("Backend error:", error);
-
- if (error.status === 404) {
- store.dispatch(openNotFoundDialog());
- } else if (error.status === 401 && error.errors[0].indexOf("Not logged in") > -1) {
+ if (error.status === 401 && error.errors[0].indexOf("Not logged in") > -1) {
// Catch auth errors when navigating and redirect to login preserving url location
store.dispatch(logout(false, true));
- } else {
- store.dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: `${error.errors ? error.errors[0] : error.message}`,
- kind: SnackbarKind.ERROR,
- hideDuration: 8000,
- })
- );
}
}
},
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index de8f2587..e50e5ed3 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -53,11 +53,14 @@ export class CollectionService extends TrashableResourceService<CollectionResour
}
async files(uuid: string) {
- const request = await this.keepWebdavClient.propfind(`c=${uuid}`);
- if (request.responseXML != null) {
- return extractFilesData(request.responseXML);
+ try {
+ const request = await this.keepWebdavClient.propfind(`c=${uuid}`);
+ if (request.responseXML != null) {
+ return extractFilesData(request.responseXML);
+ }
+ } catch (e) {
+ return Promise.reject(e);
}
-
return Promise.reject();
}
diff --git a/src/store/process-logs-panel/process-logs-panel-actions.ts b/src/store/process-logs-panel/process-logs-panel-actions.ts
index 87a2fa12..4e52431e 100644
--- a/src/store/process-logs-panel/process-logs-panel-actions.ts
+++ b/src/store/process-logs-panel/process-logs-panel-actions.ts
@@ -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 }));
}
};
diff --git a/src/store/process-panel/process-panel-actions.ts b/src/store/process-panel/process-panel-actions.ts
index 03e36aac..66de2f69 100644
--- a/src/store/process-panel/process-panel-actions.ts
+++ b/src/store/process-panel/process-panel-actions.ts
@@ -59,7 +59,7 @@ export const navigateToOutput = (uuid: string) => async (dispatch: Dispatch<any>
await services.collectionService.get(uuid);
dispatch<any>(navigateTo(uuid));
} catch {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "This collection does not exists!", hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
}
};
@@ -159,8 +159,7 @@ export const updateOutputParams = () => async (dispatch: Dispatch<any>, getState
};
export const openWorkflow = (uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(navigateToWorkflows);
- dispatch<any>(showWorkflowDetails(uuid));
+ dispatch<any>(navigateTo(uuid));
};
export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FILTERS([
diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts
index c0c0cd18..b2ddb46c 100644
--- a/src/store/project-panel/project-panel-middleware-service.ts
+++ b/src/store/project-panel/project-panel-middleware-service.ts
@@ -35,6 +35,7 @@ import { updatePublicFavorites } from "store/public-favorites/public-favorites-a
import { selectedFieldsOfGroup } from "models/group";
import { defaultCollectionSelectedFields } from "models/collection";
import { containerRequestFieldsNoMounts } from "models/container-request";
+import { openNotFoundDialog } from "store/not-found-panel/not-found-panel-action";
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
@@ -69,7 +70,12 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
rowsPerPage: dataExplorer.rowsPerPage,
})
);
- api.dispatch(couldNotFetchProjectContents());
+ if (e.status === 404) {
+ // It'll just show up as not found
+ }
+ else {
+ api.dispatch(couldNotFetchProjectContents());
+ }
} finally {
if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
}
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index b03400d5..f2dae2c5 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -109,6 +109,12 @@ export const isWorkbenchLoading = (state: RootState) => {
export const handleFirstTimeLoad = (action: any) => async (dispatch: Dispatch<any>, getState: () => RootState) => {
try {
await dispatch(action);
+ } catch (e) {
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Error " + e,
+ hideDuration: 8000,
+ kind: SnackbarKind.WARNING,
+ })
} finally {
if (isWorkbenchLoading(getState())) {
dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index 8cf19c03..eed9c7de 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -37,6 +37,7 @@ import { Link as ButtonLink } from '@material-ui/core';
import { ResourceWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
import { resourceIsFrozen } from 'common/frozen-resources';
+import { DefaultView } from "components/default-view/default-view";
type CssRules = 'root'
| 'button'
@@ -229,7 +230,17 @@ export const CollectionPanel = withStyles(styles)(connect(
</Card>
</MPVPanelContent>
</MPVContainer >
- : null;
+ : <Grid
+ container
+ alignItems="center"
+ justify="center"
+ style={{ minHeight: "100%" }}>
+ <DefaultView
+ icon={CollectionIcon}
+ messages={["Collection not found"]}
+ />
+ </Grid>
+ ;
}
handleContextMenu = (event: React.MouseEvent<any>) => {
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 4c94ab8d..2f274c97 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -6,7 +6,7 @@ import React from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import { DispatchProp, connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
-import { StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, Grid } from '@material-ui/core';
import { DataExplorer } from 'views-components/data-explorer/data-explorer';
import { DataColumns } from 'components/data-table/data-table';
@@ -51,6 +51,7 @@ import { GroupClass, GroupResource } from 'models/group';
import { CollectionResource } from 'models/collection';
import { resourceIsFrozen } from 'common/frozen-resources';
import { ProjectResource } from 'models/project';
+import { DefaultView } from "components/default-view/default-view";
type CssRules = 'root' | 'button';
@@ -238,6 +239,7 @@ const DEFAULT_VIEW_MESSAGES = ['Your project is empty.', 'Please create a projec
interface ProjectPanelDataProps {
currentItemId: string;
resources: ResourcesState;
+ project: GroupResource;
isAdmin: boolean;
userUuid: string;
dataExplorerItems: any;
@@ -245,17 +247,24 @@ interface ProjectPanelDataProps {
type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
-export const ProjectPanel = withStyles(styles)(
- connect((state: RootState) => ({
- currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+const mapStateToProps = (state: RootState) => {
+ const currentItemId = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+ const project = getResource<GroupResource>(currentItemId || "")(state.resources);
+ return {
+ currentItemId,
+ project,
resources: state.resources,
userUuid: state.auth.user!.uuid,
- }))(
+ };
+}
+
+export const ProjectPanel = withStyles(styles)(
+ connect(mapStateToProps)(
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
- return (
+ return this.props.project ?
<div data-cy='project-panel' className={classes.root}>
<DataExplorer
id={PROJECT_PANEL_ID}
@@ -267,7 +276,16 @@ export const ProjectPanel = withStyles(styles)(
defaultViewMessages={DEFAULT_VIEW_MESSAGES}
/>
</div>
- );
+ : <Grid
+ container
+ alignItems="center"
+ justify="center"
+ style={{ minHeight: "100%" }}>
+ <DefaultView
+ icon={ProjectIcon}
+ messages={["Project not found"]}
+ />
+ </Grid>;
}
isCurrentItemChild = (resource: Resource) => {
diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx
index 5973efed..da273719 100644
--- a/src/views/workflow-panel/registered-workflow-panel.tsx
+++ b/src/views/workflow-panel/registered-workflow-panel.tsx
@@ -13,6 +13,7 @@ import {
CardHeader,
CardContent,
IconButton,
+ Grid
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
@@ -26,6 +27,7 @@ import { getResource } from 'store/resources/resources';
import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
import { ProcessIOCard, ProcessIOCardType } from 'views/process-panel/process-io-card';
+import { DefaultView } from "components/default-view/default-view";
type CssRules = 'root'
| 'button'
@@ -200,7 +202,16 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
</Card>
</MPVPanelContent>
</MPVContainer>
- : null;
+ : <Grid
+ container
+ alignItems="center"
+ justify="center"
+ style={{ minHeight: "100%" }}>
+ <DefaultView
+ icon={WorkflowIcon}
+ messages={["Workflow not found"]}
+ />
+ </Grid>;
}
handleContextMenu = (event: React.MouseEvent<any>) => {
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list