diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js
index 47c89879..6c35d8f3 100644
--- a/reana-ui/src/actions.js
+++ b/reana-ui/src/actions.js
@@ -1,14 +1,15 @@
/*
- -*- coding: utf-8 -*-
+ -*- coding: utf-8 -*-
- This file is part of REANA.
- Copyright (C) 2020 CERN.
+ This file is part of REANA.
+ Copyright (C) 2020 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
*/
import _ from "lodash";
+import axios from "axios";
import { api } from "./config";
import { parseWorkflows, parseLogs, parseFiles } from "./util";
@@ -18,8 +19,12 @@ import {
getWorkflowSpecification
} from "./selectors";
+export const ERROR = "Error";
+export const CLEAR_ERROR = "Clear error";
+
export const CONFIG_FETCH = "Fetch app config info";
export const CONFIG_RECEIVED = "App config info received";
+export const CONFIG_ERROR = "Fetch app config error";
export const USER_FETCH = "Fetch user authentication info";
export const USER_RECEIVED = "User info received";
@@ -33,9 +38,11 @@ export const USER_SIGN_ERROR = "User sign in/up error";
export const USER_SIGNEDOUT = "User signed out";
export const USER_REQUEST_TOKEN = "Request user token";
export const USER_TOKEN_REQUESTED = "User token requested";
+export const USER_TOKEN_ERROR = "User token error";
export const WORKFLOWS_FETCH = "Fetch workflows info";
export const WORKFLOWS_RECEIVED = "Workflows info received";
+export const WORKFLOWS_FETCH_ERROR = "Workflows fetch error";
export const WORKFLOW_LOGS_FETCH = "Fetch workflow logs";
export const WORKFLOW_LOGS_RECEIVED = "Workflow logs received";
export const WORKFLOW_FILES_FETCH = "Fetch workflow files";
@@ -68,20 +75,23 @@ const WORKFLOW_FILES_URL = (id, { page = 1, size }) => {
return url;
};
+function errorActionCreator(error, name) {
+ const { status, data } = error?.response;
+ const { message } = data;
+ return { type: ERROR, name, status, message };
+};
+
+export const clearError = { type: CLEAR_ERROR };
+
export function loadConfig() {
return async dispatch => {
- let resp, data;
- try {
- dispatch({ type: CONFIG_FETCH });
- resp = await fetch(CONFIG_URL, { credentials: "include" });
- } catch (err) {
- throw new Error(CONFIG_URL, 0, err);
- }
- if (resp.ok) {
- data = await resp.json();
- }
- dispatch({ type: CONFIG_RECEIVED, ...data });
- return resp;
+ dispatch({ type: CONFIG_FETCH });
+ return await axios.get(CONFIG_URL, { withCredentials: true })
+ .then(resp => dispatch({ type: CONFIG_RECEIVED, ...resp.data }))
+ .catch(err => {
+ dispatch(errorActionCreator(err, CONFIG_URL));
+ dispatch({ type: CONFIG_ERROR });
+ });
};
}
@@ -159,47 +169,29 @@ export function userSignout() {
export function requestToken() {
return async dispatch => {
- let resp, data;
- try {
- dispatch({ type: USER_REQUEST_TOKEN });
- resp = await fetch(USER_REQUEST_TOKEN_URL, {
- method: "PUT",
- credentials: "include"
+ dispatch({ type: USER_REQUEST_TOKEN });
+ return await axios.put(USER_REQUEST_TOKEN_URL, null, { withCredentials: true })
+ .then(resp => dispatch({ type: USER_TOKEN_REQUESTED, ...resp.data }))
+ .catch(err => {
+ dispatch(errorActionCreator(err, USER_INFO_URL));
+ dispatch({ type: USER_TOKEN_ERROR });
});
- } catch (err) {
- throw new Error(USER_INFO_URL, 0, err);
- }
- if (resp.status === 401) {
- data = await resp.json();
- console.log(data.message);
- } else if (resp.ok) {
- data = await resp.json();
- }
- dispatch({ type: USER_TOKEN_REQUESTED, ...data });
- return resp;
};
}
export function fetchWorkflows(pagination) {
return async dispatch => {
- let resp, data;
- try {
- dispatch({ type: WORKFLOWS_FETCH });
- resp = await fetch(WORKFLOWS_URL({ ...pagination }), {
- credentials: "include"
+ dispatch({ type: WORKFLOWS_FETCH });
+ return await axios.get(WORKFLOWS_URL({ ...pagination }), { withCredentials: true })
+ .then(resp => dispatch({
+ type: WORKFLOWS_RECEIVED,
+ workflows: parseWorkflows(resp.data.items),
+ total: resp.data.total,
+ }))
+ .catch(err => {
+ dispatch(errorActionCreator(err, USER_INFO_URL));
+ dispatch({ type: WORKFLOWS_FETCH_ERROR });
});
- } catch (err) {
- throw new Error(USER_INFO_URL, 0, err);
- }
- if (resp.ok) {
- data = await resp.json();
- }
- dispatch({
- type: WORKFLOWS_RECEIVED,
- workflows: parseWorkflows(data.items),
- total: data.total
- });
- return resp;
};
}
diff --git a/reana-ui/src/components/Notification.js b/reana-ui/src/components/Notification.js
new file mode 100644
index 00000000..6e999251
--- /dev/null
+++ b/reana-ui/src/components/Notification.js
@@ -0,0 +1,64 @@
+/*
+ -*- coding: utf-8 -*-
+
+ This file is part of REANA.
+ Copyright (C) 2020 CERN.
+
+ REANA is free software; you can redistribute it and/or modify it
+ under the terms of the MIT License; see LICENSE file for more details.
+*/
+
+import React, { useRef } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { Container, Message, Transition } from "semantic-ui-react";
+import PropTypes from "prop-types";
+
+import { clearError } from "../actions";
+import { getError } from "../selectors";
+
+import styles from "./Notification.module.scss";
+
+const AUTO_CLOSE_TIMEOUT = 16000;
+
+export default function Notification({ icon, header, message, closable }) {
+ const dispatch = useDispatch();
+ const error = useSelector(getError);
+ const timer = useRef(null);
+
+ const hide = () => dispatch(clearError);
+ const visible = message || error ? true : false;
+
+ if (closable && visible) {
+ clearTimeout(timer.current);
+ timer.current = setTimeout(() => hide(), AUTO_CLOSE_TIMEOUT);
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+Notification.propTypes = {
+ icon: PropTypes.string,
+ header: PropTypes.string,
+ message: PropTypes.string,
+ closable: PropTypes.bool
+};
+
+Notification.defaultProps = {
+ icon: "warning sign",
+ header: "An error has occurred",
+ message: null,
+ closable: true
+};
diff --git a/reana-ui/src/pages/workflowDetails/WorkflowDetails.module.scss b/reana-ui/src/components/Notification.module.scss
similarity index 85%
rename from reana-ui/src/pages/workflowDetails/WorkflowDetails.module.scss
rename to reana-ui/src/components/Notification.module.scss
index 0ca7d6b3..a4a1d04b 100644
--- a/reana-ui/src/pages/workflowDetails/WorkflowDetails.module.scss
+++ b/reana-ui/src/components/Notification.module.scss
@@ -8,8 +8,8 @@
under the terms of the MIT License; see LICENSE file for more details.
*/
-@import '../../styles/palette';
+@import '../styles/palette';
-.warning {
+.container {
margin-top: 2em;
}
diff --git a/reana-ui/src/components/index.js b/reana-ui/src/components/index.js
index bfca98ba..464cb6e1 100644
--- a/reana-ui/src/components/index.js
+++ b/reana-ui/src/components/index.js
@@ -11,6 +11,7 @@
export { default as Announcement } from "./Announcement";
export { default as CodeSnippet } from "./CodeSnippet";
export { default as Footer } from "./Footer";
+export { default as Notification } from "./Notification";
export { default as Title } from "./Title";
export { default as TopHeader } from "./TopHeader";
export { default as TooltipIfTruncated } from "./TooltipIfTruncated";
diff --git a/reana-ui/src/pages/BasePage.js b/reana-ui/src/pages/BasePage.js
index 5a7c9f6d..1b361f55 100644
--- a/reana-ui/src/pages/BasePage.js
+++ b/reana-ui/src/pages/BasePage.js
@@ -9,7 +9,7 @@
*/
import React from "react";
-import { Announcement, Footer, TopHeader } from "../components";
+import { Announcement, Notification, Footer, TopHeader } from "../components";
import styles from "./BasePage.module.scss";
@@ -18,6 +18,7 @@ export default function BasePage({ children }) {
diff --git a/reana-ui/src/pages/workflowDetails/WorkflowDetails.js b/reana-ui/src/pages/workflowDetails/WorkflowDetails.js
index 16dc2046..b1101c10 100644
--- a/reana-ui/src/pages/workflowDetails/WorkflowDetails.js
+++ b/reana-ui/src/pages/workflowDetails/WorkflowDetails.js
@@ -1,8 +1,8 @@
/*
- -*- coding: utf-8 -*-
+ -*- coding: utf-8 -*-
- This file is part of REANA.
- Copyright (C) 2020 CERN.
+ This file is part of REANA.
+ Copyright (C) 2020 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
@@ -11,11 +11,12 @@
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
-import { Container, Dimmer, Loader, Message, Tab } from "semantic-ui-react";
+import { Container, Dimmer, Loader, Tab } from "semantic-ui-react";
import { fetchWorkflow } from "../../actions";
import { getWorkflow, loadingWorkflows, isWorkflowsFetched } from "../../selectors";
import BasePage from "../BasePage";
+import { Notification } from "../../components";
import {
WorkflowInfo,
WorkflowLogs,
@@ -23,8 +24,6 @@ import {
WorkflowSpecification
} from "./components";
-import styles from "./WorkflowDetails.module.scss";
-
export default function WorkflowDetailsPage() {
return (
@@ -55,14 +54,10 @@ function WorkflowDetails() {
if (!workflow) {
return (
-
-
-
+
);
}
diff --git a/reana-ui/src/pages/workflowList/components/Welcome.js b/reana-ui/src/pages/workflowList/components/Welcome.js
index 3315ccac..8cd93d5d 100644
--- a/reana-ui/src/pages/workflowList/components/Welcome.js
+++ b/reana-ui/src/pages/workflowList/components/Welcome.js
@@ -122,9 +122,7 @@ export function WelcomeNoTokenMsg() {
const loading = useSelector(loadingTokenStatus);
const dispatch = useDispatch();
- const handleRequestToken = () => {
- dispatch(requestToken());
- };
+ const handleRequestToken = () => dispatch(requestToken());
return tokenStatus === "requested" ? (
@@ -134,7 +132,7 @@ export function WelcomeNoTokenMsg() {
- {moment.utc(tokenRequestedAt).format("Do MMM YYYY HH:mm [UTC]")}
+ {moment.utc(tokenRequestedAt).format("YYYY-MM-DDThh:mm:ss")}
diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js
index c287f447..b6664e56 100644
--- a/reana-ui/src/reducers.js
+++ b/reana-ui/src/reducers.js
@@ -1,8 +1,8 @@
/*
- -*- coding: utf-8 -*-
+ -*- coding: utf-8 -*-
- This file is part of REANA.
- Copyright (C) 2020 CERN.
+ This file is part of REANA.
+ Copyright (C) 2020 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
@@ -10,8 +10,11 @@
import { combineReducers } from "redux";
import {
+ ERROR,
+ CLEAR_ERROR,
CONFIG_FETCH,
CONFIG_RECEIVED,
+ CONFIG_ERROR,
USER_FETCH,
USER_RECEIVED,
USER_FETCH_ERROR,
@@ -19,8 +22,10 @@ import {
USER_SIGN_ERROR,
USER_REQUEST_TOKEN,
USER_TOKEN_REQUESTED,
+ USER_TOKEN_ERROR,
WORKFLOWS_FETCH,
WORKFLOWS_RECEIVED,
+ WORKFLOWS_FETCH_ERROR,
WORKFLOW_LOGS_FETCH,
WORKFLOW_LOGS_RECEIVED,
WORKFLOW_SPECIFICATION_FETCH,
@@ -30,6 +35,8 @@ import {
} from "./actions";
import { USER_ERROR } from "./errors";
+const errorInitialState = null;
+
const configInitialState = {
announcement: null,
poolingSecs: null,
@@ -66,6 +73,18 @@ const detailsInitialState = {
loadingDetails: false
};
+const error = (state = errorInitialState, action) => {
+ const { name, status, message } = action;
+ switch (action.type) {
+ case ERROR:
+ return { ...state, name, status, message };
+ case CLEAR_ERROR:
+ return errorInitialState;
+ default:
+ return state;
+ }
+};
+
const config = (state = configInitialState, action) => {
switch (action.type) {
case CONFIG_FETCH:
@@ -83,6 +102,8 @@ const config = (state = configInitialState, action) => {
localUsers: action.local_users,
loading: false
};
+ case CONFIG_ERROR:
+ return { ...state, loading: false };
default:
return state;
}
@@ -133,6 +154,14 @@ const auth = (state = authInitialState, action) => {
loading: false
}
};
+ case USER_TOKEN_ERROR:
+ return {
+ ...state,
+ reanaToken: {
+ ...state.reanaToken,
+ loading: false
+ }
+ };
default:
return state;
}
@@ -150,6 +179,8 @@ const workflows = (state = workflowsInitialState, action) => {
total: action.total,
loadingWorkflows: false
};
+ case WORKFLOWS_FETCH_ERROR:
+ return { ...state, loadingWorkflows: false };
default:
return state;
}
@@ -203,6 +234,7 @@ const details = (state = detailsInitialState, action) => {
};
const reanaApp = combineReducers({
+ error,
config,
auth,
workflows,
diff --git a/reana-ui/src/selectors.js b/reana-ui/src/selectors.js
index e03edf42..036b81e2 100644
--- a/reana-ui/src/selectors.js
+++ b/reana-ui/src/selectors.js
@@ -10,6 +10,9 @@
import { USER_ERROR } from "./errors";
+// Error
+export const getError = state => state.error;
+
// Config
export const getConfig = state => state.config;