From 9c61557a453004ddb0a29c4c79a07ea6891a7de0 Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Sat, 28 May 2022 02:07:42 +0800 Subject: [PATCH 1/9] Feathr UI: new lineage experience --- ui/.env | 1 + ui/package-lock.json | 116 ++++++++--- ui/package.json | 7 +- ui/src/api/api.tsx | 10 +- ui/src/components/lineage/layouting.css | 12 -- ui/src/components/lineage/lineage.tsx | 89 --------- ui/src/components/lineage/lineageGraph.tsx | 190 +++++++++++++++++++ ui/src/components/lineage/lineageNode.tsx | 51 +++++ ui/src/components/lineage/utils.ts | 121 ++++++++++++ ui/src/components/sidemenu/siteMenu.tsx | 7 +- ui/src/index.tsx | 1 + ui/src/pages/feature/featureLineage.tsx | 101 ---------- ui/src/pages/feature/featureLineageGraph.tsx | 98 ++++++++++ ui/src/react-app-env.d.ts | 1 + ui/src/router/routes.tsx | 11 +- ui/src/site.css | 40 ++++ 16 files changed, 612 insertions(+), 244 deletions(-) delete mode 100644 ui/src/components/lineage/layouting.css delete mode 100644 ui/src/components/lineage/lineage.tsx create mode 100644 ui/src/components/lineage/lineageGraph.tsx create mode 100644 ui/src/components/lineage/lineageNode.tsx create mode 100644 ui/src/components/lineage/utils.ts delete mode 100644 ui/src/pages/feature/featureLineage.tsx create mode 100644 ui/src/pages/feature/featureLineageGraph.tsx create mode 100644 ui/src/site.css diff --git a/ui/.env b/ui/.env index c22dbf420..e3a3b7756 100644 --- a/ui/.env +++ b/ui/.env @@ -1,2 +1,3 @@ REACT_APP_AAD_APP_CLIENT_ID=db8dc4b0-202e-450c-b38d-7396ad9631a5 REACT_APP_AAD_APP_AUTHORITY=https://login.microsoftonline.com/common +REACT_APP_API_ENDPOINT=https://feathr-sql-registry.azurewebsites.net diff --git a/ui/package-lock.json b/ui/package-lock.json index 6052c5bf5..7f80289f3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -2513,6 +2513,15 @@ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", "dev": true }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -2601,8 +2610,7 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/q": { "version": "1.5.5", @@ -2626,7 +2634,6 @@ "version": "17.0.44", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.44.tgz", "integrity": "sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2642,6 +2649,17 @@ "@types/react": "^17" } }, + "@types/react-redux": { + "version": "7.1.24", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", + "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/react-resizable": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.4.tgz", @@ -2690,8 +2708,7 @@ "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@types/serve-index": { "version": "1.9.1", @@ -4596,8 +4613,7 @@ "csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", - "dev": true + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, "d3-color": { "version": "3.1.0", @@ -5834,8 +5850,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.11", @@ -10495,16 +10510,18 @@ "dev": true }, "react-flow-renderer": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-10.2.1.tgz", - "integrity": "sha512-F2JG9Uc1t7DWzc+HK6REGooW/tt6X5/8frbXnq7ug6yObAPEmfWlTQjRP917cKVkIxFzI1pU38RwHdOoPdsfJw==", + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-9.7.4.tgz", + "integrity": "sha512-GxHBXzkn8Y+TEG8pul7h6Fjo4cKrT0kW9UQ34OAGZqAnSBLbBsx9W++TF8GiULBbTn3O8o7HtHxux685Op10mQ==", "requires": { - "@babel/runtime": "^7.17.9", + "@babel/runtime": "^7.16.7", "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", - "react-draggable": "^4.4.5", - "zustand": "^3.7.2" + "fast-deep-equal": "^3.1.3", + "react-draggable": "^4.4.4", + "react-redux": "^7.2.6", + "redux": "^4.1.2" } }, "react-is": { @@ -10522,21 +10539,32 @@ "match-sorter": "^6.0.2" } }, + "react-redux": { + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz", + "integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true }, - "react-resizable": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz", - "integrity": "sha512-StnwmiESiamNzdRHbSSvA65b0ZQJ7eVQpPusrSmcpyGKzC0gojhtO62xxH6YOBmepk9dQTBi9yxidL3W4s3EBA==", - "requires": { - "prop-types": "15.x", - "react-draggable": "^4.0.3" - } - }, "react-router": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz", @@ -10568,6 +10596,33 @@ "tiny-warning": "^1.0.0" } }, + "react-router-dom-v5-compat": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.3.0.tgz", + "integrity": "sha512-lfnfDEAdfXf92VQQ692+aMFj9JKb73GZ7EbiQJeJT4sK+KUGvua3FGN2H8n2H5zSMaY1cvrihGiaI373cvv7OQ==", + "requires": { + "history": "^5.3.0", + "react-router": "6.3.0" + }, + "dependencies": { + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, + "react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "requires": { + "history": "^5.2.0" + } + } + } + }, "react-scripts": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz", @@ -10674,6 +10729,14 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", + "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -12883,11 +12946,6 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true - }, - "zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==" } } } diff --git a/ui/package.json b/ui/package.json index bfd29e6ca..a9dfc44f0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,11 +10,10 @@ "dagre": "^0.8.5", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-flow-renderer": "^10.2.1", + "react-flow-renderer": "^9.7.4", "react-query": "^3.38.0", - "react-resizable": "^3.0.4", - "react-router": "^5.0.1", - "react-router-dom": "^5.1.2" + "react-router-dom": "^5.3.1", + "react-router-dom-v5-compat": "^6.3.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.3", diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index 017c944c5..d80d4a89c 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -1,7 +1,7 @@ import Axios from "axios"; import { Features, IDataSource, IFeature, IFeatureDetail, IFeatureLineage } from "../models/model"; -const API_ENDPOINT = "https://feathr-api-test.azurewebsites.net"; +const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT + "/api"; const purview = "feathrazuretest3-purview1" const token = "mockAppServiceKey"; @@ -46,6 +46,14 @@ export const fetchFeature = async (project: string, qualifiedName: string) => { }; export const fetchProjectLineages = async (project: string) => { + return Axios + .get(`${ API_ENDPOINT }/v1/purview/${ purview }/projects/${ project }?code=${ token }`, {}) + .then((response) => { + return response.data; + }) +}; + +export const fetchFeatureLineages = async (project: string) => { return Axios .get(`${ API_ENDPOINT }/v1/purview/${ purview }/features/lineage/${ project }?code=${ token }`, {}) .then((response) => { diff --git a/ui/src/components/lineage/layouting.css b/ui/src/components/lineage/layouting.css deleted file mode 100644 index e6d369e06..000000000 --- a/ui/src/components/lineage/layouting.css +++ /dev/null @@ -1,12 +0,0 @@ -.layoutflow { - flex-grow: 1; - position: relative; - height: 800px; -} - -.layoutflow .controls { - position: absolute; - right: 10px; - top: 10px; - z-index: 10; -} diff --git a/ui/src/components/lineage/lineage.tsx b/ui/src/components/lineage/lineage.tsx deleted file mode 100644 index d7ddae07a..000000000 --- a/ui/src/components/lineage/lineage.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useCallback } from 'react'; -import ReactFlow, { - Node, Edge, - ReactFlowProvider, - addEdge, - Controls, - Connection, - CoordinateExtent, - Position, - useNodesState, - useEdgesState, -} from 'react-flow-renderer'; -import dagre from 'dagre'; - -import './layouting.css'; - -const dagreGraph = new dagre.graphlib.Graph(); -dagreGraph.setDefaultEdgeLabel(() => ({})); - -const nodeExtent: CoordinateExtent = [ - [0, 0], - [1000, 1000], -]; - -type LineageProps = { - lineageNodes: Node[]; - lineageEdges: Edge[]; -}; - -const Lineage: React.FC = ({ lineageNodes, lineageEdges }) => { - const [nodes, setNodes, onNodesChange] = useNodesState(lineageNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(lineageEdges); - - const onConnect = useCallback((connection: Connection) => { - setEdges((eds) => addEdge(connection, eds)); - }, [setEdges]); - - const onLayout = (direction: string) => { - const isHorizontal = direction === 'LR'; - dagreGraph.setGraph({ rankdir: direction }); - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: 150, height: 50 }); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - const layoutedNodes = nodes.map((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - node.targetPosition = isHorizontal ? Position.Left : Position.Top; - node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; - node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y }; - - return node; - }); - - setNodes(layoutedNodes); - }; - - return ( -
- - onLayout('LR') } - onNodesChange={ onNodesChange } - onEdgesChange={ onEdgesChange } - > - - -
- - -
-
-
- ); -}; - -export default Lineage; diff --git a/ui/src/components/lineage/lineageGraph.tsx b/ui/src/components/lineage/lineageGraph.tsx new file mode 100644 index 000000000..11b560121 --- /dev/null +++ b/ui/src/components/lineage/lineageGraph.tsx @@ -0,0 +1,190 @@ +import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState, } from 'react'; +import ReactFlow, { + ConnectionLineType, + Controls, + Edge, + Elements, + getIncomers, + getOutgoers, + isEdge, + isNode, + Node, + OnLoadParams, + ReactFlowProvider +} from 'react-flow-renderer'; +import { useSearchParams } from 'react-router-dom-v5-compat'; +import LineageNode from "./lineageNode"; +import { findNodeInElement, getLayoutElements } from "./utils"; +import { Spin } from "antd"; +import { LoadingOutlined } from "@ant-design/icons"; + +const nodeTypes = { + 'custom-node': LineageNode, +}; +type Props = { + data: Elements; + nodeId: string; +} +const LineageGraph: React.FC = ({ data, nodeId }) => { + const [, setURLSearchParams] = useSearchParams(); + const instanceRef = useRef(null); + + const { res, elementMapping } = getLayoutElements(data); + const [elements, setElements] = useState(res); + const [loading, setLoading] = useState(false); + + + useEffect(() => { + setElements(res); + }, [data, nodeId]); + + const resetHighlight = (): void => { + const values: Elements = []; + + for (let index = 0; index < elements.length; index++) { + const element = elements[index]; + + if (isNode(element)) { + values.push({ + ...element, + style: { + ...element.style, + opacity: 1, + }, + }); + } + if (isEdge(element)) { + values.push({ + ...element, + animated: false, + }); + } + } + + setElements(values); + }; + + const highlightPath = (node: Node, check: boolean): void => { + const checkElements = check ? res : elements; + + const incomerIds = new Set([...getIncomers(node, checkElements).map((i) => i.id)]); + const outgoerIds = new Set([...getOutgoers(node, checkElements).map((o) => o.id)]); + + const values: Elements = []; + for (let index = 0; index < checkElements.length; index++) { + const element = checkElements[index]; + let highlight = false; + if (isNode(element)) { + highlight = element.id === node.id + || incomerIds.has(element.id) + || outgoerIds.has(element.id); + } else { + highlight = element.source === node.id || element.target === node.id; + const animated = incomerIds.has(element.source) + && (incomerIds.has(element.target) || node.id === element.target); + + highlight = highlight || animated; + } + + if (isNode(element)) { + values.push({ + ...element, + style: { + ...element.style, + opacity: highlight ? 1 : 0.25, + }, + data: { + ...element.data, + active: element.id === node.id, + }, + }); + } + if (isEdge(element)) { + values.push({ + ...element, + animated: highlight, + }); + } + } + setElements(values); + }; + + const fitElements = (): void => { + setTimeout(() => { + instanceRef?.current?.fitView(); + }, 1); + setTimeout(() => { + setLoading(false); + }, 1000); + }; + + const onLoad = (reactFlowInstance: OnLoadParams | null) => { + instanceRef.current = reactFlowInstance; + fitElements(); + }; + + useEffect(() => { + if (nodeId) { + const node = findNodeInElement(nodeId, res); + if (node) { + resetHighlight(); // new changes to the graph + highlightPath(node, !!nodeId); + } + } + }, [nodeId]); + + useEffect(() => { + if (instanceRef.current) { + setLoading(true); + + fitElements(); + } + }, [instanceRef]); + + const onPaneClick = useCallback(() => { + resetHighlight(); + setURLSearchParams({}); + }, []); + + const onNodeDragStop = (_: ReactMouseEvent, node: Node) => { + const nodePosition = elementMapping[node.data?.id]; + const values: Elements = [ + ...elements, + ]; + values[nodePosition] = node; + + setElements(values); + }; + + return ( +
+ { loading ? } /> : ( + + { + if (isNode(element)) { + resetHighlight(); + highlightPath(element, false); + setURLSearchParams({ nodeId: element.data.id }); + } + } } + onNodeDragStop={ onNodeDragStop } + connectionLineType={ ConnectionLineType.SmoothStep } + nodeTypes={ nodeTypes } + > + + + + ) } +
+ ); +} + +export default LineageGraph; diff --git a/ui/src/components/lineage/lineageNode.tsx b/ui/src/components/lineage/lineageNode.tsx new file mode 100644 index 000000000..7e6d7dfb9 --- /dev/null +++ b/ui/src/components/lineage/lineageNode.tsx @@ -0,0 +1,51 @@ +import React, { FC, memo } from 'react'; +import { RightCircleOutlined } from "@ant-design/icons"; +import { Handle, NodeProps, Position } from 'react-flow-renderer'; +import { useNavigate, useSearchParams } from "react-router-dom-v5-compat"; +import { useParams } from "react-router"; + + +const LineageNode: FC = (props: NodeProps) => { + const navigate = useNavigate(); + // @ts-ignore + const { project, qualifiedName } = useParams(); + const [searchParams] = useSearchParams(); + const nodeId = searchParams.get('nodeId') as string; + + const { + data: { label, otherName, active, id, borderColor }, + } = props; + + const nodeColorStyle = { + border: `2px solid ${ borderColor }`, + }; + + const onIconClick = () => { + navigate(`/projects/${ project }/features/${ qualifiedName }`) + }; + + const isActive = active || (nodeId === id); + + return ( +
+
+ +
+
{ label } + { isActive && () } +
+
+ { otherName } +
+
+ + +
+
+ ); +}; + +export default memo(LineageNode); diff --git a/ui/src/components/lineage/utils.ts b/ui/src/components/lineage/utils.ts new file mode 100644 index 000000000..058b01caf --- /dev/null +++ b/ui/src/components/lineage/utils.ts @@ -0,0 +1,121 @@ +import dagre from 'dagre'; +import { ArrowHeadType, Edge, Elements, isNode, Node, Position, } from 'react-flow-renderer'; + +const DEFAULT_WIDTH = 172; +const DEFAULT_HEIGHT = 36; + +type getLayoutElementsRet = { + res: Elements, + elementMapping: Record, +}; + +const getLayoutElements = (elements: Elements, direction = 'LR'): getLayoutElementsRet => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + dagreGraph.setGraph({ rankdir: direction }); + + const isHorizontal = direction === 'LR'; + + for (let index = 0; index < elements.length; index++) { + const element: Node | Edge = elements[index]; + if (isNode(element)) { + dagreGraph.setNode(element.id, { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + }); + } else { + dagreGraph.setEdge(element.source, element.target); + } + } + + dagre.layout(dagreGraph); + + const newElements = []; + const elementsObj: Record = {}; + + for (let index = 0; index < elements.length; index++) { + const element = elements[index] as Node; + + if (isNode(element)) { + elementsObj[element.data?.id] = index; + + const nodeWithPosition = dagreGraph.node(element.id); + element.targetPosition = isHorizontal ? Position.Left : Position.Top; + element.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; + + element.position = { + x: nodeWithPosition.x - DEFAULT_WIDTH / 2 + Math.random() / 1000, + y: nodeWithPosition.y - DEFAULT_HEIGHT / 2, + }; + } + + newElements.push(element); + } + + return { + res: newElements, + elementMapping: elementsObj, + }; +}; + +const featureTypeColors: Record = { + feathr_source_v1: 'hsl(315, 100%, 50%)', + feathr_anchor_v1: 'hsl(270, 100%, 50%)', + feathr_anchor_feature_v1: 'hsl(225, 100%, 50%)', + feathr_derived_feature_v1: 'hsl(135, 100%, 50%)' +}; + +const generateNode = ({ + nodeId, index, + currentNode +// eslint-disable-next-line @typescript-eslint/no-explicit-any +}: any): any => ({ + key: nodeId, + id: index?.toString(), + type: 'custom-node', + label: currentNode.displayText, + shape: 'box', + color: { + background: featureTypeColors[currentNode.typeName], + }, + data: { + id: nodeId, + label: currentNode.displayText, + borderColor: featureTypeColors[currentNode.typeName], + otherName: currentNode.typeName, + }, +}); + +type GenerateEdgeProps = { + obj: Record, + from: string; + to: string +} + +const generateEdge = ({ obj, from, to }: GenerateEdgeProps): Edge => { + const source = obj?.[from]; + const target = obj?.[to]; + + const id = `e${source}-${target}`; + return ({ + id, + source, + target, + arrowHeadType: ArrowHeadType.ArrowClosed, + }); +}; + +export { + generateEdge, + generateNode, + getLayoutElements, +}; + +export const findNodeInElement = (nodeId: string | null, elements: Elements): Node | null => { + if (nodeId) { + const node = elements.find((element) => isNode(element) && element.data.id === nodeId); + return node as Node; + } + return null; +}; diff --git a/ui/src/components/sidemenu/siteMenu.tsx b/ui/src/components/sidemenu/siteMenu.tsx index 6f688ecfd..f1d2c8574 100644 --- a/ui/src/components/sidemenu/siteMenu.tsx +++ b/ui/src/components/sidemenu/siteMenu.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { Layout, Menu } from 'antd'; import { CopyOutlined, DatabaseOutlined, EyeOutlined, RocketOutlined } from '@ant-design/icons'; -import { withRouter } from 'react-router'; import { Link } from 'react-router-dom'; const { Sider } = Layout; - -const SideMenu = withRouter(({ history }) => { +const SideMenu = () => { return (
@@ -17,7 +15,6 @@ const SideMenu = withRouter(({ history }) => { mode="inline" defaultSelectedKeys={ ['/'] } defaultOpenKeys={ ['/'] } - selectedKeys={ [history.location.pathname] } > }> Data Sources @@ -34,6 +31,6 @@ const SideMenu = withRouter(({ history }) => { ); -}); +}; export default SideMenu; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8f0ea99a6..776313feb 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import Routes from './router/routes'; import 'antd/dist/antd.min.css'; import './index.less'; +import "./site.css" ReactDOM.render( diff --git a/ui/src/pages/feature/featureLineage.tsx b/ui/src/pages/feature/featureLineage.tsx deleted file mode 100644 index 765c56e11..000000000 --- a/ui/src/pages/feature/featureLineage.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useCallback } from 'react'; -import { Alert, Card, Spin } from 'antd'; -import { useHistory, useParams } from 'react-router'; -import { QueryStatus, useQuery } from "react-query"; -import { LoadingOutlined } from '@ant-design/icons'; -import { AxiosError } from 'axios'; -import { Edge, Node, XYPosition } from "react-flow-renderer"; -import { fetchProjectLineages } from '../../api'; -import { IFeatureLineage } from "../../models/model"; -import Lineage from "../../components/lineage/lineage"; - -type Props = {}; -type Params = { - project: string; - qualifiedName: string; -} - -const FeatureDetails: React.FC = () => { - const { project, qualifiedName } = useParams(); - const loadingIcon = ; - const history = useHistory(); - useCallback((location) => history.push(location), [history]); - const { - status, - error, - data - } = useQuery(['feature', qualifiedName], () => fetchProjectLineages(project)); - - const render = (status: QueryStatus): JSX.Element => { - switch (status) { - case "error": - return ( - - - - ); - case "idle": - return ( - - - - ); - case "loading": - return ( - - - - ); - case "success": - if (data === undefined) { - return ( - - - - ); - } else { - const position: XYPosition = { x: 0, y: 0 }; - const nodes = Object.values(data.guidEntityMap).map((entity: any) => { - return { - id: entity.guid, - type: "", - data: { label: entity.displayText }, - position: position - } as Node - }); - const edges = data.relations.map((relation: any) => { - return { - id: relation.relationshipId, - source: relation.fromEntityId, - target: relation.toEntityId - } as Edge - }); - return ( - - - - ); - } - } - } - - return ( - <> -
- { render(status) } -
- - ); -}; - -export default FeatureDetails; diff --git a/ui/src/pages/feature/featureLineageGraph.tsx b/ui/src/pages/feature/featureLineageGraph.tsx new file mode 100644 index 000000000..760ec87de --- /dev/null +++ b/ui/src/pages/feature/featureLineageGraph.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Card, Radio } from 'antd'; +import { Elements } from 'react-flow-renderer'; +import LineageGraph from "../../components/lineage/lineageGraph"; +import { generateEdge, generateNode } from "../../components/lineage/utils"; +import { fetchProjectLineages } from "../../api"; +import { useSearchParams } from "react-router-dom-v5-compat"; +import { useParams } from "react-router"; + +const FeatureLineageGraph: React.FC = () => { + const [searchParams] = useSearchParams(); + // @ts-ignore + const { project } = useParams(); + const nodeId = searchParams.get('nodeId') as string; + + const [elements, SetElements] = useState([]); + const [featureType, setFeatureType] = useState(null); + + // Fetch lineage data from server side and convert into graph nodes and edges + const fetchGraphData = useCallback(async () => { + const graphData = await fetchProjectLineages(project); + const elements: Elements = []; + const elementObj: Record = {}; + + for (let index = 0; index < Object.values(graphData.guidEntityMap).length; index++) { + const currentNode: any = Object.values(graphData.guidEntityMap)[index]; + if (currentNode.typeName === "feathr_workspace_v1") { + continue; // Open issue: should project node get displayed as well? + } + const nodeId = currentNode.guid; + + // check if model type exists and this currentNode is of that type + if (featureType && featureType !== currentNode.typeName) { + continue; + } + const node = generateNode({ + index, + nodeId, + currentNode + }); + + elementObj[nodeId] = index?.toString(); + + elements.push(node); + } + + for (let index = 0; index < graphData.relations.length; index++) { + const { fromEntityId: from, toEntityId: to, relationshipType } = graphData.relations[index]; + const edge = generateEdge({ obj: elementObj, from, to }); + if (edge?.source && edge?.target) { + if (relationshipType === "Produces") { // Drop edges with other relationship like Consumes, BelongsTo, etc) + elements.push(edge); + } + } + } + SetElements(elements); + }, [featureType]) + + useEffect(() => { + fetchGraphData(); + }, [fetchProjectLineages, featureType]) + + const toggleFeatureType = (type: string) => { + setFeatureType((prevType: string | null) => { + if (type === "all_nodes") { + return null; + } + if (prevType === type) { + return null; + } + return type; + }); + }; + + const toggleAllFeatures = async () => { + setFeatureType(null); + }; + + return ( + +
+ toggleFeatureType(e.target.value) }> + Source + Anchor + Anchor Feature + Derived Feature + All Features + + +
+
+ { elements && } +
+
+ ); +} + +export default FeatureLineageGraph; diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts index 585499d3f..b816657d6 100644 --- a/ui/src/react-app-env.d.ts +++ b/ui/src/react-app-env.d.ts @@ -7,6 +7,7 @@ declare global { PWD: string; REACT_APP_AAD_APP_CLIENT_ID: string; REACT_APP_AAD_APP_AUTHORITY: string; + REACT_APP_API_ENDPOINT: string; } } } diff --git a/ui/src/router/routes.tsx b/ui/src/router/routes.tsx index 4337f9f04..aac074007 100644 --- a/ui/src/router/routes.tsx +++ b/ui/src/router/routes.tsx @@ -1,5 +1,6 @@ import React, { Suspense } from "react"; import { BrowserRouter, Route, Switch, withRouter } from "react-router-dom"; +import { CompatRouter } from "react-router-dom-v5-compat"; import { Layout } from "antd"; import { QueryClient, QueryClientProvider } from "react-query"; import { Configuration, InteractionType, PublicClientApplication, } from "@azure/msal-browser"; @@ -10,9 +11,9 @@ import Features from "../pages/feature/features"; import NewFeature from "../pages/feature/newFeature"; import FeatureDetails from "../pages/feature/featureDetails"; import DataSources from "../pages/dataSource/dataSources"; -import FeatureLineage from "../pages/feature/featureLineage"; import Jobs from "../pages/jobs/jobs"; import Monitoring from "../pages/monitoring/monitoring"; +import FeatureLineageGraph from "../pages/feature/featureLineageGraph"; type Props = {}; const queryClient = new QueryClient(); @@ -31,7 +32,8 @@ const Routes: React.FC = () => { - + +
@@ -43,7 +45,9 @@ const Routes: React.FC = () => { + component={ withRouter(FeatureLineageGraph) } /> + {/* {publicRoutes} */ } @@ -52,6 +56,7 @@ const Routes: React.FC = () => { + diff --git a/ui/src/site.css b/ui/src/site.css new file mode 100644 index 000000000..b6f2fb2ff --- /dev/null +++ b/ui/src/site.css @@ -0,0 +1,40 @@ +.lineage-graph { + margin-top: 1.5rem; +} + +.lineage-node { + padding: 4px 12px 7px; +} + +.lineage-node-active { + overflow: hidden; + border-radius: 0.25rem; + border-width: 2px; + border-style: solid; + --tw-border-opacity: 1; + border-color: rgba(57, 35, 150, var(--tw-border-opacity)); + --tw-bg-opacity: 1; + background-color: rgba(57, 35, 150, var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgba(255, 255, 255, var(--tw-text-opacity)); + opacity: 1; +} + +.lineage-node-title { + font-size: 14px; + font-weight: 700; +} + +.lineage-node-subtitle { + font-size: 10px; + font-style: italic; + text-overflow: ellipsis; + max-width: 135px; + overflow: hidden; + white-space: nowrap; +} + +.lineage-navigate { + padding: 4px 12px 7px; +} + From 860291ae4f06b06f0a23110b97547f863d4b9389 Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Mon, 30 May 2022 00:47:26 +0800 Subject: [PATCH 2/9] Upgrade to react router 6 and clean up codes --- ui/package-lock.json | 146 ++---------------- ui/package.json | 6 +- ui/src/app.tsx | 56 +++++++ ui/src/components/dataSourceList.tsx | 5 +- ui/src/components/featureForm.tsx | 6 +- ui/src/components/featureList.tsx | 9 +- .../lineage/{lineageGraph.tsx => graph.tsx} | 38 +++-- ui/src/components/lineage/graphNode.tsx | 45 ++++++ ui/src/components/lineage/lineageNode.tsx | 51 ------ ui/src/components/lineage/utils.ts | 23 +-- ui/src/index.tsx | 4 +- ui/src/pages/feature/featureDetails.tsx | 17 +- ui/src/pages/feature/features.tsx | 6 +- ...atureLineageGraph.tsx => lineageGraph.tsx} | 21 ++- ui/src/router/routes.tsx | 67 -------- ui/src/site.css | 8 +- 16 files changed, 189 insertions(+), 319 deletions(-) create mode 100644 ui/src/app.tsx rename ui/src/components/lineage/{lineageGraph.tsx => graph.tsx} (77%) create mode 100644 ui/src/components/lineage/graphNode.tsx delete mode 100644 ui/src/components/lineage/lineageNode.tsx rename ui/src/pages/feature/{featureLineageGraph.tsx => lineageGraph.tsx} (87%) delete mode 100644 ui/src/router/routes.tsx diff --git a/ui/package-lock.json b/ui/package-lock.json index 7f80289f3..0c83d5ba8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -2507,12 +2507,6 @@ "@types/node": "*" } }, - "@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true - }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -2660,36 +2654,6 @@ "redux": "^4.0.0" } }, - "@types/react-resizable": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.4.tgz", - "integrity": "sha512-+xsGkd+Gvb9+8mLR1EyhNN8kBRJcsT1uJF4WpkFpFPIoApX2S89BmJA2RVtMdkhwe6YxV4RbHfaJ3bIdcgHc7g==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/react-router": { - "version": "5.1.18", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz", - "integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==", - "dev": true, - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6419,16 +6383,11 @@ "dev": true }, "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "hoist-non-react-statics": { @@ -6981,11 +6940,6 @@ "is-docker": "^2.0.0" } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8442,15 +8396,6 @@ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true }, - "mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "requires": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" - } - }, "mini-css-extract-plugin": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz", @@ -8944,14 +8889,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10566,61 +10503,20 @@ "dev": true }, "react-router": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz", - "integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0" } }, "react-router-dom": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz", - "integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==", - "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - } - }, - "react-router-dom-v5-compat": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.3.0.tgz", - "integrity": "sha512-lfnfDEAdfXf92VQQ692+aMFj9JKb73GZ7EbiQJeJT4sK+KUGvua3FGN2H8n2H5zSMaY1cvrihGiaI373cvv7OQ==", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "requires": { - "history": "^5.3.0", + "history": "^5.2.0", "react-router": "6.3.0" - }, - "dependencies": { - "history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "requires": { - "@babel/runtime": "^7.7.6" - } - }, - "react-router": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", - "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", - "requires": { - "history": "^5.2.0" - } - } } }, "react-scripts": { @@ -10965,11 +10861,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "resolve-url-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", @@ -11923,16 +11814,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "tiny-invariant": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", - "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" - }, - "tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -12235,11 +12116,6 @@ } } }, - "value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/ui/package.json b/ui/package.json index a9dfc44f0..1663868b0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,8 +12,7 @@ "react-dom": "^17.0.2", "react-flow-renderer": "^9.7.4", "react-query": "^3.38.0", - "react-router-dom": "^5.3.1", - "react-router-dom-v5-compat": "^6.3.0" + "react-router-dom": "^6.3.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.3", @@ -24,9 +23,6 @@ "@types/node": "^16.11.26", "@types/react": "^17.0.43", "@types/react-dom": "^17.0.14", - "@types/react-resizable": "^1.7.4", - "@types/react-router": "^5.1.8", - "@types/react-router-dom": "^5.1.4", "react-scripts": "5.0.0", "typescript": "^4.6.3", "web-vitals": "^2.1.4" diff --git a/ui/src/app.tsx b/ui/src/app.tsx new file mode 100644 index 000000000..c61ef40a6 --- /dev/null +++ b/ui/src/app.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { Layout } from "antd"; +import { QueryClient, QueryClientProvider } from "react-query"; +import { Configuration, InteractionType, PublicClientApplication, } from "@azure/msal-browser"; +import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react"; +import Header from "./components/header/header"; +import SideMenu from "./components/sidemenu/siteMenu"; +import Features from "./pages/feature/features"; +import NewFeature from "./pages/feature/newFeature"; +import FeatureDetails from "./pages/feature/featureDetails"; +import DataSources from "./pages/dataSource/dataSources"; +import Jobs from "./pages/jobs/jobs"; +import Monitoring from "./pages/monitoring/monitoring"; +import LineageGraph from "./pages/feature/lineageGraph"; + +type Props = {}; +const queryClient = new QueryClient(); + +const msalConfig: Configuration = { + auth: { + clientId: process.env.REACT_APP_AAD_APP_CLIENT_ID, + authority: process.env.REACT_APP_AAD_APP_AUTHORITY, + redirectUri: window.location.origin, + }, +}; +const pca = new PublicClientApplication(msalConfig); +const App: React.FC = () => { + return ( + + + + + + + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + + + ); +}; + +export default App; diff --git a/ui/src/components/dataSourceList.tsx b/ui/src/components/dataSourceList.tsx index 466cbe865..76e641a1e 100644 --- a/ui/src/components/dataSourceList.tsx +++ b/ui/src/components/dataSourceList.tsx @@ -1,13 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; import { LoadingOutlined } from "@ant-design/icons"; import { Form, Select, Table } from "antd"; import { DataSourceAttributes, IDataSource } from "../models/model"; import { fetchDataSources, fetchProjects } from "../api"; const DataSourceList: React.FC = () => { - const history = useHistory(); - useCallback((location) => history.push(location), [history]); const columns = [ { title:
Name
, @@ -100,7 +97,7 @@ const DataSourceList: React.FC = () => { useEffect(() => { loadProjects(); - }, []) + }, [loadProjects]) const onProjectChange = async (value: string) => { setProject(value); diff --git a/ui/src/components/featureForm.tsx b/ui/src/components/featureForm.tsx index bca7ddac3..bdd89b5f4 100644 --- a/ui/src/components/featureForm.tsx +++ b/ui/src/components/featureForm.tsx @@ -1,8 +1,8 @@ import React, { CSSProperties, useEffect, useState } from 'react'; +import { UpCircleOutlined } from '@ant-design/icons' import { BackTop, Button, Form, Input, message, Space } from 'antd'; +import { Navigate } from "react-router-dom"; import { createFeature, updateFeature } from '../api'; -import { Redirect } from 'react-router'; -import { UpCircleOutlined } from '@ant-design/icons' import { FeatureAttributes, IFeature } from "../models/model"; type FeatureFormProps = { @@ -84,7 +84,7 @@ const FeatureForm: React.FC = ({ isNew, editMode, feature }) = - { fireRedirect && () } + { fireRedirect && () } ); }; diff --git a/ui/src/components/featureList.tsx b/ui/src/components/featureList.tsx index 8d13fe985..d3d60341a 100644 --- a/ui/src/components/featureList.tsx +++ b/ui/src/components/featureList.tsx @@ -1,13 +1,12 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Link, useHistory } from 'react-router-dom'; +import { Link, useNavigate } from "react-router-dom"; import { DownOutlined, LoadingOutlined } from '@ant-design/icons'; import { Button, Dropdown, Input, Menu, message, Popconfirm, Select, Tooltip, Form, Table } from 'antd'; import { IFeature } from "../models/model"; import { deleteFeature, fetchProjects, fetchFeatures } from "../api"; const FeatureList: React.FC = () => { - const history = useHistory(); - const navigateTo = useCallback((location) => history.push(location), [history]); + const navigate = useNavigate(); const columns = [ { title:
Name
, @@ -17,7 +16,7 @@ const FeatureList: React.FC = () => { render: (name: string, row: IFeature) => { return ( ) }, @@ -56,7 +55,7 @@ const FeatureList: React.FC = () => { diff --git a/ui/src/components/lineage/lineageGraph.tsx b/ui/src/components/lineage/graph.tsx similarity index 77% rename from ui/src/components/lineage/lineageGraph.tsx rename to ui/src/components/lineage/graph.tsx index 11b560121..460ae2e7d 100644 --- a/ui/src/components/lineage/lineageGraph.tsx +++ b/ui/src/components/lineage/graph.tsx @@ -12,10 +12,10 @@ import ReactFlow, { OnLoadParams, ReactFlowProvider } from 'react-flow-renderer'; -import { useSearchParams } from 'react-router-dom-v5-compat'; -import LineageNode from "./lineageNode"; -import { findNodeInElement, getLayoutElements } from "./utils"; +import { useSearchParams } from 'react-router-dom'; import { Spin } from "antd"; +import LineageNode from "./graphNode"; +import { findNodeInElement, getLayoutedElements } from "./utils"; import { LoadingOutlined } from "@ant-design/icons"; const nodeTypes = { @@ -25,20 +25,24 @@ type Props = { data: Elements; nodeId: string; } -const LineageGraph: React.FC = ({ data, nodeId }) => { +const Graph: React.FC = ({ data, nodeId }) => { const [, setURLSearchParams] = useSearchParams(); const instanceRef = useRef(null); - const { res, elementMapping } = getLayoutElements(data); - const [elements, setElements] = useState(res); + const { layoutedElements, elementMapping } = getLayoutedElements(data); + const [elements, setElements] = useState(layoutedElements); const [loading, setLoading] = useState(false); - useEffect(() => { - setElements(res); + setElements(layoutedElements); + console.log("setElements fired in useEffect, elements count = ", layoutedElements.length); }, [data, nodeId]); const resetHighlight = (): void => { + if (!elements || elements.length === 0) { + return; + } + const values: Elements = []; for (let index = 0; index < elements.length; index++) { @@ -62,10 +66,11 @@ const LineageGraph: React.FC = ({ data, nodeId }) => { } setElements(values); + console.log("setElements fired in resetHighlight, elements count = ", values.length); }; const highlightPath = (node: Node, check: boolean): void => { - const checkElements = check ? res : elements; + const checkElements = check ? layoutedElements : elements; const incomerIds = new Set([...getIncomers(node, checkElements).map((i) => i.id)]); const outgoerIds = new Set([...getOutgoers(node, checkElements).map((o) => o.id)]); @@ -106,7 +111,9 @@ const LineageGraph: React.FC = ({ data, nodeId }) => { }); } } + setElements(values); + console.log("setElements fired in highlightPath, elements count = ", values.length); }; const fitElements = (): void => { @@ -125,9 +132,10 @@ const LineageGraph: React.FC = ({ data, nodeId }) => { useEffect(() => { if (nodeId) { - const node = findNodeInElement(nodeId, res); + const node = findNodeInElement(nodeId, layoutedElements); if (node) { resetHighlight(); // new changes to the graph + console.log("resetHighlight fired"); highlightPath(node, !!nodeId); } } @@ -142,6 +150,7 @@ const LineageGraph: React.FC = ({ data, nodeId }) => { }, [instanceRef]); const onPaneClick = useCallback(() => { + console.log("=====> onPaneClick fired"); resetHighlight(); setURLSearchParams({}); }, []); @@ -154,11 +163,15 @@ const LineageGraph: React.FC = ({ data, nodeId }) => { values[nodePosition] = node; setElements(values); + console.log("setElements fired in onNodeDragStop, elements count = ", values.length); }; + + console.log("lineGraph rendered, elements count = ", layoutedElements.length); + return (
- { loading ? } /> : ( + { loading ? } /> : ( = ({ data, nodeId }) => { onElementClick={ (_: ReactMouseEvent, element: Node | Edge): void => { if (isNode(element)) { resetHighlight(); + console.log("resetHighlight fired"); highlightPath(element, false); setURLSearchParams({ nodeId: element.data.id }); } @@ -187,4 +201,4 @@ const LineageGraph: React.FC = ({ data, nodeId }) => { ); } -export default LineageGraph; +export default Graph; diff --git a/ui/src/components/lineage/graphNode.tsx b/ui/src/components/lineage/graphNode.tsx new file mode 100644 index 000000000..f04ce1f94 --- /dev/null +++ b/ui/src/components/lineage/graphNode.tsx @@ -0,0 +1,45 @@ +import React, { FC, memo } from 'react'; +import { RightCircleOutlined } from "@ant-design/icons"; +import { Handle, NodeProps, Position } from 'react-flow-renderer'; +import { useNavigate, useParams } from "react-router-dom"; + +type Params = { + project: string; +} +const GraphNode: FC = (props: NodeProps) => { + const navigate = useNavigate(); + const { project } = useParams(); + + const { data: { id, title, subtitle, qualifiedName, borderColor, active } } = props; + + const nodeColorStyle = { + border: `2px solid ${ borderColor }`, + }; + + const onNodeIconClick = () => { + navigate(`/projects/${ project }/features/${ qualifiedName }`) + }; + + return ( +
+
+ +
+
{ title } + { active && () } +
+
+ { subtitle } +
+
+ + +
+
+ ); +}; + +export default memo(GraphNode); diff --git a/ui/src/components/lineage/lineageNode.tsx b/ui/src/components/lineage/lineageNode.tsx deleted file mode 100644 index 7e6d7dfb9..000000000 --- a/ui/src/components/lineage/lineageNode.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { FC, memo } from 'react'; -import { RightCircleOutlined } from "@ant-design/icons"; -import { Handle, NodeProps, Position } from 'react-flow-renderer'; -import { useNavigate, useSearchParams } from "react-router-dom-v5-compat"; -import { useParams } from "react-router"; - - -const LineageNode: FC = (props: NodeProps) => { - const navigate = useNavigate(); - // @ts-ignore - const { project, qualifiedName } = useParams(); - const [searchParams] = useSearchParams(); - const nodeId = searchParams.get('nodeId') as string; - - const { - data: { label, otherName, active, id, borderColor }, - } = props; - - const nodeColorStyle = { - border: `2px solid ${ borderColor }`, - }; - - const onIconClick = () => { - navigate(`/projects/${ project }/features/${ qualifiedName }`) - }; - - const isActive = active || (nodeId === id); - - return ( -
-
- -
-
{ label } - { isActive && () } -
-
- { otherName } -
-
- - -
-
- ); -}; - -export default memo(LineageNode); diff --git a/ui/src/components/lineage/utils.ts b/ui/src/components/lineage/utils.ts index 058b01caf..9988bc9ea 100644 --- a/ui/src/components/lineage/utils.ts +++ b/ui/src/components/lineage/utils.ts @@ -5,11 +5,11 @@ const DEFAULT_WIDTH = 172; const DEFAULT_HEIGHT = 36; type getLayoutElementsRet = { - res: Elements, + layoutedElements: Elements, elementMapping: Record, }; -const getLayoutElements = (elements: Elements, direction = 'LR'): getLayoutElementsRet => { +const getLayoutedElements = (elements: Elements, direction = 'LR'): getLayoutElementsRet => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); @@ -45,7 +45,7 @@ const getLayoutElements = (elements: Elements, direction = 'LR'): getLayoutEleme element.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; element.position = { - x: nodeWithPosition.x - DEFAULT_WIDTH / 2 + Math.random() / 1000, + x: nodeWithPosition.x - DEFAULT_WIDTH / 2, y: nodeWithPosition.y - DEFAULT_HEIGHT / 2, }; } @@ -54,7 +54,7 @@ const getLayoutElements = (elements: Elements, direction = 'LR'): getLayoutEleme } return { - res: newElements, + layoutedElements: newElements, elementMapping: elementsObj, }; }; @@ -67,10 +67,10 @@ const featureTypeColors: Record = { }; const generateNode = ({ - nodeId, index, - currentNode + nodeId, index, + currentNode // eslint-disable-next-line @typescript-eslint/no-explicit-any -}: any): any => ({ + }: any): any => ({ key: nodeId, id: index?.toString(), type: 'custom-node', @@ -81,9 +81,10 @@ const generateNode = ({ }, data: { id: nodeId, - label: currentNode.displayText, + title: currentNode.displayText, + subtitle: currentNode.typeName, + qualifiedName: currentNode.attributes.qualifiedName, borderColor: featureTypeColors[currentNode.typeName], - otherName: currentNode.typeName, }, }); @@ -97,7 +98,7 @@ const generateEdge = ({ obj, from, to }: GenerateEdgeProps): Edge => { const source = obj?.[from]; const target = obj?.[to]; - const id = `e${source}-${target}`; + const id = `e${ source }-${ target }`; return ({ id, source, @@ -109,7 +110,7 @@ const generateEdge = ({ obj, from, to }: GenerateEdgeProps): Edge => { export { generateEdge, generateNode, - getLayoutElements, + getLayoutedElements, }; export const findNodeInElement = (nodeId: string | null, elements: Elements): Node | null => { diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 776313feb..5b2d5e60f 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,13 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import Routes from './router/routes'; +import App from "./app"; import 'antd/dist/antd.min.css'; import './index.less'; import "./site.css" ReactDOM.render( - + , document.getElementById('root')); diff --git a/ui/src/pages/feature/featureDetails.tsx b/ui/src/pages/feature/featureDetails.tsx index fef6f7f86..89c5c49a7 100644 --- a/ui/src/pages/feature/featureDetails.tsx +++ b/ui/src/pages/feature/featureDetails.tsx @@ -1,7 +1,7 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { Alert, Button, Card, Col, Modal, Row, Space, Spin } from 'antd'; import { ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'; -import { useHistory, useParams } from 'react-router'; +import { useNavigate, useParams } from "react-router-dom"; import { QueryStatus, useQuery } from "react-query"; import { deleteFeature, fetchFeature } from '../../api'; import { AxiosError } from 'axios'; @@ -16,10 +16,9 @@ type Params = { } const FeatureDetails: React.FC = () => { - const { project, qualifiedName } = useParams(); + const { project, qualifiedName } = useParams() as Params; + const navigate = useNavigate(); const loadingIcon = ; - const history = useHistory(); - const navigateTo = useCallback((location) => history.push(location), [history]); const { status, error, @@ -27,8 +26,8 @@ const FeatureDetails: React.FC = () => { } = useQuery(['feature', qualifiedName], () => fetchFeature(project, qualifiedName)); const openLineageWindow = () => { - const lineageUrl = `/projects/${ project }/features/${ qualifiedName }/lineage`; - window.open(lineageUrl); + const lineageUrl = `/projects/${ project }/lineage`; + navigate(lineageUrl); } const onClickDeleteFeature = () => { @@ -41,7 +40,7 @@ const FeatureDetails: React.FC = () => { icon: , async onOk() { await deleteFeature(qualifiedName); - history.push('/features'); + navigate('/features'); }, onCancel() { console.log('Cancel clicked'); @@ -68,7 +67,7 @@ const FeatureDetails: React.FC = () => {
    { features.map((_) => ( )) }
diff --git a/ui/src/pages/feature/features.tsx b/ui/src/pages/feature/features.tsx index 02d161029..15d011832 100644 --- a/ui/src/pages/feature/features.tsx +++ b/ui/src/pages/feature/features.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { Button, Card, Space } from 'antd'; -import { useHistory } from 'react-router'; +import { useNavigate } from "react-router-dom"; import FeatureList from "../../components/featureList"; type Props = {}; const Features: React.FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const onCreateFeatureClick = () => { - history.push('/new-feature'); + navigate('/new-feature'); }; return ( diff --git a/ui/src/pages/feature/featureLineageGraph.tsx b/ui/src/pages/feature/lineageGraph.tsx similarity index 87% rename from ui/src/pages/feature/featureLineageGraph.tsx rename to ui/src/pages/feature/lineageGraph.tsx index 760ec87de..e00455ad4 100644 --- a/ui/src/pages/feature/featureLineageGraph.tsx +++ b/ui/src/pages/feature/lineageGraph.tsx @@ -1,16 +1,17 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Button, Card, Radio } from 'antd'; +import { useParams, useSearchParams } from "react-router-dom"; import { Elements } from 'react-flow-renderer'; -import LineageGraph from "../../components/lineage/lineageGraph"; +import Graph from "../../components/lineage/graph"; import { generateEdge, generateNode } from "../../components/lineage/utils"; import { fetchProjectLineages } from "../../api"; -import { useSearchParams } from "react-router-dom-v5-compat"; -import { useParams } from "react-router"; -const FeatureLineageGraph: React.FC = () => { +type Params = { + project: string; +} +const LineageGraph: React.FC = () => { + const { project } = useParams() as Params; const [searchParams] = useSearchParams(); - // @ts-ignore - const { project } = useParams(); const nodeId = searchParams.get('nodeId') as string; const [elements, SetElements] = useState([]); @@ -24,9 +25,11 @@ const FeatureLineageGraph: React.FC = () => { for (let index = 0; index < Object.values(graphData.guidEntityMap).length; index++) { const currentNode: any = Object.values(graphData.guidEntityMap)[index]; + if (currentNode.typeName === "feathr_workspace_v1") { continue; // Open issue: should project node get displayed as well? } + const nodeId = currentNode.guid; // check if model type exists and this currentNode is of that type @@ -54,6 +57,8 @@ const FeatureLineageGraph: React.FC = () => { } } SetElements(elements); + + console.log("setElements fired in fetchGraphData, elements count = ", elements.length); }, [featureType]) useEffect(() => { @@ -89,10 +94,10 @@ const FeatureLineageGraph: React.FC = () => {
- { elements && } + { elements && }
); } -export default FeatureLineageGraph; +export default LineageGraph; diff --git a/ui/src/router/routes.tsx b/ui/src/router/routes.tsx deleted file mode 100644 index aac074007..000000000 --- a/ui/src/router/routes.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Suspense } from "react"; -import { BrowserRouter, Route, Switch, withRouter } from "react-router-dom"; -import { CompatRouter } from "react-router-dom-v5-compat"; -import { Layout } from "antd"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { Configuration, InteractionType, PublicClientApplication, } from "@azure/msal-browser"; -import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react"; -import Header from "../components/header/header"; -import SideMenu from "../components/sidemenu/siteMenu"; -import Features from "../pages/feature/features"; -import NewFeature from "../pages/feature/newFeature"; -import FeatureDetails from "../pages/feature/featureDetails"; -import DataSources from "../pages/dataSource/dataSources"; -import Jobs from "../pages/jobs/jobs"; -import Monitoring from "../pages/monitoring/monitoring"; -import FeatureLineageGraph from "../pages/feature/featureLineageGraph"; - -type Props = {}; -const queryClient = new QueryClient(); - -const msalConfig: Configuration = { - auth: { - clientId: process.env.REACT_APP_AAD_APP_CLIENT_ID, - authority: process.env.REACT_APP_AAD_APP_AUTHORITY, - redirectUri: window.location.origin, - }, -}; -const pca = new PublicClientApplication(msalConfig); -const Routes: React.FC = () => { - return ( - - - - - - - - -
- - }> - - - - - - - - - {/* {publicRoutes} */ } - {/* */ } - - - - - - - - - - ); -}; - -export default Routes; diff --git a/ui/src/site.css b/ui/src/site.css index b6f2fb2ff..7a314f46f 100644 --- a/ui/src/site.css +++ b/ui/src/site.css @@ -2,10 +2,6 @@ margin-top: 1.5rem; } -.lineage-node { - padding: 4px 12px 7px; -} - .lineage-node-active { overflow: hidden; border-radius: 0.25rem; @@ -20,6 +16,10 @@ opacity: 1; } +.lineage-node-box { + padding: 4px 12px 7px; +} + .lineage-node-title { font-size: 14px; font-weight: 700; From 4c34c56473df7a607bbcf51bb436da1a4c1cb21e Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Mon, 30 May 2022 01:35:40 +0800 Subject: [PATCH 3/9] Code clean up --- ui/src/components/lineage/graph.tsx | 12 +----------- ui/src/pages/feature/lineageGraph.tsx | 9 ++++----- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/ui/src/components/lineage/graph.tsx b/ui/src/components/lineage/graph.tsx index 460ae2e7d..e21ed2972 100644 --- a/ui/src/components/lineage/graph.tsx +++ b/ui/src/components/lineage/graph.tsx @@ -35,7 +35,6 @@ const Graph: React.FC = ({ data, nodeId }) => { useEffect(() => { setElements(layoutedElements); - console.log("setElements fired in useEffect, elements count = ", layoutedElements.length); }, [data, nodeId]); const resetHighlight = (): void => { @@ -66,7 +65,6 @@ const Graph: React.FC = ({ data, nodeId }) => { } setElements(values); - console.log("setElements fired in resetHighlight, elements count = ", values.length); }; const highlightPath = (node: Node, check: boolean): void => { @@ -113,7 +111,6 @@ const Graph: React.FC = ({ data, nodeId }) => { } setElements(values); - console.log("setElements fired in highlightPath, elements count = ", values.length); }; const fitElements = (): void => { @@ -134,8 +131,7 @@ const Graph: React.FC = ({ data, nodeId }) => { if (nodeId) { const node = findNodeInElement(nodeId, layoutedElements); if (node) { - resetHighlight(); // new changes to the graph - console.log("resetHighlight fired"); + resetHighlight(); highlightPath(node, !!nodeId); } } @@ -150,7 +146,6 @@ const Graph: React.FC = ({ data, nodeId }) => { }, [instanceRef]); const onPaneClick = useCallback(() => { - console.log("=====> onPaneClick fired"); resetHighlight(); setURLSearchParams({}); }, []); @@ -163,12 +158,8 @@ const Graph: React.FC = ({ data, nodeId }) => { values[nodePosition] = node; setElements(values); - console.log("setElements fired in onNodeDragStop, elements count = ", values.length); }; - - console.log("lineGraph rendered, elements count = ", layoutedElements.length); - return (
{ loading ? } /> : ( @@ -184,7 +175,6 @@ const Graph: React.FC = ({ data, nodeId }) => { onElementClick={ (_: ReactMouseEvent, element: Node | Edge): void => { if (isNode(element)) { resetHighlight(); - console.log("resetHighlight fired"); highlightPath(element, false); setURLSearchParams({ nodeId: element.data.id }); } diff --git a/ui/src/pages/feature/lineageGraph.tsx b/ui/src/pages/feature/lineageGraph.tsx index e00455ad4..e14854d95 100644 --- a/ui/src/pages/feature/lineageGraph.tsx +++ b/ui/src/pages/feature/lineageGraph.tsx @@ -32,7 +32,7 @@ const LineageGraph: React.FC = () => { const nodeId = currentNode.guid; - // check if model type exists and this currentNode is of that type + // If toggled feature type exists, skip other types if (featureType && featureType !== currentNode.typeName) { continue; } @@ -51,14 +51,13 @@ const LineageGraph: React.FC = () => { const { fromEntityId: from, toEntityId: to, relationshipType } = graphData.relations[index]; const edge = generateEdge({ obj: elementObj, from, to }); if (edge?.source && edge?.target) { - if (relationshipType === "Produces") { // Drop edges with other relationship like Consumes, BelongsTo, etc) + // Drop edges with other relationship e.g, Consumes, BelongsTo + if (relationshipType === "Produces") { elements.push(edge); } } } SetElements(elements); - - console.log("setElements fired in fetchGraphData, elements count = ", elements.length); }, [featureType]) useEffect(() => { @@ -94,7 +93,7 @@ const LineageGraph: React.FC = () => {
- { elements && } + { elements && }
); From c3fd9f8805383196ab000ab7f0d47edbd856b6c4 Mon Sep 17 00:00:00 2001 From: Yuqing Wei Date: Mon, 30 May 2022 21:45:02 +0800 Subject: [PATCH 4/9] Add RBAC UI with mock data (#1) --- ui/src/api/api.tsx | 41 +++++- ui/src/api/mock/userrole.json | 63 +++++++++ ui/src/app.tsx | 4 + ui/src/components/roleManagementForm.tsx | 88 ++++++++++++ ui/src/components/sidemenu/siteMenu.tsx | 3 +- ui/src/components/userRoles.tsx | 150 +++++++++++++++++++++ ui/src/models/model.ts | 17 +++ ui/src/pages/management/management.tsx | 19 +++ ui/src/pages/management/roleManagement.tsx | 17 +++ 9 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 ui/src/api/mock/userrole.json create mode 100644 ui/src/components/roleManagementForm.tsx create mode 100644 ui/src/components/userRoles.tsx create mode 100644 ui/src/pages/management/management.tsx create mode 100644 ui/src/pages/management/roleManagement.tsx diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index d80d4a89c..73990f854 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -1,5 +1,6 @@ import Axios from "axios"; -import { Features, IDataSource, IFeature, IFeatureDetail, IFeatureLineage } from "../models/model"; +import { Features, IDataSource, IFeature, IFeatureDetail, IFeatureLineage, IUserRole, RoleForm } from "../models/model"; +import mockUserRole from "./mock/userrole.json"; const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT + "/api"; const purview = "feathrazuretest3-purview1" @@ -101,3 +102,41 @@ export const deleteFeature = async (qualifiedName: string) => { }); }; +export const listUserRole = async () => { + let data:IUserRole[] = mockUserRole + return data +}; + +export const getUserRole = async (userName: string) => { + return await Axios + .get(`${ API_ENDPOINT }/user/${userName}/userroles?code=${ token }`, {}) + .then((response) => { + return response.data; + }) +} + +export const addUserRole = async (roleForm: RoleForm) => { + return await Axios + .post(`${ API_ENDPOINT }/user/${roleForm.userName}/userroles/new`, roleForm, + { + headers: { "Content-Type": "application/json;" }, + params: {}, + }).then((response) => { + return response; + }).catch((error) => { + return error.response; + }); +} + +export const deleteUserRole = async (roleForm: RoleForm) => { + return await Axios + .post(`${ API_ENDPOINT }/user/${roleForm.userName}/userroles/delete`, roleForm, + { + headers: { "Content-Type": "application/json;" }, + params: {}, + }).then((response) => { + return response; + }).catch((error) => { + return error.response; + }); +} \ No newline at end of file diff --git a/ui/src/api/mock/userrole.json b/ui/src/api/mock/userrole.json new file mode 100644 index 000000000..20535b8ec --- /dev/null +++ b/ui/src/api/mock/userrole.json @@ -0,0 +1,63 @@ +[ + { + "id": 1, + "scope": "Global", + "userName": "edwinc@microsoft.com", + "roleName": "Admin", + "permissions": [ + "Read", + "Write", + "Management" + ], + "createReason": "Resource Owner", + "createTime": "2022/5/15" + }, + { + "id": 1, + "scope": "Global", + "userName": "yuqwe@microsoft.com", + "roleName": "Admin", + "permissions": [ + "Read", + "Write", + "Management" + ], + "createReason": "Test Purpose", + "createTime": "2022/5/16" + }, + { + "id": 2, + "scope": "Project A: Frontend Datasets", + "userName": "blairch@microsoft.com", + "roleName": "Producer", + "permissions": [ + "Read", + "Write" + ], + "createReason": "Project Owner", + "createTime": "2022/5/16" + }, + { + "id": 3, + "scope": "Project B: Backend Datasets", + "userName": "xuchen@microsoft.com", + "roleName": "Producer", + "permissions": [ + "Read", + "Write" + ], + "createReason": "Project Owner", + "createTime": "2022/5/16" + }, + { + "id": 4, + "scope": "Project B: Backend Datasets", + "userName": "yihgu@microsoft.com", + "roleName": "Consumer", + "permissions": [ + "Read" + ], + "createReason": "Data Engineering", + "createTime": "2022/5/17" + } +] \ No newline at end of file diff --git a/ui/src/app.tsx b/ui/src/app.tsx index c61ef40a6..4de809f1b 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -13,6 +13,8 @@ import DataSources from "./pages/dataSource/dataSources"; import Jobs from "./pages/jobs/jobs"; import Monitoring from "./pages/monitoring/monitoring"; import LineageGraph from "./pages/feature/lineageGraph"; +import Management from "./pages/management/management"; +import RoleManagement from "./pages/management/roleManagement"; type Props = {}; const queryClient = new QueryClient(); @@ -43,6 +45,8 @@ const App: React.FC = () => { } /> } /> } /> + } /> + } /> diff --git a/ui/src/components/roleManagementForm.tsx b/ui/src/components/roleManagementForm.tsx new file mode 100644 index 000000000..78837f720 --- /dev/null +++ b/ui/src/components/roleManagementForm.tsx @@ -0,0 +1,88 @@ +import React, { CSSProperties, useEffect, useState } from 'react'; +import { BackTop, Button, Form, Input, Select, Space } from 'antd'; +import { Navigate } from "react-router-dom"; +import { addUserRole} from '../api'; +import { UpCircleOutlined } from '@ant-design/icons' +import { RoleForm, IUserRole } from "../models/model"; + +type RoleManagementFormProps = { + isNew: boolean; + editMode: boolean; + userRole?: IUserRole; +}; + +const Admin = "Admin" +const Producer = "Producer" +const Consumer = "Consumer" + +const RoleManagementForm: React.FC = ({ isNew, editMode, userRole }) => { + const [fireRedirect, setRedirect] = useState(false); + const [createLoading, setCreateLoading] = useState(false); + + const [form] = Form.useForm(); + const { Option } = Select; + + useEffect(() => { + if (userRole !== undefined) { + form.setFieldsValue(userRole); + } + }, [userRole, form]); + + const onClickSave = async () => { + setCreateLoading(true); + const roleForm: RoleForm = form.getFieldsValue(); + await addUserRole(roleForm); + setCreateLoading(false); + } + + const styling: CSSProperties = { width: "92%" } + return ( + <> +
+ + + + + + + + + + + + + + + + + + +
+ { fireRedirect && () } + + ); +}; + +export default RoleManagementForm \ No newline at end of file diff --git a/ui/src/components/sidemenu/siteMenu.tsx b/ui/src/components/sidemenu/siteMenu.tsx index f1d2c8574..d99638756 100644 --- a/ui/src/components/sidemenu/siteMenu.tsx +++ b/ui/src/components/sidemenu/siteMenu.tsx @@ -1,6 +1,5 @@ -import React from 'react'; import { Layout, Menu } from 'antd'; -import { CopyOutlined, DatabaseOutlined, EyeOutlined, RocketOutlined } from '@ant-design/icons'; +import { CopyOutlined, DatabaseOutlined, EyeOutlined, RocketOutlined} from '@ant-design/icons'; import { Link } from 'react-router-dom'; const { Sider } = Layout; diff --git a/ui/src/components/userRoles.tsx b/ui/src/components/userRoles.tsx new file mode 100644 index 000000000..5b81a5087 --- /dev/null +++ b/ui/src/components/userRoles.tsx @@ -0,0 +1,150 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button, Modal, PageHeader, Row, Space, Table, Tag } from "antd"; +import { IUserRole } from "../models/model"; +import { listUserRole } from "../api"; +import { useMsal } from '@azure/msal-react'; + +const UserRoles: React.FC = () => { + const navigate = useNavigate(); + const [visible, setVisible] = React.useState(false); + const [confirmLoading, setConfirmLoading] = React.useState(false); + const [modalText, setModalText] = React.useState('Content of the modal'); + + const { accounts, instance } = useMsal(); + const showModal = () => { + setVisible(true); + setModalText("This Role Assignment will be deleted."); + }; + const handleOk = () => { + setModalText('The modal will be closed after two seconds'); + setConfirmLoading(true); + setTimeout(() => { + setVisible(false); + setConfirmLoading(false); + }, 2000); + }; + + const handleCancel = () => { + console.log('Clicked cancel button'); + setVisible(false); + }; + const columns = [ + { + title:
Scope
, + dataIndex: 'scope', + key: 'scope', + align: 'center' as 'center', + }, + { + title:
User
, + dataIndex: 'userName', + key: 'userName', + align: 'center' as 'center', + }, + { + title:
Role
, + dataIndex: 'roleName', + key: 'roleName', + align: 'center' as 'center', + }, + { + title:
Permissions
, + key: 'permissions', + dataIndex: 'permissions', + render: (tags: any[]) => ( + <> + {tags.map(tag => { + let color = tag.length > 5 ? 'red' : 'green'; + if (tag === 'Write') color = 'blue' + return ( + + {tag.toUpperCase()} + + ); + })} + + ), + }, + { + title:
Create Reason
, + dataIndex: 'createReason', + key: 'createReason', + align: 'center' as 'center', + }, + { + title:
Create Time
, + dataIndex: 'createTime', + key: 'createTime', + align: 'center' as 'center', + }, + { + title: 'Action', + key: 'action', + render: (text: any) => ( + + + +

{modalText}

+
+
+ ), + }, + ]; + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [tableData, setTableData] = useState(); + + const fetchData = useCallback(async () => { + setLoading(true); + const result = await listUserRole(); + console.log(result); + setPage(page); + setTableData(result); + setLoading(false); + }, [page]) + + const onClickRoleAssign = () => { + navigate('/role-management'); + return; + } + + useEffect(() => { + fetchData() + }, []) + + return ( +
+ + +
+ <> +

+ Below is the mock data for now. Will connect with Management APIs. +

+ +
+
+
+ + + + ; + + ); +} + +export default UserRoles; diff --git a/ui/src/models/model.ts b/ui/src/models/model.ts index 235855fc1..7c2b03cbc 100644 --- a/ui/src/models/model.ts +++ b/ui/src/models/model.ts @@ -74,3 +74,20 @@ export interface IFeatureLineage { relations: any; } +export interface IUserRole { + id: number; + scope: string; + userName: string; + roleName: string; + createTime: string; + createReason: string; + deleteTime?: any; + deleteReason?: any; +} + +export interface RoleForm { + scope: string; + userName: string; + roleName: string; + reason: string; +} diff --git a/ui/src/pages/management/management.tsx b/ui/src/pages/management/management.tsx new file mode 100644 index 000000000..3788fd84c --- /dev/null +++ b/ui/src/pages/management/management.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Card } from 'antd'; +import UserRoles from '../../components/userRoles'; + +type Props = {}; + +const Management: React.FC = () => { + return ( + <> +
+ + + +
+ + ); +}; + +export default Management; \ No newline at end of file diff --git a/ui/src/pages/management/roleManagement.tsx b/ui/src/pages/management/roleManagement.tsx new file mode 100644 index 000000000..a2261c0b2 --- /dev/null +++ b/ui/src/pages/management/roleManagement.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Card } from 'antd'; +import RoleManagementForm from '../../components/roleManagementForm'; + +type Props = {}; + +const RoleManagement: React.FC = () => { + return ( +
+ + + +
+ ); +}; + +export default RoleManagement; \ No newline at end of file From 2a590dceb3d87fad29b344d20f71e03ceeef1385 Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Mon, 30 May 2022 22:24:38 +0800 Subject: [PATCH 5/9] Address code review comments and clean up code. Merge yuqing's RBAC UI flighting code --- ui/src/app.tsx | 7 +- .../components/{lineage => graph}/graph.tsx | 35 +---- .../{lineage => graph}/graphNode.tsx | 2 +- ui/src/components/{lineage => graph}/utils.ts | 0 ui/src/components/roleManagementForm.tsx | 4 +- ui/src/components/userRoles.tsx | 8 +- ui/src/pages/feature/lineageGraph.tsx | 122 ++++++++++-------- 7 files changed, 82 insertions(+), 96 deletions(-) rename ui/src/components/{lineage => graph}/graph.tsx (83%) rename ui/src/components/{lineage => graph}/graphNode.tsx (93%) rename ui/src/components/{lineage => graph}/utils.ts (100%) diff --git a/ui/src/app.tsx b/ui/src/app.tsx index 4de809f1b..5d2caca3a 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -16,7 +16,6 @@ import LineageGraph from "./pages/feature/lineageGraph"; import Management from "./pages/management/management"; import RoleManagement from "./pages/management/roleManagement"; -type Props = {}; const queryClient = new QueryClient(); const msalConfig: Configuration = { @@ -26,10 +25,10 @@ const msalConfig: Configuration = { redirectUri: window.location.origin, }, }; -const pca = new PublicClientApplication(msalConfig); -const App: React.FC = () => { +const msalClient = new PublicClientApplication(msalConfig); +const App: React.FC = () => { return ( - + diff --git a/ui/src/components/lineage/graph.tsx b/ui/src/components/graph/graph.tsx similarity index 83% rename from ui/src/components/lineage/graph.tsx rename to ui/src/components/graph/graph.tsx index e21ed2972..166cf19ae 100644 --- a/ui/src/components/lineage/graph.tsx +++ b/ui/src/components/graph/graph.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useRef, useState, } from 'react'; +import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useState, } from 'react'; import ReactFlow, { ConnectionLineType, Controls, @@ -9,14 +9,11 @@ import ReactFlow, { isEdge, isNode, Node, - OnLoadParams, ReactFlowProvider } from 'react-flow-renderer'; import { useSearchParams } from 'react-router-dom'; -import { Spin } from "antd"; import LineageNode from "./graphNode"; import { findNodeInElement, getLayoutedElements } from "./utils"; -import { LoadingOutlined } from "@ant-design/icons"; const nodeTypes = { 'custom-node': LineageNode, @@ -27,16 +24,15 @@ type Props = { } const Graph: React.FC = ({ data, nodeId }) => { const [, setURLSearchParams] = useSearchParams(); - const instanceRef = useRef(null); const { layoutedElements, elementMapping } = getLayoutedElements(data); const [elements, setElements] = useState(layoutedElements); - const [loading, setLoading] = useState(false); useEffect(() => { setElements(layoutedElements); }, [data, nodeId]); + // Reset all node highlight status const resetHighlight = (): void => { if (!elements || elements.length === 0) { return; @@ -67,6 +63,7 @@ const Graph: React.FC = ({ data, nodeId }) => { setElements(values); }; + // Highlight path of selected node, including all linked up and down stream nodes const highlightPath = (node: Node, check: boolean): void => { const checkElements = check ? layoutedElements : elements; @@ -113,20 +110,6 @@ const Graph: React.FC = ({ data, nodeId }) => { setElements(values); }; - const fitElements = (): void => { - setTimeout(() => { - instanceRef?.current?.fitView(); - }, 1); - setTimeout(() => { - setLoading(false); - }, 1000); - }; - - const onLoad = (reactFlowInstance: OnLoadParams | null) => { - instanceRef.current = reactFlowInstance; - fitElements(); - }; - useEffect(() => { if (nodeId) { const node = findNodeInElement(nodeId, layoutedElements); @@ -137,14 +120,7 @@ const Graph: React.FC = ({ data, nodeId }) => { } }, [nodeId]); - useEffect(() => { - if (instanceRef.current) { - setLoading(true); - - fitElements(); - } - }, [instanceRef]); - + // When panel is clicked, reset all highlighted path, and remove the nodeId query string in url path. const onPaneClick = useCallback(() => { resetHighlight(); setURLSearchParams({}); @@ -162,12 +138,10 @@ const Graph: React.FC = ({ data, nodeId }) => { return (
- { loading ? } /> : ( = ({ data, nodeId }) => { - ) }
); } diff --git a/ui/src/components/lineage/graphNode.tsx b/ui/src/components/graph/graphNode.tsx similarity index 93% rename from ui/src/components/lineage/graphNode.tsx rename to ui/src/components/graph/graphNode.tsx index f04ce1f94..71f3a426b 100644 --- a/ui/src/components/lineage/graphNode.tsx +++ b/ui/src/components/graph/graphNode.tsx @@ -10,7 +10,7 @@ const GraphNode: FC = (props: NodeProps) => { const navigate = useNavigate(); const { project } = useParams(); - const { data: { id, title, subtitle, qualifiedName, borderColor, active } } = props; + const { data: { title, subtitle, qualifiedName, borderColor, active } } = props; const nodeColorStyle = { border: `2px solid ${ borderColor }`, diff --git a/ui/src/components/lineage/utils.ts b/ui/src/components/graph/utils.ts similarity index 100% rename from ui/src/components/lineage/utils.ts rename to ui/src/components/graph/utils.ts diff --git a/ui/src/components/roleManagementForm.tsx b/ui/src/components/roleManagementForm.tsx index 78837f720..bdcfa5457 100644 --- a/ui/src/components/roleManagementForm.tsx +++ b/ui/src/components/roleManagementForm.tsx @@ -15,8 +15,8 @@ const Admin = "Admin" const Producer = "Producer" const Consumer = "Consumer" -const RoleManagementForm: React.FC = ({ isNew, editMode, userRole }) => { - const [fireRedirect, setRedirect] = useState(false); +const RoleManagementForm: React.FC = ({ editMode, userRole }) => { + const [fireRedirect] = useState(false); const [createLoading, setCreateLoading] = useState(false); const [form] = Form.useForm(); diff --git a/ui/src/components/userRoles.tsx b/ui/src/components/userRoles.tsx index 5b81a5087..b8d5a008c 100644 --- a/ui/src/components/userRoles.tsx +++ b/ui/src/components/userRoles.tsx @@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom'; import { Button, Modal, PageHeader, Row, Space, Table, Tag } from "antd"; import { IUserRole } from "../models/model"; import { listUserRole } from "../api"; -import { useMsal } from '@azure/msal-react'; const UserRoles: React.FC = () => { const navigate = useNavigate(); @@ -11,7 +10,6 @@ const UserRoles: React.FC = () => { const [confirmLoading, setConfirmLoading] = React.useState(false); const [modalText, setModalText] = React.useState('Content of the modal'); - const { accounts, instance } = useMsal(); const showModal = () => { setVisible(true); setModalText("This Role Assignment will be deleted."); @@ -81,7 +79,7 @@ const UserRoles: React.FC = () => { { title: 'Action', key: 'action', - render: (text: any) => ( + render: () => (
- { elements && } + { loading + ? } /> + : }
); From 689dcec693ce159edc02b29d0d13f9fcb4b2d33c Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Wed, 1 Jun 2022 01:59:24 +0800 Subject: [PATCH 6/9] Integrate with latest api schema --- ui/.env | 2 +- ui/src/api/api.tsx | 25 +++++---- ui/src/app.tsx | 2 +- ui/src/components/featureList.tsx | 14 +++--- ui/src/models/model.ts | 30 ++++------- ui/src/pages/feature/featureDetails.tsx | 67 +++++++++++++------------ ui/src/pages/feature/lineageGraph.tsx | 2 +- 7 files changed, 67 insertions(+), 75 deletions(-) diff --git a/ui/.env b/ui/.env index e3a3b7756..0a9fb0b61 100644 --- a/ui/.env +++ b/ui/.env @@ -1,3 +1,3 @@ REACT_APP_AAD_APP_CLIENT_ID=db8dc4b0-202e-450c-b38d-7396ad9631a5 REACT_APP_AAD_APP_AUTHORITY=https://login.microsoftonline.com/common -REACT_APP_API_ENDPOINT=https://feathr-sql-registry.azurewebsites.net +REACT_APP_API_ENDPOINT=https://feathr-registry.azurewebsites.net diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index 73990f854..93a9e10dd 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -1,14 +1,13 @@ import Axios from "axios"; -import { Features, IDataSource, IFeature, IFeatureDetail, IFeatureLineage, IUserRole, RoleForm } from "../models/model"; +import { IDataSource, IFeature, IFeatureLineage, IUserRole, RoleForm } from "../models/model"; import mockUserRole from "./mock/userrole.json"; -const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT + "/api"; -const purview = "feathrazuretest3-purview1" +const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT + "/api/v1"; const token = "mockAppServiceKey"; export const fetchDataSources = async (project: string) => { return Axios - .get(`${ API_ENDPOINT }/v1/purview/${ purview }/projects/${ project }/datasources?code=${ token }`, + .get(`${ API_ENDPOINT }/projects/${ project }/datasources?code=${ token }`, { headers: {} }) .then((response) => { return response.data; @@ -17,7 +16,7 @@ export const fetchDataSources = async (project: string) => { export const fetchProjects = async () => { return Axios - .get<[]>(`${ API_ENDPOINT }/v1/purview/${ purview }/projects?code=${ token }`, + .get<[]>(`${ API_ENDPOINT }/projects?code=${ token }`, { headers: {} }) @@ -28,27 +27,27 @@ export const fetchProjects = async () => { export const fetchFeatures = async (project: string, page: number, limit: number, keyword: string) => { return Axios - .get(`${ API_ENDPOINT }/v1/purview/${ purview }/projects/${ project }/features?code=${ token }`, + .get(`${ API_ENDPOINT }/projects/${ project }/features?code=${ token }`, { params: { 'keyword': keyword, 'page': page, 'limit': limit }, headers: {} }) .then((response) => { - return response.data.features; + return response.data; }) }; -export const fetchFeature = async (project: string, qualifiedName: string) => { +export const fetchFeature = async (project: string, featureId: string) => { return Axios - .get(`${ API_ENDPOINT }/v1/purview/${ purview }/features/${ qualifiedName }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/features/${ featureId }?code=${ token }`, {}) .then((response) => { - return response.data?.entity?.attributes; + return response.data; }) }; export const fetchProjectLineages = async (project: string) => { return Axios - .get(`${ API_ENDPOINT }/v1/purview/${ purview }/projects/${ project }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/projects/${ project }?code=${ token }`, {}) .then((response) => { return response.data; }) @@ -56,7 +55,7 @@ export const fetchProjectLineages = async (project: string) => { export const fetchFeatureLineages = async (project: string) => { return Axios - .get(`${ API_ENDPOINT }/v1/purview/${ purview }/features/lineage/${ project }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/features/lineage/${ project }?code=${ token }`, {}) .then((response) => { return response.data; }) @@ -77,7 +76,7 @@ export const createFeature = async (feature: IFeature) => { } export const updateFeature = async (feature: IFeature, id: string) => { - feature.id = id; + feature.guid = id; return await Axios.put(`${ API_ENDPOINT }/features/${ id }`, feature, { headers: { "Content-Type": "application/json;" }, diff --git a/ui/src/app.tsx b/ui/src/app.tsx index 5d2caca3a..2a21eb373 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -40,7 +40,7 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/ui/src/components/featureList.tsx b/ui/src/components/featureList.tsx index d3d60341a..92003cde2 100644 --- a/ui/src/components/featureList.tsx +++ b/ui/src/components/featureList.tsx @@ -10,13 +10,13 @@ const FeatureList: React.FC = () => { const columns = [ { title:
Name
, - dataIndex: 'name', + dataIndex: 'displayText', key: 'name', width: 150, render: (name: string, row: IFeature) => { return ( ) }, @@ -29,9 +29,9 @@ const FeatureList: React.FC = () => { } }, { - title:
Qualified Name
, - dataIndex: 'qualifiedName', - key: 'qualifiedName', + title:
Type
, + dataIndex: 'typeName', + key: 'type', align: 'center' as 'center', width: 190, onCell: () => { @@ -55,7 +55,7 @@ const FeatureList: React.FC = () => { @@ -63,7 +63,7 @@ const FeatureList: React.FC = () => { placement="left" title="Are you sure to delete?" onConfirm={ () => { - onDelete(row.id) + onDelete(row.guid) } } > Delete diff --git a/ui/src/models/model.ts b/ui/src/models/model.ts index 7c2b03cbc..99a1c2686 100644 --- a/ui/src/models/model.ts +++ b/ui/src/models/model.ts @@ -1,30 +1,15 @@ -export interface Features { - features: IFeature[]; -} - export interface IFeature { - id: string; - name: string; - qualifiedName: string; - description: string; + guid: string; status: string; - featureType: string; - dataSource: string; - owners: string; -} - -export interface IFeatureDetail { - entity: IFeatureEntity; -} - -export interface IFeatureEntity { + displayText: string; + typeName: string; attributes: FeatureAttributes; } export interface FeatureAttributes { qualifiedName: string; name: string; - type: string; + type: FeatureType; transformation: FeatureTransformation; key: FeatureKey[]; window: string; @@ -32,6 +17,13 @@ export interface FeatureAttributes { input_derived_features: InputDerivedFeatures[] } +export interface FeatureType { + type: string, + tensor_category: string, + dimension_type: string[], + val_type: string +} + export interface InputAnchorFeatures { uniqueAttributes: FeatureAttributes; } diff --git a/ui/src/pages/feature/featureDetails.tsx b/ui/src/pages/feature/featureDetails.tsx index 89c5c49a7..d31535a88 100644 --- a/ui/src/pages/feature/featureDetails.tsx +++ b/ui/src/pages/feature/featureDetails.tsx @@ -3,27 +3,27 @@ import { Alert, Button, Card, Col, Modal, Row, Space, Spin } from 'antd'; import { ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'; import { useNavigate, useParams } from "react-router-dom"; import { QueryStatus, useQuery } from "react-query"; -import { deleteFeature, fetchFeature } from '../../api'; import { AxiosError } from 'axios'; -import { FeatureAttributes, InputAnchorFeatures } from "../../models/model"; +import { deleteFeature, fetchFeature } from '../../api'; +import { IFeature, InputAnchorFeatures } from "../../models/model"; const { confirm } = Modal; type Props = {}; type Params = { project: string; - qualifiedName: string; + featureId: string; } const FeatureDetails: React.FC = () => { - const { project, qualifiedName } = useParams() as Params; + const { project, featureId } = useParams() as Params; const navigate = useNavigate(); const loadingIcon = ; const { status, error, data - } = useQuery(['feature', qualifiedName], () => fetchFeature(project, qualifiedName)); + } = useQuery(['featureId', featureId], () => fetchFeature(project, featureId)); const openLineageWindow = () => { const lineageUrl = `/projects/${ project }/lineage`; @@ -39,7 +39,7 @@ const FeatureDetails: React.FC = () => { title: 'Are you sure you want to delete this feature?', icon: , async onOk() { - await deleteFeature(qualifiedName); + await deleteFeature(featureId); navigate('/features'); }, onCancel() { @@ -74,56 +74,59 @@ const FeatureDetails: React.FC = () => { ); } - const renderFeature = (feature: FeatureAttributes): JSX.Element => { + const renderFeature = (feature: IFeature): JSX.Element => { return (
- { feature?.key && feature.key.length > 0 && + { feature.attributes.key && feature.attributes.key.length > 0 &&
-

full_name: { feature.key[0].full_name }

-

key_column: { feature.key[0].key_column }

-

description: { feature.key[0].description }

-

key_column_alias: { feature.key[0].key_column_alias }

-

key_column_type: { feature.key[0].key_column_type }

+

full_name: { feature.attributes.key[0].full_name }

+

key_column: { feature.attributes.key[0].key_column }

+

description: { feature.attributes.key[0].description }

+

key_column_alias: { feature.attributes.key[0].key_column_alias }

+

key_column_type: { feature.attributes.key[0].key_column_type }

} - { feature?.type && + { feature.attributes.type && - { feature.type } +

dimension_type: { feature.attributes.type.dimension_type }

+

tensor_category: { feature.attributes.type.tensor_category }

+

type: { feature.attributes.type.type }

+

val_type: { feature.attributes.type.val_type }

} - { feature?.transformation && + { feature.attributes.transformation && -

transform_expr: { feature.transformation.transform_expr ?? "N/A" }

-

filter: { feature.transformation.filter ?? "N/A" }

-

agg_func: { feature.transformation.agg_func ?? "N/A" }

-

limit: { feature.transformation.limit ?? "N/A" }

-

group_by: { feature.transformation.group_by ?? "N/A" }

-

window: { feature.transformation.window ?? "N/A" }

-

def_expr: { feature.transformation.def_expr ?? "N/A" }

+

transform_expr: { feature.attributes.transformation.transform_expr ?? "N/A" }

+

filter: { feature.attributes.transformation.filter ?? "N/A" }

+

agg_func: { feature.attributes.transformation.agg_func ?? "N/A" }

+

limit: { feature.attributes.transformation.limit ?? "N/A" }

+

group_by: { feature.attributes.transformation.group_by ?? "N/A" }

+

window: { feature.attributes.transformation.window ?? "N/A" }

+

def_expr: { feature.attributes.transformation.def_expr ?? "N/A" }

} - { feature?.input_anchor_features && feature?.input_anchor_features.length > 0 && + { feature.attributes.input_anchor_features && feature.attributes.input_anchor_features.length > 0 && - { renderInputFeatureList(feature.input_anchor_features) } + { renderInputFeatureList(feature.attributes.input_anchor_features) } } - { feature?.input_derived_features && feature?.input_derived_features.length > 0 && + { feature.attributes.input_derived_features && feature.attributes.input_derived_features.length > 0 && - { renderInputFeatureList(feature.input_derived_features) } + { renderInputFeatureList(feature.attributes.input_derived_features) } } @@ -171,7 +174,7 @@ const FeatureDetails: React.FC = () => { ); } else { return ( - + { renderCommandButtons() } { renderFeature(data) } @@ -181,11 +184,9 @@ const FeatureDetails: React.FC = () => { } return ( - <> -
- { render(status) } -
- +
+ { render(status) } +
); }; diff --git a/ui/src/pages/feature/lineageGraph.tsx b/ui/src/pages/feature/lineageGraph.tsx index 75f516575..32caa8246 100644 --- a/ui/src/pages/feature/lineageGraph.tsx +++ b/ui/src/pages/feature/lineageGraph.tsx @@ -73,7 +73,7 @@ const LineageGraph: React.FC = () => { const edge = generateEdge({ obj: elementObj, from, to }); if (edge?.source && edge?.target) { // Drop edges with other relationship e.g, Consumes, BelongsTo - if (relationshipType === "Produces") { + if (relationshipType === "Contains") { elements.push(edge); } } From d6d725c7ecde5b8a67772766e86e141ced6f221b Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Thu, 2 Jun 2022 00:07:36 +0800 Subject: [PATCH 7/9] Use feature id instead of qualifiedName --- ui/src/components/graph/graphNode.tsx | 4 ++-- ui/src/components/graph/utils.ts | 2 +- ui/src/pages/feature/lineageGraph.tsx | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ui/src/components/graph/graphNode.tsx b/ui/src/components/graph/graphNode.tsx index 71f3a426b..24119b778 100644 --- a/ui/src/components/graph/graphNode.tsx +++ b/ui/src/components/graph/graphNode.tsx @@ -10,14 +10,14 @@ const GraphNode: FC = (props: NodeProps) => { const navigate = useNavigate(); const { project } = useParams(); - const { data: { title, subtitle, qualifiedName, borderColor, active } } = props; + const { data: { title, subtitle, featureId, borderColor, active } } = props; const nodeColorStyle = { border: `2px solid ${ borderColor }`, }; const onNodeIconClick = () => { - navigate(`/projects/${ project }/features/${ qualifiedName }`) + navigate(`/projects/${ project }/features/${ featureId }`) }; return ( diff --git a/ui/src/components/graph/utils.ts b/ui/src/components/graph/utils.ts index 9988bc9ea..d8fcf67d6 100644 --- a/ui/src/components/graph/utils.ts +++ b/ui/src/components/graph/utils.ts @@ -83,7 +83,7 @@ const generateNode = ({ id: nodeId, title: currentNode.displayText, subtitle: currentNode.typeName, - qualifiedName: currentNode.attributes.qualifiedName, + featureId: currentNode.guid, borderColor: featureTypeColors[currentNode.typeName], }, }); diff --git a/ui/src/pages/feature/lineageGraph.tsx b/ui/src/pages/feature/lineageGraph.tsx index 32caa8246..d2f033344 100644 --- a/ui/src/pages/feature/lineageGraph.tsx +++ b/ui/src/pages/feature/lineageGraph.tsx @@ -52,7 +52,6 @@ const LineageGraph: React.FC = () => { const nodeId = currentNode.guid; // If toggled feature type exists, skip other types - console.log("featureType = ", featureType); if (featureType && featureType !== "all_nodes" && currentNode.typeName !== featureType) { continue; } @@ -72,14 +71,13 @@ const LineageGraph: React.FC = () => { const { fromEntityId: from, toEntityId: to, relationshipType } = lineageData.relations[index]; const edge = generateEdge({ obj: elementObj, from, to }); if (edge?.source && edge?.target) { - // Drop edges with other relationship e.g, Consumes, BelongsTo - if (relationshipType === "Contains") { + // Currently, API returns all relationships, filter out Contains, Consumes, etc + if (relationshipType === "Produces") { elements.push(edge); } } } - console.log("elements updated to count = ", elements.length); SetElements(elements); }; From 41534fa3bb8bd9102c0e45b27cfd6ff6268ab094 Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Thu, 2 Jun 2022 00:42:16 +0800 Subject: [PATCH 8/9] In feature detail page, display input anchor and deverid features if exist --- ui/src/models/model.ts | 5 ++-- ui/src/pages/feature/featureDetails.tsx | 31 ++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ui/src/models/model.ts b/ui/src/models/model.ts index 99a1c2686..e9686423c 100644 --- a/ui/src/models/model.ts +++ b/ui/src/models/model.ts @@ -1,4 +1,5 @@ export interface IFeature { + id: string; guid: string; status: string; displayText: string; @@ -13,8 +14,8 @@ export interface FeatureAttributes { transformation: FeatureTransformation; key: FeatureKey[]; window: string; - input_anchor_features: InputAnchorFeatures[]; - input_derived_features: InputDerivedFeatures[] + _input_anchor_features: IFeature[]; + _input_derived_features: IFeature[] } export interface FeatureType { diff --git a/ui/src/pages/feature/featureDetails.tsx b/ui/src/pages/feature/featureDetails.tsx index d31535a88..c8aec59bf 100644 --- a/ui/src/pages/feature/featureDetails.tsx +++ b/ui/src/pages/feature/featureDetails.tsx @@ -5,7 +5,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { QueryStatus, useQuery } from "react-query"; import { AxiosError } from 'axios'; import { deleteFeature, fetchFeature } from '../../api'; -import { IFeature, InputAnchorFeatures } from "../../models/model"; +import { IFeature } from "../../models/model"; const { confirm } = Modal; @@ -62,17 +62,6 @@ const FeatureDetails: React.FC = () => { ) } - const renderInputFeatureList = (features: InputAnchorFeatures[]) => { - return ( -
    - { features.map((_) => ( - - )) } -
- ); - } const renderFeature = (feature: IFeature): JSX.Element => { return ( @@ -114,19 +103,29 @@ const FeatureDetails: React.FC = () => { } - { feature.attributes.input_anchor_features && feature.attributes.input_anchor_features.length > 0 && + { feature.attributes._input_anchor_features && feature.attributes._input_anchor_features.length > 0 &&
- { renderInputFeatureList(feature.attributes.input_anchor_features) } + { + feature.attributes._input_anchor_features.map((feature) => + ) + } } - { feature.attributes.input_derived_features && feature.attributes.input_derived_features.length > 0 && + { feature.attributes._input_derived_features && feature.attributes._input_derived_features.length > 0 && - { renderInputFeatureList(feature.attributes.input_derived_features) } + { + feature.attributes._input_derived_features.map((feature) => + ) + } } From 20f08abd2fc91ee5adf835d32fa514962473969a Mon Sep 17 00:00:00 2001 From: Blair Chen Date: Thu, 2 Jun 2022 12:50:01 +0800 Subject: [PATCH 9/9] Get of Ixxx pattern in model naming convention --- ui/src/api/api.tsx | 30 ++++++++++++------------ ui/src/components/dataSourceList.tsx | 4 ++-- ui/src/components/featureForm.tsx | 4 ++-- ui/src/components/featureList.tsx | 8 +++---- ui/src/components/roleManagementForm.tsx | 8 +++---- ui/src/components/userRoles.tsx | 4 ++-- ui/src/models/model.ts | 23 ++++++------------ ui/src/pages/feature/featureDetails.tsx | 6 ++--- ui/src/pages/feature/lineageGraph.tsx | 4 ++-- 9 files changed, 41 insertions(+), 50 deletions(-) diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index 93a9e10dd..653a2677a 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -1,5 +1,5 @@ import Axios from "axios"; -import { IDataSource, IFeature, IFeatureLineage, IUserRole, RoleForm } from "../models/model"; +import { DataSource, Feature, FeatureLineage, UserRole, Role } from "../models/model"; import mockUserRole from "./mock/userrole.json"; const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT + "/api/v1"; @@ -7,7 +7,7 @@ const token = "mockAppServiceKey"; export const fetchDataSources = async (project: string) => { return Axios - .get(`${ API_ENDPOINT }/projects/${ project }/datasources?code=${ token }`, + .get(`${ API_ENDPOINT }/projects/${ project }/datasources?code=${ token }`, { headers: {} }) .then((response) => { return response.data; @@ -27,7 +27,7 @@ export const fetchProjects = async () => { export const fetchFeatures = async (project: string, page: number, limit: number, keyword: string) => { return Axios - .get(`${ API_ENDPOINT }/projects/${ project }/features?code=${ token }`, + .get(`${ API_ENDPOINT }/projects/${ project }/features?code=${ token }`, { params: { 'keyword': keyword, 'page': page, 'limit': limit }, headers: {} @@ -39,7 +39,7 @@ export const fetchFeatures = async (project: string, page: number, limit: number export const fetchFeature = async (project: string, featureId: string) => { return Axios - .get(`${ API_ENDPOINT }/features/${ featureId }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/features/${ featureId }?code=${ token }`, {}) .then((response) => { return response.data; }) @@ -47,7 +47,7 @@ export const fetchFeature = async (project: string, featureId: string) => { export const fetchProjectLineages = async (project: string) => { return Axios - .get(`${ API_ENDPOINT }/projects/${ project }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/projects/${ project }?code=${ token }`, {}) .then((response) => { return response.data; }) @@ -55,14 +55,14 @@ export const fetchProjectLineages = async (project: string) => { export const fetchFeatureLineages = async (project: string) => { return Axios - .get(`${ API_ENDPOINT }/features/lineage/${ project }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/features/lineage/${ project }?code=${ token }`, {}) .then((response) => { return response.data; }) }; // Following are place-holder code -export const createFeature = async (feature: IFeature) => { +export const createFeature = async (feature: Feature) => { return Axios .post(`${ API_ENDPOINT }/features`, feature, { @@ -75,7 +75,7 @@ export const createFeature = async (feature: IFeature) => { }); } -export const updateFeature = async (feature: IFeature, id: string) => { +export const updateFeature = async (feature: Feature, id: string) => { feature.guid = id; return await Axios.put(`${ API_ENDPOINT }/features/${ id }`, feature, { @@ -102,21 +102,21 @@ export const deleteFeature = async (qualifiedName: string) => { }; export const listUserRole = async () => { - let data:IUserRole[] = mockUserRole + let data:UserRole[] = mockUserRole return data }; export const getUserRole = async (userName: string) => { return await Axios - .get(`${ API_ENDPOINT }/user/${userName}/userroles?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/user/${userName}/userroles?code=${ token }`, {}) .then((response) => { return response.data; }) } -export const addUserRole = async (roleForm: RoleForm) => { +export const addUserRole = async (role: Role) => { return await Axios - .post(`${ API_ENDPOINT }/user/${roleForm.userName}/userroles/new`, roleForm, + .post(`${ API_ENDPOINT }/user/${role.userName}/userroles/new`, role, { headers: { "Content-Type": "application/json;" }, params: {}, @@ -127,9 +127,9 @@ export const addUserRole = async (roleForm: RoleForm) => { }); } -export const deleteUserRole = async (roleForm: RoleForm) => { +export const deleteUserRole = async (role: Role) => { return await Axios - .post(`${ API_ENDPOINT }/user/${roleForm.userName}/userroles/delete`, roleForm, + .post(`${ API_ENDPOINT }/user/${role.userName}/userroles/delete`, role, { headers: { "Content-Type": "application/json;" }, params: {}, @@ -138,4 +138,4 @@ export const deleteUserRole = async (roleForm: RoleForm) => { }).catch((error) => { return error.response; }); -} \ No newline at end of file +} diff --git a/ui/src/components/dataSourceList.tsx b/ui/src/components/dataSourceList.tsx index 76e641a1e..33a2c47fc 100644 --- a/ui/src/components/dataSourceList.tsx +++ b/ui/src/components/dataSourceList.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { LoadingOutlined } from "@ant-design/icons"; import { Form, Select, Table } from "antd"; -import { DataSourceAttributes, IDataSource } from "../models/model"; +import { DataSourceAttributes, DataSource } from "../models/model"; import { fetchDataSources, fetchProjects } from "../api"; const DataSourceList: React.FC = () => { @@ -77,7 +77,7 @@ const DataSourceList: React.FC = () => { ]; const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [tableData, setTableData] = useState(); + const [tableData, setTableData] = useState(); const [projects, setProjects] = useState([]); const [project, setProject] = useState(""); diff --git a/ui/src/components/featureForm.tsx b/ui/src/components/featureForm.tsx index bdd89b5f4..f418f4270 100644 --- a/ui/src/components/featureForm.tsx +++ b/ui/src/components/featureForm.tsx @@ -3,7 +3,7 @@ import { UpCircleOutlined } from '@ant-design/icons' import { BackTop, Button, Form, Input, message, Space } from 'antd'; import { Navigate } from "react-router-dom"; import { createFeature, updateFeature } from '../api'; -import { FeatureAttributes, IFeature } from "../models/model"; +import { FeatureAttributes, Feature } from "../models/model"; type FeatureFormProps = { isNew: boolean; @@ -25,7 +25,7 @@ const FeatureForm: React.FC = ({ isNew, editMode, feature }) = const onClickSave = async () => { setCreateLoading(true); - const featureToSave: IFeature = form.getFieldsValue(); + const featureToSave: Feature = form.getFieldsValue(); if (isNew) { const resp = await createFeature(featureToSave); if (resp.status === 201) { diff --git a/ui/src/components/featureList.tsx b/ui/src/components/featureList.tsx index 92003cde2..8fddc225c 100644 --- a/ui/src/components/featureList.tsx +++ b/ui/src/components/featureList.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Link, useNavigate } from "react-router-dom"; import { DownOutlined, LoadingOutlined } from '@ant-design/icons'; import { Button, Dropdown, Input, Menu, message, Popconfirm, Select, Tooltip, Form, Table } from 'antd'; -import { IFeature } from "../models/model"; +import { Feature } from "../models/model"; import { deleteFeature, fetchProjects, fetchFeatures } from "../api"; const FeatureList: React.FC = () => { @@ -13,7 +13,7 @@ const FeatureList: React.FC = () => { dataIndex: 'displayText', key: 'name', width: 150, - render: (name: string, row: IFeature) => { + render: (name: string, row: Feature) => { return (