Skip to content
Snippets Groups Projects
Commit fd7477c6 authored by Behrisch, M. (Michael)'s avatar Behrisch, M. (Michael)
Browse files

fix(schema): :art: adds ui/pills shared library and uses it for schema

Pills and React Flow components impl can be shared for consistency.
Schema vs QueryBuilder can provide subclasses.
parent c41d3108
No related branches found
No related tags found
2 merge requests!17fix(storybook): :ambulance: adds babel config for module resolution of util projects,!15May 2022 merge request
Showing
with 713 additions and 76 deletions
......@@ -19,9 +19,7 @@
},
"build": {
"executor": "@nrwl/web:webpack",
"outputs": [
"{options.outputPath}"
],
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"compiler": "babel",
......@@ -35,18 +33,18 @@
"apps/web-graphpolaris/src/favicon.ico",
"apps/web-graphpolaris/src/assets"
],
"styles": [
"apps/web-graphpolaris/src/styles.scss"
],
"styles": ["apps/web-graphpolaris/src/styles.scss"],
"scripts": [],
"webpackConfig": "@nrwl/react/plugins/webpack"
},
"configurations": {
"production": {
"fileReplacements": [{
"replace": "apps/graphpolaris/src/environments/environment.ts",
"with": "apps/graphpolaris/src/environments/environment.prod.ts"
}],
"fileReplacements": [
{
"replace": "apps/graphpolaris/src/environments/environment.ts",
"with": "apps/graphpolaris/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
......@@ -71,20 +69,14 @@
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": [
"{options.outputFile}"
],
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"apps/graphpolaris/**/*.{ts,tsx,js,jsx}"
]
"lintFilePatterns": ["apps/graphpolaris/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": [
"coverage/apps/graphpolaris"
],
"outputs": ["coverage/apps/graphpolaris"],
"options": {
"jestConfig": "apps/web-graphpolaris/jest.config.js",
"passWithNoTests": true
......@@ -113,9 +105,7 @@
},
"build-storybook": {
"executor": "@nrwl/storybook:build",
"outputs": [
"{options.outputPath}"
],
"outputs": ["{options.outputPath}"],
"options": {
"uiFramework": "@storybook/react",
"outputPath": "dist/storybook/graphpolaris",
......@@ -131,4 +121,4 @@
}
},
"tags": []
}
\ No newline at end of file
}
......@@ -40,6 +40,7 @@ const QueryBuilder = (props: {}) => {
const nodes = useQuerybuilderNodes();
const dispatch = useAppDispatch();
const isDraggingPill = useRef(false);
console.log('inputnodes', nodes);
const elements = useMemo(() => createReactFlowElements(nodes), [nodes]);
......
......@@ -18,6 +18,18 @@ import ReactFlow, {
} from 'react-flow-renderer';
import styles from './schema.module.scss';
import {
EntityRFPill,
RelationRFPill,
AttributeRFPill,
ConnectionDragLine,
ConnectionLine
} from '@graphpolaris/shared/ui/pills';
// import ConnectionDragLine from '@graphpolaris/shared/ui/pills';
// import AttributeRFPill from '@graphpolaris/shared/ui/pills';
// import EntityRFPill from '@graphpolaris/shared/ui/pills';
// import RelationRFPill from '@graphpolaris/shared/ui/pills';
interface Props {
// content: string;
}
......@@ -26,6 +38,15 @@ const onLoad = (reactFlowInstance: any) => {
setTimeout(() => reactFlowInstance.fitView(), 100);
};
const nodeTypes = {
entity: EntityRFPill,
relation: RelationRFPill,
attribute: AttributeRFPill,
};
const edgeTypes = {
connection: ConnectionLine,
};
const Schema = (props: Props) => {
const [elements, setElements] = useState([] as FlowElement[]);
// In case the schema is updated
......@@ -76,6 +97,9 @@ const Schema = (props: Props) => {
className={styles.schemaPanel}
onlyRenderVisibleElements={false}
nodesDraggable={false}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
connectionLineComponent={ConnectionDragLine}
elements={elements}
style={graphStyles}
onLoad={onLoad}
......
......@@ -8,19 +8,25 @@ const ANIMATEDEDGES = false;
export function expandSchema(graph: Graph): Graph {
const newGraph = graph.copy();
// newGraph.forEachNode((node, attributes) => {
// console.log(node, attributes);
// });
newGraph.forEachNode((node, attributes) => {
// console.log(node, attributes);
newGraph.mergeNodeAttributes(node, { type: 'entity' });
});
//makeNewRelationNodes
graph.forEachEdge((edge, attributes, source, target): void => {
const newID = 'RelationNode:' + edge;
console.log('making relationnode', edge, attributes, source, target, newID);
// console.log('making relationnode', edge, attributes, source, target, newID);
newGraph.addNode(newID, {
name: edge,
data: {
label: edge,
name: edge,
},
attributes,
x: 0,
y: 0,
type: 'relation',
});
const id = 'RelationEdge' + source + '->' + newID;
......@@ -58,8 +64,10 @@ export function createReactFlowNodes(graph: Graph): Elements<Node> {
id: node,
data: {
label: attributes.name,
name: attributes.name,
},
position: { x: attributes.x, y: attributes.y },
type: attributes.type,
};
nodeElements.push(newNode);
});
......@@ -67,53 +75,6 @@ export function createReactFlowNodes(graph: Graph): Elements<Node> {
return nodeElements;
}
export function createReactFlowRelationNodes(graph: Graph): Elements<Node> {
const nodeElements: Elements<Node> = [];
graph.forEachEdge((edge, attributes, source, target): void => {
const newRelationNode: Node = {
id: edge,
data: {
label: edge,
},
position: { x: attributes.x, y: attributes.y },
};
nodeElements.push(newRelationNode);
});
return nodeElements;
}
export function createReactFlowRelationEdges(graph: Graph): Elements<Edge> {
const edgeElements: Elements<Edge> = [];
graph.forEachEdge((edge, attributes, source, target): void => {
const newEdgeIncoming: Edge = {
//into relation node
id: edge + '' + source,
source: source,
target: edge,
// label: edge,
type: 'smoothstep',
animated: ANIMATEDEDGES,
arrowHeadType: ArrowHeadType.ArrowClosed,
};
edgeElements.push(newEdgeIncoming);
const newEdgeOutgoing: Edge = {
//out of relation node
id: edge + '' + target,
source: edge,
target: target,
// label: edge,
type: 'smoothstep',
animated: ANIMATEDEDGES,
arrowHeadType: ArrowHeadType.ArrowClosed,
};
edgeElements.push(newEdgeOutgoing);
});
return edgeElements;
}
export function createReactFlowEdges(graph: Graph): Elements<Edge> {
const edgeElements: Elements<Edge> = [];
......@@ -134,6 +95,55 @@ export function createReactFlowEdges(graph: Graph): Elements<Edge> {
return edgeElements;
}
// export function createReactFlowRelationNodes(graph: Graph): Elements<Node> {
// const nodeElements: Elements<Node> = [];
// graph.forEachEdge((edge, attributes, source, target): void => {
// const newRelationNode: Node = {
// id: edge,
// data: {
// label: edge,
// name: edge,
// },
// position: { x: attributes.x, y: attributes.y },
// type: 'relation',
// };
// nodeElements.push(newRelationNode);
// });
// return nodeElements;
// }
// export function createReactFlowRelationEdges(graph: Graph): Elements<Edge> {
// const edgeElements: Elements<Edge> = [];
// graph.forEachEdge((edge, attributes, source, target): void => {
// const newEdgeIncoming: Edge = {
// //into relation node
// id: edge + '' + source,
// source: source,
// target: edge,
// // label: edge,
// type: 'smoothstep',
// animated: ANIMATEDEDGES,
// arrowHeadType: ArrowHeadType.ArrowClosed,
// };
// edgeElements.push(newEdgeIncoming);
// const newEdgeOutgoing: Edge = {
// //out of relation node
// id: edge + '' + target,
// source: edge,
// target: target,
// // label: edge,
// type: 'smoothstep',
// animated: ANIMATEDEDGES,
// arrowHeadType: ArrowHeadType.ArrowClosed,
// };
// edgeElements.push(newEdgeOutgoing);
// });
// return edgeElements;
// }
// export function parseSchemaFromBackend(
// schemaFromBackend: SchemaFromBackend
// ): Graph {
......
......@@ -244,10 +244,10 @@ class CytoscapeKlay extends Cytoscape {
// boundingBox: undefined,
ready: function () {
console.log('Layout.ready');
console.info('Layout.ready');
}, // on layoutready
stop: function () {
console.log('Layout.stop');
console.debug('Layout.stop');
}, // on layoutstop
} as any);
layout.run();
......
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
{
"extends": ["plugin:@nrwl/nx/react", "../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
# shared-ui-pills
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test shared-ui-pills` to execute the unit tests via [Jest](https://jestjs.io).
module.exports = {
displayName: 'shared-ui-pills',
preset: '../../../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../../../coverage/libs/shared/ui/pills',
};
{
"root": "libs/shared/ui/pills",
"sourceRoot": "libs/shared/ui/pills/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/shared/ui/pills/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/shared/ui/pills"],
"options": {
"jestConfig": "libs/shared/ui/pills/jest.config.js",
"passWithNoTests": true
}
}
}
}
import { handles } from '@graphpolaris/querybuilder/usecases';
import React from 'react';
import { EdgeProps, getSmoothStepPath, Position } from 'react-flow-renderer';
/**
* A custom query element edge line component.
* @param {EdgeProps} param0 The coordinates for the start and end point, the id and the style.
*/
// export const EntityRFPill = React.memo(({ data }: { data: any }) => {
export function ConnectionLine({
id,
sourceX,
sourceY,
targetX,
targetY,
style,
sourceHandleId,
targetHandleId,
}: EdgeProps) {
//Centering the line
sourceY -= 3;
targetY -= 3;
// Correct line positions with hardcoded numbers, because react flow lacks this functionality
// if (sourceHandleId == ) sourceX += 2;
// if (targetHandleId == Handles.ToAttributeHandle) targetX += 2;
let spos: Position = Position.Bottom;
if (sourceHandleId == handles.relation.fromEntity) {
spos = Position.Left;
sourceX += 7;
sourceY += 3;
} else if (sourceHandleId == handles.relation.toEntity) {
spos = Position.Right;
sourceX -= 2;
sourceY -= 3;
} else if (
sourceHandleId !== undefined &&
sourceHandleId !== null &&
sourceHandleId.includes('functionHandle')
) {
spos = Position.Top;
sourceX -= 4;
sourceY += 3;
}
let tpos: Position = Position.Bottom;
if (targetHandleId == handles.relation.fromEntity) {
tpos = Position.Left;
targetX += 7;
targetY += 3;
} else if (targetHandleId == handles.relation.toEntity) {
tpos = Position.Right;
targetX -= 2;
targetY -= 3;
}
// Create smoothstep line
const path = getSmoothStepPath({
sourceX: sourceX,
sourceY: sourceY,
sourcePosition: spos,
targetX: targetX,
targetY: targetY,
targetPosition: tpos,
});
return (
<g stroke="#2e2e2e">
<path id={id} fill="none" strokeWidth={3} style={style} d={path} />
</g>
);
}
import React from 'react';
import { ConnectionLineComponentProps } from 'react-flow-renderer';
/**
* A custom query element to render the line when connecting flow elements.
* @param {ConnectionLineComponentProps} param0 Source and target coordinates of the edges.
*/
export function ConnectionDragLine({
sourceX,
sourceY,
targetX,
targetY,
}: ConnectionLineComponentProps) {
return (
<g>
<path
fill="none"
stroke="#222"
strokeWidth={2.5}
className="animated"
d={`M${sourceX},${sourceY}L ${targetX},${targetY}`}
/>
<circle
cx={sourceX}
cy={sourceY}
fill="#fff"
r={3}
stroke="#222"
strokeWidth={1.5}
/>
<circle
cx={targetX}
cy={targetY}
fill="#fff"
r={3}
stroke="#222"
strokeWidth={1.5}
/>
</g>
);
}
@use './variables.module.scss';
.attribute {
display: flex;
font-family: monospace;
font-weight: bold;
font-size: variables.$fontsize;
border-radius: 2px;
}
// .handle {
// border: 0px;
// border-radius: 10px;
// left: 12px;
// width: 7px;
// height: 7px;
// margin-bottom: 11px;
// background: rgba(255, 255, 255, 0.6);
// box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
// transform-origin: center;
// }
.contentWrapper {
display: flex;
align-items: center;
.content {
padding: variables.$ypad 0 variables.$ypad 1ch;
max-width: 15ch;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.attributeInput {
float: right;
padding: 0 1ch 0 0;
display: flex;
align-items: center;
input {
background-color: rgba(100, 100, 100, 0.1);
font-family: monospace;
font-size: variables.$fontsize;
border: 1px solid rgba(100, 100, 100, 0.3);
border-radius: 2px;
height: variables.$height;
outline: none;
transition: border 0.3s;
color: black;
&::placeholder {
color: black;
}
&:focus {
border: 1px solid rgba(0, 0, 0, 0.3);
}
}
}
import {
CheckDatatypeConstraint,
GetAttributeBoolOperators,
} from '@graphpolaris/querybuilder/usecases';
import {
updateQBAttributeOperator,
updateQBAttributeValue,
useAppDispatch,
} from '@graphpolaris/shared/data-access/store';
import { useTheme } from '@mui/material';
import React, { useMemo, useState } from 'react';
import styles from './attributepill.module.scss';
import AttributeOperatorSelect from './operatorselect';
/**
* Component to render an attribute flow element
* @param {FlowElement<EntityData>)} param0 The data of an entity flow element.
*/
export const AttributeRFPill = React.memo(
({ id, data }: { id: string; data: any }) => {
const theme = useTheme();
const dispatch = useAppDispatch();
const [value, setValue] = useState(data?.value || '');
const onChange = (e: any) => {
setValue(e.target.value);
};
const validateInput = () => {
const newValue = CheckDatatypeConstraint(data.datatype, value);
setValue(newValue);
dispatch(updateQBAttributeValue({ id, value: newValue }));
};
// Calculates the size of the input
const getInputWidth = () => {
if (value == '') return 1;
else if (value.length > 10) return 10;
return value.length;
};
const boolOperators = useMemo(
() => GetAttributeBoolOperators(data?.datatype),
[data?.datatype]
);
// Determine the backgroundcolor based on if the attribute is connected to a entity or relation
let bgcolor;
if (data?.attributeOfA == 'entity')
bgcolor = theme.palette.queryBuilder.entity.lighterbg;
else if (data?.attributeOfA == 'relation')
bgcolor = theme.palette.queryBuilder.relation.lighterbg;
else bgcolor = theme.palette.queryBuilder.attribute.background;
return (
<div
className={styles.attribute}
style={{
background: bgcolor,
color: theme.palette.queryBuilder.text,
}}
>
{/* <Handle
id={Handles.Attribute}
type="source"
position={Position.Bottom}
className={styles.handle}
/> */}
<div className={styles.contentWrapper}>
<span className={styles.content} title={data.name}>
{data.name}
</span>
<AttributeOperatorSelect
selected={data?.operator}
options={boolOperators}
changed={(o) =>
dispatch(updateQBAttributeOperator({ id, operator: o.value }))
}
/>
<span className={styles.attributeInput}>
<input
style={{ maxWidth: `${getInputWidth()}ch` }}
type="string"
placeholder={'?'}
value={value}
onChange={onChange}
onBlur={validateInput}
onKeyDown={(e) => e.key == 'Enter' && validateInput()}
></input>
</span>
</div>
</div>
);
}
);
export default AttributeRFPill;
@use './variables.module.scss';
.container {
position: relative;
vertical-align: baseline;
margin: 0 1ch;
font-weight: normal;
font-size: 7px;
}
.valueContainer {
color: #6a6a6a;
border: 1px solid rgba(0, 0, 0, 0);
border-radius: 2px;
background-color: transparent;
transition: border-color 0.2s;
height: variables.$height;
align-items: center;
display: flex;
padding: 0 1px 1px 1px;
&.highlighted,
&:hover {
border-color: rgba(0, 0, 0, 0.4);
}
}
.listbox {
font-size: 10px;
box-sizing: border-box;
padding: 5px;
margin: 5px 0 0 0;
list-style: none;
position: absolute;
height: auto;
box-shadow: 0 5px 13px -3px #e0e3e7;
background: white;
border: 1px solid #cdd2d7;
border-radius: 0.75em;
color: #1a2027;
overflow: auto;
z-index: 1;
outline: 0px;
left: -8px;
&.hidden {
opacity: 0;
visibility: hidden;
transition: opacity 0.4s 0.1s ease, visibility 0.4s 0.1s step-end;
}
& > li {
padding: 1px 4px;
border-radius: 2px;
&.selected {
background: #f1f1f1;
}
&:hover {
background: #e7ebf0;
}
&[aria-selected='true'] {
background: #e0e3e7;
}
}
}
import * as React from 'react';
import { SelectOption } from '@mui/base';
import styles from './operatorselect.module.scss';
import { useRef, useState } from 'react';
// const grey = {
// 100: '#E7EBF0',
// 200: '#E0E3E7',
// 300: '#CDD2D7',
// 400: '#B2BAC2',
// 500: '#A0AAB4',
// 600: '#6F7E8C',
// 700: '#3E5060',
// 800: '#2D3843',
// 900: '#1A2027',
// };
interface Props {
options: SelectOption<string>[];
selected: string;
changed?: (newSelected: SelectOption<string>) => void;
}
function AttributeOperatorSelect({
options,
selected,
changed = () => {},
}: Props) {
const listboxRef = useRef<HTMLUListElement>(null);
const [listboxVisible, setListboxVisible] = useState(false);
const [currSelected, setCurrSelected] = useState(
options.find((o) => o.value == selected)?.label || options[0].label
);
React.useEffect(() => {
if (listboxVisible) {
listboxRef.current?.focus();
}
}, [listboxVisible]);
const changeSelected = (option: SelectOption<string>) => {
if (option.label != currSelected) {
setCurrSelected(option.label);
changed(option);
}
};
return (
<div
className={styles.container}
// onMouseOver={() => setListboxVisible(true)}
onMouseOut={() => setListboxVisible(false)}
onClick={() => setListboxVisible(true)}
onFocus={() => setListboxVisible(true)}
onBlur={() => setListboxVisible(false)}
>
<div
className={
styles.valueContainer + ' ' + (listboxVisible && styles.highlighted)
}
>
<span>{currSelected}</span>
</div>
{options.length > 1 && (
<ul
className={styles.listbox + ' ' + (!listboxVisible && styles.hidden)}
ref={listboxRef}
onMouseOver={() => setListboxVisible(true)}
>
{options.map((option) => (
<li
className={option.label == currSelected ? styles.selected : ''}
key={option.value}
onClick={() => changeSelected(option)}
>
{option.label}
</li>
))}
</ul>
)}
</div>
);
}
export default AttributeOperatorSelect;
$height: 5px;
$fontsize: 6px;
$ypad: 2px;
.entity {
display: flex;
font-family: monospace;
font-weight: bold;
font-size: 10px;
padding: 4px 2ch;
border-radius: 3px;
}
.highlighted {
box-shadow: black 0 0 2px;
}
.handleLeft {
border: 0px;
border-radius: 0px;
left: 12px;
width: 7px;
height: 7px;
margin-bottom: 11px;
background: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
transform-origin: center;
}
// .handleBottom {
// border: 0px;
// border-radius: 0px;
// width: 7px;
// height: 7px;
// left: 27.5px;
// margin-bottom: 11px;
// background: rgba(255, 255, 255, 0.6);
// box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
// transform: rotate(-45deg);
// transform-origin: center;
// }
.contentWrapper {
margin-left: 3ch;
span {
max-width: 20ch;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: block;
}
}
import { handles } from '@graphpolaris/querybuilder/usecases';
import { useTheme } from '@mui/material';
import React, { useEffect } from 'react';
import { FlowElement, Handle, Position } from 'react-flow-renderer';
import styles from './entitypill.module.scss';
import cn from 'classnames';
// export const Handless = {
// entity: {
// attributes: 'attributesHandle',
// relations: 'relationsHandle',
// },
// };
// /** Links need handles to what they are connected to (and which side) */
// export enum Handles {
// RelationLeft = 'leftEntityHandle', //target
// RelationRight = 'rightEntityHandle', //target
// ToAttributeHandle = 'attributesHandle', //target
// ToRelation = 'relationsHandle', //source
// Attribute = 'AttributeHandle', //source
// ReceiveFunction = 'receiveFunctionHandle', //target
// FunctionBase = 'functionHandle_', // + name from FunctionTypes args //source
// }
/**
* Component to render an entity flow element
* @param {FlowElement<EntityData>)} param0 The data of an entity flow element.
*/
export const EntityRFPill = React.memo(({ data }: { data: any }) => {
const theme = useTheme();
// console.log('EntityRFPill', data);
return (
<div
className={cn(styles['entity'], {
[styles['highlighted']]: data.suggestedForConnection,
})}
style={{
background: theme.palette.queryBuilder.entity.background,
color: theme.palette.queryBuilder.text,
}}
>
<Handle
id={handles.entity.relation}
type="source"
position={Position.Bottom}
className={styles['handleLeft']}
style={data?.isConnected ? { backgroundColor: '#2e2e2e' } : {}}
/>
{/* <Handle
id={Handles.ToAttributeHandle}
type="target"
position={Position.Bottom}
className={styles.handleBottom}
/> */}
<div className={styles['contentWrapper']}>
<span title={data.name}>{data.name}</span>
</div>
</div>
);
});
export default EntityRFPill;
export * from './entitypill';
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment