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 }) {
+
{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() {
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;