From 64b0cdc951466a4a021e4af11cd7dc837c61f980 Mon Sep 17 00:00:00 2001 From: ModZero Date: Thu, 24 Oct 2019 04:12:49 +0200 Subject: [PATCH] Playing with sagas for a stuf implementation of tasks --- package-lock.json | 21 +++++++++ package.json | 3 ++ src/frontend/components/TaskList.tsx | 5 ++- src/frontend/store/index.ts | 13 +++++- src/frontend/store/sagas.ts | 35 +++++++++++++++ src/frontend/store/tasks/actions.ts | 35 ++++++++++++++- src/frontend/store/tasks/reducers.ts | 43 ++++++++++++++++++- src/frontend/store/tasks/types.ts | 64 ++++++++++++++++++++++++++-- src/frontend/tslint.json | 19 ++++++--- tslint.json | 38 +++++++++-------- 10 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 src/frontend/store/sagas.ts diff --git a/package-lock.json b/package-lock.json index 9a730d7..e0cae9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -295,6 +295,15 @@ "integrity": "sha512-dsfE4BHJkLQW+reOS6b17xhZ/6FB1rB8eRRvO08nn5o+voxf3i74tuyFWNH6djdfgX7Sm5s6LD8t6mJug4dpDw==", "dev": true }, + "@types/object-hash": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.0.tgz", + "integrity": "sha512-il4NIe4jTx4lfhkKaksmmGHw5EsVkO8sHWkpJHM9m59r1dtsVadLSrJqdE8zU74NENDAsR3oLIOlooRAXlPLNA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/pg": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.11.2.tgz", @@ -4652,6 +4661,12 @@ } } }, + "object-hash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.0.tgz", + "integrity": "sha512-I7zGBH0rDKwVGeGZpZoFaDhIwvJa3l1CZE+8VchylXbInNiCj7sxxea9P5dTM4ftKR5//nrqxrdeGSTWL2VpBA==", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -5697,6 +5712,12 @@ "symbol-observable": "^1.2.0" } }, + "redux-devtools-extension": { + "version": "2.13.8", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz", + "integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==", + "dev": true + }, "redux-saga": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.1.tgz", diff --git a/package.json b/package.json index a8daeea..5fff49d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/http-errors": "^1.6.2", "@types/luxon": "^1.15.2", "@types/node": "^12.11.2", + "@types/object-hash": "^1.3.0", "@types/pg": "^7.11.2", "@types/pino": "^5.8.12", "@types/react": "^16.9.9", @@ -57,6 +58,7 @@ "@types/webpack-dev-middleware": "^2.0.3", "@types/yargs": "^13.0.3", "css-loader": "^3.2.0", + "object-hash": "^2.0.0", "pg-monitor": "^1.3.1", "pino-pretty": "^3.2.2", "react": "^16.11.0", @@ -65,6 +67,7 @@ "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "redux": "^4.0.4", + "redux-devtools-extension": "^2.13.8", "redux-saga": "^1.1.1", "ts-loader": "^6.2.0", "ts-node": "^8.4.1", diff --git a/src/frontend/components/TaskList.tsx b/src/frontend/components/TaskList.tsx index 9222f6d..a67bda5 100644 --- a/src/frontend/components/TaskList.tsx +++ b/src/frontend/components/TaskList.tsx @@ -1,13 +1,16 @@ import { AppState } from "@kredens/frontend/store"; import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions"; import { Task, TaskScheduleType } from "@kredens/frontend/store/tasks/types"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; export default () => { const [taskName, setTaskName] = useState(""); const tasks = useSelector(state => state.tasks.items); const dispatch = useDispatch(); + useEffect(() => { + dispatch({type: "FETCH_TASKS"}); + }, []) const onTaskAddClick = () => { dispatch(scheduleTask(Math.random().toString(36), { diff --git a/src/frontend/store/index.ts b/src/frontend/store/index.ts index 20b96d4..c2b72c9 100644 --- a/src/frontend/store/index.ts +++ b/src/frontend/store/index.ts @@ -1,4 +1,7 @@ -import { combineReducers, createStore } from "redux"; +import { applyMiddleware, combineReducers, createStore } from "redux"; +import { composeWithDevTools } from "redux-devtools-extension"; +import createSagaMiddleware from "redux-saga"; +import rootSaga from "./sagas"; import { tasksReducer } from "./tasks/reducers"; const rootReducer = combineReducers({ @@ -7,8 +10,14 @@ const rootReducer = combineReducers({ export type AppState = ReturnType; +const sagaMiddleware = createSagaMiddleware(); export default function configureStore() { - const store = createStore(rootReducer); + const store = createStore( + rootReducer, + composeWithDevTools(applyMiddleware(sagaMiddleware)) + ); + + sagaMiddleware.run(rootSaga); return store; } diff --git a/src/frontend/store/sagas.ts b/src/frontend/store/sagas.ts new file mode 100644 index 0000000..cae8307 --- /dev/null +++ b/src/frontend/store/sagas.ts @@ -0,0 +1,35 @@ +import { all, call, put, takeEvery } from "redux-saga/effects"; +import { getTasks, Task as APITask } from "../api/tasks"; +import { taskFetchError, taskFetchOk, taskFetchStart } from "./tasks/actions"; +import { Task, TaskQuery, TaskScheduleType } from "./tasks/types"; + +export function* fetchTasksSaga(query: TaskQuery = { limit: 10 }) { + yield put(taskFetchStart(query)); + + try { + const tasks: APITask[] = yield call(getTasks); + yield put( + taskFetchOk( + query, + tasks + .map<[string, Task]>(t => [ + t.id.toString(), + { + name: t.name, + schedule: { + type: TaskScheduleType.Once, + due: t.due.toISO() + } + } + ]) + .reduce((res, [id, task]) => ({ ...res, [id]: task }), {}) + ) + ); + } catch (error) { + yield put(taskFetchError(query, `${error}`)); + } +} + +export default function* rootSaga() { + yield all([yield takeEvery("FETCH_TASKS", fetchTasksSaga)]); +} diff --git a/src/frontend/store/tasks/actions.ts b/src/frontend/store/tasks/actions.ts index 0fe0435..e666432 100644 --- a/src/frontend/store/tasks/actions.ts +++ b/src/frontend/store/tasks/actions.ts @@ -1,4 +1,5 @@ -import { Task, TasksAction, TasksActionType } from "./types"; +import { DateTime } from "luxon"; +import { Task, TaskQuery, TasksAction, TasksActionType } from "./types"; export function scheduleTask(id: string, task: Task): TasksActionType { return { @@ -14,3 +15,35 @@ export function deleteTask(id: string): TasksActionType { id }; } + +export function taskFetchStart(query: TaskQuery): TasksActionType { + return { + type: TasksAction.TASKS_FETCH_START, + query, + started: DateTime.utc().toISO() + }; +} + +export function taskFetchOk( + query: TaskQuery, + results: { [key: string]: Task } +): TasksActionType { + return { + type: TasksAction.TASKS_FETCH_OK, + query, + results, + fetched: DateTime.utc().toISO() + }; +} + +export function taskFetchError( + query: TaskQuery, + error: string +): TasksActionType { + return { + type: TasksAction.TASKS_FETCH_ERROR, + query, + error, + fetched: DateTime.utc().toISO() + }; +} diff --git a/src/frontend/store/tasks/reducers.ts b/src/frontend/store/tasks/reducers.ts index 118ce7b..a48be12 100644 --- a/src/frontend/store/tasks/reducers.ts +++ b/src/frontend/store/tasks/reducers.ts @@ -1,7 +1,9 @@ +import objectHash from "object-hash"; import { TasksAction, TasksActionType, TasksState } from "./types"; const initialState: TasksState = { - items: {} + items: {}, + queries: {} }; export function tasksReducer( @@ -24,6 +26,45 @@ export function tasksReducer( .filter(([key]) => key !== action.id) .reduce((res, [key, task]) => ({ ...res, [key]: task }), {}) }; + case TasksAction.TASKS_FETCH_START: + return { + ...state, + queries: { + ...state.queries, + [objectHash(action.query)]: { + result: "fetching", + started: action.started + } + } + }; + case TasksAction.TASKS_FETCH_OK: + return { + ...state, + items: { + ...state.items, + ...action.results + }, + queries: { + ...state.queries, + [objectHash(action.query)]: { + result: "ok", + items: Object.keys(action.results), + fetched: action.fetched + } + } + }; + case TasksAction.TASKS_FETCH_ERROR: + return { + ...state, + queries: { + ...state.queries, + [objectHash(action.query)]: { + result: "error", + error: action.error, + fetched: action.fetched + } + } + }; default: { return state; } diff --git a/src/frontend/store/tasks/types.ts b/src/frontend/store/tasks/types.ts index 1ace47d..57299bf 100644 --- a/src/frontend/store/tasks/types.ts +++ b/src/frontend/store/tasks/types.ts @@ -19,26 +19,82 @@ export interface Task { schedule: TaskSchedule; } +export interface TaskQuery { + query?: string; + after?: string; + limit: number; +} + +interface TaskQueryOk { + result: "ok"; + items: string[]; + fetched: string; +} + +interface TaskQueryFetching { + result: "fetching"; + started: string; +} + +interface TaskQueryError { + result: "error"; + error: string; + fetched: string; +} + +export type TaskQueryResult = TaskQueryOk | TaskQueryFetching | TaskQueryError; + export interface TasksState { items: { [key: string]: Task; }; + queries: { + [key: string]: TaskQueryResult; + }; } export enum TasksAction { SCHEDULE_TASK = "SCHEDULE_TASK", - DELETE_TASK = "DELETE_TASK" + DELETE_TASK = "DELETE_TASK", + TASKS_FETCH_START = "TASKS_FETCH_START", + TASKS_FETCH_OK = "TASKS_FETCH_OK", + TASKS_FETCH_ERROR = "TASKS_FETCH_ERROR" } interface ScheduleTaskAction { - type: typeof TasksAction.SCHEDULE_TASK; + type: TasksAction.SCHEDULE_TASK; id: string; task: Task; } interface DeleteTaskAction { - type: typeof TasksAction.DELETE_TASK; + type: TasksAction.DELETE_TASK; id: string; } -export type TasksActionType = ScheduleTaskAction | DeleteTaskAction; +interface TaskFetchStartAction { + type: TasksAction.TASKS_FETCH_START; + query: TaskQuery; + started: string; +} + +interface TaskFetchOkAction { + type: TasksAction.TASKS_FETCH_OK; + query: TaskQuery; + results: { [key: string]: Task }; + fetched: string; +} + +interface TaskFetchErrorAction { + type: TasksAction.TASKS_FETCH_ERROR; + query: TaskQuery; + error: string; + fetched: string; +} + +export type TasksActionType = + | ScheduleTaskAction + | DeleteTaskAction + | TaskFetchOkAction + | TaskFetchStartAction + | TaskFetchErrorAction; diff --git a/src/frontend/tslint.json b/src/frontend/tslint.json index e5414e7..8ac9f45 100644 --- a/src/frontend/tslint.json +++ b/src/frontend/tslint.json @@ -1,6 +1,15 @@ { - "extends": "../../tslint.json", - "rules": { - "no-implicit-dependencies": [true, "dev", ["@kredens/frontend"]] - } -} \ No newline at end of file + "extends": "../../tslint.json", + "rules": { + "no-implicit-dependencies": [ + true, + "dev", + ["@kredens/frontend"] + ], + "no-submodule-imports": [ + true, + "@kredens/frontend", + "redux-saga/effects" + ] + } +} diff --git a/tslint.json b/tslint.json index 821e80c..eaed67f 100644 --- a/tslint.json +++ b/tslint.json @@ -1,19 +1,23 @@ { - "defaultSeverity": "error", - "extends": ["tslint:latest", "tslint-config-prettier"], - "jsRules": {}, - "rules": { - "interface-name": [true, "never-prefix"], - "no-submodule-imports": [ - true, - "graphql/language", - "graphql/type", - "vue-loader/lib/plugin", - "@kredens" - ], - "no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]], - "object-literal-sort-keys": [true, "match-declaration-order-only", "shorthand-first"], - "max-classes-per-file": false - }, - "rulesDirectory": [] + "defaultSeverity": "error", + "extends": ["tslint:latest", "tslint-config-prettier"], + "jsRules": {}, + "rules": { + "interface-name": [true, "never-prefix"], + "no-submodule-imports": [ + true, + "graphql/language", + "graphql/type", + "vue-loader/lib/plugin", + "@kredens" + ], + "no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]], + "object-literal-sort-keys": [ + true, + "match-declaration-order-only", + "shorthand-first" + ], + "max-classes-per-file": false + }, + "rulesDirectory": [] }