Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[KED-1650] New graphing algorithm & feature flags (#185)
* Remove unnecessary null props

* Don't use a button for node-list-row when !onClick

There's no onClick event, so the button element is unnecessary

* Remove unused modifier classes

* Add --button modifier class for row hover state

* Combine duplicate classes and remove unused

* Add new selected node state

This to distinguish clicked (i.e. selected) from active (i.e. hovered/focused)

* Improve consistency of row transitions

* Prevent a node being selected when disabled

* Remove node clicked state when disabled

* Fix node-list-row button styling

* Use disabled button instead of text

* Fix icon opacity when disabled

* Fix tests

* Add a new test for TOGGLE_NODES_DISABLED reducer

Check that it sets nodeClicked to null if the selected node is being disabled

* Fix node-list hover/selected bg colours

* [KED-989] Added new graphing approach concept

* [KED-1650] Refactor and tidy graph code

* [KED-1650] Improve graph concept

* [KED-1650] Refactor graph concept

* [KED-1650] Refactor graph concept

* [KED-1698] Add layers to graph concept and improve layout

* [KED-1697] Added feature flags with flag for graph concept

* [KED-1650] Refactoring, add comments and docs

* [KED-1650] Added tests and some refactoring

* [KED-1650] Fix exception object

* [KED-1698] Fix layer constraints

* [KED-1650]  Change flags URL format

* [KED-1650]  Refactor and simplify

* [KED-1650]  Improve code clarity

* [KED-1650] Add empty value for flags

* [KED-1650] Improve routing

* [KED-1698] Fix layer solving

* [KED-1650] Update readme with flags

* [KED-1650] Update license info

* [KED-1650] Update readme

* [KED-1650] Update license info

* [KED-1650] Update license info

* [KED-1650] Update readme

* [KED-1650] Improve clarity

Co-authored-by: Richard Westenra <rjwestenra@gmail.com>
  • Loading branch information
bru5 and richardwestenra committed Jun 23, 2020
1 parent 7351f68 commit 5ed4903
Show file tree
Hide file tree
Showing 28 changed files with 2,045 additions and 73 deletions.
71 changes: 71 additions & 0 deletions COPYING.txt
@@ -0,0 +1,71 @@
=========================
The Kiwi licensing terms
=========================
Kiwi is licensed under the terms of the Modified BSD License (also known as
New or Revised BSD), as follows:

Copyright (c) 2013, Nucleic Development Team

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.

Neither the name of the Nucleic Development Team nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

About Kiwi
----------
Chris Colbert began the Kiwi project in December 2013 in an effort to
create a blisteringly fast UI constraint solver. Chris is still the
project lead.

The Nucleic Development Team is the set of all contributors to the Nucleic
project and its subprojects.

The core team that coordinates development on GitHub can be found here:
http://github.com/nucleic. The current team consists of:

* Chris Colbert

Our Copyright Policy
--------------------
Nucleic uses a shared copyright model. Each contributor maintains copyright
over their contributions to Nucleic. But, it is important to note that these
contributions are typically only changes to the repositories. Thus, the Nucleic
source code, in its entirety is not the copyright of any single person or
institution. Instead, it is the collective copyright of the entire Nucleic
Development Team. If individual contributors want to maintain a record of what
changes/contributions they have specific copyright on, they should indicate
their copyright in the commit message of the change, when they commit the
change to one of the Nucleic repositories.

With this in mind, the following banner should be used in any source code file
to indicate the copyright and license terms:

#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team & H. Rutjes.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
18 changes: 18 additions & 0 deletions README.md
Expand Up @@ -100,6 +100,24 @@ As a JavaScript React component, the project is designed to be used in two diffe

The React component exposes props that can be used to supply data and customise its behaviour. For information about the props, their expected prop-types and default values, see [/src/components/app/index.js](https://github.com/quantumblacklabs/kedro-viz/blob/master/src/components/app/index.js). For examples of the expected data input format, see the mock data examples in [/src/utils/data](https://github.com/quantumblacklabs/kedro-viz/tree/master/src/utils/data), and compare the [resulting demo](https://quantumblacklabs.github.io/kedro-viz/).

## Flags

The following flags are available to toggle experimental features:

- `newgraph` - From release v3.4.0. Improved graphing algorithm (default `false`).

### Setting flags

To enable or disable a flagged feature, add the flag as a parameter with the value `true` or `false` to the end of the URL in your browser when running Kedro-Viz, e.g.

`http://localhost:4141/?data=demo&newgraph=true`

The setting you provide persists for all sessions on your machine, until you change it.

### Viewing flags

Kedro-Viz will log a message in your browser's [developer console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools#The_JavaScript_console) regarding the available flags and their values as currently set on your machine.

## What licence do you use?

Kedro-Viz is licensed under the [Apache 2.0](https://github.com/quantumblacklabs/kedro-viz/blob/master/LICENSE.md) License.
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -41,6 +41,7 @@
"d3-transition": "^1.2.0",
"d3-zoom": "^1.7.3",
"dagre": "git+https://github.com/richardwestenra/dagre.git#manual-ranking",
"kiwi.js": "^1.1.2",
"konami-code": "^0.2.1",
"react-custom-scrollbars": "^4.2.1",
"react-flip-toolkit": "^7.0.7",
Expand Down
11 changes: 11 additions & 0 deletions src/actions/actions.test.js
@@ -1,5 +1,6 @@
import animals from '../utils/data/animals.mock';
import {
CHANGE_FLAG,
RESET_DATA,
TOGGLE_LAYERS,
TOGGLE_EXPORT_MODAL,
Expand All @@ -8,6 +9,7 @@ import {
TOGGLE_THEME,
UPDATE_CHART_SIZE,
UPDATE_FONT_LOADED,
changeFlag,
resetData,
toggleLayers,
toggleExportModal,
Expand Down Expand Up @@ -174,4 +176,13 @@ describe('actions', () => {
};
expect(updateFontLoaded(fontLoaded)).toEqual(expectedAction);
});

it('should create an action to change a flag', () => {
const expectedAction = {
type: CHANGE_FLAG,
name: 'testFlag',
value: true
};
expect(changeFlag('testFlag', true)).toEqual(expectedAction);
});
});
15 changes: 15 additions & 0 deletions src/actions/index.js
Expand Up @@ -116,3 +116,18 @@ export function updateFontLoaded(fontLoaded) {
fontLoaded
};
}

export const CHANGE_FLAG = 'CHANGE_FLAG';

/**
* Change the given feature flag
* @param {string} name The flag name
* @param {value} value The value to set
*/
export function changeFlag(name, value) {
return {
type: CHANGE_FLAG,
name,
value
};
}
9 changes: 9 additions & 0 deletions src/components/app/app.test.js
Expand Up @@ -4,6 +4,7 @@ import App from './index';
import getRandomPipeline from '../../utils/random-data';
import animals from '../../utils/data/animals.mock';
import loremIpsum from '../../utils/data/lorem-ipsum.mock';
import { Flags } from '../../utils/flags';

describe('App', () => {
describe('renders without crashing', () => {
Expand Down Expand Up @@ -35,6 +36,14 @@ describe('App', () => {
});
});

describe('feature flags', () => {
it('it announces flags', () => {
const announceFlags = jest.spyOn(App.prototype, 'announceFlags');
shallow(<App data={loremIpsum} />);
expect(announceFlags).toHaveBeenCalledWith(Flags.defaults());
});
});

describe('throws an error', () => {
it('when data prop is empty', () => {
expect(() => shallow(<App />)).toThrow();
Expand Down
13 changes: 13 additions & 0 deletions src/components/app/index.js
Expand Up @@ -8,6 +8,7 @@ import Wrapper from '../wrapper';
import getInitialState from '../../store/initial-state';
import loadData from '../../store/load-data';
import normalizeData from '../../store/normalize-data';
import { getFlagsMessage } from '../../utils/flags';
import '@quantumblack/kedro-ui/lib/styles/app.css';
import './app.css';

Expand All @@ -19,6 +20,7 @@ class App extends React.Component {
super(props);
const initialState = getInitialState(props);
this.store = configureStore(initialState);
this.announceFlags(initialState.flags);
}

componentDidMount() {
Expand All @@ -32,6 +34,17 @@ class App extends React.Component {
}
}

/**
* Shows a console message regarding the given flags
*/
announceFlags(flags) {
const message = getFlagsMessage(flags);

if (message && typeof jest === 'undefined') {
console.info(message);
}
}

/**
* Load data asynchronously from a JSON file then update the store
*/
Expand Down
8 changes: 8 additions & 0 deletions src/config.js
Expand Up @@ -8,3 +8,11 @@ export const sidebarWidth = {
open: 400,
closed: 60
};

export const flags = {
newgraph: {
description: 'Improved graphing algorithm',
default: false,
icon: '📈'
}
};
16 changes: 16 additions & 0 deletions src/reducers/flags.js
@@ -0,0 +1,16 @@
import { CHANGE_FLAG } from '../actions';

function flagsReducer(flagsState = {}, action) {
switch (action.type) {
case CHANGE_FLAG: {
return Object.assign({}, flagsState, {
[action.name]: action.value
});
}

default:
return flagsState;
}
}

export default flagsReducer;
2 changes: 2 additions & 0 deletions src/reducers/index.js
Expand Up @@ -3,6 +3,7 @@ import node from './nodes';
import tag from './tags';
import nodeType from './node-type';
import visible from './visible';
import flags from './flags';
import {
RESET_DATA,
TOGGLE_TEXT_LABELS,
Expand Down Expand Up @@ -46,6 +47,7 @@ const combinedReducer = combineReducers({
nodeType,
tag,
visible,
flags,
edge: (state = {}) => state,
id: (state = null) => state,
layer: (state = {}) => state,
Expand Down
12 changes: 12 additions & 0 deletions src/reducers/reducers.test.js
Expand Up @@ -4,6 +4,7 @@ import { mockState } from '../utils/state.mock';
import reducer from './index';
import normalizeData from '../store/normalize-data';
import {
CHANGE_FLAG,
RESET_DATA,
TOGGLE_LAYERS,
TOGGLE_SIDEBAR,
Expand Down Expand Up @@ -194,4 +195,15 @@ describe('Reducer', () => {
expect(newState.fontLoaded).toBe(true);
});
});

describe('CHANGE_FLAG', () => {
it('should update the state when a flag is changed', () => {
const newState = reducer(mockState.lorem, {
type: CHANGE_FLAG,
name: 'testFlag',
value: true
});
expect(newState.flags.testFlag).toBe(true);
});
});
});
11 changes: 11 additions & 0 deletions src/selectors/flags.js
@@ -0,0 +1,11 @@
import { createSelector } from 'reselect';

const getFlagsState = state => state.flags;

/**
* Get current flag status from state
*/
export const getCurrentFlags = createSelector(
[getFlagsState],
flags => ({ ...flags })
);
10 changes: 10 additions & 0 deletions src/selectors/flags.test.js
@@ -0,0 +1,10 @@
import { getCurrentFlags } from './flags';

describe('getCurrentFlags function', () => {
it('should return the current flags from state', () => {
const flags = getCurrentFlags({
flags: { mockFlagA: true, mockFlagB: false }
});
expect(flags).toEqual({ mockFlagA: true, mockFlagB: false });
});
});
57 changes: 30 additions & 27 deletions src/selectors/layers.js
Expand Up @@ -14,41 +14,44 @@ export const getLayers = createSelector(
return [];
}

// Get list of layer Y positions from nodes
const layerY = nodes.reduce((layerY, node) => {
if (!layerY[node.layer]) {
layerY[node.layer] = [];
const bounds = {};

for (const node of nodes) {
const layer = node.nearestLayer || node.layer;

if (layer) {
const bound = bounds[layer] || (bounds[layer] = [Infinity, -Infinity]);

if (node.y - node.height < bound[0]) {
bound[0] = node.y - node.height;
}

if (node.y + node.height > bound[1]) {
bound[1] = node.y + node.height;
}
}
layerY[node.layer].push(node.y);
return layerY;
}, {});

/**
* Determine the y position and height of a layer band
* @param {number} id
*/
const calculateYPos = (layerID, prevID, nextID) => {
const yMin = Math.min(...layerY[layerID]);
const yMax = Math.max(...layerY[layerID]);
const prev = layerY[prevID];
const next = layerY[nextID];
const topYGap = prev && yMin - Math.max(...prev);
const bottomYGap = next && Math.min(...next) - yMax;
const yGap = (topYGap || bottomYGap) / 2;
const y = yMin - yGap;
const height = yMax + yGap - y;
return { y, height };
};
}

return layerIDs.map((id, i) => {
const prevID = layerIDs[i - 1];
const nextID = layerIDs[i + 1];
const currentBound = bounds[id] || [0, 0];
const prevBound = bounds[layerIDs[i - 1]] || [
currentBound[0],
currentBound[0]
];
const nextBound = bounds[layerIDs[i + 1]] || [
currentBound[1],
currentBound[1]
];
const start = (prevBound[1] + currentBound[0]) / 2;
const end = (currentBound[1] + nextBound[0]) / 2;

return {
id,
name: layerName[id],
x: -width / 2,
y: start,
width: width * 2,
...calculateYPos(id, prevID, nextID)
height: Math.max(end - start, 0)
};
});
}
Expand Down

0 comments on commit 5ed4903

Please sign in to comment.