From 1de27f16bb006e99ae503ce19d188800938aff16 Mon Sep 17 00:00:00 2001 From: SalmanDeveloper786 Date: Sun, 20 Nov 2022 15:08:09 +0500 Subject: [PATCH] salman-coding-task-20-nov --- src/components/Node.tsx | 52 ++++++++++++- src/constants/colors.js | 1 + src/containers/Nodes.spec.tsx | 7 +- src/reducers/blocks.spec.ts | 138 ++++++++++++++++++++++++++++++++++ src/reducers/blocks.ts | 46 ++++++++++++ src/reducers/initialState.ts | 8 +- src/reducers/nodes.ts | 2 + src/store/configureStore.ts | 2 + src/store/store.spec.ts | 33 ++++++-- 9 files changed, 279 insertions(+), 10 deletions(-) create mode 100644 src/reducers/blocks.spec.ts create mode 100644 src/reducers/blocks.ts diff --git a/src/components/Node.tsx b/src/components/Node.tsx index 065e5fc..a5428ce 100644 --- a/src/components/Node.tsx +++ b/src/components/Node.tsx @@ -11,6 +11,8 @@ import { styled } from "@mui/material/styles"; import colors from "../constants/colors"; import Status from "./Status"; import { Node as NodeType } from "../types/Node"; +import { useDispatch, useSelector } from "react-redux"; +import { getBlocksForNode } from "../reducers/blocks"; type Props = { node: NodeType; @@ -46,6 +48,30 @@ const BoxSummaryContent = styled(Box)({ paddingRight: 20, }); +const Blocks = styled(Box)({ + display: "flex", + flexDirection: "column", + width: "100%", + backgroundColor:"#e0e0e0", + marginTop:3, + padding:5, + borderRadius:2 +}); + +const BlocksID = styled(Typography)({ + fontSize: 10, + display: "block", + color: colors.blue, + lineHeight: 1.5, +}); + +const BlocksText = styled(Typography)({ + fontSize: 17, + display: "block", + color: colors.text, + lineHeight: 1.5, +}); + const TypographyHeading = styled(Typography)({ fontSize: 17, display: "block", @@ -60,11 +86,17 @@ const TypographySecondaryHeading = styled(Typography)(({ theme }) => ({ })); const Node: React.FC = ({ node, expanded, toggleNodeExpanded }) => { + const dispatch=useDispatch() + const state=useSelector((state:any)=>state.blocks); + console.log('State',state) return ( toggleNodeExpanded(node)} + onChange={() => { + dispatch(getBlocksForNode(node)) + toggleNodeExpanded(node) + }} > }> @@ -80,7 +112,23 @@ const Node: React.FC = ({ node, expanded, toggleNodeExpanded }) => { - Blocks go here + {state.isLoading? + Loading ... + : + state.allBlocks ? state.allBlocks?.data?.map((data:any,id:any)=>( + + + 00{id} + + + {data?.attributes?.data} + + + )) + : + There is no Blocks + + } ); diff --git a/src/constants/colors.js b/src/constants/colors.js index 3c2127a..cd8796d 100644 --- a/src/constants/colors.js +++ b/src/constants/colors.js @@ -8,6 +8,7 @@ const colors = { contentBackground: "#f8f8f8", border: "#aaaaaa", white: "#ffffff", + blue:"#304FFE" }; export default colors; diff --git a/src/containers/Nodes.spec.tsx b/src/containers/Nodes.spec.tsx index cc9d740..5ff1ad2 100644 --- a/src/containers/Nodes.spec.tsx +++ b/src/containers/Nodes.spec.tsx @@ -26,11 +26,16 @@ describe("", () => { ], }; + const blocks={ + isLoading:false, + allBlocks:{"id":"5","type":"blocks","attributes":{"index":1,"timestamp":1530679678,"data":"The Human Car","previous-hash":"KsmmdGrKVDr43/OYlM/oFzr7oh6wHG+uM9UpRyIoVe8=","hash":"oHkxOJWOKy02vA9r4iRHVqTgqT+Afc6OYFcNYzyhGEc="}} + } + let store: MockStoreEnhanced; function setup(): JSX.Element { const middlewares = [thunk]; - store = configureMockStore(middlewares)({ nodes }); + store = configureMockStore(middlewares)({ nodes,blocks }); return ( diff --git a/src/reducers/blocks.spec.ts b/src/reducers/blocks.spec.ts new file mode 100644 index 0000000..29c7941 --- /dev/null +++ b/src/reducers/blocks.spec.ts @@ -0,0 +1,138 @@ +import mockFetch from "cross-fetch"; +import reducer, { getBlocksForNode } from "./blocks"; +import { Node } from "../types/Node"; +import initialState from "./initialState"; + +jest.mock("cross-fetch"); + +const mockedFech: jest.Mock = mockFetch as any; + +describe("Reducers::Nodes", () => { + const getInitialState = () => { + return initialState().blocks; + }; + + const nodeA: Node = { + url: "http://localhost:3002", + online: false, + name: "Node 1", + loading: false, + }; + + const blocks={"id":"5","type":"blocks","attributes":{"index":1,"timestamp":1530679678,"data":"The Human Car","previous-hash":"KsmmdGrKVDr43/OYlM/oFzr7oh6wHG+uM9UpRyIoVe8=","hash":"oHkxOJWOKy02vA9r4iRHVqTgqT+Afc6OYFcNYzyhGEc="}} + + + it("should set initial state by default", () => { + const action = { type: "unknown" }; + const expected = getInitialState(); + + expect(reducer(undefined, action)).toEqual(expected); + }); + + it("should handle getBlocksForNode.pending", () => { + const appState = { + isLoading:true, + allBlocks:null + }; + const action = { type: getBlocksForNode.pending, meta: { arg: nodeA } }; + const expected = { + isLoading:true, + allBlocks:null + }; + + expect(reducer(appState, action)).toEqual(expected); + }); + + it("should handle getBlocksForNode.fulfilled", () => { + const appState = { + isLoading:false, + allBlocks:blocks + }; + const action = { + type: getBlocksForNode.fulfilled, + meta: { arg: nodeA }, + payload: blocks, + }; + const expected = { + isLoading:false, + allBlocks:blocks + }; + + expect(reducer(appState, action)).toEqual(expected); + }); + + it("should handle getBlocksForNode.rejected", () => { + const appState = { + isLoading:false, + allBlocks:null + }; + const action = { type: getBlocksForNode.rejected, meta: { arg: nodeA } }; + const expected = { + isLoading:false, + allBlocks:null + }; + + expect(reducer(appState, action)).toEqual(expected); + }); +}); + +describe("Actions::Nodes", () => { + const dispatch = jest.fn(); + + afterAll(() => { + dispatch.mockClear(); + mockedFech.mockClear(); + }); + + const node: Node = { + url: "http://localhost:3002", + online: false, + name: "Node 1", + loading: false, + }; + const blocks={"id":"5","type":"blocks","attributes":{"index":1,"timestamp":1530679678,"data":"The Human Car","previous-hash":"KsmmdGrKVDr43/OYlM/oFzr7oh6wHG+uM9UpRyIoVe8=","hash":"oHkxOJWOKy02vA9r4iRHVqTgqT+Afc6OYFcNYzyhGEc="}} + + + it("should fetch the node blocks", async () => { + mockedFech.mockReturnValueOnce( + Promise.resolve({ + status: 200, + json() { + return Promise.resolve(blocks); + }, + }) + ); + await getBlocksForNode(node)(dispatch, () => {}, {}); + + const expected = expect.arrayContaining([ + expect.objectContaining({ + type: getBlocksForNode.pending.type, + meta: expect.objectContaining({ arg: node }), + }), + expect.objectContaining({ + type: getBlocksForNode.fulfilled.type, + meta: expect.objectContaining({ arg: node }), + payload: blocks, + }), + ]); + expect(dispatch.mock.calls.flat()).toEqual(expected); + }); + + it("should fail to fetch the node blocks", async () => { + mockedFech.mockReturnValueOnce(Promise.reject(new Error("Network Error"))); + await getBlocksForNode(node)(dispatch, () => {}, {}); + const expected = expect.arrayContaining([ + expect.objectContaining({ + type: getBlocksForNode.pending.type, + meta: expect.objectContaining({ arg: node }), + }), + expect.objectContaining({ + type: getBlocksForNode.rejected.type, + meta: expect.objectContaining({ arg: node }), + error: expect.objectContaining({ message: "Network Error" }), + }), + ]); + + expect(dispatch.mock.calls.flat()).toEqual(expected); + }); +}); diff --git a/src/reducers/blocks.ts b/src/reducers/blocks.ts new file mode 100644 index 0000000..0f57d0d --- /dev/null +++ b/src/reducers/blocks.ts @@ -0,0 +1,46 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import initialState from "./initialState"; +import { Node } from "../types/Node"; +import { RootState } from "../store/configureStore"; +import fetch from "cross-fetch"; + +export interface BlocksState { + isLoading:Boolean, + allBlocks:any +} + +export const getBlocksForNode = createAsyncThunk( + "nodes/getBlocksForNode", + async (node: Node) => { + const response = await fetch(`${node.url}/api/v1/blocks`); + const data = await response.json(); + return data; + } +); + + +export const blocksSlice = createSlice({ + name: "blocks", + initialState: { + isLoading:false, + allBlocks:null + }, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(getBlocksForNode.pending, (state, action) => { + state.isLoading=true; + state.allBlocks=null + }); + builder.addCase(getBlocksForNode.fulfilled, (state, action) => { + state.isLoading=false; + state.allBlocks=action.payload + + }); + builder.addCase(getBlocksForNode.rejected, (state, action) => { + state.isLoading=false; + }); + }, +}); + +export const selectNodes = (state: RootState) => state.nodes.list; +export default blocksSlice.reducer; diff --git a/src/reducers/initialState.ts b/src/reducers/initialState.ts index e43e634..dc056a7 100644 --- a/src/reducers/initialState.ts +++ b/src/reducers/initialState.ts @@ -14,9 +14,9 @@ const initialState = () => ({ loading: false, }, { - url: "https://servername.herokuapp.com", // Deployed local Server + url: "https://ancient-headland-67857.herokuapp.com", // Deployed local Server online: false, - name: "Node 3", + name: "Demo Node", loading: false, }, { @@ -27,5 +27,9 @@ const initialState = () => ({ }, ], }, + blocks:{ + isLoading:false, + allBlocks:null + } }); export default initialState; diff --git a/src/reducers/nodes.ts b/src/reducers/nodes.ts index 32ded6f..f217272 100644 --- a/src/reducers/nodes.ts +++ b/src/reducers/nodes.ts @@ -37,6 +37,8 @@ export const nodesSlice = createSlice({ if (node) node.loading = true; }); builder.addCase(checkNodeStatus.fulfilled, (state, action) => { + + console.log("action.meta.arg.url",action.meta.arg.url) const node = state.list.find((n) => n.url === action.meta.arg.url); if (node) { node.online = true; diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts index 82f48c0..4ba4e2f 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -1,10 +1,12 @@ import { configureStore } from "@reduxjs/toolkit"; import { TypedUseSelectorHook, useSelector } from "react-redux"; import nodesReducer from "../reducers/nodes"; +import blocksReducer from "../reducers/blocks"; export const store = configureStore({ reducer: { nodes: nodesReducer, + blocks: blocksReducer, }, }); diff --git a/src/store/store.spec.ts b/src/store/store.spec.ts index 9f71c82..19d490a 100644 --- a/src/store/store.spec.ts +++ b/src/store/store.spec.ts @@ -1,6 +1,8 @@ import { AnyAction, configureStore, EnhancedStore } from "@reduxjs/toolkit"; import { ThunkMiddleware } from "redux-thunk"; +import { BlocksState, getBlocksForNode } from "../reducers/blocks"; import nodesReducer, { checkNodeStatus, NodesState } from "../reducers/nodes"; +import blocksReducer from "../reducers/blocks"; describe("Store", () => { const nodes = { @@ -12,12 +14,22 @@ describe("Store", () => { ], }; + const blockData={"id":"5","type":"blocks","attributes":{"index":1,"timestamp":1530679678,"data":"The Human Car","previous-hash":"KsmmdGrKVDr43/OYlM/oFzr7oh6wHG+uM9UpRyIoVe8=","hash":"oHkxOJWOKy02vA9r4iRHVqTgqT+Afc6OYFcNYzyhGEc="}} + const blocks={ + isLoading:false, + allBlocks:null + } + let store: EnhancedStore< - { nodes: NodesState }, + { nodes: NodesState, + blocks:BlocksState + }, AnyAction, [ | ThunkMiddleware<{ nodes: NodesState }, AnyAction, null> | ThunkMiddleware<{ nodes: NodesState }, AnyAction, undefined> + | ThunkMiddleware<{ blocks: BlocksState }, AnyAction, null> + | ThunkMiddleware<{ blocks: BlocksState }, AnyAction, undefined> ] >; @@ -25,8 +37,9 @@ describe("Store", () => { store = configureStore({ reducer: { nodes: nodesReducer, + blocks: blocksReducer, }, - preloadedState: { nodes }, + preloadedState: { nodes,blocks }, }); }); afterAll(() => {}); @@ -73,19 +86,29 @@ describe("Store", () => { meta: { arg: nodes.list[0] }, payload: { node_name: "theta" }, }, + { + type: getBlocksForNode.fulfilled.type, + meta: { arg: nodes }, + payload: blockData, + }, ]; actions.forEach((action) => store.dispatch(action)); const actual = store.getState(); - const expected = { + const expected = {nodes:{ list: [ { url: "a.com", online: true, name: "theta", loading: false }, { url: "b.com", online: true, name: "epsilon", loading: false }, { url: "c.com", online: true, name: "delta", loading: false }, { url: "d.com", online: false, name: "", loading: false }, ], - }; + }, + blocks:{ + isLoading:false, + allBlocks:blockData + } + }; - expect(actual.nodes).toEqual(expected); + expect(actual).toEqual(expected); }); });