Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { useDispatch } from 'react-redux';
import Hero from './pages/Hero/Hero.jsx';
import TriggerWorkflowAuthenticated from './pages/TriggerWorkflow/TriggerWorkflow.jsx';
import TriggerWorkflowFormAuthenticated from './pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx';
import ManageWorkflowAuthenticated from './pages/ManageWorkflow/ManageWorkflow.jsx';
import { LoginStatus, ROUTE } from './constants.js';
import { api } from './api';
import HomeAuthenticated from './pages/Home/Home.jsx';
import {
authorizeUser,
closeLoadingCircleInPopup,
Expand All @@ -30,7 +32,7 @@ function App() {

const { data: userInfo } = await api.acg.callbackExecute(code);
dispatch(authorizeUser(LoginStatus.ACG, userInfo.name, userInfo.email));
navigate(ROUTE.TRIGGER);
navigate(ROUTE.HOME);
dispatch(closePopupWindow());
dispatch(closeLoadingCircleInPopup());
};
Expand All @@ -41,7 +43,9 @@ function App() {
return (
<Routes>
<Route path={ROUTE.ROOT} element={<Hero />} />
<Route path={ROUTE.HOME} element={<HomeAuthenticated />} />
<Route path={ROUTE.TRIGGER} element={<TriggerWorkflowAuthenticated />} />
<Route path={ROUTE.MANAGE} element={<ManageWorkflowAuthenticated />} />
<Route path={`${ROUTE.TRIGGERFORM}/:workflowId`} element={<TriggerWorkflowFormAuthenticated />} />
</Routes>
);
Expand Down
24 changes: 24 additions & 0 deletions client/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,29 @@ export const api = Object.freeze({
return error.response;
}
},
pauseWorkflow: async workflow => {
try {
const response = await instance.post(`/workflows/${workflow.id}/pause`);
return response;
} catch (error) {
return error.response;
}
},
resumeWorkflow: async workflow => {
try {
const response = await instance.post(`/workflows/${workflow.id}/resume`);
return response;
} catch (error) {
return error.response;
}
},
getWorkflowInstances: async workflowId => {
const response = await instance.get(`/workflows/${workflowId}/instances`);
return response;
},
cancelWorkflowInstance: async (workflowId, instanceId) => {
const response = await instance.post(`/workflows/${workflowId}/instances/${instanceId}/cancel`);
return response;
}
},
});
7 changes: 7 additions & 0 deletions client/src/assets/img/dropdown-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions client/src/assets/img/instance-canceled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions client/src/assets/img/instance-completed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions client/src/assets/img/workflow-manage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 32 additions & 2 deletions client/src/assets/text.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
"field2": "HR manager email"
}
},
"home": {
"card1": {
"title": "Trigger a workflow",
"description": "Get a list of workflow definitions and trigger a workflow instance"
},
"card2": {
"title": "Manage workflows",
"description": "Get the status of existing workflow instances and cancel instances"
}
},
"loader": {
"title": "Waiting for log in"
},
Expand All @@ -60,6 +70,12 @@
"step1Description": "Display all of the workflow definitions in the user's account by calling the WorkflowManagment: getWorkflowDefinitions endpoint. Each row on this page represents a workflow definition.",
"step2Description": "After the user chooses a workflow to trigger, the next step is to get the workflow definition by calling the WorkflowMangement: getWorkflowDefinition endpoint. The response for this API call includes a triggerURL property that will be used in the next step.",
"step3Description": "Finally, call the WorkflowTrigger: triggerWorkflow endpoint to trigger the workflow instance. The response of this call includes the workflowInstanceUrl where the workflow participant can complete the workflow steps."
},
"manageWorkflow": {
"mainDescription": "This scenario displays a list of workflow instances, the status of those instances, and the option to cancel them.",
"step1Description": "The WorkflowInstanceManagement: getWorkflowInstances endpoint is called to get a list of the workflow instances that have been triggered by the sample app.",
"step2Description": "To get the status of a given workflow instance, the WorkflowInstanceManagement: getWorkflowInstance method is called.",
"step3Description": "If a user chooses to cancel a workflow instance, the WorkflowInstanceManagement: cancelWorkflowInstance method is called."
}
},
"workflowList": {
Expand All @@ -68,7 +84,16 @@
"columns": {
"lastRunStatus": "Status of last run",
"workflowName": "Workflow name",
"workflowType": "Workflow type"
"workflowType": "Workflow type",
"workflowStatus": "Workflow status"
}
},
"instanceList": {
"cancelButton": "Cancel",
"columns": {
"instance": "Instance",
"progress": "Progress",
"startedBy": "Started by"
}
},
"pageTitles": {
Expand All @@ -93,7 +118,12 @@
"triggerNewWorkflow": "Trigger new workflow ->",
"getStarted": "Get started",
"moreInfo": "More info",
"backHome": "← Back to workflows"
"backToWorkflows": "← Back to workflows",
"updateWorkflow": "Update workflow status",
"cancelWorkflow": "Cancel workflow",
"backHome": "← Back to home",
"pauseWorkflow": "Pause the creation of instances",
"resumeWorkflow": "Resume the creation of instances"
},
"links": {
"github": "https://github.com/docusign/sample-app-workflows-node",
Expand Down
22 changes: 22 additions & 0 deletions client/src/components/Card/Card.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import styles from './Card.module.css';
import textContent from '../../assets/text.json';

const Card = ({ img, title, description, linkTo }) => {
return (
<div className={styles.cardBody}>
<div className={styles.cardContainer}>
<img src={img} alt="" />
<h4>{title}</h4>
<h5>{description}</h5>
<Link to={linkTo}>
<button className="btn btn-secondary" type="button">
{textContent.buttons.getStarted}
</button>
</Link>
</div>
</div>
);
};

export default Card;
76 changes: 76 additions & 0 deletions client/src/components/Card/Card.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.cardBody {
display: flex;
height: 254px;
width: 384px;
background-color: var(--grey-light);
border-radius: 8px;
overflow: visible;
}

.cardBody img {
width: 44px;
height: 44px;
}

.cardBody h4 {
margin: 8px auto;
color: var(--black-main);
text-align: center;
font-size: 20px;
line-height: 24px;
font-weight: 500;
}

.cardBody h5 {
margin: 16px auto 0 auto;
min-height: 42px;
color: var(--black-main);
text-align: center;
font-size: 14px;
line-height: 20px;
font-weight: 300;
}

.cardBody button {
margin-top: 10px;
height: 40px;
max-width: 156px;
background-color: var(--primary-main);
border-radius: 2px;

color: var(--white-main);
font-size: 16px;
font-weight: 400;
line-height: 24px;
}

.cardBody button:hover {
background-color: var(--secondary-main);
}

.cardContainer {
height: 100%;
width: 100%;
padding: 32px;
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
}

.buttonGroup {
display: flex;
flex-direction: row;
}

.moreInfo {
line-height: 10px !important;
background-color: var(--grey-light) !important;
color: var(--black-light) !important;
}

.moreInfo:hover,
.moreInfo:focus,
.moreInfo:active {
border: none;
}
74 changes: 74 additions & 0 deletions client/src/components/InstanceInfo/InstanceInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import styles from './InstanceInfo.module.css';
import instanceCompletedSvg from '../../assets/img/instance-completed.svg';
import instanceCanceledSvg from '../../assets/img/instance-canceled.svg';
import textContent from '../../assets/text.json';
import { api } from '../../api';
import { useState } from 'react';
import StatusLoader from '../StatusLoader/StatusLoader.jsx';
import { useDispatch, useSelector } from 'react-redux';
import { updateWorkflowDefinitions } from '../../store/actions/workflows.action.js';

const InstanceInfo = ({ workflowId, instance }) => {
const dispatch = useDispatch();
const workflows = useSelector(state => state.workflows.workflows);
const [isStatusRefreshing, setIsStatusRefreshing] = useState(false);

const { lastCompletedStep, totalSteps, workflowStatus } = instance;
const progress = Math.min((lastCompletedStep / totalSteps) * 100, 100);
const isFailed = workflowStatus.toLowerCase() === 'failed';
const isInProgress = workflowStatus.toLowerCase() === 'in progress';
const isSuccessful = workflowStatus.toLowerCase() === 'completed';

const handleCancelInstance = async () => {
setIsStatusRefreshing(true);
await api.workflows.cancelWorkflowInstance(workflowId, instance.id);

const updatedWorkflows = workflows.map(workflow => {
if (workflow.id !== workflowId) return workflow;

const newInstanceList = workflow.instances.map(inst => {
if (inst.id !== instance.id) return inst;

const updatedInstance = { ...inst, workflowStatus: 'Canceled' };
return updatedInstance;
})

const updatedWorkflow = { ...workflow, instances: newInstanceList };
return updatedWorkflow;
})

dispatch(updateWorkflowDefinitions(updatedWorkflows));
setIsStatusRefreshing(false);
}

return (
<div key={instance.id} className={styles.instanceListRow}>
<div className={styles.cell}>
<h4>{instance.name}</h4>
</div>

<div className={styles.cell}>
{isStatusRefreshing ? (
<StatusLoader />
) : isInProgress || isFailed ? (
<>
<progress className={isFailed ? styles.progressFail : styles.progressSuccess} max={100} value={progress} />
<h5>{instance.workflowStatus}</h5>
</>
) : (
<h4><img src={isSuccessful ? instanceCompletedSvg : instanceCanceledSvg} alt={workflowStatus} />{workflowStatus}</h4>
)}
</div>

<div className={styles.cell}>
<h4>{instance.startedByName}</h4>
</div>

<div className={styles.cell}>
<button disabled={!isInProgress} onClick={() => handleCancelInstance()}>{textContent.instanceList.cancelButton}</button>
</div>
</div>
);
}

export default InstanceInfo;
Loading