diff --git a/ui/.env b/ui/.env index c22dbf420..0a9fb0b61 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-registry.azurewebsites.net diff --git a/ui/package-lock.json b/ui/package-lock.json index 6052c5bf5..0c83d5ba8 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -2507,11 +2507,14 @@ "@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", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } }, "@types/html-minifier-terser": { "version": "6.1.0", @@ -2601,8 +2604,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 +2628,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,34 +2643,15 @@ "@types/react": "^17" } }, - "@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, + "@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/history": "^4.7.11", + "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", - "@types/react-router": "*" + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" } }, "@types/resolve": { @@ -2690,8 +2672,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 +4577,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 +5814,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", @@ -6404,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": { @@ -6966,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", @@ -8427,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", @@ -8929,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", @@ -10495,16 +10447,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,50 +10476,47 @@ "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", - "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==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "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" + "history": "^5.2.0", + "react-router": "6.3.0" } }, "react-scripts": { @@ -10674,6 +10625,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", @@ -10902,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", @@ -11860,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", @@ -12172,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", @@ -12883,11 +12822,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..1663868b0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,11 +10,9 @@ "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": "^6.3.0" }, "devDependencies": { "@testing-library/jest-dom": "^5.16.3", @@ -25,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/api/api.tsx b/ui/src/api/api.tsx index 017c944c5..653a2677a 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -1,13 +1,13 @@ import Axios from "axios"; -import { Features, IDataSource, IFeature, IFeatureDetail, IFeatureLineage } from "../models/model"; +import { DataSource, Feature, FeatureLineage, UserRole, Role } from "../models/model"; +import mockUserRole from "./mock/userrole.json"; -const API_ENDPOINT = "https://feathr-api-test.azurewebsites.net"; -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; @@ -16,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: {} }) @@ -27,34 +27,42 @@ 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 }/features/lineage/${ project }?code=${ token }`, {}) + .get(`${ API_ENDPOINT }/projects/${ project }?code=${ token }`, {}) + .then((response) => { + return response.data; + }) +}; + +export const fetchFeatureLineages = async (project: string) => { + return Axios + .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, { @@ -67,8 +75,8 @@ export const createFeature = async (feature: IFeature) => { }); } -export const updateFeature = async (feature: IFeature, id: string) => { - feature.id = id; +export const updateFeature = async (feature: Feature, id: string) => { + feature.guid = id; return await Axios.put(`${ API_ENDPOINT }/features/${ id }`, feature, { headers: { "Content-Type": "application/json;" }, @@ -93,3 +101,41 @@ export const deleteFeature = async (qualifiedName: string) => { }); }; +export const listUserRole = async () => { + let data:UserRole[] = 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 (role: Role) => { + return await Axios + .post(`${ API_ENDPOINT }/user/${role.userName}/userroles/new`, role, + { + headers: { "Content-Type": "application/json;" }, + params: {}, + }).then((response) => { + return response; + }).catch((error) => { + return error.response; + }); +} + +export const deleteUserRole = async (role: Role) => { + return await Axios + .post(`${ API_ENDPOINT }/user/${role.userName}/userroles/delete`, role, + { + headers: { "Content-Type": "application/json;" }, + params: {}, + }).then((response) => { + return response; + }).catch((error) => { + return error.response; + }); +} 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 new file mode 100644 index 000000000..2a21eb373 --- /dev/null +++ b/ui/src/app.tsx @@ -0,0 +1,59 @@ +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"; +import Management from "./pages/management/management"; +import RoleManagement from "./pages/management/roleManagement"; + +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 msalClient = 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..33a2c47fc 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 { DataSourceAttributes, DataSource } 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
, @@ -80,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(""); @@ -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..f418f4270 100644 --- a/ui/src/components/featureForm.tsx +++ b/ui/src/components/featureForm.tsx @@ -1,9 +1,9 @@ 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"; +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) { @@ -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..8fddc225c 100644 --- a/ui/src/components/featureList.tsx +++ b/ui/src/components/featureList.tsx @@ -1,23 +1,22 @@ 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 { Feature } 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
, - dataIndex: 'name', + dataIndex: 'displayText', key: 'name', width: 150, - render: (name: string, row: IFeature) => { + render: (name: string, row: Feature) => { return ( ) }, @@ -30,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: () => { @@ -50,13 +49,13 @@ const FeatureList: React.FC = () => { key: 'action', align: 'center' as 'center', width: 120, - render: (name: string, row: IFeature) => ( + render: (name: string, row: Feature) => ( { return ( @@ -64,7 +63,7 @@ const FeatureList: React.FC = () => { placement="left" title="Are you sure to delete?" onConfirm={ () => { - onDelete(row.id) + onDelete(row.guid) } } > Delete @@ -84,7 +83,7 @@ const FeatureList: React.FC = () => { const defaultPage = 1; const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [tableData, setTableData] = useState(); + const [tableData, setTableData] = useState(); const [query, setQuery] = useState(""); const [projects, setProjects] = useState([]); const [project, setProject] = useState(""); diff --git a/ui/src/components/graph/graph.tsx b/ui/src/components/graph/graph.tsx new file mode 100644 index 000000000..166cf19ae --- /dev/null +++ b/ui/src/components/graph/graph.tsx @@ -0,0 +1,167 @@ +import React, { MouseEvent as ReactMouseEvent, useCallback, useEffect, useState, } from 'react'; +import ReactFlow, { + ConnectionLineType, + Controls, + Edge, + Elements, + getIncomers, + getOutgoers, + isEdge, + isNode, + Node, + ReactFlowProvider +} from 'react-flow-renderer'; +import { useSearchParams } from 'react-router-dom'; +import LineageNode from "./graphNode"; +import { findNodeInElement, getLayoutedElements } from "./utils"; + +const nodeTypes = { + 'custom-node': LineageNode, +}; +type Props = { + data: Elements; + nodeId: string; +} +const Graph: React.FC = ({ data, nodeId }) => { + const [, setURLSearchParams] = useSearchParams(); + + const { layoutedElements, elementMapping } = getLayoutedElements(data); + const [elements, setElements] = useState(layoutedElements); + + useEffect(() => { + setElements(layoutedElements); + }, [data, nodeId]); + + // Reset all node highlight status + const resetHighlight = (): void => { + if (!elements || elements.length === 0) { + return; + } + + 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); + }; + + // 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; + + 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); + }; + + useEffect(() => { + if (nodeId) { + const node = findNodeInElement(nodeId, layoutedElements); + if (node) { + resetHighlight(); + highlightPath(node, !!nodeId); + } + } + }, [nodeId]); + + // When panel is clicked, reset all highlighted path, and remove the nodeId query string in url path. + 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 ( +
+ + { + if (isNode(element)) { + resetHighlight(); + highlightPath(element, false); + setURLSearchParams({ nodeId: element.data.id }); + } + } } + onNodeDragStop={ onNodeDragStop } + connectionLineType={ ConnectionLineType.SmoothStep } + nodeTypes={ nodeTypes } + > + + + +
+ ); +} + +export default Graph; diff --git a/ui/src/components/graph/graphNode.tsx b/ui/src/components/graph/graphNode.tsx new file mode 100644 index 000000000..24119b778 --- /dev/null +++ b/ui/src/components/graph/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: { title, subtitle, featureId, borderColor, active } } = props; + + const nodeColorStyle = { + border: `2px solid ${ borderColor }`, + }; + + const onNodeIconClick = () => { + navigate(`/projects/${ project }/features/${ featureId }`) + }; + + return ( +
+
+ +
+
{ title } + { active && () } +
+
+ { subtitle } +
+
+ + +
+
+ ); +}; + +export default memo(GraphNode); diff --git a/ui/src/components/graph/utils.ts b/ui/src/components/graph/utils.ts new file mode 100644 index 000000000..d8fcf67d6 --- /dev/null +++ b/ui/src/components/graph/utils.ts @@ -0,0 +1,122 @@ +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 = { + layoutedElements: Elements, + elementMapping: Record, +}; + +const getLayoutedElements = (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, + y: nodeWithPosition.y - DEFAULT_HEIGHT / 2, + }; + } + + newElements.push(element); + } + + return { + layoutedElements: 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, + title: currentNode.displayText, + subtitle: currentNode.typeName, + featureId: currentNode.guid, + borderColor: featureTypeColors[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, + getLayoutedElements, +}; + +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/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/roleManagementForm.tsx b/ui/src/components/roleManagementForm.tsx new file mode 100644 index 000000000..fa8a6a4ac --- /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 { Role, UserRole } from "../models/model"; + +type RoleManagementFormProps = { + isNew: boolean; + editMode: boolean; + userRole?: UserRole; +}; + +const Admin = "Admin" +const Producer = "Producer" +const Consumer = "Consumer" + +const RoleManagementForm: React.FC = ({ editMode, userRole }) => { + const [fireRedirect] = 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: Role = form.getFieldsValue(); + await addUserRole(roleForm); + setCreateLoading(false); + } + + const styling: CSSProperties = { width: "92%" } + return ( + <> +
+ + + + + + + + + + + + + + + + + + +
+ { fireRedirect && () } + + ); +}; + +export default RoleManagementForm diff --git a/ui/src/components/sidemenu/siteMenu.tsx b/ui/src/components/sidemenu/siteMenu.tsx index 6f688ecfd..d99638756 100644 --- a/ui/src/components/sidemenu/siteMenu.tsx +++ b/ui/src/components/sidemenu/siteMenu.tsx @@ -1,12 +1,9 @@ -import React from 'react'; import { Layout, Menu } from 'antd'; -import { CopyOutlined, DatabaseOutlined, EyeOutlined, RocketOutlined } from '@ant-design/icons'; -import { withRouter } from 'react-router'; +import { CopyOutlined, DatabaseOutlined, EyeOutlined, RocketOutlined} from '@ant-design/icons'; import { Link } from 'react-router-dom'; const { Sider } = Layout; - -const SideMenu = withRouter(({ history }) => { +const SideMenu = () => { return (
@@ -17,7 +14,6 @@ const SideMenu = withRouter(({ history }) => { mode="inline" defaultSelectedKeys={ ['/'] } defaultOpenKeys={ ['/'] } - selectedKeys={ [history.location.pathname] } > }> Data Sources @@ -34,6 +30,6 @@ const SideMenu = withRouter(({ history }) => {
); -}); +}; export default SideMenu; diff --git a/ui/src/components/userRoles.tsx b/ui/src/components/userRoles.tsx new file mode 100644 index 000000000..2fe18f005 --- /dev/null +++ b/ui/src/components/userRoles.tsx @@ -0,0 +1,148 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button, Modal, PageHeader, Row, Space, Table, Tag } from "antd"; +import { UserRole } from "../models/model"; +import { listUserRole } from "../api"; + +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 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: () => ( + + + +

{modalText}

+
+
+ ), + }, + ]; + const [page, setPage] = useState(1); + const [, 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() + }, [fetchData]) + + return ( +
+ + +
+ <> +

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

+ +
+
+
+ + + + ; + + ); +} + +export default UserRoles; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8f0ea99a6..5b2d5e60f 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,12 +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/models/model.ts b/ui/src/models/model.ts index 235855fc1..c89c31b26 100644 --- a/ui/src/models/model.ts +++ b/ui/src/models/model.ts @@ -1,45 +1,29 @@ -export interface Features { - features: IFeature[]; -} - -export interface IFeature { +export interface Feature { 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; - input_anchor_features: InputAnchorFeatures[]; - input_derived_features: InputDerivedFeatures[] -} - -export interface InputAnchorFeatures { - uniqueAttributes: FeatureAttributes; + _input_anchor_features: Feature[]; + _input_derived_features: Feature[] } -export interface InputDerivedFeatures { - uniqueAttributes: FeatureAttributes; +export interface FeatureType { + type: string, + tensor_category: string, + dimension_type: string[], + val_type: string } - export interface FeatureTransformation { transform_expr: string, filter: string, @@ -58,7 +42,7 @@ export interface FeatureKey { key_column_type: string } -export interface IDataSource { +export interface DataSource { attributes: DataSourceAttributes; } @@ -69,8 +53,25 @@ export interface DataSourceAttributes { } -export interface IFeatureLineage { +export interface FeatureLineage { guidEntityMap: any; relations: any; } +export interface UserRole { + id: number; + scope: string; + userName: string; + roleName: string; + createTime: string; + createReason: string; + deleteTime?: any; + deleteReason?: any; +} + +export interface Role { + scope: string; + userName: string; + roleName: string; + reason: string; +} diff --git a/ui/src/pages/feature/featureDetails.tsx b/ui/src/pages/feature/featureDetails.tsx index fef6f7f86..c4fc32314 100644 --- a/ui/src/pages/feature/featureDetails.tsx +++ b/ui/src/pages/feature/featureDetails.tsx @@ -1,34 +1,33 @@ -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'; -import { FeatureAttributes, InputAnchorFeatures } from "../../models/model"; +import { deleteFeature, fetchFeature } from '../../api'; +import { Feature } from "../../models/model"; const { confirm } = Modal; type Props = {}; type Params = { project: string; - qualifiedName: string; + featureId: string; } const FeatureDetails: React.FC = () => { - const { project, qualifiedName } = useParams(); + const { project, featureId } = useParams() as Params; + const navigate = useNavigate(); const loadingIcon = ; - const history = useHistory(); - const navigateTo = useCallback((location) => history.push(location), [history]); const { status, error, data - } = useQuery(['feature', qualifiedName], () => fetchFeature(project, qualifiedName)); + } = useQuery(['featureId', featureId], () => fetchFeature(project, featureId)); const openLineageWindow = () => { - const lineageUrl = `/projects/${ project }/features/${ qualifiedName }/lineage`; - window.open(lineageUrl); + const lineageUrl = `/projects/${ project }/lineage`; + navigate(lineageUrl); } const onClickDeleteFeature = () => { @@ -40,8 +39,8 @@ const FeatureDetails: React.FC = () => { title: 'Are you sure you want to delete this feature?', icon: , async onOk() { - await deleteFeature(qualifiedName); - history.push('/features'); + await deleteFeature(featureId); + navigate('/features'); }, onCancel() { console.log('Cancel clicked'); @@ -63,68 +62,70 @@ const FeatureDetails: React.FC = () => { ) } - const renderInputFeatureList = (features: InputAnchorFeatures[]) => { - return ( -
    - { features.map((_) => ( - - )) } -
- ); - } - const renderFeature = (feature: FeatureAttributes): JSX.Element => { + const renderFeature = (feature: Feature): 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) } + { + feature.attributes._input_anchor_features.map((feature) => + ) + } } - { 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) } + { + feature.attributes._input_derived_features.map((feature) => + ) + } } @@ -172,7 +173,7 @@ const FeatureDetails: React.FC = () => { ); } else { return ( - + { renderCommandButtons() } { renderFeature(data) } @@ -182,11 +183,9 @@ const FeatureDetails: React.FC = () => { } return ( - <> -
- { render(status) } -
- +
+ { render(status) } +
); }; 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/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/lineageGraph.tsx b/ui/src/pages/feature/lineageGraph.tsx new file mode 100644 index 000000000..3cfcf5452 --- /dev/null +++ b/ui/src/pages/feature/lineageGraph.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import { Card, Radio, Spin } from 'antd'; +import { useParams, useSearchParams } from "react-router-dom"; +import { Elements } from 'react-flow-renderer'; +import Graph from "../../components/graph/graph"; +import { generateEdge, generateNode } from "../../components/graph/utils"; +import { fetchProjectLineages } from "../../api"; +import { FeatureLineage } from "../../models/model"; +import { LoadingOutlined } from "@ant-design/icons"; + +type Params = { + project: string; +} +const LineageGraph: React.FC = () => { + const { project } = useParams() as Params; + const [searchParams] = useSearchParams(); + const nodeId = searchParams.get('nodeId') as string; + + const [lineageData, setLineageData] = useState({ guidEntityMap: null, relations: null }); + const [loading, setLoading] = useState(false); + const [elements, SetElements] = useState([]); + const [featureType, setFeatureType] = useState("all_nodes"); + + // Fetch lineage data from server side, invoked immediately after component is mounted + useEffect(() => { + const fetchLineageData = async () => { + const data = await fetchProjectLineages(project); + setLineageData(data); + setLoading(false); + }; + + fetchLineageData(); + }, [project]); + + // Generate graph data on client side, invoked after graphData or featureType is changed + useEffect(() => { + const generateGraphData = async () => { + if (lineageData.guidEntityMap === null && lineageData.relations === null) { + return; + } + + const elements: Elements = []; + const elementObj: Record = {}; + + for (let index = 0; index < Object.values(lineageData.guidEntityMap).length; index++) { + const currentNode: any = Object.values(lineageData.guidEntityMap)[index]; + + if (currentNode.typeName === "feathr_workspace_v1") { + continue; // Open issue: should project node get displayed as well? + } + + const nodeId = currentNode.guid; + + // If toggled feature type exists, skip other types + if (featureType && featureType !== "all_nodes" && currentNode.typeName !== featureType) { + continue; + } + + const node = generateNode({ + index, + nodeId, + currentNode + }); + + elementObj[nodeId] = index?.toString(); + + elements.push(node); + } + + for (let index = 0; index < lineageData.relations.length; index++) { + const { fromEntityId: from, toEntityId: to, relationshipType } = lineageData.relations[index]; + const edge = generateEdge({ obj: elementObj, from, to }); + if (edge?.source && edge?.target) { + // Currently, API returns all relationships, filter out Contains, Consumes, etc + if (relationshipType === "Produces") { + elements.push(edge); + } + } + } + + SetElements(elements); + }; + + generateGraphData(); + }, [lineageData, featureType]) + + const toggleFeatureType = (type: string) => { + setFeatureType((prevType: string | null) => { + if (prevType === type) { + return null; + } + return type; + }); + }; + + return ( + +
+ toggleFeatureType(e.target.value) }> + All Features + Source + Anchor + Anchor Feature + Derived Feature + +
+
+ { loading + ? } /> + : } +
+
+ ); +} + +export default LineageGraph; 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 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 deleted file mode 100644 index 4337f9f04..000000000 --- a/ui/src/router/routes.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Suspense } from "react"; -import { BrowserRouter, Route, Switch, withRouter } 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 FeatureLineage from "../pages/feature/featureLineage"; -import Jobs from "../pages/jobs/jobs"; -import Monitoring from "../pages/monitoring/monitoring"; - -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 new file mode 100644 index 000000000..7a314f46f --- /dev/null +++ b/ui/src/site.css @@ -0,0 +1,40 @@ +.lineage-graph { + margin-top: 1.5rem; +} + +.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-box { + padding: 4px 12px 7px; +} + +.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; +} +