Skip to content
This repository has been archived by the owner on Dec 1, 2022. It is now read-only.

GeoBlock visual component #977

Merged
merged 68 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
b2c69ac
GeoBlockVisualFlow component with react-flow
hoanphungt Oct 7, 2021
d8ca185
build components
hoanphungt Oct 8, 2021
5ee6a29
Create eedges in createGraphLayout
hoanphungt Oct 8, 2021
fba18d1
Fix number blocks with same id
hoanphungt Oct 8, 2021
19bd226
Group block
hoanphungt Oct 8, 2021
df2e40b
Output block
hoanphungt Oct 8, 2021
1fab698
merge outputElement with blockElements
hoanphungt Oct 8, 2021
6531edd
Change input of input blocks
hoanphungt Oct 8, 2021
82a08c7
block components
hoanphungt Oct 8, 2021
f632dc7
Sidebar component
hoanphungt Oct 8, 2021
ae1757b
Drag and drop feature for new blocks
hoanphungt Oct 11, 2021
8e745de
small fix for raster block style
hoanphungt Oct 11, 2021
e000990
Improve GroupBlock component
hoanphungt Oct 11, 2021
b9296e9
JSON validator
hoanphungt Oct 11, 2021
e6cec93
save geoblock graph to json string
hoanphungt Oct 11, 2021
582973f
Add reset button
hoanphungt Oct 11, 2021
b1f394f
Change variable names
hoanphungt Oct 11, 2021
a86e15a
Add more blocks to type definition
hoanphungt Oct 12, 2021
61816a1
dynamic height for blocks based on number of inputs
hoanphungt Oct 12, 2021
9bcf73f
type of FillNoData block
hoanphungt Oct 12, 2021
fbe23f0
improve
hoanphungt Oct 12, 2021
a0f5aaa
refactor helper function onBlockValueChange
hoanphungt Oct 12, 2021
5a654f8
set output block dynamically
hoanphungt Oct 12, 2021
546c211
Fix a bug with number block
hoanphungt Oct 12, 2021
820b1d3
Check for number of output nodes in the graph
hoanphungt Oct 12, 2021
39361d7
styling
hoanphungt Oct 12, 2021
57c652e
hanlde output hanlders for group blocks
hoanphungt Oct 12, 2021
8a0d5bc
Add more blocks to the type definition
hoanphungt Oct 12, 2021
0b0fd23
Add title to handles and blocks
hoanphungt Oct 12, 2021
b39dbae
styling of blocks
hoanphungt Oct 12, 2021
ea4fda4
Edges with colors
hoanphungt Oct 12, 2021
2a35b4b
Boolean block
hoanphungt Oct 12, 2021
e0b80d2
improve styling
hoanphungt Oct 12, 2021
e5fc368
Improve styling of input fields
hoanphungt Oct 12, 2021
06a5fba
Get raster elements and block elements
hoanphungt Oct 12, 2021
084a74d
Add more blocks to type definition
hoanphungt Oct 13, 2021
f6e3627
add block class to title
hoanphungt Oct 13, 2021
00231e6
Remove demo components
hoanphungt Oct 13, 2021
c71b676
Refactor elements and setElements hooks to parent
hoanphungt Oct 14, 2021
a604c2e
Raster UUID validator
hoanphungt Oct 14, 2021
76ed436
Output block validation
hoanphungt Oct 14, 2021
82533d4
Fix title of GroupBlock
hoanphungt Oct 14, 2021
ff2a990
Prevent connect wrong type of block data
hoanphungt Oct 14, 2021
4739a98
Not allow to connect to target handle that is already used
hoanphungt Oct 14, 2021
af3ad9a
Fix reset view button to rebuild the geoblock source
hoanphungt Oct 14, 2021
6e86009
Remove reset view button
hoanphungt Oct 15, 2021
22efe3b
Test validate button
hoanphungt Oct 15, 2021
658aca7
target handle validator
hoanphungt Oct 15, 2021
931639d
Validate orphan parts of the block
hoanphungt Oct 15, 2021
9026317
Validation first
hoanphungt Oct 15, 2021
550a034
Add string blocks
hoanphungt Oct 18, 2021
cac937a
Add new string block
hoanphungt Oct 18, 2021
0da4152
Add title to block
hoanphungt Oct 18, 2021
c4a29d8
Handle array value input
hoanphungt Oct 19, 2021
36c687a
Handle wrong raster input block
hoanphungt Oct 19, 2021
1947f1a
Array block improvements
hoanphungt Oct 19, 2021
265cfcf
improvements
hoanphungt Oct 19, 2021
2f15ca3
Improve check for blocks to add to graph in convertElementsToGraph fu…
hoanphungt Oct 19, 2021
dc542d5
Build source
hoanphungt Oct 19, 2021
6a423dc
Validation with dry-run query
hoanphungt Oct 19, 2021
b2f2d9a
Release 0.2.84
hoanphungt Oct 19, 2021
945dfb6
Handle new geoblock case or geoblock with no value
hoanphungt Oct 19, 2021
5ca56f0
Release 0.2.85
hoanphungt Oct 19, 2021
1b8f0cc
reset elements to empty array when unmounted
hoanphungt Oct 19, 2021
50e5efc
add comment
hoanphungt Oct 19, 2021
ffe2274
Add minimap for very big geoblock
hoanphungt Oct 19, 2021
87fca2c
refactor geoblock validate function
hoanphungt Oct 20, 2021
94b74c0
remove the MVP
hoanphungt Oct 20, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lizard-management-client",
"version": "0.2.83",
"version": "0.2.85",
"private": true,
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions src/data_management/geoblocks/GeoBlockForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ const GeoBlockForm: React.FC<Props & DispatchProps & RouteComponentProps> = (pro
) : null}
{buildModal ? (
<GeoBlockBuildModal
uuid={currentRecord ? currentRecord.uuid : null}
source={values.source}
onChange={value => handleValueChange('source', value)}
handleClose={() => setBuildModal(false)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Elements } from 'react-flow-renderer';
import { GeoBlockJsonComponent } from './GeoBlockJsonComponent';
import { GeoBlockVisualComponent } from './GeoBlockVisualComponent';
import { SubmitButton } from '../../../form/SubmitButton';
import { addNotification } from '../../../actions';
import { jsonValidator } from '../../../form/validators';
import { createGraphLayout } from '../../../utils/createGraphLayout';
import { fetchGeoBlock } from '../../../utils/geoblockValidators';
import {
convertElementsToGeoBlockSource,
convertGeoblockSourceToFlowElements
} from '../../../utils/geoblockUtils';
import ModalBackground from '../../../components/ModalBackground';
import styles from './GeoBlockBuildModal.module.css';
import formStyles from './../../../styles/Forms.module.css';
import buttonStyles from './../../../styles/Buttons.module.css';

interface MyProps {
uuid: string | null,
source: Object | null,
onChange: (value: Object) => void,
handleClose: () => void
Expand All @@ -20,6 +29,23 @@ function GeoBlockBuildModal (props: MyProps & DispatchProps) {
const [jsonString, setJsonString] = useState<string>(sourceString);
const [geoBlockView, setGeoBlockView] = useState<'json' | 'visual'>('json');

// Block elements of a geoblock for React-Flow are kept in here
// to make use of the SAVE, SWITCH and VALIDATE buttons
const [elements, setElements] = useState<Elements>([]);

// useEffect to create geoblock elements and build the graph layout using dagre library
// useEffect only gets called when the visual component is open
// use the useEffect here instead of the GeoBlockVisualComponent to avoid Maximum Depth Exceeded error
useEffect(() => {
if (geoBlockView === 'visual') {
const geoblockElements = convertGeoblockSourceToFlowElements(jsonString, setElements);
const layoutedElements = createGraphLayout(jsonString, geoblockElements);
setElements(layoutedElements);
};
// setElements back to [] when component unmounted
return () => setElements([]);
}, [jsonString, setElements, geoBlockView]);

return (
<ModalBackground
title={'Geo Block Builder'}
Expand All @@ -35,6 +61,8 @@ function GeoBlockBuildModal (props: MyProps & DispatchProps) {
/>
) : (
<GeoBlockVisualComponent
elements={elements}
setElements={setElements}
/>
)}
<div className={`${formStyles.ButtonContainer} ${formStyles.FixedButtonContainer}`}>
Expand All @@ -46,19 +74,49 @@ function GeoBlockBuildModal (props: MyProps & DispatchProps) {
</button>
<button
onClick={() => {
if (geoBlockView === 'json') {
setGeoBlockView('visual');
if (jsonValidator(jsonString)) {
return alert(jsonValidator(jsonString));
};
if (geoBlockView === 'visual') {
const geoBlockSource = convertElementsToGeoBlockSource(elements, jsonString, setJsonString);
if (geoBlockSource) setGeoBlockView('json');
} else {
setGeoBlockView('json')
setGeoBlockView('visual');
};
}}
>
Switch
</button>
<button
onClick={() => {
if (geoBlockView === 'visual') {
const geoBlockSource = convertElementsToGeoBlockSource(elements, jsonString, setJsonString);
if (geoBlockSource) fetchGeoBlock(props.uuid, geoBlockSource);
} else {
if (jsonValidator(jsonString)) {
return alert(jsonValidator(jsonString));
};
fetchGeoBlock(props.uuid, JSON.parse(jsonString))
};
}}
>
Validate
</button>
<SubmitButton
onClick={() => {
props.onChange(JSON.parse(jsonString));
props.handleClose();
if (jsonValidator(jsonString)) {
return alert(jsonValidator(jsonString));
};
if (geoBlockView === 'visual') {
const geoBlockSource = convertElementsToGeoBlockSource(elements, jsonString, setJsonString);
if (geoBlockSource) {
props.onChange(geoBlockSource);
props.handleClose();
};
} else {
props.onChange(JSON.parse(jsonString));
props.handleClose();
};
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ interface MyProps {
setJsonString: (e: string) => void
}

const prettyJsonFormat = (e: Object) => {
return JSON.stringify(e, null, 4);
};

export const GeoBlockJsonComponent = (props: MyProps) => {
const { jsonString, setJsonString } = props;
const [jsonTextView, setJsonTextView] = useState<boolean>(true);

// Helper function to convert json to pretty format
const setJsonStringInPrettyFormat = (e: Object) => {
setJsonString(JSON.stringify(e, undefined, 4));
};

return (
<>
<button
Expand Down Expand Up @@ -45,7 +44,7 @@ export const GeoBlockJsonComponent = (props: MyProps) => {
return alert(jsonValidator(jsonString));
};
const object = JSON.parse(jsonString);
setJsonStringInPrettyFormat(object);
setJsonString(prettyJsonFormat(object));
}}
style={{
position: 'absolute',
Expand Down Expand Up @@ -73,9 +72,9 @@ export const GeoBlockJsonComponent = (props: MyProps) => {
src={JSON.parse(jsonString)}
name="source"
theme="shapeshifter:inverted"
onEdit={e => setJsonStringInPrettyFormat(e.updated_src)}
onAdd={e => setJsonStringInPrettyFormat(e.updated_src)}
onDelete={e => setJsonStringInPrettyFormat(e.updated_src)}
onEdit={e => setJsonString(prettyJsonFormat(e.updated_src))}
onAdd={e => setJsonString(prettyJsonFormat(e.updated_src))}
onDelete={e => setJsonString(prettyJsonFormat(e.updated_src))}
displayDataTypes={false}
displayObjectSize={false}
quotesOnKeys={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,181 @@
import React from 'react';
import React, { useRef, useState } from 'react';
import ReactFlow, {
addEdge,
Connection,
ConnectionLineType,
Controls,
Edge,
Elements,
isNode,
MiniMap,
Position,
ReactFlowProvider,
removeElements,
updateEdge,
useUpdateNodeInternals
} from 'react-flow-renderer';
import { SideBar } from './blockComponents/SideBar';
import { Block } from './blockComponents/Block';
import { BooleanBlock } from './blockComponents/BooleanBlock';
import { GroupBlock } from './blockComponents/GroupBlock';
import { NumberBlock } from './blockComponents/NumberBlock';
import { RasterBlock } from './blockComponents/RasterBlock';
import { StringBlock } from './blockComponents/StringBlock';
import { ArrayBlock } from './blockComponents/ArrayBlock';
import { geoblockType } from '../../../types/geoBlockType';
import { getBlockData } from '../../../utils/geoblockUtils';
import { targetHandleValidator } from '../../../utils/geoblockValidators';
import edgeStyle from './blockComponents/Edge.module.css';

interface MyProps {
elements: Elements,
setElements: React.Dispatch<React.SetStateAction<Elements<any>>>
}

const GeoBlockVisualFlow = (props: MyProps) => {
const { elements, setElements } = props;
const reactFlowWrapper = useRef<any>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);

// gets called after end of edge gets dragged to another source or target
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => {
setElements((els) => updateEdge(oldEdge, newConnection, els));
newConnection.target && updateNodeInternals(newConnection.target); // update node internals
};

const onConnect = (params: Edge | Connection) => {
const connectError = targetHandleValidator(elements, params);
if (connectError) return console.error(connectError.errorMessage);

setElements((els) => {
const source = els.find(el => el.id === params.source)!;
return addEdge({
...params,
type: ConnectionLineType.SmoothStep,
animated: true,
className: (
source.type === 'NumberBlock' ? edgeStyle.NumberEdge :
source.type === 'StringBlock' ? edgeStyle.StringEdge :
source.type === 'BooleanBlock' ? edgeStyle.BooleanEdge :
edgeStyle.BlockEdge
)
}, els);
});
params.target && updateNodeInternals(params.target); // update node internals
};

const onElementsRemove = (elementsToRemove: Elements) => {
setElements((els) => removeElements(elementsToRemove, els))
};

const onLoad = (_reactFlowInstance: any) => {
setReactFlowInstance(_reactFlowInstance);
};

// Drag and drop actions
const onDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
};

const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
if (reactFlowWrapper && reactFlowWrapper.current) {
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const blockName = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
const sourcePosition = Position.Right;
const targetPosition = Position.Left;

// Keep track of number of block elements in the graph to create block id
const numberOfBlocks = elements.filter(elm => {
// @ts-ignore
return isNode(elm) && elm.data && elm.data.classOfBlock === geoblockType[blockName].class;
}).length;
const idOfNewBlock = blockName + numberOfBlocks;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this id always needs to be unique, then maybe use a library to generate a uuid instead.
I used "nanoid" before.

What for example happens if a user deletes nodes? Now for example node 10 might still be in the graph, but the amount of nodes might be 5.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this id needs to be unique. What you suggested is good. I will look into it. Thanks


const newBlock = {
id: idOfNewBlock,
type: (
blockName === 'RasterBlock' || blockName === 'NumberBlock' || blockName === 'BooleanBlock' || blockName === 'StringBlock' ? blockName :
blockName === 'Group' || blockName === 'FillNoData' ? 'GroupBlock' : 'Block'
),
position,
sourcePosition,
targetPosition,
data: getBlockData(blockName, numberOfBlocks, idOfNewBlock, setElements)
};
console.log('newBlock', newBlock);
setElements((es) => es.concat(newBlock));
};
};

export const GeoBlockVisualComponent = () => {
return (
<div>
GeoBlock Visual Component
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 150px',
columnGap: 10,
margin: '20px 0',
height: 550
}}
>
<ReactFlow
ref={reactFlowWrapper}
elements={elements}
onElementsRemove={onElementsRemove}
style={{
position: 'relative',
border: '1px solid lightgrey',
borderRadius: 10
}}
snapToGrid
onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect}
onLoad={onLoad}
onDragOver={onDragOver}
onDrop={onDrop}
nodeTypes={{
Block: Block,
BooleanBlock: BooleanBlock,
GroupBlock: GroupBlock,
RasterBlock: RasterBlock,
NumberBlock: NumberBlock,
StringBlock: StringBlock,
ArrayBlock: ArrayBlock
}}
>
{elements.length > 100 ? (
<MiniMap
nodeColor={(node) => {
if (node.type === 'RasterBlock') return 'orange';
if (node.type === 'Block' && node.data.outputBlock) return 'red';
if (node.type === 'Block' || node.type === 'GroupBlock') return 'green';
return 'lightgrey';
}}
nodeStrokeWidth={3}
/>
) : null}
<Controls />
</ReactFlow>
<SideBar />
</div>
)
}
}

export const GeoBlockVisualComponent = (props: MyProps) => (
// Wrap the GeoBlockVisualFlow component inside the ReactFlowProvider
// to have access to the useUpdateNodeInternals hook of react-flow.
// the useUpdateNodeInternals hook is used to update the node manually
// when there are changes that cannot be automatically updated to the node.
<ReactFlowProvider>
<GeoBlockVisualFlow
elements={props.elements}
setElements={props.setElements}
/>
</ReactFlowProvider>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { Handle, Node, Position } from 'react-flow-renderer';
import formStyles from './../../../../styles/Forms.module.css';
// import styles from './Block.module.css';

interface ArrayBlockInput {
value: string,
onChange: (value: string) => void,
}

export const ArrayBlock = (props: Node<ArrayBlockInput>) => {
const { value } = props.data!;
return (
<div>
<input
type="text"
title={JSON.stringify(value)}
className={formStyles.FormControl}
// onChange={e => onChange(e.target.value)}
value={JSON.stringify(value)}
readOnly
style={{
width: 140,
fontSize: 12
}}
/>
<Handle
type="source"
position={Position.Right}
/>
</div>
)
}
Loading