1 Commits

Author SHA1 Message Date
m0n02hz
88dc87db05 Even.in take home task 2021-09-05 02:29:03 +05:30
13 changed files with 40386 additions and 8775 deletions

31445
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"babel-loader": "8.0.4", "babel-loader": "8.0.4",
"bootstrap": "^5.1.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"react": "^16.7.0", "react": "^16.7.0",
"react-dom": "^16.7.0", "react-dom": "^16.7.0",
@@ -11,7 +12,7 @@
"react-scripts": "2.1.3", "react-scripts": "2.1.3",
"react-timeago": "^4.3.0", "react-timeago": "^4.3.0",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-observable": "^1.0.0", "redux-observable": "^1.2.0",
"rxjs": "^6.4.0", "rxjs": "^6.4.0",
"typesafe-actions": "^3.0.0", "typesafe-actions": "^3.0.0",
"typescript": "3.2.4" "typescript": "3.2.4"

View File

@@ -1 +1,71 @@
/* .App {} */ /* .App {} */
.app-root {
color: rgb(82, 1, 245);
}
.header {
padding-bottom: 2rem;
border-bottom: 3px solid rgb(82, 1, 245)
}
.border {
border: 3px solid rgb(82, 1, 245) !important;
}
button {
background-color: rgb(82, 1, 245);
color: white;
border: none;
padding: 0.5rem 1rem;
margin-top: 3rem;
}
.form-container {
margin-top: 3.375rem;
max-height: 21rem;
}
.list-container {
margin-right: 5rem;
}
input[type=range]{
-webkit-appearance: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 300px;
height: 5px;
background: rgb(82, 1, 245);
border: none;
border-radius: 3px;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 16px;
width: 16px;
/* border-radius: 50%; */
background: rgb(82, 1, 245);
margin-top: -4px;
}
input[type=range]:focus {
outline: none;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: rgb(82, 1, 245);
}
.primary-details {
margin-right: auto;
}
.WorkloadItem-secondaryButton {
margin-left: 1.5rem;
background: rgb(82, 1, 245, 0.4);
color: rgb(82, 1, 245);
}

View File

@@ -6,20 +6,21 @@ import './App.css';
class App extends PureComponent { class App extends PureComponent {
nowHandler () : number {
return Date.now();
}
render() { render() {
return ( return (
<div> <div className="app-root container mt-5 pt-5 d-flex flex-column">
<h1>CloudWork</h1> <h1 className="header">CloudWork</h1>
<hr /> <div className="d-flex flex-row-reverse mt-5">
<div className="form-container border p-5" style={{flex: 2}}><WorkloadFormContainer /></div>
<div > <div className="list-container" style={{flex: 5}}>
<WorkloadFormContainer /> <h2 className="mb-3">Workloads</h2>
<WorkloadListContainer nowHandler={this.nowHandler}/>
</div> </div>
<hr />
<div>
<h2>Workloads</h2>
<WorkloadListContainer />
</div> </div>
</div> </div>
); );

View File

@@ -31,13 +31,13 @@ class WorkloadForm extends React.PureComponent<WorkloadFormProps, WorkloadFormSt
render() { render() {
return ( return (
<form> <form>
<h2>Create workload</h2> <h2 className="form-header mb-5">Create workload</h2>
<div> <div>
<label> <label className="d-block w-100">
Complexity: {this.state.complexity} <span className="mb-2 d-block">Complexity: {this.state.complexity}</span>
<br />
<input <input
className="w-100"
value={this.state.complexity} value={this.state.complexity}
onChange={(e) => this.setState({ complexity: Number(e.target.value) })} onChange={(e) => this.setState({ complexity: Number(e.target.value) })}
type="range" type="range"

View File

@@ -1,8 +1,12 @@
import React from 'react'; import React, { useEffect } from 'react';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import { Status } from '../../state/workloads' import { Status } from '../../state/workloads'
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { RootAction } from '../../state';
import { updateStatus } from '../../state/workloads/actions';
const statusMap = {CANCELED: "Cancelled", SUCCESS: "Successful", FAILURE: "Failed"};
export interface WorkloadItemStateProps { export interface WorkloadItemStateProps {
id: number; id: number;
complexity: number; complexity: number;
@@ -12,6 +16,7 @@ export interface WorkloadItemStateProps {
export interface WorkloadItemMethodProps { export interface WorkloadItemMethodProps {
onCancel: () => void; onCancel: () => void;
nowHandler: () => number;
} }
export interface WorkloadItemProps extends export interface WorkloadItemProps extends
@@ -19,19 +24,21 @@ export interface WorkloadItemProps extends
WorkloadItemMethodProps {} WorkloadItemMethodProps {}
const WorkloadItem: React.SFC<WorkloadItemProps> = (props) => ( const WorkloadItem: React.SFC<WorkloadItemProps> = (props) => {
<div className="WorkloadItem"> // console.log(props.completeDate)
<div>
return <div className="WorkloadItem border p-4 mb-4 d-flex">
<div className="primary-details">
<h3 className="WorkloadItem-heading">Workload #{props.id}</h3> <h3 className="WorkloadItem-heading">Workload #{props.id}</h3>
<span className="WorkloadItem-subHeading">Complexity: {props.complexity}</span> <span className="WorkloadItem-subHeading">Complexity {props.complexity}</span>
</div> </div>
<div> <div className="d-flex">
{props.status === 'WORKING' {props.status === 'WORKING'
? ( ? (
<> <>
<span><TimeAgo date={props.completeDate} /></span> <span className="my-auto"><TimeAgo date={props.completeDate} now={props.nowHandler} /></span>
<button <button
className="WorkloadItem-secondaryButton" className="WorkloadItem-secondaryButton ml-3 my-auto"
onClick={props.onCancel} onClick={props.onCancel}
> >
Cancel Cancel
@@ -39,16 +46,23 @@ const WorkloadItem: React.SFC<WorkloadItemProps> = (props) => (
</> </>
) )
: ( : (
<span className="WorkloadItem-statusText">{props.status.toLowerCase()}</span> <span className="WorkloadItem-statusText my-auto">{statusMap[props.status]}</span>
) )
} }
</div> </div>
</div> </div>
); };
const mapDispatchToProps = (dispatch: Dispatch<RootAction>) => ({
udpateWorkloadStatus: (id: number, status: Status) => dispatch(updateStatus({ id, status })),
})
const WorkloadItemContainer = connect(null, mapDispatchToProps)(WorkloadItem);
export { export {
WorkloadItem, WorkloadItem,
WorkloadItemContainer
}; };
export default WorkloadItem; export default WorkloadItem;

View File

@@ -12,6 +12,7 @@ export interface WorkloadListStateProps {
export interface WorkloadListDispatchProps { export interface WorkloadListDispatchProps {
cancelWorkload: (id: number) => void; cancelWorkload: (id: number) => void;
nowHandler: () => number;
} }
export interface WorkloadListProps extends export interface WorkloadListProps extends
@@ -19,19 +20,19 @@ export interface WorkloadListProps extends
WorkloadListDispatchProps {} WorkloadListDispatchProps {}
const WorkloadList: React.SFC<WorkloadListProps> = ({ workloads, cancelWorkload }) => ( const WorkloadList: React.SFC<WorkloadListProps> = ({ workloads, cancelWorkload, nowHandler }) => (
!workloads.length !workloads.length
? ( ? (
<span>No workloads to display</span> <span>No workloads to display</span>
) )
: ( : (
<ol> <ul className="list-unstyled">
{workloads.map((workload) => ( {workloads.map((workload) => (
<li key={workload.id}> <li key={workload.id}>
<WorkloadItem {...workload} onCancel={() => cancelWorkload(workload.id)} /> <WorkloadItem {...workload} onCancel={() => cancelWorkload(workload.id)} nowHandler={nowHandler} />
</li> </li>
))} ))}
</ol> </ul>
) )
); );
@@ -40,7 +41,7 @@ const mapStateToProps = (state: RootState): WorkloadListStateProps => ({
workloads: Object.values(state.workloads), workloads: Object.values(state.workloads),
}); });
const mapDispatchToProps = (dispatch: Dispatch<RootAction>): WorkloadListDispatchProps => ({ const mapDispatchToProps = (dispatch: Dispatch<RootAction>) => ({
cancelWorkload: (id: number) => dispatch(cancel({ id })), cancelWorkload: (id: number) => dispatch(cancel({ id })),
}) })

View File

@@ -3,10 +3,11 @@ import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { createEpicMiddleware } from 'redux-observable'; import { createEpicMiddleware } from 'redux-observable';
import moment from 'moment'; // import moment from 'moment';
import { reducer, epics, RootAction, RootState } from './state'; import { reducer, epics, RootAction, RootState } from './state';
import * as WorkloadActions from './state/workloads/actions'; import * as WorkloadActions from './state/workloads/actions';
import "bootstrap/dist/css/bootstrap.min.css"
import './index.css'; import './index.css';
import App from './components/App'; import App from './components/App';
@@ -20,7 +21,7 @@ epicMiddleware.run(epics);
// demo actions // demo actions
store.dispatch(WorkloadActions.submit({ complexity: 10 })); store.dispatch(WorkloadActions.submit({ complexity: 10 }));
store.dispatch(WorkloadActions.created({ id: 999, complexity: 10, completeDate: moment().add(10, 'second').toDate(), status: 'WORKING' })); // store.dispatch(WorkloadActions.created({ id: 999, complexity: 10, completeDate: moment().add(10, 'second').toDate(), status: 'WORKING' }));
ReactDOM.render( ReactDOM.render(

View File

@@ -1,18 +1,22 @@
import { createAction } from 'typesafe-actions'; import { createAction } from 'typesafe-actions';
import { Status } from './types'; import { Status } from './types';
import { SUBMIT, CREATED, CANCEL, UPDATE_STATUS } from './constants'; import { SUBMIT, CREATED, CANCEL, FAILED, UPDATE_STATUS } from './constants';
export const submit = createAction(SUBMIT, resolve => (params: { complexity: number }) => resolve({ complexity: params.complexity })); export const submit = createAction(SUBMIT, resolve => (params: { complexity: number }) => resolve({ complexity: params.complexity }));
export const created = createAction(CREATED, resolve => export const created = createAction(CREATED, resolve => {
(params: { id: number, status: Status, complexity: number, completeDate: Date }) => resolve({ return (params: { id: number, status: Status, complexity: number, completeDate: Date }) => {
return resolve({
id: params.id, id: params.id,
status: params.status, status: params.status,
completeDate: params.completeDate, completeDate: params.completeDate,
complexity: params.complexity, complexity: params.complexity,
})); })
}
});
export const failed = createAction(FAILED, resolve => (params: { message: string }) => resolve({ message: params.message }));
export const cancel = createAction(CANCEL, resolve => (params: { id: number }) => resolve({ id: params.id })); export const cancel = createAction(CANCEL, resolve => (params: { id: number }) => resolve({ id: params.id }));

View File

@@ -2,3 +2,4 @@ export const SUBMIT = 'workload/SUBMIT';
export const CREATED = 'workload/CREATED'; export const CREATED = 'workload/CREATED';
export const CANCEL = 'workload/CANCEL'; export const CANCEL = 'workload/CANCEL';
export const UPDATE_STATUS = 'workload/UPDATE_STATUS'; export const UPDATE_STATUS = 'workload/UPDATE_STATUS';
export const FAILED = 'workload/FAILED';

View File

@@ -1,25 +1,44 @@
import { from, timer } from 'rxjs';
import { combineEpics, Epic } from 'redux-observable'; import { combineEpics, Epic } from 'redux-observable';
import { filter, map, tap, ignoreElements } from 'rxjs/operators'; import { filter, map, switchMap, delayWhen } from 'rxjs/operators';
import { isActionOf } from 'typesafe-actions'; import { isActionOf } from 'typesafe-actions';
import { RootAction, RootState } from '../reducer'; import { RootAction, RootState } from '../reducer';
import * as workloadsActions from './actions'; import * as workloadsActions from './actions';
import {service} from "../workloads/services";
type AppEpic = Epic<RootAction, RootAction, RootState>; type AppEpic = Epic<RootAction, RootAction, RootState>;
const logWorkloadSubmissions: AppEpic = (action$, state$) => ( const createWorkload: AppEpic = (action$, state$) => (
action$.pipe( action$.pipe(
filter(isActionOf(workloadsActions.submit)), filter(isActionOf(workloadsActions.submit)),
map(action => action.payload), switchMap(action => from(service.create(action.payload))),
tap((payload) => console.log('Workload submitted', payload)), map(res => workloadsActions.created(res)),
ignoreElements(),
) )
); );
const cancelWorkload: AppEpic = (action$, state$) => (
action$.pipe(
filter(isActionOf(workloadsActions.cancel)),
switchMap(action => from(service.cancel(action.payload))),
map(res => workloadsActions.updateStatus(res))
)
);
const updateWorkload: AppEpic = action$ => (
action$.pipe(
filter(isActionOf(workloadsActions.created)),
delayWhen(action => timer(action.payload.complexity * 1000)),
switchMap(action => from(service.checkStatus(action.payload))),
map(res => workloadsActions.updateStatus(res))
)
);
export const epics = combineEpics( export const epics = combineEpics(
logWorkloadSubmissions, createWorkload,
cancelWorkload,
updateWorkload
); );
export default epics; export default epics;

View File

@@ -2,8 +2,7 @@ import moment from 'moment';
import { Status } from './types'; import { Status } from './types';
class WorkloadService {
export class WorkloadService {
private workLoads: { [key in number]: Work } = {}; private workLoads: { [key in number]: Work } = {};
private counter = 0; private counter = 0;
@@ -65,3 +64,6 @@ interface Work {
status: Status; status: Status;
timer: NodeJS.Timeout; timer: NodeJS.Timeout;
} }
const service = new WorkloadService();
export {WorkloadService, service}

17496
yarn.lock

File diff suppressed because it is too large Load Diff