Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce onActionCallback prop in Kedro-Viz react component #2027

Merged
merged 10 commits into from
Aug 22, 2024
4 changes: 3 additions & 1 deletion README.npm.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
| 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 |
Expand All @@ -166,7 +167,8 @@


### 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.

Check warning on line 170 in README.npm.md

View workflow job for this annotation

GitHub Actions / vale

[vale] README.npm.md#L170

[Kedro-viz.weaselwords] 'only' is a weasel word!
Raw output
{"message": "[Kedro-viz.weaselwords] 'only' is a weasel word!", "location": {"path": "README.npm.md", "range": {"start": {"line": 170, "column": 34}}}, "severity": "WARNING"}
- When `display.sidebar` is `false`, `display.miniMap` prop will be ignored.

All components are annotated to understand their positions in the Kedro-Viz UI.

Expand Down
4 changes: 4 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Please follow the established format:
-->
# Upcoming Release

## Major features and improvements

- Introduce `onActionCallback` prop in Kedro-Viz react component. (#2022)

## Bug fixes and other changes

- Fixes design issues in metadata panel. (#2009)
Expand Down
6 changes: 5 additions & 1 deletion src/components/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.onActionCallback
);
}

componentDidMount() {
Expand Down
49 changes: 49 additions & 0 deletions src/store/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 =
jitu5 marked this conversation as resolved.
Show resolved Hide resolved
(callback) => (store) => (next) => (action) => {
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);
};

export default createCallbackMiddleware;
74 changes: 74 additions & 0 deletions src/store/middleware.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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 nodeClickAction = { type: 'TOGGLE_NODE_CLICKED', payload: node };
const action = { type: 'TOGGLE_NODE_CLICKED', nodeClicked };

store.getState.mockReturnValue({
graph: {
nodes: [node],
},
});

middleware(action);

expect(callback).toHaveBeenCalledWith(nodeClickAction);
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);
});
});
12 changes: 10 additions & 2 deletions src/store/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,15 +71,22 @@ 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} onActionCallback Callback function to be called on specific action
* @return {Object} Redux store
*/
export default function configureStore(initialState, dataType) {
export default function configureStore(
initialState,
dataType,
onActionCallback
) {
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const callbackMiddleware = createCallbackMiddleware(onActionCallback);
const store = createStore(
reducer,
initialState,
composeEnhancers(applyMiddleware(thunk))
composeEnhancers(applyMiddleware(thunk, callbackMiddleware))
);

// dispatch the calculateGraph action to ensure the graph nodes still gets rendered
Expand Down
Loading