From 74aaf91bca9a81dadce231005414e5a30f9c65e8 Mon Sep 17 00:00:00 2001 From: Jitendra Gundaniya Date: Tue, 6 Aug 2024 16:17:39 +0100 Subject: [PATCH 1/6] Node click callback added Signed-off-by: Jitendra Gundaniya --- src/components/app/app.js | 6 +++++- src/store/middleware.js | 19 +++++++++++++++++++ src/store/store.js | 8 ++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/store/middleware.js diff --git a/src/components/app/app.js b/src/components/app/app.js index 2eb3a2abe6..b694af1b60 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -25,7 +25,11 @@ class App extends React.Component { constructor(props) { super(props); const initialState = getInitialState(props); - this.store = configureStore(initialState, this.props.data); + this.store = configureStore( + initialState, + this.props.data, + this.props.onNodeClickCallback + ); } componentDidMount() { diff --git a/src/store/middleware.js b/src/store/middleware.js new file mode 100644 index 0000000000..f068b34acf --- /dev/null +++ b/src/store/middleware.js @@ -0,0 +1,19 @@ +const createCallbackMiddleware = + (callback) => (store) => (next) => (action) => { + if ( + action.type === 'TOGGLE_NODE_CLICKED' && + action.nodeClicked && + callback + ) { + const state = store.getState(); + const node = state?.graph?.nodes?.find( + (node) => node.id === action.nodeClicked + ); + if (node) { + callback(node); + } + } + return next(action); + }; + +export default createCallbackMiddleware; diff --git a/src/store/store.js b/src/store/store.js index 6b20e60c3e..fd837d83cb 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -6,6 +6,7 @@ import { getGraphInput } from '../selectors/layout'; import { calculateGraph } from '../actions/graph'; import { saveLocalStorage, pruneFalseyKeys } from './helpers'; import { localStorageName, localStorageRunsMetadata } from '../config'; +import createCallbackMiddleware from './middleware'; /** * Watch the getGraphInput selector, and dispatch an asynchronous action to @@ -70,15 +71,18 @@ const saveStateToLocalStorage = (state) => { * Configure initial state and create the Redux store * @param {Object} initialState Initial Redux state (from initial-state.js) * @param {Object} dataType type of pipeline data - "static" or "json" (if data is loaded from API) + * @param {Function} onNodeClick Callback function to handle node click event from React component as prop * @return {Object} Redux store */ -export default function configureStore(initialState, dataType) { +export default function configureStore(initialState, dataType, onNodeClick) { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + + const callbackMiddleware = createCallbackMiddleware(onNodeClick); const store = createStore( reducer, initialState, - composeEnhancers(applyMiddleware(thunk)) + composeEnhancers(applyMiddleware(thunk, callbackMiddleware)) ); // dispatch the calculateGraph action to ensure the graph nodes still gets rendered From f3234ba77e1714b2c2407ad8ced68858034514c7 Mon Sep 17 00:00:00 2001 From: Jitendra Gundaniya Date: Tue, 6 Aug 2024 17:12:29 +0100 Subject: [PATCH 2/6] Release note added Signed-off-by: Jitendra Gundaniya --- RELEASE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 724ac54768..4f9c49c838 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,6 +5,14 @@ Please follow the established format: - Use present tense (e.g. 'Add new feature') - Include the ID number for the related PR (or PRs) in parentheses --> +# Release 9.3.0 + +## Major features and improvements + +- Introduce `onNodeClickCallback` prop in Kedro-Viz react component. (#2022) + +## Bug fixes and other changes + # Release 9.2.0 ## Major features and improvements From 7b8bcf36f272470ad03d00c47f5542691a2de7c6 Mon Sep 17 00:00:00 2001 From: Jitendra Gundaniya Date: Tue, 6 Aug 2024 17:24:58 +0100 Subject: [PATCH 3/6] middleware test added Signed-off-by: Jitendra Gundaniya --- src/store/middleware.test.js | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/store/middleware.test.js diff --git a/src/store/middleware.test.js b/src/store/middleware.test.js new file mode 100644 index 0000000000..7413530392 --- /dev/null +++ b/src/store/middleware.test.js @@ -0,0 +1,73 @@ +import createCallbackMiddleware from './middleware'; + +describe('createCallbackMiddleware', () => { + let store; + let next; + let callback; + let middleware; + + beforeEach(() => { + store = { + getState: jest.fn(), + }; + next = jest.fn(); + callback = jest.fn(); + middleware = createCallbackMiddleware(callback)(store)(next); + }); + + it('should call the callback with the correct node when action type is TOGGLE_NODE_CLICKED and node is found', () => { + const nodeClicked = '123'; + const node = { id: nodeClicked, name: 'Node 123' }; + const action = { type: 'TOGGLE_NODE_CLICKED', nodeClicked }; + + store.getState.mockReturnValue({ + graph: { + nodes: [node], + }, + }); + + middleware(action); + + expect(callback).toHaveBeenCalledWith(node); + expect(next).toHaveBeenCalledWith(action); + }); + + it('should not call the callback if action type is different', () => { + const action = { type: 'OTHER_ACTION', nodeClicked: '123' }; + + middleware(action); + + expect(callback).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(action); + }); + + it('should not call the callback if node is not found', () => { + const action = { type: 'TOGGLE_NODE_CLICKED', nodeClicked: '123' }; + + store.getState.mockReturnValue({ + graph: { + nodes: [{ id: '456', name: 'Node 456' }], + }, + }); + + middleware(action); + + expect(callback).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(action); + }); + + it('should not call the callback if callback is not provided', () => { + middleware = createCallbackMiddleware(null)(store)(next); + const action = { type: 'TOGGLE_NODE_CLICKED', nodeClicked: '123' }; + + store.getState.mockReturnValue({ + graph: { + nodes: [{ id: '123', name: 'Node 123' }], + }, + }); + + middleware(action); + + expect(next).toHaveBeenCalledWith(action); + }); +}); From ed97780a17f344ecdc8f28d42df1f10150158170 Mon Sep 17 00:00:00 2001 From: Jitendra Gundaniya Date: Fri, 16 Aug 2024 14:55:39 +0100 Subject: [PATCH 4/6] Generic callback added for any type of actions Signed-off-by: Jitendra Gundaniya --- RELEASE.md | 2 +- src/components/app/app.js | 2 +- src/store/middleware.js | 54 ++++++++++++++++++++++++++++++--------- src/store/store.js | 10 +++++--- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 4f9c49c838..f639e868ed 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -9,7 +9,7 @@ Please follow the established format: ## Major features and improvements -- Introduce `onNodeClickCallback` prop in Kedro-Viz react component. (#2022) +- Introduce `onActionCallback` prop in Kedro-Viz react component. (#2022) ## Bug fixes and other changes diff --git a/src/components/app/app.js b/src/components/app/app.js index b694af1b60..340dc1e44e 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -28,7 +28,7 @@ class App extends React.Component { this.store = configureStore( initialState, this.props.data, - this.props.onNodeClickCallback + this.props.onActionCallback ); } diff --git a/src/store/middleware.js b/src/store/middleware.js index f068b34acf..0ebb7b76aa 100644 --- a/src/store/middleware.js +++ b/src/store/middleware.js @@ -1,17 +1,47 @@ +import { TOGGLE_NODE_CLICKED } from '../actions/nodes'; + +/** + * Middleware to handle custom callback actions in the Redux store. + * + * This middleware intercepts actions dispatched to the Redux store and checks for the action type. + * If the action type matches and callback function is provided, + * Then it invokes the provided callback function with type and payload. + * + * @param {Function} callback - The callback function to be invoked when the specified action is dispatched. + * @returns {Function} - A middleware function to be used in the Redux store. + */ const createCallbackMiddleware = (callback) => (store) => (next) => (action) => { - if ( - action.type === 'TOGGLE_NODE_CLICKED' && - action.nodeClicked && - callback - ) { - const state = store.getState(); - const node = state?.graph?.nodes?.find( - (node) => node.id === action.nodeClicked - ); - if (node) { - callback(node); - } + if (!callback) { + return next(action); + } + + switch (action.type) { + case TOGGLE_NODE_CLICKED: + if (action.nodeClicked) { + const state = store.getState(); + const node = state?.graph?.nodes?.find( + (node) => node.id === action.nodeClicked + ); + if (node) { + const nodeClickAction = { + type: TOGGLE_NODE_CLICKED, + payload: node, + }; + callback(nodeClickAction); + } + } + break; + /** + * Add additional cases here to handle other action types. + * Ensure on whatever action you want to trigger, It should be a Redux action. + * And payload should be in Redux state. + * Example: + * const action = { type: SLICE_PIPELINE, payload: runCommand }; + callback(action); + */ + default: + break; } return next(action); }; diff --git a/src/store/store.js b/src/store/store.js index fd837d83cb..a532a1b988 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -71,14 +71,18 @@ const saveStateToLocalStorage = (state) => { * Configure initial state and create the Redux store * @param {Object} initialState Initial Redux state (from initial-state.js) * @param {Object} dataType type of pipeline data - "static" or "json" (if data is loaded from API) - * @param {Function} onNodeClick Callback function to handle node click event from React component as prop + * @param {Function} onActionCallback Callback function to be called on specific action * @return {Object} Redux store */ -export default function configureStore(initialState, dataType, onNodeClick) { +export default function configureStore( + initialState, + dataType, + onActionCallback +) { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const callbackMiddleware = createCallbackMiddleware(onNodeClick); + const callbackMiddleware = createCallbackMiddleware(onActionCallback); const store = createStore( reducer, initialState, From 8272a488cdbc7c635c50232b598a0faa31a735e0 Mon Sep 17 00:00:00 2001 From: Jitendra Gundaniya Date: Fri, 16 Aug 2024 15:00:54 +0100 Subject: [PATCH 5/6] Test fix Signed-off-by: Jitendra Gundaniya --- src/store/middleware.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/store/middleware.test.js b/src/store/middleware.test.js index 7413530392..eebdbc9f9d 100644 --- a/src/store/middleware.test.js +++ b/src/store/middleware.test.js @@ -18,6 +18,7 @@ describe('createCallbackMiddleware', () => { it('should call the callback with the correct node when action type is TOGGLE_NODE_CLICKED and node is found', () => { const nodeClicked = '123'; const node = { id: nodeClicked, name: 'Node 123' }; + const nodeClickAction = { type: 'TOGGLE_NODE_CLICKED', payload: node }; const action = { type: 'TOGGLE_NODE_CLICKED', nodeClicked }; store.getState.mockReturnValue({ @@ -28,7 +29,7 @@ describe('createCallbackMiddleware', () => { middleware(action); - expect(callback).toHaveBeenCalledWith(node); + expect(callback).toHaveBeenCalledWith(nodeClickAction); expect(next).toHaveBeenCalledWith(action); }); From d08aa565ea62a2e80949b86066bad92bfc94770c Mon Sep 17 00:00:00 2001 From: Jitendra Gundaniya Date: Thu, 22 Aug 2024 10:20:36 +0100 Subject: [PATCH 6/6] Doc added Signed-off-by: Jitendra Gundaniya --- README.npm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.npm.md b/README.npm.md index c271389115..038bf75e46 100644 --- a/README.npm.md +++ b/README.npm.md @@ -149,6 +149,7 @@ The example below demonstrates how to configure your kedro-viz using different ` | Name | Type | Default | Description | | ------------ | ------- | ------- | ----------- | | `data` | `{ edges: array (required), layers: array, nodes: array (required), tags: array }` | - | Pipeline data to be displayed on the chart | +| `onActionCallback` | function | - | Callback function to be invoked when the specified action is dispatched. e.g. `const action = { type: NODE_CLICK, payload: node }; onActionCallback(action);` | | options.display | | | | | `expandPipelinesBtn` | boolean | true | Show/Hide expand pipelines button | | `exportBtn` | boolean | true | Show/Hide export button | @@ -166,7 +167,8 @@ The example below demonstrates how to configure your kedro-viz using different ` ### Note -When `display.sidebar` is `false`, `display.miniMap` prop will be ignored. +- `onActionCallback` callback is only called when the user clicks on a node in the flowchart, and we are passing the node object as the payload in the callback argument. In future releases, we will add more actions to be dispatched in this callback. +- When `display.sidebar` is `false`, `display.miniMap` prop will be ignored. All components are annotated to understand their positions in the Kedro-Viz UI.