diff --git a/catalog-rest-service/src/main/resources/ui/package-lock.json b/catalog-rest-service/src/main/resources/ui/package-lock.json
index 8617aea8bb7..14bdb2671d0 100644
--- a/catalog-rest-service/src/main/resources/ui/package-lock.json
+++ b/catalog-rest-service/src/main/resources/ui/package-lock.json
@@ -4,6 +4,43 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "@ant-design/colors": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
+ "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+ "requires": {
+ "@ctrl/tinycolor": "^3.4.0"
+ }
+ },
+ "@ant-design/icons": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.7.0.tgz",
+ "integrity": "sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==",
+ "requires": {
+ "@ant-design/colors": "^6.0.0",
+ "@ant-design/icons-svg": "^4.2.1",
+ "@babel/runtime": "^7.11.2",
+ "classnames": "^2.2.6",
+ "rc-util": "^5.9.4"
+ }
+ },
+ "@ant-design/icons-svg": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz",
+ "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw=="
+ },
+ "@ant-design/react-slick": {
+ "version": "0.28.4",
+ "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-0.28.4.tgz",
+ "integrity": "sha512-j9eAHTn7GxbXUFNknJoHS2ceAsqrQi2j8XykjZE1IXCD8kJF+t28EvhBLniDpbOsBk/3kjalnhriTfZcjBHNqg==",
+ "requires": {
+ "@babel/runtime": "^7.10.4",
+ "classnames": "^2.2.5",
+ "json2mq": "^0.2.0",
+ "lodash": "^4.17.21",
+ "resize-observer-polyfill": "^1.5.0"
+ }
+ },
"@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
@@ -1304,6 +1341,11 @@
"webpack-merge": "^4.2.2"
}
},
+ "@ctrl/tinycolor": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz",
+ "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ=="
+ },
"@deuex-solutions/redoc": {
"version": "2.0.0-rc.27",
"resolved": "https://registry.npmjs.org/@deuex-solutions/redoc/-/redoc-2.0.0-rc.27.tgz",
@@ -4659,6 +4701,54 @@
"color-convert": "^1.9.0"
}
},
+ "antd": {
+ "version": "4.16.13",
+ "resolved": "https://registry.npmjs.org/antd/-/antd-4.16.13.tgz",
+ "integrity": "sha512-EMPD3fzKe7oayx9keD/GA1oKatcx7j5CGlkJj5eLS0/eEDDEkxVj3DFmKOPuHYt4BK7ltTzMFS+quSTmqUXPiw==",
+ "requires": {
+ "@ant-design/colors": "^6.0.0",
+ "@ant-design/icons": "^4.6.3",
+ "@ant-design/react-slick": "~0.28.1",
+ "@babel/runtime": "^7.12.5",
+ "array-tree-filter": "^2.1.0",
+ "classnames": "^2.2.6",
+ "copy-to-clipboard": "^3.2.0",
+ "lodash": "^4.17.21",
+ "moment": "^2.25.3",
+ "rc-cascader": "~1.4.0",
+ "rc-checkbox": "~2.3.0",
+ "rc-collapse": "~3.1.0",
+ "rc-dialog": "~8.6.0",
+ "rc-drawer": "~4.3.0",
+ "rc-dropdown": "~3.2.0",
+ "rc-field-form": "~1.20.0",
+ "rc-image": "~5.2.5",
+ "rc-input-number": "~7.1.0",
+ "rc-mentions": "~1.6.1",
+ "rc-menu": "~9.0.12",
+ "rc-motion": "^2.4.0",
+ "rc-notification": "~4.5.7",
+ "rc-pagination": "~3.1.9",
+ "rc-picker": "~2.5.10",
+ "rc-progress": "~3.1.0",
+ "rc-rate": "~2.9.0",
+ "rc-resize-observer": "^1.0.0",
+ "rc-select": "~12.1.6",
+ "rc-slider": "~9.7.1",
+ "rc-steps": "~4.1.0",
+ "rc-switch": "~3.2.0",
+ "rc-table": "~7.15.1",
+ "rc-tabs": "~11.10.0",
+ "rc-textarea": "~0.3.0",
+ "rc-tooltip": "~5.1.1",
+ "rc-tree": "~4.2.1",
+ "rc-tree-select": "~4.3.0",
+ "rc-trigger": "^5.2.10",
+ "rc-upload": "~4.3.0",
+ "rc-util": "^5.13.1",
+ "scroll-into-view-if-needed": "^2.2.25"
+ }
+ },
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
@@ -4743,6 +4833,11 @@
"is-string": "^1.0.5"
}
},
+ "array-tree-filter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+ "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw=="
+ },
"array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -4841,6 +4936,11 @@
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true
},
+ "async-validator": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.2.tgz",
+ "integrity": "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ=="
+ },
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -6281,6 +6381,14 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true
},
+ "copy-to-clipboard": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
+ "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
+ "requires": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
"copy-webpack-plugin": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-7.0.0.tgz",
@@ -6412,6 +6520,11 @@
}
}
},
+ "cronstrue": {
+ "version": "1.122.0",
+ "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-1.122.0.tgz",
+ "integrity": "sha512-PFuhZd+iPQQ0AWTXIEYX+t3nFGzBrWxmTWUKJOrsGRewaBSLKZ4I1f8s2kryU75nNxgyugZgiGh2OJsCTA/XlA=="
+ },
"cross-fetch": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
@@ -6826,6 +6939,16 @@
"whatwg-url": "^8.0.0"
}
},
+ "date-fns": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz",
+ "integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w=="
+ },
+ "dayjs": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
+ "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+ },
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
@@ -7137,6 +7260,11 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.3.0.tgz",
"integrity": "sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA=="
},
+ "dom-align": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.2.tgz",
+ "integrity": "sha512-pHuazgqrsTFrGU2WLDdXxCFabkdQDx72ddkraZNih1KsMcN5qsRSTR9O4VJRlwTPCPb5COYg3LOfiMHHcPInHg=="
+ },
"dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -15977,6 +16105,11 @@
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz",
"integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA=="
},
+ "moment": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
+ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
+ },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -17558,6 +17691,384 @@
"unpipe": "1.0.0"
}
},
+ "rc-align": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.11.tgz",
+ "integrity": "sha512-n9mQfIYQbbNTbefyQnRHZPWuTEwG1rY4a9yKlIWHSTbgwI+XUMGRYd0uJ5pE2UbrNX0WvnMBA1zJ3Lrecpra/A==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "dom-align": "^1.7.0",
+ "lodash": "^4.17.21",
+ "rc-util": "^5.3.0",
+ "resize-observer-polyfill": "^1.5.1"
+ }
+ },
+ "rc-cascader": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-1.4.3.tgz",
+ "integrity": "sha512-Q4l9Mv8aaISJ+giVnM9IaXxDeMqHUGLvi4F+LksS6pHlaKlN4awop/L+IMjIXpL+ug/ojaCyv/ixcVopJYYCVA==",
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "array-tree-filter": "^2.1.0",
+ "rc-trigger": "^5.0.4",
+ "rc-util": "^5.0.1",
+ "warning": "^4.0.1"
+ }
+ },
+ "rc-checkbox": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-2.3.2.tgz",
+ "integrity": "sha512-afVi1FYiGv1U0JlpNH/UaEXdh6WUJjcWokj/nUN2TgG80bfG+MDdbfHKlLcNNba94mbjy2/SXJ1HDgrOkXGAjg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.1"
+ }
+ },
+ "rc-collapse": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.1.2.tgz",
+ "integrity": "sha512-HujcKq7mghk/gVKeI6EjzTbb8e19XUZpakrYazu1MblEZ3Hu3WBMSN4A3QmvbF6n1g7x6lUlZvsHZ5shABWYOQ==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-motion": "^2.3.4",
+ "rc-util": "^5.2.1",
+ "shallowequal": "^1.1.0"
+ }
+ },
+ "rc-dialog": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-8.6.0.tgz",
+ "integrity": "sha512-GSbkfqjqxpZC5/zc+8H332+q5l/DKUhpQr0vdX2uDsxo5K0PhvaMEVjyoJUTkZ3+JstEADQji1PVLVb/2bJeOQ==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6",
+ "rc-motion": "^2.3.0",
+ "rc-util": "^5.6.1"
+ }
+ },
+ "rc-drawer": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-4.3.1.tgz",
+ "integrity": "sha512-GMfFy4maqxS9faYXEhQ+0cA1xtkddEQzraf6SAdzWbn444DrrLogwYPk1NXSpdXjLCLxgxOj9MYtyYG42JsfXg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6",
+ "rc-util": "^5.7.0"
+ }
+ },
+ "rc-dropdown": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.0.tgz",
+ "integrity": "sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6",
+ "rc-trigger": "^5.0.4"
+ }
+ },
+ "rc-field-form": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.20.1.tgz",
+ "integrity": "sha512-f64KEZop7zSlrG4ef/PLlH12SLn6iHDQ3sTG+RfKBM45hikwV1i8qMf53xoX12NvXXWg1VwchggX/FSso4bWaA==",
+ "requires": {
+ "@babel/runtime": "^7.8.4",
+ "async-validator": "^3.0.3",
+ "rc-util": "^5.8.0"
+ }
+ },
+ "rc-image": {
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-5.2.5.tgz",
+ "integrity": "sha512-qUfZjYIODxO0c8a8P5GeuclYXZjzW4hV/5hyo27XqSFo1DmTCs2HkVeQObkcIk5kNsJtgsj1KoPThVsSc/PXOw==",
+ "requires": {
+ "@babel/runtime": "^7.11.2",
+ "classnames": "^2.2.6",
+ "rc-dialog": "~8.6.0",
+ "rc-util": "^5.0.6"
+ }
+ },
+ "rc-input-number": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.1.4.tgz",
+ "integrity": "sha512-EG4iqkqyqzLRu/Dq+fw2od7nlgvXLEatE+J6uhi3HXE1qlM3C7L6a7o/hL9Ly9nimkES2IeQoj3Qda3I0izj3Q==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-util": "^5.9.8"
+ }
+ },
+ "rc-mentions": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-1.6.1.tgz",
+ "integrity": "sha512-LDzGI8jJVGnkhpTZxZuYBhMz3avcZZqPGejikchh97xPni/g4ht714Flh7DVvuzHQ+BoKHhIjobHnw1rcP8erg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6",
+ "rc-menu": "^9.0.0",
+ "rc-textarea": "^0.3.0",
+ "rc-trigger": "^5.0.4",
+ "rc-util": "^5.0.1"
+ }
+ },
+ "rc-menu": {
+ "version": "9.0.14",
+ "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.0.14.tgz",
+ "integrity": "sha512-CIox5mZeLDAi32SlHrV7UeSjv7tmJJhwRyxQtZCKt351w3q59XlL4WMFOmtT9gwIfP9h0XoxdBZUMe/xzkp78A==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-motion": "^2.4.3",
+ "rc-overflow": "^1.2.0",
+ "rc-trigger": "^5.1.2",
+ "rc-util": "^5.12.0",
+ "shallowequal": "^1.1.0"
+ }
+ },
+ "rc-motion": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.4.4.tgz",
+ "integrity": "sha512-ms7n1+/TZQBS0Ydd2Q5P4+wJTSOrhIrwNxLXCZpR7Fa3/oac7Yi803HDALc2hLAKaCTQtw9LmQeB58zcwOsqlQ==",
+ "requires": {
+ "@babel/runtime": "^7.11.1",
+ "classnames": "^2.2.1",
+ "rc-util": "^5.2.1"
+ }
+ },
+ "rc-notification": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-4.5.7.tgz",
+ "integrity": "sha512-zhTGUjBIItbx96SiRu3KVURcLOydLUHZCPpYEn1zvh+re//Tnq/wSxN4FKgp38n4HOgHSVxcLEeSxBMTeBBDdw==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-motion": "^2.2.0",
+ "rc-util": "^5.0.1"
+ }
+ },
+ "rc-overflow": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.2.2.tgz",
+ "integrity": "sha512-X5kj9LDU1ue5wHkqvCprJWLKC+ZLs3p4He/oxjZ1Q4NKaqKBaYf5OdSzRSgh3WH8kSdrfU8LjvlbWnHgJOEkNQ==",
+ "requires": {
+ "@babel/runtime": "^7.11.1",
+ "classnames": "^2.2.1",
+ "rc-resize-observer": "^1.0.0",
+ "rc-util": "^5.5.1"
+ }
+ },
+ "rc-pagination": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-3.1.9.tgz",
+ "integrity": "sha512-IKBKaJ4icVPeEk9qRHrFBJmHxBUrCp3+nENBYob4Ofqsu3RXjBOy4N36zONO7oubgLyiG3PxVmyAuVlTkoc7Jg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.1"
+ }
+ },
+ "rc-picker": {
+ "version": "2.5.19",
+ "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-2.5.19.tgz",
+ "integrity": "sha512-u6myoCu/qiQ0vLbNzSzNrzTQhs7mldArCpPHrEI6OUiifs+IPXmbesqSm0zilJjfzrZJLgYeyyOMSznSlh0GKA==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.1",
+ "date-fns": "2.x",
+ "dayjs": "1.x",
+ "moment": "^2.24.0",
+ "rc-trigger": "^5.0.4",
+ "rc-util": "^5.4.0",
+ "shallowequal": "^1.1.0"
+ }
+ },
+ "rc-progress": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.4.tgz",
+ "integrity": "sha512-XBAif08eunHssGeIdxMXOmRQRULdHaDdIFENQ578CMb4dyewahmmfJRyab+hw4KH4XssEzzYOkAInTLS7JJG+Q==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.6"
+ }
+ },
+ "rc-rate": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.9.1.tgz",
+ "integrity": "sha512-MmIU7FT8W4LYRRHJD1sgG366qKtSaKb67D0/vVvJYR0lrCuRrCiVQ5qhfT5ghVO4wuVIORGpZs7ZKaYu+KMUzA==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-util": "^5.0.1"
+ }
+ },
+ "rc-resize-observer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.0.1.tgz",
+ "integrity": "sha512-OxO2mJI9e8610CAWBFfm52SPvWib0eNKjaSsRbbKHmLaJIxw944P+D61DlLJ/w2vuOjGNcalJu8VdqyNm/XCRg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.1",
+ "rc-util": "^5.0.0",
+ "resize-observer-polyfill": "^1.5.1"
+ }
+ },
+ "rc-select": {
+ "version": "12.1.13",
+ "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-12.1.13.tgz",
+ "integrity": "sha512-cPI+aesP6dgCAaey4t4upDbEukJe+XN0DK6oO/6flcCX5o28o7KNZD7JAiVtC/6fCwqwI/kSs7S/43dvHmBl+A==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-motion": "^2.0.1",
+ "rc-overflow": "^1.0.0",
+ "rc-trigger": "^5.0.4",
+ "rc-util": "^5.9.8",
+ "rc-virtual-list": "^3.2.0"
+ }
+ },
+ "rc-slider": {
+ "version": "9.7.4",
+ "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-9.7.4.tgz",
+ "integrity": "sha512-pjLKLiDKiaL7/pNywfIBD+lDo5TtVo05KuIBSWEIoqu6FHh6IMWvthCiaODuYaVs3RLeF2nXOP5AjkD2Lt2Rwg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-tooltip": "^5.0.1",
+ "rc-util": "^5.0.0",
+ "shallowequal": "^1.1.0"
+ }
+ },
+ "rc-steps": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-4.1.4.tgz",
+ "integrity": "sha512-qoCqKZWSpkh/b03ASGx1WhpKnuZcRWmvuW+ZUu4mvMdfvFzVxblTwUM+9aBd0mlEUFmt6GW8FXhMpHkK3Uzp3w==",
+ "requires": {
+ "@babel/runtime": "^7.10.2",
+ "classnames": "^2.2.3",
+ "rc-util": "^5.0.1"
+ }
+ },
+ "rc-switch": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-3.2.2.tgz",
+ "integrity": "sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.1",
+ "rc-util": "^5.0.1"
+ }
+ },
+ "rc-table": {
+ "version": "7.15.2",
+ "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.15.2.tgz",
+ "integrity": "sha512-TAs7kCpIZwc2mtvD8CMrXSM6TqJDUsy0rUEV1YgRru33T8bjtAtc+9xW/KC1VWROJlHSpU0R0kXjFs9h/6+IzQ==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-resize-observer": "^1.0.0",
+ "rc-util": "^5.13.0",
+ "shallowequal": "^1.1.0"
+ }
+ },
+ "rc-tabs": {
+ "version": "11.10.3",
+ "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-11.10.3.tgz",
+ "integrity": "sha512-rPxsci+76/nnJowNOBO3LTi4eL6trG49cR9yPc4XbuyHXhCHUujN5F4+jFl7trISy+yVN6gCZ/wiTtVnkcR/UA==",
+ "requires": {
+ "@babel/runtime": "^7.11.2",
+ "classnames": "2.x",
+ "rc-dropdown": "^3.2.0",
+ "rc-menu": "^9.0.0",
+ "rc-resize-observer": "^1.0.0",
+ "rc-util": "^5.5.0"
+ }
+ },
+ "rc-textarea": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-0.3.5.tgz",
+ "integrity": "sha512-qa+k5vDn9ct65qr+SgD2KwJ9Xz6P84lG2z+TDht/RBr71WnM/K61PqHUAcUyU6YqTJD26IXgjPuuhZR7HMw7eA==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.1",
+ "rc-resize-observer": "^1.0.0",
+ "rc-util": "^5.7.0"
+ }
+ },
+ "rc-tooltip": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-5.1.1.tgz",
+ "integrity": "sha512-alt8eGMJulio6+4/uDm7nvV+rJq9bsfxFDCI0ljPdbuoygUscbsMYb6EQgwib/uqsXQUvzk+S7A59uYHmEgmDA==",
+ "requires": {
+ "@babel/runtime": "^7.11.2",
+ "rc-trigger": "^5.0.0"
+ }
+ },
+ "rc-tree": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-4.2.2.tgz",
+ "integrity": "sha512-V1hkJt092VrOVjNyfj5IYbZKRMHxWihZarvA5hPL/eqm7o2+0SNkeidFYm7LVVBrAKBpOpa0l8xt04uiqOd+6w==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-motion": "^2.0.1",
+ "rc-util": "^5.0.0",
+ "rc-virtual-list": "^3.0.1"
+ }
+ },
+ "rc-tree-select": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-4.3.3.tgz",
+ "integrity": "sha512-0tilOHLJA6p+TNg4kD559XnDX3PTEYuoSF7m7ryzFLAYvdEEPtjn0QZc5z6L0sMKBiBlj8a2kf0auw8XyHU3lA==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-select": "^12.0.0",
+ "rc-tree": "^4.0.0",
+ "rc-util": "^5.0.5"
+ }
+ },
+ "rc-trigger": {
+ "version": "5.2.10",
+ "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.2.10.tgz",
+ "integrity": "sha512-FkUf4H9BOFDaIwu42fvRycXMAvkttph9AlbCZXssZDVzz2L+QZ0ERvfB/4nX3ZFPh1Zd+uVGr1DEDeXxq4J1TA==",
+ "requires": {
+ "@babel/runtime": "^7.11.2",
+ "classnames": "^2.2.6",
+ "rc-align": "^4.0.0",
+ "rc-motion": "^2.0.0",
+ "rc-util": "^5.5.0"
+ }
+ },
+ "rc-upload": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.3.2.tgz",
+ "integrity": "sha512-v0HdwC/19xKAn1OYZ4hTMUSqSs/IA0n1v4p/cioSSnKubHrdpcCXC45N+TFMSOZtBlf4+xMNCFo3KDih31lAMg==",
+ "requires": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-util": "^5.2.0"
+ }
+ },
+ "rc-util": {
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.15.0.tgz",
+ "integrity": "sha512-8RI8sjOCXD3FhD3dzQNBQetpGol6BBd3sHQ/8jSGk9NPT0CH3JGtBfPODnASyE7AdDpCFQMOmgcp9CBs3S/1hg==",
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "react-is": "^16.12.0",
+ "shallowequal": "^1.1.0"
+ }
+ },
+ "rc-virtual-list": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.4.2.tgz",
+ "integrity": "sha512-OyVrrPvvFcHvV0ssz5EDZ+7Rf5qLat/+mmujjchNw5FfbJWNDwkpQ99EcVE6+FtNRmX9wFa1LGNpZLUTvp/4GQ==",
+ "requires": {
+ "classnames": "^2.2.6",
+ "rc-resize-observer": "^1.0.0",
+ "rc-util": "^5.0.7"
+ }
+ },
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
@@ -17699,6 +18210,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-js-cron": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/react-js-cron/-/react-js-cron-1.2.1.tgz",
+ "integrity": "sha512-3iesosu5l/JsmbSZj8kb3OPcGFA+yM6WvetbsDk8SC8mgaxyAWU+1VS27rrd1OPCnCAHjx42trBxAteQRbtYqg=="
+ },
"react-js-pagination": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/react-js-pagination/-/react-js-pagination-3.0.3.tgz",
@@ -20681,6 +21197,11 @@
"is-number": "^7.0.0"
}
},
+ "toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
+ },
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
diff --git a/catalog-rest-service/src/main/resources/ui/package.json b/catalog-rest-service/src/main/resources/ui/package.json
index 936029b6b0d..ea37fa0f210 100644
--- a/catalog-rest-service/src/main/resources/ui/package.json
+++ b/catalog-rest-service/src/main/resources/ui/package.json
@@ -17,6 +17,7 @@
"@types/react-dom": "^17.0.5",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
+ "antd": "^4.16.13",
"autoprefixer": "^9.8.6",
"axios": "^0.21.1",
"babel-plugin-named-asset-import": "^0.3.6",
@@ -26,6 +27,7 @@
"codemirror": "^5.62.3",
"cookie-storage": "^6.1.0",
"core-js": "^3.10.1",
+ "cronstrue": "^1.122.0",
"diff": "^5.0.0",
"draft-js": "^0.11.7",
"eslint": "^6.6.0",
@@ -59,6 +61,7 @@
"react-dom": "^16.14.0",
"react-draft-wysiwyg": "^1.14.7",
"react-flow-renderer": "^9.6.8",
+ "react-js-cron": "^1.2.1",
"react-js-pagination": "^3.0.3",
"react-markdown": "^6.0.3",
"react-oidc": "^1.0.3",
diff --git a/catalog-rest-service/src/main/resources/ui/src/assets/svg/deploy-icon.svg b/catalog-rest-service/src/main/resources/ui/src/assets/svg/deploy-icon.svg
new file mode 100644
index 00000000000..d1ee599b366
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/assets/svg/deploy-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/ingestionWorkflowAPI.ts b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/ingestionWorkflowAPI.ts
new file mode 100644
index 00000000000..3902c8731d2
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/axiosAPIs/ingestionWorkflowAPI.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import { AxiosResponse } from 'axios';
+import { Operation } from 'fast-json-patch';
+import { IngestionData } from '../components/Ingestion/ingestion.interface';
+import { getURLWithQueryFields } from '../utils/APIUtils';
+import APIClient from './index';
+
+const operationsBaseUrl = '/api/operations/v1';
+
+export const addIngestionWorkflow = (
+ data: IngestionData
+): Promise => {
+ const url = '/ingestion';
+
+ return APIClient({
+ method: 'post',
+ url,
+ baseURL: operationsBaseUrl,
+ data: data,
+ });
+};
+
+export const getIngestionWorkflows = (
+ arrQueryFields: Array,
+ paging?: string
+): Promise => {
+ const url = `${getURLWithQueryFields('/ingestion', arrQueryFields)}${
+ paging ? paging : ''
+ }`;
+
+ return APIClient({ method: 'get', url, baseURL: operationsBaseUrl });
+};
+
+export const triggerIngestionWorkflowsById = (
+ id: string,
+ arrQueryFields = ''
+): Promise => {
+ const url = getURLWithQueryFields(`/ingestion/trigger/${id}`, arrQueryFields);
+
+ return APIClient({ method: 'post', url, baseURL: operationsBaseUrl });
+};
+
+export const deleteIngestionWorkflowsById = (
+ id: string,
+ arrQueryFields = ''
+): Promise => {
+ const url = getURLWithQueryFields(`/ingestion/${id}`, arrQueryFields);
+
+ return APIClient({ method: 'delete', url, baseURL: operationsBaseUrl });
+};
+
+export const patchIngestionWorkflowBtId = (
+ id: string,
+ data: Array
+): Promise => {
+ const url = `/ingestion/${id}`;
+
+ return APIClient({
+ method: 'patch',
+ url,
+ baseURL: operationsBaseUrl,
+ data: data,
+ headers: { 'Content-type': 'application/json-patch+json' },
+ });
+};
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Ingestion/Ingestion.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/Ingestion/Ingestion.component.tsx
new file mode 100644
index 00000000000..3fdb44b7718
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/Ingestion/Ingestion.component.tsx
@@ -0,0 +1,460 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import classNames from 'classnames';
+import cronstrue from 'cronstrue';
+import { compare } from 'fast-json-patch';
+import { capitalize, isNil, lowerCase } from 'lodash';
+import React, { useCallback, useState } from 'react';
+import { Link } from 'react-router-dom';
+import {
+ getServiceDetailsPath,
+ TITLE_FOR_NON_ADMIN_ACTION,
+} from '../../constants/constants';
+import { NoDataFoundPlaceHolder } from '../../constants/services.const';
+import { useAuth } from '../../hooks/authHooks';
+import { isEven } from '../../utils/CommonUtils';
+import { Button } from '../buttons/Button/Button';
+import NextPrevious from '../common/next-previous/NextPrevious';
+import NonAdminAction from '../common/non-admin-action/NonAdminAction';
+import PopOver from '../common/popover/PopOver';
+import Searchbar from '../common/searchbar/Searchbar';
+import PageContainer from '../containers/PageContainer';
+import IngestionModal from '../IngestionModal/IngestionModal.component';
+import Loader from '../Loader/Loader';
+import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
+import { IngestionData, Props } from './ingestion.interface';
+
+const Ingestion: React.FC = ({
+ ingestionList,
+ serviceList,
+ deleteIngestion,
+ triggerIngestion,
+ addIngestion,
+ updateIngestion,
+ paging,
+ pagingHandler,
+}: Props) => {
+ const { isAdminUser, isAuthDisabled } = useAuth();
+ const [searchText, setSearchText] = useState('');
+ const [currTriggerId, setCurrTriggerId] = useState({ id: '', state: '' });
+ const [isAdding, setIsAdding] = useState(false);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
+ const [deleteSelection, setDeleteSelection] = useState({
+ id: '',
+ name: '',
+ state: '',
+ });
+ const [updateSelection, setUpdateSelection] = useState({
+ id: '',
+ name: '',
+ state: '',
+ ingestion: {} as IngestionData,
+ });
+
+ const handleSearchAction = (searchValue: string) => {
+ setSearchText(searchValue);
+ };
+
+ const handleTriggerIngestion = (id: string, displayName: string) => {
+ setCurrTriggerId({ id, state: 'waiting' });
+ triggerIngestion(id, displayName)
+ .then(() => {
+ setCurrTriggerId({ id, state: 'success' });
+ setTimeout(() => setCurrTriggerId({ id: '', state: '' }), 1500);
+ })
+ .catch(() => setCurrTriggerId({ id: '', state: '' }));
+ };
+
+ const handleCancelConfirmationModal = () => {
+ setIsConfirmationModalOpen(false);
+ setDeleteSelection({
+ id: '',
+ name: '',
+ state: '',
+ });
+ };
+
+ const handleUpdate = (ingestion: IngestionData) => {
+ setUpdateSelection({
+ id: ingestion.id as string,
+ name: ingestion.displayName,
+ state: '',
+ ingestion: ingestion,
+ });
+ setIsUpdating(true);
+ };
+
+ const handleCancelUpdate = () => {
+ setUpdateSelection({
+ id: '',
+ name: '',
+ state: '',
+ ingestion: {} as IngestionData,
+ });
+ setIsUpdating(false);
+ };
+ const handleUpdateIngestion = (
+ data: IngestionData,
+ triggerIngestion?: boolean
+ ) => {
+ const { service, owner } = updateSelection.ingestion;
+ const updatedData = {
+ ...updateSelection.ingestion,
+ ...data,
+ service,
+ owner,
+ };
+ const patch = compare(updateSelection.ingestion, updatedData);
+ setUpdateSelection((prev) => ({ ...prev, state: 'waiting' }));
+ updateIngestion(
+ updateSelection.id,
+ updateSelection.name,
+ patch,
+ triggerIngestion
+ )
+ .then(() => {
+ setTimeout(() => {
+ setUpdateSelection((prev) => ({ ...prev, state: 'success' }));
+ handleCancelUpdate();
+ }, 500);
+ })
+ .catch(() => {
+ handleCancelUpdate();
+ });
+ };
+
+ const handleDelete = (id: string, displayName: string) => {
+ setDeleteSelection({ id, name: displayName, state: 'waiting' });
+ deleteIngestion(id, displayName)
+ .then(() => {
+ setTimeout(() => {
+ setDeleteSelection({ id, name: displayName, state: 'success' });
+ handleCancelConfirmationModal();
+ }, 500);
+ })
+ .catch(() => {
+ handleCancelConfirmationModal();
+ });
+ };
+
+ const ConfirmDelete = (id: string, name: string) => {
+ setDeleteSelection({
+ id,
+ name,
+ state: '',
+ });
+ setIsConfirmationModalOpen(true);
+ };
+
+ const getServiceTypeFromName = (serviceName = ''): string => {
+ return (
+ serviceList.find((service) => service.name === serviceName)
+ ?.serviceType || ''
+ );
+ };
+
+ const getSearchedIngestions = useCallback(() => {
+ const sText = lowerCase(searchText);
+
+ return sText
+ ? ingestionList.filter(
+ (ing) =>
+ lowerCase(ing.displayName).includes(sText) ||
+ lowerCase(ing.name).includes(sText)
+ )
+ : ingestionList;
+ }, [searchText, ingestionList]);
+
+ const getStatuses = (ingestion: IngestionData) => {
+ const lastFiveIngestions = ingestion.ingestionStatuses
+ ?.sort((a, b) => {
+ // Turn your strings into millis, and then subtract them
+ // to get a value that is either negative, positive, or zero.
+ const date1 = new Date(a.startDate);
+ const date2 = new Date(b.startDate);
+
+ return date1.getTime() - date2.getTime();
+ })
+ .slice(Math.max(ingestion.ingestionStatuses.length - 5, 0));
+
+ return lastFiveIngestions?.map((r, i) => {
+ return (
+
+ {r.startDate ? (
+ Start Date: {new Date(r.startDate).toUTCString()}
+ ) : null}
+ {r.endDate ? (
+ End Date: {new Date(r.endDate).toUTCString()}
+ ) : null}
+
+ }
+ key={i}
+ position="bottom"
+ theme="light"
+ trigger="mouseenter">
+ {i === lastFiveIngestions.length - 1 ? (
+
+ {capitalize(r.state)}
+
+ ) : (
+
+ )}
+
+ );
+ });
+ };
+
+ return (
+
+
+
+
+ {searchText || getSearchedIngestions().length > 0 ? (
+
+ ) : null}
+
+
+
+ setIsAdding(true)}>
+ Add Ingestion
+
+
+
+
+ {getSearchedIngestions().length ? (
+
+
+
+
+ Name
+ Type
+ Service
+ Schedule
+ Recent Runs
+ {/* Next Run */}
+ Actions
+
+
+
+ {getSearchedIngestions().map((ingestion, index) => (
+
+ {ingestion.displayName}
+
+ {ingestion.ingestionType}
+
+
+
+ {ingestion.service.name}
+
+
+
+
+ {cronstrue.toString(
+ ingestion.scheduleInterval || '',
+ {
+ use24HourTimeFormat: true,
+ verbose: true,
+ }
+ )}
+
+ }
+ position="bottom"
+ theme="light"
+ trigger="mouseenter">
+ {ingestion.scheduleInterval}
+
+
+
+ {getStatuses(ingestion)}
+
+ {/*
+ {ingestion.nextExecutionDate || '--'}
+ */}
+
+
+
+
+ handleTriggerIngestion(
+ ingestion.id as string,
+ ingestion.displayName
+ )
+ }>
+ {currTriggerId.id === ingestion.id ? (
+ currTriggerId.state === 'success' ? (
+
+ ) : (
+
+ )
+ ) : (
+ 'Run'
+ )}
+
+
handleUpdate(ingestion)}>
+ {updateSelection.id === ingestion.id ? (
+ updateSelection.state === 'success' ? (
+
+ ) : (
+
+ )
+ ) : (
+ 'Edit'
+ )}
+
+
+ ConfirmDelete(
+ ingestion.id as string,
+ ingestion.displayName
+ )
+ }>
+ {deleteSelection.id === ingestion.id ? (
+ deleteSelection.state === 'success' ? (
+
+ ) : (
+
+ )
+ ) : (
+ 'Delete'
+ )}
+
+
+
+
+
+ ))}
+
+
+ {Boolean(!isNil(paging.after) || !isNil(paging.before)) && (
+
+ )}
+
+ ) : (
+
+
+
+
+
+
+ {`No ingestion workflows found ${
+ searchText ? `for "${searchText}"` : ''
+ }`}
+
+
+
+ )}
+
+ {isAdding ? (
+ {
+ setIsAdding(false);
+ addIngestion(data, triggerIngestion);
+ }}
+ header="Add Ingestion"
+ ingestionList={ingestionList}
+ name=""
+ service=""
+ serviceList={serviceList.map((s) => ({
+ name: s.name,
+ serviceType: s.serviceType,
+ }))}
+ type=""
+ onCancel={() => setIsAdding(false)}
+ />
+ ) : null}
+ {isUpdating ? (
+ {`Edit ${updateSelection.name}`}
}
+ ingestionList={ingestionList}
+ selectedIngestion={updateSelection.ingestion}
+ serviceList={serviceList.map((s) => ({
+ name: s.name,
+ serviceType: s.serviceType,
+ }))}
+ updateIngestion={(data, triggerIngestion) => {
+ setIsUpdating(false);
+ handleUpdateIngestion(data, triggerIngestion);
+ }}
+ onCancel={() => handleCancelUpdate()}
+ />
+ ) : null}
+ {isConfirmationModalOpen && (
+
+ ) : deleteSelection.state === 'success' ? (
+
+ ) : (
+ 'Delete'
+ )
+ }
+ header="Are you sure?"
+ onCancel={handleCancelConfirmationModal}
+ onConfirm={() =>
+ handleDelete(deleteSelection.id, deleteSelection.name)
+ }
+ />
+ )}
+
+ );
+};
+
+export default Ingestion;
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Ingestion/ingestion.interface.ts b/catalog-rest-service/src/main/resources/ui/src/components/Ingestion/ingestion.interface.ts
new file mode 100644
index 00000000000..adf9b54a081
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/Ingestion/ingestion.interface.ts
@@ -0,0 +1,52 @@
+import { Operation } from 'fast-json-patch';
+import { Paging } from 'Models';
+import { IngestionType } from '../../enums/service.enum';
+import { DatabaseService } from '../../generated/entity/services/databaseService';
+import { EntityReference } from '../../generated/type/entityReference';
+
+export interface ConnectorConfig {
+ username: string;
+ password: string;
+ host: string;
+ database: string;
+ includeFilterPattern: Array;
+ excludeFilterPattern: Array;
+ includeViews: boolean;
+ excludeDataProfiler?: boolean;
+ enableDataProfiler?: boolean;
+}
+export interface IngestionData {
+ id?: string;
+ name: string;
+ displayName: string;
+ ingestionType: IngestionType;
+ service: EntityReference;
+ scheduleInterval: string;
+ ingestionStatuses?: Array<{
+ state: string;
+ startDate: string;
+ endDate: string;
+ }>;
+ nextExecutionDate?: string;
+ connectorConfig?: ConnectorConfig;
+ forceDeploy?: boolean;
+ owner?: { id: string; name?: string; type: string };
+ startDate?: string;
+ endDate?: string;
+}
+
+export interface Props {
+ paging: Paging;
+ ingestionList: Array;
+ serviceList: Array;
+ pagingHandler: (value: string) => void;
+ deleteIngestion: (id: string, displayName: string) => Promise;
+ triggerIngestion: (id: string, displayName: string) => Promise;
+ addIngestion: (data: IngestionData, triggerIngestion?: boolean) => void;
+ updateIngestion: (
+ id: string,
+ displayName: string,
+ patch: Array,
+ triggerIngestion?: boolean
+ ) => Promise;
+}
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.component.tsx
new file mode 100644
index 00000000000..dc8182e799f
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.component.tsx
@@ -0,0 +1,715 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import classNames from 'classnames';
+import cronstrue from 'cronstrue';
+import React, { Fragment, useEffect, useState } from 'react';
+import { CronError } from 'react-js-cron';
+import { IngestionType } from '../../enums/service.enum';
+import { getIngestionTypeList } from '../../utils/ServiceUtils';
+import SVGIcons from '../../utils/SvgUtils';
+import { Button } from '../buttons/Button/Button';
+import CronEditor from '../common/CronEditor/CronEditor.component';
+import IngestionStepper from '../IngestionStepper/IngestionStepper.component';
+import { Steps } from '../IngestionStepper/IngestionStepper.interface';
+import './IngestionModal.css';
+import {
+ IngestionModalProps,
+ ServiceData,
+ ValidationErrorMsg,
+} from './IngestionModal.interface';
+
+const errorMsg = (value: string) => {
+ return (
+
+ {value}
+
+ );
+};
+
+const STEPS: Array = [
+ { name: 'Ingestion details', step: 1 },
+ { name: 'Connector config', step: 2 },
+ { name: 'Scheduling', step: 3 },
+ { name: 'Review and Deploy', step: 4 },
+];
+
+const requiredField = (label: string) => (
+ <>
+ {label} *
+ >
+);
+
+const Field = ({ children }: { children: React.ReactNode }) => {
+ return {children}
;
+};
+
+const PreviewSection = ({
+ header,
+ data,
+ className,
+}: {
+ header: string;
+ data: Array<{ key: string; value: string }>;
+ className: string;
+}) => {
+ return (
+
+ {/*
*/}
+
{header}
+
+ {data.map((d, i) => (
+
+
+ {d.key}
+
+
{d.value}
+
+ ))}
+
+
+ );
+};
+
+const getServiceName = (service: string) => {
+ return service.split('$$').splice(1).join('$$');
+};
+
+const getIngestionName = (name: string) => {
+ const nameString = name.trim().replace(/\s+/g, '_');
+
+ return nameString.toLowerCase();
+};
+
+const getCurrentDate = () => {
+ return `${new Date().toLocaleDateString('en-CA')}`;
+};
+
+const setService = (
+ serviceList: Array,
+ currentservice: string
+) => {
+ const service = serviceList.find((s) => s.name === currentservice);
+
+ return service ? `${service?.serviceType}$$${service?.name}` : '';
+};
+
+const IngestionModal: React.FC = ({
+ isUpdating,
+ header,
+ serviceList = [], // TODO: remove default assignment after resolving prop validation warning
+ ingestionList,
+ onCancel,
+ addIngestion,
+ updateIngestion,
+ selectedIngestion,
+}: IngestionModalProps) => {
+ const [activeStep, setActiveStep] = useState(1);
+
+ const [startDate, setStartDate] = useState(
+ selectedIngestion?.startDate || getCurrentDate()
+ );
+ const [endDate, setEndDate] = useState(
+ selectedIngestion?.endDate || ''
+ );
+
+ const [ingestionName, setIngestionName] = useState(
+ selectedIngestion?.displayName || ''
+ );
+ const [ingestionType, setIngestionType] = useState(
+ selectedIngestion?.ingestionType || ''
+ );
+ const [ingestionService, setIngestionService] = useState(
+ setService(serviceList, selectedIngestion?.service?.name as string) || ''
+ );
+
+ const [username, setUsername] = useState(
+ selectedIngestion?.connectorConfig?.username || ''
+ );
+ const [password, setPassword] = useState(
+ selectedIngestion?.connectorConfig?.password || ''
+ );
+ const [host, setHost] = useState(
+ selectedIngestion?.connectorConfig?.host || ''
+ );
+ const [database, setDatabase] = useState(
+ selectedIngestion?.connectorConfig?.database || ''
+ );
+ const [includeFilterPattern, setIncludeFilterPattern] = useState<
+ Array
+ >(selectedIngestion?.connectorConfig?.includeFilterPattern || []);
+ const [excludeFilterPattern, setExcludeFilterPattern] = useState<
+ Array
+ >(selectedIngestion?.connectorConfig?.excludeFilterPattern || []);
+ const [includeViews, setIncludeViews] = useState(
+ selectedIngestion?.connectorConfig?.includeViews || true
+ );
+ const [excludeDataProfiler, setExcludeDataProfiler] = useState(
+ selectedIngestion?.connectorConfig?.enableDataProfiler || false
+ );
+
+ const [ingestionSchedule, setIngestionSchedule] = useState(
+ selectedIngestion?.scheduleInterval || '*/5 * * * *'
+ );
+ const [cronError, setCronError] = useState();
+
+ const [showErrorMsg, setShowErrorMsg] = useState({
+ selectService: false,
+ name: false,
+ username: false,
+ password: false,
+ ingestionType: false,
+ host: false,
+ database: false,
+ ingestionSchedule: false,
+ isPipelineExists: false,
+ isPipelineNameExists: false,
+ });
+
+ const isPipelineExists = () => {
+ return ingestionList.some(
+ (i) =>
+ i.service.name === getServiceName(ingestionService) &&
+ i.ingestionType === ingestionType &&
+ i.service.name !== selectedIngestion?.name &&
+ i.service.displayName === selectedIngestion?.ingestionType
+ );
+ };
+
+ const isPipeLineNameExists = () => {
+ return ingestionList.some(
+ (i) =>
+ i.name === getIngestionName(ingestionName) &&
+ i.name !== selectedIngestion?.name
+ );
+ };
+
+ const handleValidation = (
+ event: React.ChangeEvent
+ ) => {
+ const value = event.target.value;
+ const name = event.target.name;
+
+ switch (name) {
+ case 'name':
+ setIngestionName(value);
+
+ break;
+ case 'selectService':
+ setIngestionService(value);
+ setIngestionType('');
+
+ break;
+ case 'ingestionType':
+ setIngestionType(value);
+
+ break;
+ case 'username':
+ setUsername(value);
+
+ break;
+ case 'password':
+ setPassword(value);
+
+ break;
+ case 'host':
+ setHost(value);
+
+ break;
+ case 'database':
+ setDatabase(value);
+
+ break;
+ case 'ingestionSchedule':
+ setIngestionSchedule(value);
+
+ break;
+
+ default:
+ break;
+ }
+ setShowErrorMsg({
+ ...showErrorMsg,
+ [name]: !value,
+ });
+ };
+
+ const forwardStepHandler = (activeStep: number) => {
+ let isValid = false;
+ switch (activeStep) {
+ case 1:
+ isValid = Boolean(
+ ingestionName &&
+ ingestionService &&
+ ingestionType &&
+ !isPipelineExists()
+ );
+ setShowErrorMsg({
+ ...showErrorMsg,
+ name: !ingestionName,
+ ingestionType: !ingestionType,
+ selectService: !ingestionService,
+ });
+
+ break;
+ case 2:
+ isValid = Boolean(username && password && host && database);
+ setShowErrorMsg({
+ ...showErrorMsg,
+ username: !username,
+ password: !password,
+ host: !host,
+ database: !database,
+ });
+
+ break;
+ case 3:
+ isValid = Boolean(ingestionSchedule && !cronError);
+ setShowErrorMsg({
+ ...showErrorMsg,
+ ingestionSchedule: !ingestionSchedule,
+ });
+
+ break;
+
+ default:
+ break;
+ }
+ setActiveStep((pre) => (pre < STEPS.length && isValid ? pre + 1 : pre));
+ };
+
+ const getActiveStepFields = (activeStep: number) => {
+ switch (activeStep) {
+ case 1:
+ return (
+
+
+
+ {requiredField('Name:')}
+
+
+ {showErrorMsg.name && errorMsg('Ingestion Name is required')}
+ {showErrorMsg.isPipelineNameExists &&
+ errorMsg(`Ingestion with similar name already exists.`)}
+
+
+
+
+ {requiredField('Select Service:')}
+
+
+ Select Service
+ {serviceList.map((service, index) => (
+
+ {service.name}
+
+ ))}
+
+ {showErrorMsg.selectService && errorMsg('Service is required')}
+
+
+
+ {requiredField('Type of ingestion:')}
+
+
+ Select ingestion type
+ {(
+ getIngestionTypeList(ingestionService?.split('$$')?.[0]) || []
+ ).map((service, index) => (
+
+ {service}
+
+ ))}
+
+ {showErrorMsg.ingestionType &&
+ errorMsg('Ingestion Type is required')}
+ {showErrorMsg.isPipelineExists &&
+ errorMsg(
+ `Ingestion with service ${getServiceName(
+ ingestionService
+ )} and ingestion-type ${ingestionType} already exists `
+ )}
+
+
+ );
+
+ case 2:
+ return (
+
+
+
+ {requiredField('Username:')}
+
+
+ {showErrorMsg.username && errorMsg('Username is required')}
+
+
+
+ {requiredField('Password:')}
+
+
+ {showErrorMsg.password && errorMsg('Password is required')}
+
+
+
+ {requiredField('Host:')}
+
+
+ {showErrorMsg.host && errorMsg('Host is required')}
+
+
+
+ {requiredField('Database:')}
+
+
+ {showErrorMsg.database && errorMsg('Database is required')}
+
+
+
+ Include Filter Patterns:
+
+ setIncludeFilterPattern([e.target.value])}
+ />
+
+
+
+ Exclude Filter Patterns:
+
+ setExcludeFilterPattern([e.target.value])}
+ />
+
+
+
+
+ Include views:
+ setIncludeViews(!includeViews)}>
+
+
+
+
+ Enable data profiler:
+
+ setExcludeDataProfiler(!excludeDataProfiler)
+ }>
+
+
+
+
+
+
+ );
+ case 3:
+ return (
+
+
+
{requiredField('Schedule interval:')}
+
+
setIngestionSchedule(v)}
+ onError={setCronError}>
+
+ (
+ {ingestionSchedule}
+ in UTC Timezone )
+
+ {showErrorMsg.ingestionSchedule
+ ? errorMsg('Ingestion schedule is required')
+ : cronError && errorMsg(cronError.description)}
+
+
+
+
+ Start date:
+ setStartDate(e.target.value)}
+ />
+
+
+ End date:
+ setEndDate(e.target.value)}
+ />
+
+
+ );
+ case 4:
+ return (
+
+
+
+
+
+
+ {cronstrue.toString(ingestionSchedule || '', {
+ use24HourTimeFormat: true,
+ verbose: true,
+ })}
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ const onSaveHandler = (triggerIngestion = false) => {
+ const ingestionData = {
+ ingestionType: ingestionType as IngestionType,
+ displayName: ingestionName,
+ name: getIngestionName(ingestionName),
+ service: { name: getServiceName(ingestionService), id: '', type: '' },
+ startDate: startDate || getCurrentDate(),
+ endDate: endDate || '',
+ scheduleInterval: ingestionSchedule,
+ forceDeploy: true,
+ connectorConfig: {
+ database: database,
+ enableDataProfiler: excludeDataProfiler,
+ excludeFilterPattern: excludeFilterPattern,
+ host: host,
+ includeFilterPattern: includeFilterPattern,
+ includeViews: includeViews,
+ password: password,
+ username: username,
+ },
+ };
+ addIngestion?.(ingestionData, triggerIngestion);
+ updateIngestion?.(ingestionData, triggerIngestion);
+ };
+
+ useEffect(() => {
+ setShowErrorMsg({
+ ...showErrorMsg,
+ isPipelineExists: isPipelineExists(),
+ isPipelineNameExists: isPipeLineNameExists(),
+ });
+ }, [ingestionType, ingestionService, ingestionName]);
+
+ useEffect(() => {
+ if (endDate) {
+ const startDt = new Date(startDate);
+ const endDt = new Date(endDate);
+ if (endDt.getTime() < startDt.getTime()) {
+ setEndDate('');
+ }
+ }
+ }, [startDate]);
+
+ return (
+
+
+
+
+
+
+
setActiveStep((pre) => (pre > 1 ? pre - 1 : pre))}>
+ {' '}
+ Previous
+
+
+ {activeStep === 4 ? (
+
+ onSaveHandler()}>
+ Deploy
+
+
+
+ ) : (
+
forwardStepHandler(activeStep)}>
+ Next
+
+
+ )}
+
+
+
+ );
+};
+
+export default IngestionModal;
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.css b/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.css
new file mode 100644
index 00000000000..249b82e34c1
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.css
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+.preview-header {
+ transform: translateY(-13px);
+ font-size: 14px;
+ display: inline-block;
+ background: white;
+}
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.interface.ts b/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.interface.ts
new file mode 100644
index 00000000000..c1e56ac3fba
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/IngestionModal/IngestionModal.interface.ts
@@ -0,0 +1,39 @@
+import React from 'react';
+import {
+ ConnectorConfig,
+ IngestionData,
+} from '../Ingestion/ingestion.interface';
+
+export interface ServiceData {
+ serviceType: string;
+ name: string;
+}
+
+export interface IngestionModalProps {
+ isUpdating?: boolean;
+ ingestionList: Array;
+ header: string | React.ReactNode;
+ name?: string;
+ service?: string;
+ serviceList: Array;
+ type?: string;
+ schedule?: string;
+ connectorConfig?: ConnectorConfig;
+ selectedIngestion?: IngestionData;
+ addIngestion?: (data: IngestionData, triggerIngestion?: boolean) => void;
+ updateIngestion?: (data: IngestionData, triggerIngestion?: boolean) => void;
+ onCancel: () => void;
+}
+
+export interface ValidationErrorMsg {
+ selectService: boolean;
+ name: boolean;
+ username: boolean;
+ password: boolean;
+ ingestionType: boolean;
+ host: boolean;
+ database: boolean;
+ ingestionSchedule: boolean;
+ isPipelineExists: boolean;
+ isPipelineNameExists: boolean;
+}
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.component.tsx
new file mode 100644
index 00000000000..e9b0aa13eca
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.component.tsx
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import classNames from 'classnames';
+import React, { Fragment } from 'react';
+import './IngestionStepper.css';
+type Props = {
+ steps: Array<{ name: string; step: number }>;
+ activeStep: number;
+};
+const IngestionStepper = ({ steps, activeStep }: Props) => {
+ return (
+
+ {steps.map((step, index) => (
+
+ {index > 0 && index < steps.length && (
+
+ )}
+
+
+
+
+ {step.name}
+
+
+
+
+ ))}
+
+ );
+};
+
+export default IngestionStepper;
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.css b/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.css
new file mode 100644
index 00000000000..2697e1e2589
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.css
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+.ingestion-content {
+ display: flex;
+ justify-content: space-around;
+}
+.ingestion-wrapper {
+ display: flex;
+}
+.ingestion-rounder {
+ display: block;
+ width: 18px;
+ height: 18px;
+ border: 3px solid #d9ceee;
+ background: white;
+ border-radius: 50%;
+ margin-top: 0.15rem;
+ z-index: 100;
+}
+.ingestion-rounder.active {
+ border-color: #7147e8;
+}
+.ingestion-rounder.completed {
+ background-color: #d9ceee;
+}
+
+.ingestion-rounder.completed::after {
+ content: '\2713';
+ display: block;
+ margin-top: -5px;
+ font-weight: 900;
+ color: #7147e8;
+}
+
+.ingestion-line::before {
+ top: 10px;
+ bottom: 0;
+ position: absolute;
+ content: ' ';
+ width: 78%;
+ height: 2px;
+ left: 64px;
+ background-color: #d9ceee;
+}
+.ingestion-line-se {
+ display: block;
+ width: 2px;
+ height: 15px;
+ background-color: #d9ceee;
+ transform: translate(8px, 0);
+}
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.interface.ts b/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.interface.ts
new file mode 100644
index 00000000000..90e3901a108
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/IngestionStepper/IngestionStepper.interface.ts
@@ -0,0 +1,4 @@
+export type Steps = {
+ name: string;
+ step: number;
+};
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx b/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx
index 3f3290cdd8f..41e668a43e4 100644
--- a/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx
+++ b/catalog-rest-service/src/main/resources/ui/src/components/Modals/AddServiceModal/AddServiceModal.tsx
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import classNames from 'classnames';
+// import classNames from 'classnames';
import { ServiceTypes } from 'Models';
import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import { serviceTypes } from '../../../constants/services.const';
@@ -28,14 +28,14 @@ import {
import { DatabaseService } from '../../../generated/entity/services/databaseService';
import { MessagingService } from '../../../generated/entity/services/messagingService';
import { PipelineService } from '../../../generated/entity/services/pipelineService';
-import { fromISOString } from '../../../utils/ServiceUtils';
+// import { fromISOString } from '../../../utils/ServiceUtils';
import { Button } from '../../buttons/Button/Button';
import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview';
// import { serviceType } from '../../../constants/services.const';
export type DataObj = {
description: string | undefined;
- ingestionSchedule:
+ ingestionSchedule?:
| {
repeatFrequency: string;
startDate: string;
@@ -128,15 +128,15 @@ const requiredField = (label: string) => (
>
);
-const generateOptions = (count: number, initialValue = 0) => {
- return Array(count)
- .fill(null)
- .map((_, i) => (
-
- {i + initialValue}
-
- ));
-};
+// const generateOptions = (count: number, initialValue = 0) => {
+// return Array(count)
+// .fill(null)
+// .map((_, i) => (
+//
+// {i + initialValue}
+//
+// ));
+// };
const generateName = (data: Array) => {
const newArr: string[] = [];
@@ -187,7 +187,7 @@ export const AddServiceModal: FunctionComponent = ({
);
const [parseUrl] = useState(seprateUrl(data?.jdbc?.connectionUrl) || {});
const [existingNames] = useState(generateName(serviceList));
- const [ingestion, setIngestion] = useState(!!data?.ingestionSchedule);
+ // const [ingestion, setIngestion] = useState(!!data?.ingestionSchedule);
const [selectService, setSelectService] = useState(data?.serviceType || '');
const [name, setName] = useState(data?.name || '');
// const [userName, setUserName] = useState(parseUrl?.userName || '');
@@ -214,9 +214,9 @@ export const AddServiceModal: FunctionComponent = ({
const [server, setServer] = useState(data?.server || '');
const [env, setEnv] = useState(data?.env || '');
const [pipelineUrl, setPipelineUrl] = useState(data?.pipelineUrl || '');
- const [frequency, setFrequency] = useState(
- fromISOString(data?.ingestionSchedule?.repeatFrequency)
- );
+ // const [frequency, setFrequency] = useState(
+ // fromISOString(data?.ingestionSchedule?.repeatFrequency)
+ // );
const [showErrorMsg, setShowErrorMsg] = useState({
selectService: false,
name: false,
@@ -242,13 +242,13 @@ export const AddServiceModal: FunctionComponent = ({
: 'hostname1:port1, hostname2:port2';
};
- const handleChangeFrequency = (
- event: React.ChangeEvent
- ) => {
- const name = event.target.name,
- value = +event.target.value;
- setFrequency({ ...frequency, [name]: value });
- };
+ // const handleChangeFrequency = (
+ // event: React.ChangeEvent
+ // ) => {
+ // const name = event.target.name,
+ // value = +event.target.value;
+ // setFrequency({ ...frequency, [name]: value });
+ // };
const handleValidation = (
event: React.ChangeEvent
@@ -420,16 +420,16 @@ export const AddServiceModal: FunctionComponent = ({
}
setShowErrorMsg(setMsg);
if (onSaveHelper(setMsg)) {
- const { day, hour, minute } = frequency;
- const date = new Date();
+ // const { day, hour, minute } = frequency;
+ // const date = new Date();
let dataObj: DataObj = {
description: markdownRef.current?.getEditorContent(),
- ingestionSchedule: ingestion
- ? {
- repeatFrequency: `P${day}DT${hour}H${minute}M`,
- startDate: date.toISOString(),
- }
- : undefined,
+ // ingestionSchedule: ingestion
+ // ? {
+ // repeatFrequency: `P${day}DT${hour}H${minute}M`,
+ // startDate: date.toISOString(),
+ // }
+ // : undefined,
name: name,
serviceType: selectService,
};
@@ -956,7 +956,7 @@ export const AddServiceModal: FunctionComponent = ({
value={data?.description || ''}
/>
-
+ {/*
Enable Ingestion
= ({
onClick={() => setIngestion(!ingestion)}>
-
- {ingestion && (
+
*/}
+ {/* {ingestion && (
@@ -1024,7 +1024,7 @@ export const AddServiceModal: FunctionComponent = ({
- )}
+ )} */}
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx b/catalog-rest-service/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx
index 65523b75257..e5d4287d657 100644
--- a/catalog-rest-service/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx
+++ b/catalog-rest-service/src/main/resources/ui/src/components/Modals/ConfirmationModal/ConfirmationModal.tsx
@@ -1,9 +1,10 @@
import classNames from 'classnames';
import React from 'react';
+import { ReactNode } from 'react-markdown';
import { Button } from '../../buttons/Button/Button';
type Props = {
cancelText: string;
- confirmText: string;
+ confirmText: string | ReactNode;
bodyText: string;
header: string;
headerClassName?: string;
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/common/CronEditor/CronEditor.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/common/CronEditor/CronEditor.component.tsx
new file mode 100644
index 00000000000..f485b489f65
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/components/common/CronEditor/CronEditor.component.tsx
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import 'antd/dist/antd.css';
+import React, { useCallback, useEffect, useState } from 'react';
+import Cron, { CronError } from 'react-js-cron';
+
+type Props = {
+ defaultValue: string;
+ onChangeHandler?: (v: string) => void;
+ children?: React.ReactNode;
+ isReadOnly?: boolean;
+ className?: string;
+ onError?: (v: CronError) => void;
+};
+
+const CronEditor = ({
+ defaultValue,
+ onChangeHandler,
+ children,
+ isReadOnly = false,
+ className,
+ onError,
+}: Props) => {
+ const [value, setValue] = useState(defaultValue);
+ const customSetValue = useCallback((newValue) => {
+ setValue(newValue);
+ onChangeHandler?.(newValue);
+ }, []);
+
+ useEffect(() => {
+ setValue(defaultValue);
+ }, [defaultValue]);
+
+ return (
+
+
+ {children ? <>{children}> : null}
+
+ );
+};
+
+export default CronEditor;
diff --git a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts
index 1323daf0100..1c829074599 100644
--- a/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts
+++ b/catalog-rest-service/src/main/resources/ui/src/constants/constants.ts
@@ -133,6 +133,7 @@ export const ROUTES = {
PIPELINE_DETAILS: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}`,
PIPELINE_DETAILS_WITH_TAB: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
ONBOARDING: '/onboarding',
+ INGESTION: '/ingestion',
};
export const IN_PAGE_SEARCH_ROUTES: Record
> = {
@@ -253,6 +254,7 @@ export const navLinkSettings = [
{ name: 'Tags', to: '/tags', disabled: false },
// { name: 'Store', to: '/store', disabled: false },
{ name: 'Services', to: '/services', disabled: false },
+ { name: 'Ingestions', to: '/ingestion', disabled: false },
// { name: 'Marketplace', to: '/marketplace', disabled: true },
// { name: 'Preferences', to: '/preference', disabled: true },
];
diff --git a/catalog-rest-service/src/main/resources/ui/src/enums/service.enum.ts b/catalog-rest-service/src/main/resources/ui/src/enums/service.enum.ts
index 6d73ce065e2..d84f083cc54 100644
--- a/catalog-rest-service/src/main/resources/ui/src/enums/service.enum.ts
+++ b/catalog-rest-service/src/main/resources/ui/src/enums/service.enum.ts
@@ -52,3 +52,18 @@ export enum PipelineServiceType {
AIRFLOW = 'Airflow',
PREFECT = 'Prefect',
}
+
+export enum IngestionType {
+ BIGQUERY = 'bigquery',
+ BIGQUERY_USAGE = 'bigquery-usage',
+ REDSHIFT = 'redshift',
+ REDSHIFT_USAGE = 'redshift-usage',
+ SNOWFLAKE = 'snowflake',
+ SNOWFLAKE_USAGE = 'snowflake-usage',
+ HIVE = 'hive',
+ MSSQL = 'mssql',
+ MYSQL = 'mysql',
+ POSTGRES = 'postgres',
+ TRINO = 'trino',
+ VERTICA = 'vertica',
+}
diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/IngestionPage/IngestionPage.component.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/IngestionPage/IngestionPage.component.tsx
new file mode 100644
index 00000000000..8d0806f0d70
--- /dev/null
+++ b/catalog-rest-service/src/main/resources/ui/src/pages/IngestionPage/IngestionPage.component.tsx
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+import { AxiosError, AxiosResponse } from 'axios';
+import { Operation } from 'fast-json-patch';
+import { Paging } from 'Models';
+import React, { useEffect, useState } from 'react';
+import {
+ addIngestionWorkflow,
+ deleteIngestionWorkflowsById,
+ getIngestionWorkflows,
+ patchIngestionWorkflowBtId,
+ triggerIngestionWorkflowsById,
+} from '../../axiosAPIs/ingestionWorkflowAPI';
+import { getServices } from '../../axiosAPIs/serviceAPI';
+import Ingestion from '../../components/Ingestion/Ingestion.component';
+import { IngestionData } from '../../components/Ingestion/ingestion.interface';
+import Loader from '../../components/Loader/Loader';
+import { DatabaseService } from '../../generated/entity/services/databaseService';
+import { EntityReference } from '../../generated/type/entityReference';
+import useToastContext from '../../hooks/useToastContext';
+import { getCurrentUserId } from '../../utils/CommonUtils';
+import { getOwnerFromId } from '../../utils/TableUtils';
+
+const IngestionPage = () => {
+ const showToast = useToastContext();
+ const [isLoading, setIsLoading] = useState(true);
+ const [ingestions, setIngestions] = useState([]);
+ const [serviceList, setServiceList] = useState>([]);
+ const [paging, setPaging] = useState({} as Paging);
+ const getDatabaseServices = () => {
+ getServices('databaseServices')
+ .then((res: AxiosResponse) => {
+ setServiceList(res.data.data);
+ })
+ .catch((err: AxiosError) => {
+ // eslint-disable-next-line
+ console.log(err);
+ });
+ };
+
+ const getAllIngestionWorkflows = (paging?: string) => {
+ getIngestionWorkflows(['owner, service, tags, status'], paging)
+ .then((res) => {
+ if (res.data.data) {
+ setIngestions(res.data.data);
+ setPaging(res.data.paging);
+ setIsLoading(false);
+ } else {
+ setPaging({} as Paging);
+ }
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body: msg ?? `Error while getting ingestion workflow`,
+ });
+ });
+ };
+
+ const triggerIngestionById = (
+ id: string,
+ displayName: string
+ ): Promise => {
+ return new Promise((resolve, reject) => {
+ triggerIngestionWorkflowsById(id)
+ .then((res) => {
+ if (res.data) {
+ resolve();
+ getAllIngestionWorkflows();
+ } else {
+ reject();
+ }
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body:
+ msg ?? `Error while triggering ingestion workflow ${displayName}`,
+ });
+ reject();
+ });
+ });
+ };
+
+ const deleteIngestionById = (
+ id: string,
+ displayName: string
+ ): Promise => {
+ return new Promise((resolve, reject) => {
+ deleteIngestionWorkflowsById(id)
+ .then(() => {
+ resolve();
+ getAllIngestionWorkflows();
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body:
+ msg ?? `Error while deleting ingestion workflow ${displayName}`,
+ });
+ reject();
+ });
+ });
+ };
+
+ const updateIngestionById = (
+ id: string,
+ displayName: string,
+ patch: Array,
+ triggerIngestion?: boolean
+ ): Promise => {
+ return new Promise((resolve, reject) => {
+ patchIngestionWorkflowBtId(id, patch)
+ .then(() => {
+ resolve();
+ getAllIngestionWorkflows();
+ if (triggerIngestion) {
+ triggerIngestionById(id, displayName).then();
+ }
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body:
+ msg ?? `Error while updating ingestion workflow ${displayName}`,
+ });
+ reject();
+ });
+ });
+ };
+
+ const addIngestionWorkflowHandler = (
+ data: IngestionData,
+ triggerIngestion?: boolean
+ ) => {
+ setIsLoading(true);
+ const service = serviceList.find((s) => s.name === data.service.name);
+ const owner = getOwnerFromId(getCurrentUserId());
+ const ingestionData = {
+ ...data,
+ service: {
+ id: service?.id,
+ type: 'databaseService',
+ name: data.service.name,
+ } as EntityReference,
+ owner: {
+ id: owner?.id as string,
+ name: owner?.name,
+ type: 'user',
+ },
+ };
+
+ addIngestionWorkflow(ingestionData)
+ .then((res: AxiosResponse) => {
+ const { id, displayName } = res.data;
+ setIsLoading(false);
+ getAllIngestionWorkflows();
+ if (triggerIngestion) {
+ triggerIngestionById(id, displayName).then();
+ }
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body: msg ?? `Something went wrong`,
+ });
+ setIsLoading(false);
+ });
+ };
+
+ const pagingHandler = (cursorType: string) => {
+ const pagingString = `&${cursorType}=${
+ paging[cursorType as keyof typeof paging]
+ }`;
+ getAllIngestionWorkflows(pagingString);
+ };
+
+ useEffect(() => {
+ getDatabaseServices();
+ getAllIngestionWorkflows();
+ }, []);
+
+ return (
+ <>
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default IngestionPage;
diff --git a/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx b/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx
index d5daf19eb01..fca37797f1e 100644
--- a/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx
+++ b/catalog-rest-service/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx
@@ -26,6 +26,7 @@ import DatabaseDetails from '../pages/database-details/index';
import DatasetDetailsPage from '../pages/DatasetDetailsPage/DatasetDetailsPage.component';
import EntityVersionPage from '../pages/EntityVersionPage/EntityVersionPage.component';
import ExplorePage from '../pages/explore/ExplorePage.component';
+import IngestionPage from '../pages/IngestionPage/IngestionPage.component';
import MyDataPage from '../pages/MyDataPage/MyDataPage.component';
import PipelineDetailsPage from '../pages/PipelineDetails/PipelineDetailsPage.component';
import ReportsPage from '../pages/reports';
@@ -110,6 +111,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
component={EntityVersionPage}
path={ROUTES.DATASET_VERSION}
/>
+
diff --git a/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css b/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css
index 39aebc634eb..f06b59d4a3e 100644
--- a/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css
+++ b/catalog-rest-service/src/main/resources/ui/src/styles/tailwind.css
@@ -317,6 +317,22 @@
box-shadow: 0 0 0 0.5px #7147e8;
}
+ /* Cron editor css */
+ .my-project-cron {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ .my-project-cron-select {
+ @apply tw-rounded tw-border tw-border-main
+ focus:tw-outline-none focus:tw-border-focus hover:tw-border-hover focus:tw-shadow-none hover:tw-shadow-none;
+ }
+ .my-project-cron-clear-button {
+ background-color: #7147e8 !important;
+ border: none;
+ border-radius: 4px;
+ }
+
/* react-slick */
.slick-dots {
diff --git a/catalog-rest-service/src/main/resources/ui/src/styles/temp.css b/catalog-rest-service/src/main/resources/ui/src/styles/temp.css
index e95a7693dd0..8344b4020be 100644
--- a/catalog-rest-service/src/main/resources/ui/src/styles/temp.css
+++ b/catalog-rest-service/src/main/resources/ui/src/styles/temp.css
@@ -703,3 +703,17 @@ body .list-option.rdw-option-active {
text-decoration: line-through;
width: fit-content;
}
+
+/* status style */
+.tw-bg-status-success {
+ background-color: #07a35a;
+}
+.tw-bg-status-failed {
+ background-color: #e54937;
+}
+.tw-bg-status-running {
+ background-color: #276ef1;
+}
+.tw-bg-status-queued {
+ background-color: #777777;
+}
diff --git a/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts b/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts
index ae883f7456f..1f1d6484099 100644
--- a/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts
+++ b/catalog-rest-service/src/main/resources/ui/src/utils/ServiceUtils.ts
@@ -28,6 +28,7 @@ import {
import {
DashboardServiceType,
DatabaseServiceType,
+ IngestionType,
MessagingServiceType,
PipelineServiceType,
} from '../enums/service.enum';
@@ -249,3 +250,54 @@ export const getTotalEntityCountByService = (buckets: Array = []) => {
return entityCounts;
};
+
+export const getIngestionTypeList = (
+ serviceType: string
+): Array | undefined => {
+ let ingestionType: Array | undefined;
+ switch (serviceType) {
+ case DatabaseServiceType.BIGQUERY:
+ ingestionType = [IngestionType.BIGQUERY, IngestionType.BIGQUERY_USAGE];
+
+ break;
+ case DatabaseServiceType.HIVE:
+ ingestionType = [IngestionType.HIVE];
+
+ break;
+
+ case DatabaseServiceType.MSSQL:
+ ingestionType = [IngestionType.MSSQL];
+
+ break;
+
+ case DatabaseServiceType.MYSQL:
+ ingestionType = [IngestionType.MYSQL];
+
+ break;
+
+ case DatabaseServiceType.POSTGRES:
+ ingestionType = [IngestionType.POSTGRES];
+
+ break;
+
+ case DatabaseServiceType.REDSHIFT:
+ ingestionType = [IngestionType.REDSHIFT, IngestionType.REDSHIFT_USAGE];
+
+ break;
+
+ case DatabaseServiceType.TRINO:
+ ingestionType = [IngestionType.TRINO];
+
+ break;
+
+ case DatabaseServiceType.SNOWFLAKE:
+ ingestionType = [IngestionType.SNOWFLAKE, IngestionType.SNOWFLAKE_USAGE];
+
+ break;
+
+ default:
+ break;
+ }
+
+ return ingestionType;
+};
diff --git a/catalog-rest-service/src/main/resources/ui/src/utils/SvgUtils.tsx b/catalog-rest-service/src/main/resources/ui/src/utils/SvgUtils.tsx
index 8c583dd2513..a1286ea7424 100644
--- a/catalog-rest-service/src/main/resources/ui/src/utils/SvgUtils.tsx
+++ b/catalog-rest-service/src/main/resources/ui/src/utils/SvgUtils.tsx
@@ -10,6 +10,7 @@ import IconConfig from '../assets/svg/config.svg';
import IconDashboardGrey from '../assets/svg/dashboard-grey.svg';
import IconDashboard from '../assets/svg/dashboard.svg';
import IconAsstest from '../assets/svg/data-assets.svg';
+import IconDeploy from '../assets/svg/deploy-icon.svg';
import IconDoc from '../assets/svg/doc.svg';
import IconError from '../assets/svg/error.svg';
import IconExternalLink from '../assets/svg/external-link.svg';
@@ -146,6 +147,7 @@ export const Icons = {
PIPELINE_GREY: 'pipeline-grey',
VERSION: 'icon-version',
VERSION_WHITE: 'icon-version-white',
+ ICON_DEPLOY: 'icon-deploy',
};
const SVGIcons: FunctionComponent = ({
@@ -435,6 +437,10 @@ const SVGIcons: FunctionComponent = ({
case Icons.VERSION_WHITE:
IconComponent = IconVersionWhite;
+ break;
+ case Icons.ICON_DEPLOY:
+ IconComponent = IconDeploy;
+
break;
default:
diff --git a/catalog-rest-service/src/main/resources/ui/tailwind.config.js b/catalog-rest-service/src/main/resources/ui/tailwind.config.js
index ffe07aba7fe..3559261df4a 100644
--- a/catalog-rest-service/src/main/resources/ui/tailwind.config.js
+++ b/catalog-rest-service/src/main/resources/ui/tailwind.config.js
@@ -33,6 +33,13 @@ const info = '#1890FF';
const warning = '#FFC34E';
const warningBG = '#FFC34E40';
+// status colors
+
+const statusSuccess = '#07A35A';
+const statusFailed = '#E54937';
+const statusRunning = '#276EF1';
+const statusQueued = '#777777';
+
// Background colors
const bodyBG = '#FCFBFE';
const bodyHoverBG = '#F9F8FD';
@@ -94,6 +101,10 @@ module.exports = {
error: error,
warning: warning,
'warning-lite': warningBG,
+ 'status-success': statusSuccess,
+ 'status-failed': statusFailed,
+ 'status-running': statusRunning,
+ 'status-queued': statusQueued,
info: info,
separator: mainSeparator,
},