Feat : Integration with OpenMetadata Airflow APIs to make ingestion jobs scheduled through UI. (#1153)

* Feat : Integration with OpenMetadata Airflow APIs to make ingestion jobs scheduled through UI.

* minor changed in mocked_data

* added first wizard

* added fields for details and config.

* Feat :Added Cron Editor

* Style: added styling for completed step label

* Integrated ingestion APIs for list, trigger and delete
And fixed Add Ingestion modal states

* minor css changes

* Removed enable ingestion from add service modal.

* wrote effect to get updated cron value.

* Added validation for required field.

* handled undefined case for status

* show actual data on preview

* Added ingestionType to listing page

* Added validation for each step.

* fixed popover issue for status

* added validation for already exists ingestion pipeline.

* Minor UI tweaks for ingestion workflow list

* Integrated add ingestion API

* Added suppport for deploy and run ingestion.

* Added support for Edit ingestion through PATCH API

* Added search for ingestions

* Style : added deploy icon for button.

* Added pagination for ingestion list.

* minor fix

* Minor changes

* Added no data placeholder and fixed non-parsing css

Co-authored-by: darth-coder00 <aashit@getcollate.io>
This commit is contained in:
Sachin Chaurasiya 2021-11-16 22:40:13 +05:30 committed by GitHub
parent 496fa143d4
commit 01f9e54874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2466 additions and 36 deletions

View File

@ -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",

View File

@ -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",

View File

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.1538 0.075705L0.292674 6.34163C-0.131459 6.58534 -0.0775638 7.17584 0.344226 7.35393L2.83513 8.39903L9.56737 2.46585C9.69625 2.35103 9.87903 2.52678 9.7689 2.66034L4.12394 9.53787V11.4242C4.12394 11.9772 4.79177 12.1951 5.11983 11.7944L6.60781 9.98309L9.52754 11.2063C9.86028 11.3469 10.2399 11.1383 10.3008 10.7798L11.988 0.656838C12.0677 0.183496 11.5592 -0.158623 11.1538 0.075705Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -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<AxiosResponse> => {
const url = '/ingestion';
return APIClient({
method: 'post',
url,
baseURL: operationsBaseUrl,
data: data,
});
};
export const getIngestionWorkflows = (
arrQueryFields: Array<string>,
paging?: string
): Promise<AxiosResponse> => {
const url = `${getURLWithQueryFields('/ingestion', arrQueryFields)}${
paging ? paging : ''
}`;
return APIClient({ method: 'get', url, baseURL: operationsBaseUrl });
};
export const triggerIngestionWorkflowsById = (
id: string,
arrQueryFields = ''
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/ingestion/trigger/${id}`, arrQueryFields);
return APIClient({ method: 'post', url, baseURL: operationsBaseUrl });
};
export const deleteIngestionWorkflowsById = (
id: string,
arrQueryFields = ''
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/ingestion/${id}`, arrQueryFields);
return APIClient({ method: 'delete', url, baseURL: operationsBaseUrl });
};
export const patchIngestionWorkflowBtId = (
id: string,
data: Array<Operation>
): Promise<AxiosResponse> => {
const url = `/ingestion/${id}`;
return APIClient({
method: 'patch',
url,
baseURL: operationsBaseUrl,
data: data,
headers: { 'Content-type': 'application/json-patch+json' },
});
};

View File

@ -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<Props> = ({
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<boolean>(false);
const [isUpdating, setIsUpdating] = useState<boolean>(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 (
<PopOver
html={
<div className="tw-text-left">
{r.startDate ? (
<p>Start Date: {new Date(r.startDate).toUTCString()}</p>
) : null}
{r.endDate ? (
<p>End Date: {new Date(r.endDate).toUTCString()}</p>
) : null}
</div>
}
key={i}
position="bottom"
theme="light"
trigger="mouseenter">
{i === lastFiveIngestions.length - 1 ? (
<p
className={`tw-h-5 tw-w-16 tw-rounded-sm tw-bg-status-${r.state} tw-mr-1 tw-px-1 tw-text-white tw-text-center`}>
{capitalize(r.state)}
</p>
) : (
<p
className={`tw-w-4 tw-h-5 tw-rounded-sm tw-bg-status-${r.state} tw-mr-1`}
/>
)}
</PopOver>
);
});
};
return (
<PageContainer className="tw-bg-white">
<div className="tw-px-4">
<div className="tw-flex">
<div className="tw-w-4/12">
{searchText || getSearchedIngestions().length > 0 ? (
<Searchbar
placeholder="Search for ingestion..."
searchValue={searchText}
typingInterval={500}
onSearch={handleSearchAction}
/>
) : null}
</div>
<div className="tw-w-8/12 tw-flex tw-justify-end">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-2', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
data-testid="add-new-user-button"
size="small"
theme="primary"
variant="contained"
onClick={() => setIsAdding(true)}>
Add Ingestion
</Button>
</NonAdminAction>
</div>
</div>
{getSearchedIngestions().length ? (
<div className="tw-table-responsive tw-my-6">
<table className="tw-w-full" data-testid="ingestion-table">
<thead>
<tr className="tableHead-row">
<th className="tableHead-cell">Name</th>
<th className="tableHead-cell">Type</th>
<th className="tableHead-cell">Service</th>
<th className="tableHead-cell">Schedule</th>
<th className="tableHead-cell">Recent Runs</th>
{/* <th className="tableHead-cell">Next Run</th> */}
<th className="tableHead-cell">Actions</th>
</tr>
</thead>
<tbody className="tableBody">
{getSearchedIngestions().map((ingestion, index) => (
<tr
className={classNames(
'tableBody-row',
!isEven(index + 1) ? 'odd-row' : null
)}
key={index}>
<td className="tableBody-cell">{ingestion.displayName}</td>
<td className="tableBody-cell">
{ingestion.ingestionType}
</td>
<td className="tableBody-cell">
<Link
to={getServiceDetailsPath(
ingestion.service.name as string,
getServiceTypeFromName(ingestion.service.name)
)}>
{ingestion.service.name}
</Link>
</td>
<td className="tableBody-cell">
<PopOver
html={
<div>
{cronstrue.toString(
ingestion.scheduleInterval || '',
{
use24HourTimeFormat: true,
verbose: true,
}
)}
</div>
}
position="bottom"
theme="light"
trigger="mouseenter">
<span>{ingestion.scheduleInterval}</span>
</PopOver>
</td>
<td className="tableBody-cell">
<div className="tw-flex">{getStatuses(ingestion)}</div>
</td>
{/* <td className="tableBody-cell">
{ingestion.nextExecutionDate || '--'}
</td> */}
<td className="tableBody-cell">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<div className="tw-flex">
<div
className="link-text tw-mr-2"
onClick={() =>
handleTriggerIngestion(
ingestion.id as string,
ingestion.displayName
)
}>
{currTriggerId.id === ingestion.id ? (
currTriggerId.state === 'success' ? (
<i aria-hidden="true" className="fa fa-check" />
) : (
<Loader size="small" type="default" />
)
) : (
'Run'
)}
</div>
<p
className="link-text tw-mr-2"
onClick={() => handleUpdate(ingestion)}>
{updateSelection.id === ingestion.id ? (
updateSelection.state === 'success' ? (
<i aria-hidden="true" className="fa fa-check" />
) : (
<Loader size="small" type="default" />
)
) : (
'Edit'
)}
</p>
<div
className="link-text tw-mr-2"
onClick={() =>
ConfirmDelete(
ingestion.id as string,
ingestion.displayName
)
}>
{deleteSelection.id === ingestion.id ? (
deleteSelection.state === 'success' ? (
<i aria-hidden="true" className="fa fa-check" />
) : (
<Loader size="small" type="default" />
)
) : (
'Delete'
)}
</div>
</div>
</NonAdminAction>
</td>
</tr>
))}
</tbody>
</table>
{Boolean(!isNil(paging.after) || !isNil(paging.before)) && (
<NextPrevious paging={paging} pagingHandler={pagingHandler} />
)}
</div>
) : (
<div className="tw-flex tw-items-center tw-flex-col">
<div className="tw-mt-24">
<img alt="No Service" src={NoDataFoundPlaceHolder} width={250} />
</div>
<div className="tw-mt-11">
<p className="tw-text-lg tw-text-center">
{`No ingestion workflows found ${
searchText ? `for "${searchText}"` : ''
}`}
</p>
</div>
</div>
)}
</div>
{isAdding ? (
<IngestionModal
addIngestion={(data, triggerIngestion) => {
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 ? (
<IngestionModal
isUpdating
header={<p>{`Edit ${updateSelection.name}`}</p>}
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 && (
<ConfirmationModal
bodyText={`You want to delete ingestion ${deleteSelection.name} permanently? This action cannot be reverted.`}
cancelText="Discard"
confirmButtonCss="tw-bg-error hover:tw-bg-error focus:tw-bg-error"
confirmText={
deleteSelection.state === 'waiting' ? (
<Loader size="small" type="white" />
) : deleteSelection.state === 'success' ? (
<i aria-hidden="true" className="fa fa-check" />
) : (
'Delete'
)
}
header="Are you sure?"
onCancel={handleCancelConfirmationModal}
onConfirm={() =>
handleDelete(deleteSelection.id, deleteSelection.name)
}
/>
)}
</PageContainer>
);
};
export default Ingestion;

View File

@ -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<string>;
excludeFilterPattern: Array<string>;
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<IngestionData>;
serviceList: Array<DatabaseService>;
pagingHandler: (value: string) => void;
deleteIngestion: (id: string, displayName: string) => Promise<void>;
triggerIngestion: (id: string, displayName: string) => Promise<void>;
addIngestion: (data: IngestionData, triggerIngestion?: boolean) => void;
updateIngestion: (
id: string,
displayName: string,
patch: Array<Operation>,
triggerIngestion?: boolean
) => Promise<void>;
}

View File

@ -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 (
<div className="tw-mt-1">
<strong className="tw-text-red-500 tw-text-xs tw-italic">{value}</strong>
</div>
);
};
const STEPS: Array<Steps> = [
{ 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} <span className="tw-text-red-500">&nbsp;*</span>
</>
);
const Field = ({ children }: { children: React.ReactNode }) => {
return <div className="tw-mt-6">{children}</div>;
};
const PreviewSection = ({
header,
data,
className,
}: {
header: string;
data: Array<{ key: string; value: string }>;
className: string;
}) => {
return (
<div className={className}>
{/* <hr className="tw-border-separator" /> */}
<p className="preview-header tw-px-1">{header}</p>
<div className="tw-grid tw-gap-4 tw-grid-cols-3 tw-place-content-center tw-pl-6">
{data.map((d, i) => (
<div key={i}>
<p className="tw-text-xs tw-font-normal tw-text-grey-muted">
{d.key}
</p>
<p>{d.value}</p>
</div>
))}
</div>
</div>
);
};
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<ServiceData>,
currentservice: string
) => {
const service = serviceList.find((s) => s.name === currentservice);
return service ? `${service?.serviceType}$$${service?.name}` : '';
};
const IngestionModal: React.FC<IngestionModalProps> = ({
isUpdating,
header,
serviceList = [], // TODO: remove default assignment after resolving prop validation warning
ingestionList,
onCancel,
addIngestion,
updateIngestion,
selectedIngestion,
}: IngestionModalProps) => {
const [activeStep, setActiveStep] = useState<number>(1);
const [startDate, setStartDate] = useState<string>(
selectedIngestion?.startDate || getCurrentDate()
);
const [endDate, setEndDate] = useState<string>(
selectedIngestion?.endDate || ''
);
const [ingestionName, setIngestionName] = useState<string>(
selectedIngestion?.displayName || ''
);
const [ingestionType, setIngestionType] = useState<string>(
selectedIngestion?.ingestionType || ''
);
const [ingestionService, setIngestionService] = useState<string>(
setService(serviceList, selectedIngestion?.service?.name as string) || ''
);
const [username, setUsername] = useState<string>(
selectedIngestion?.connectorConfig?.username || ''
);
const [password, setPassword] = useState<string>(
selectedIngestion?.connectorConfig?.password || ''
);
const [host, setHost] = useState<string>(
selectedIngestion?.connectorConfig?.host || ''
);
const [database, setDatabase] = useState<string>(
selectedIngestion?.connectorConfig?.database || ''
);
const [includeFilterPattern, setIncludeFilterPattern] = useState<
Array<string>
>(selectedIngestion?.connectorConfig?.includeFilterPattern || []);
const [excludeFilterPattern, setExcludeFilterPattern] = useState<
Array<string>
>(selectedIngestion?.connectorConfig?.excludeFilterPattern || []);
const [includeViews, setIncludeViews] = useState<boolean>(
selectedIngestion?.connectorConfig?.includeViews || true
);
const [excludeDataProfiler, setExcludeDataProfiler] = useState<boolean>(
selectedIngestion?.connectorConfig?.enableDataProfiler || false
);
const [ingestionSchedule, setIngestionSchedule] = useState<string>(
selectedIngestion?.scheduleInterval || '*/5 * * * *'
);
const [cronError, setCronError] = useState<CronError>();
const [showErrorMsg, setShowErrorMsg] = useState<ValidationErrorMsg>({
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<HTMLInputElement | HTMLSelectElement>
) => {
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 (
<Fragment>
<Field>
<label className="tw-block" htmlFor="name">
{requiredField('Name:')}
</label>
<input
className={classNames('tw-form-inputs tw-px-3 tw-py-1', {
'tw-cursor-not-allowed': isUpdating,
})}
id="name"
name="name"
placeholder="Ingestion name"
readOnly={isUpdating}
type="text"
value={ingestionName}
onChange={handleValidation}
/>
{showErrorMsg.name && errorMsg('Ingestion Name is required')}
{showErrorMsg.isPipelineNameExists &&
errorMsg(`Ingestion with similar name already exists.`)}
</Field>
<Field>
<label className="tw-block" htmlFor="selectService">
{requiredField('Select Service:')}
</label>
<select
className={classNames('tw-form-inputs tw-px-3 tw-py-1', {
'tw-cursor-not-allowed': isUpdating,
})}
data-testid="selectService"
disabled={isUpdating}
id="selectService"
name="selectService"
value={ingestionService}
onChange={handleValidation}>
<option value="">Select Service</option>
{serviceList.map((service, index) => (
<option
key={index}
value={`${service.serviceType}$$${service.name}`}>
{service.name}
</option>
))}
</select>
{showErrorMsg.selectService && errorMsg('Service is required')}
</Field>
<Field>
<label className="tw-block " htmlFor="ingestionType">
{requiredField('Type of ingestion:')}
</label>
<select
className={classNames('tw-form-inputs tw-px-3 tw-py-1', {
'tw-cursor-not-allowed': !ingestionService,
})}
data-testid="selectService"
disabled={!ingestionService || isUpdating}
id="ingestionType"
name="ingestionType"
value={ingestionType}
onChange={handleValidation}>
<option value="">Select ingestion type</option>
{(
getIngestionTypeList(ingestionService?.split('$$')?.[0]) || []
).map((service, index) => (
<option key={index} value={service}>
{service}
</option>
))}
</select>
{showErrorMsg.ingestionType &&
errorMsg('Ingestion Type is required')}
{showErrorMsg.isPipelineExists &&
errorMsg(
`Ingestion with service ${getServiceName(
ingestionService
)} and ingestion-type ${ingestionType} already exists `
)}
</Field>
</Fragment>
);
case 2:
return (
<Fragment>
<Field>
<label className="tw-block" htmlFor="username">
{requiredField('Username:')}
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id="username"
name="username"
placeholder="User name"
type="text"
value={username}
onChange={handleValidation}
/>
{showErrorMsg.username && errorMsg('Username is required')}
</Field>
<Field>
<label className="tw-block" htmlFor="password">
{requiredField('Password:')}
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id="password"
name="password"
placeholder="Password"
type="password"
value={password}
onChange={handleValidation}
/>
{showErrorMsg.password && errorMsg('Password is required')}
</Field>
<Field>
<label className="tw-block" htmlFor="host">
{requiredField('Host:')}
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id="host"
name="host"
placeholder="Host"
type="text"
value={host}
onChange={handleValidation}
/>
{showErrorMsg.host && errorMsg('Host is required')}
</Field>
<Field>
<label className="tw-block" htmlFor="database">
{requiredField('Database:')}
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id="database"
name="database"
placeholder="Database"
type="text"
value={database}
onChange={handleValidation}
/>
{showErrorMsg.database && errorMsg('Database is required')}
</Field>
<Field>
<label className="tw-block" htmlFor="includeFilterPattern">
Include Filter Patterns:
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id="includeFilterPattern"
name="includeFilterPattern"
placeholder="Include filter patterns comma seperated"
type="text"
value={includeFilterPattern}
onChange={(e) => setIncludeFilterPattern([e.target.value])}
/>
</Field>
<Field>
<label className="tw-block" htmlFor="excludeFilterPattern">
Exclude Filter Patterns:
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id="excludeFilterPattern"
name="excludeFilterPattern"
placeholder="Exclude filter patterns comma seperated"
type="text"
value={excludeFilterPattern}
onChange={(e) => setExcludeFilterPattern([e.target.value])}
/>
</Field>
<Field>
<div className="tw-flex tw-justify-between">
<Fragment>
<label>Include views:</label>
<div
className={classNames(
'toggle-switch',
includeViews ? 'open' : null
)}
onClick={() => setIncludeViews(!includeViews)}>
<div className="switch" />
</div>
</Fragment>
<Fragment>
<label>Enable data profiler:</label>
<div
className={classNames(
'toggle-switch',
excludeDataProfiler ? 'open' : null
)}
onClick={() =>
setExcludeDataProfiler(!excludeDataProfiler)
}>
<div className="switch" />
</div>
</Fragment>
</div>
</Field>
</Fragment>
);
case 3:
return (
<Fragment>
<div className="tw-mt-4">
<label htmlFor="">{requiredField('Schedule interval:')}</label>
<div className="tw-flex tw-justify-items-start tw-mt-2">
<CronEditor
defaultValue={ingestionSchedule}
onChangeHandler={(v) => setIngestionSchedule(v)}
onError={setCronError}>
<p className="tw-text-grey-muted tw-text-xs tw-mt-1">
<span>( </span>
<span className="tw-font-normal">{ingestionSchedule}</span>
<span> in UTC Timezone ) </span>
</p>
{showErrorMsg.ingestionSchedule
? errorMsg('Ingestion schedule is required')
: cronError && errorMsg(cronError.description)}
</CronEditor>
</div>
</div>
<Field>
<label htmlFor="startDate">Start date:</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
pattern="YY-MM-DD"
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</Field>
<Field>
<label htmlFor="endDate">End date:</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
min={startDate}
pattern="YY-MM-DD"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</Field>
</Fragment>
);
case 4:
return (
<Fragment>
<div className="tw-flex tw-flex-col tw-mt-6">
<PreviewSection
className="tw-mb-4 tw-mt-4"
data={[
{ key: 'Name', value: ingestionName },
{
key: 'Service Type',
value: getServiceName(ingestionService),
},
{ key: 'Ingestion Type', value: ingestionType },
]}
header="Ingestion Details"
/>
<PreviewSection
className="tw-mb-4 tw-mt-6"
data={[
{ key: 'Username', value: username },
{ key: 'Password', value: password },
{ key: 'Host', value: host },
{ key: 'Database', value: database },
{ key: 'Include views', value: includeViews ? 'Yes' : 'No' },
{
key: 'Enable Data Profiler',
value: excludeDataProfiler ? 'Yes' : 'No',
},
]}
header="Connector Config"
/>
<PreviewSection
className="tw-mt-6"
data={[]}
header="Scheduling"
/>
<p className="tw-pl-6">
{cronstrue.toString(ingestionSchedule || '', {
use24HourTimeFormat: true,
verbose: true,
})}
</p>
</div>
</Fragment>
);
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 (
<dialog className="tw-modal" data-testid="service-modal">
<div className="tw-modal-backdrop" />
<div className="tw-modal-container tw-max-w-2xl">
<div className="tw-modal-header">
<p className="tw-modal-title">{header}</p>
<div className="tw-flex">
<svg
className="tw-w-6 tw-h-6 tw-ml-1 tw-cursor-pointer"
data-testid="closeWhatsNew"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
onClick={onCancel}>
<path
d="M6 18L18 6M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
</div>
</div>
<div className="tw-modal-body">
<IngestionStepper activeStep={activeStep} steps={STEPS} />
<form className="tw-min-w-full" data-testid="form">
<div className="tw-px-4">{getActiveStepFields(activeStep)}</div>
</form>
</div>
<div className="tw-modal-footer tw-justify-between">
<Button
className={classNames('tw-mr-2', {
'tw-invisible': activeStep === 1,
})}
data-testid="cancel"
size="regular"
theme="primary"
variant="text"
onClick={() => setActiveStep((pre) => (pre > 1 ? pre - 1 : pre))}>
<i className="fas fa-arrow-left tw-text-sm tw-align-middle tw-pr-1.5" />{' '}
<span>Previous</span>
</Button>
{activeStep === 4 ? (
<div className="tw-flex">
<Button
data-testid="save-button"
size="regular"
theme="primary"
type="submit"
variant="contained"
onClick={() => onSaveHandler()}>
<span className="tw-mr-2">Deploy</span>
<SVGIcons alt="Deploy" icon="icon-deploy" />
</Button>
</div>
) : (
<Button
data-testid="next-button"
size="regular"
theme="primary"
variant="contained"
onClick={() => forwardStepHandler(activeStep)}>
<span>Next</span>
<i className="fas fa-arrow-right tw-text-sm tw-align-middle tw-pl-1.5" />
</Button>
)}
</div>
</div>
</dialog>
);
};
export default IngestionModal;

View File

@ -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;
}

View File

@ -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<IngestionData>;
header: string | React.ReactNode;
name?: string;
service?: string;
serviceList: Array<ServiceData>;
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;
}

View File

@ -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 (
<div className="ingestion-content tw-relative">
{steps.map((step, index) => (
<Fragment key={index}>
{index > 0 && index < steps.length && (
<span className="ingestion-line" />
)}
<div className="ingestion-wrapper" key={index}>
<span className="tw-flex tw-flex-col">
<span
className={classNames(
'ingestion-rounder tw-self-center',
{
active: step.step === activeStep,
},
{ completed: step.step < activeStep }
)}
/>
<span
className={classNames('tw-mt-2 tw-text-xs', {
'tw-text-primary': step.step <= activeStep,
})}>
{step.name}
</span>
</span>
</div>
</Fragment>
))}
</div>
);
};
export default IngestionStepper;

View File

@ -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);
}

View File

@ -0,0 +1,4 @@
export type Steps = {
name: string;
step: number;
};

View File

@ -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) => (
<option key={i + initialValue} value={i + initialValue}>
{i + initialValue}
</option>
));
};
// const generateOptions = (count: number, initialValue = 0) => {
// return Array(count)
// .fill(null)
// .map((_, i) => (
// <option key={i + initialValue} value={i + initialValue}>
// {i + initialValue}
// </option>
// ));
// };
const generateName = (data: Array<ServiceDataObj>) => {
const newArr: string[] = [];
@ -187,7 +187,7 @@ export const AddServiceModal: FunctionComponent<Props> = ({
);
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<Props> = ({
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<ErrorMsg>({
selectService: false,
name: false,
@ -242,13 +242,13 @@ export const AddServiceModal: FunctionComponent<Props> = ({
: 'hostname1:port1, hostname2:port2';
};
const handleChangeFrequency = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
const name = event.target.name,
value = +event.target.value;
setFrequency({ ...frequency, [name]: value });
};
// const handleChangeFrequency = (
// event: React.ChangeEvent<HTMLSelectElement>
// ) => {
// const name = event.target.name,
// value = +event.target.value;
// setFrequency({ ...frequency, [name]: value });
// };
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
@ -420,16 +420,16 @@ export const AddServiceModal: FunctionComponent<Props> = ({
}
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<Props> = ({
value={data?.description || ''}
/>
</div>
<div className="tw-mt-4 tw-flex tw-items-center">
{/* <div className="tw-mt-4 tw-flex tw-items-center">
<label className="tw-form-label tw-mb-0">Enable Ingestion</label>
<div
className={classNames(
@ -967,8 +967,8 @@ export const AddServiceModal: FunctionComponent<Props> = ({
onClick={() => setIngestion(!ingestion)}>
<div className="switch" />
</div>
</div>
{ingestion && (
</div> */}
{/* {ingestion && (
<div className="tw-grid tw-grid-cols-3 tw-gap-2 tw-gap-y-0 tw-mt-4">
<div className="tw-col-span-3">
<label className="tw-block tw-form-label" htmlFor="frequency">
@ -1024,7 +1024,7 @@ export const AddServiceModal: FunctionComponent<Props> = ({
</select>
</div>
</div>
)}
)} */}
</form>
</div>
<div className="tw-modal-footer tw-justify-end">

View File

@ -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;

View File

@ -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 (
<div className={className}>
<Cron
className="tw-z-9999 my-project-cron"
readOnly={isReadOnly}
setValue={customSetValue}
value={value}
onError={onError}
/>
{children ? <>{children}</> : null}
</div>
);
};
export default CronEditor;

View File

@ -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<string, Array<string>> = {
@ -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 },
];

View File

@ -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',
}

View File

@ -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<Array<DatabaseService>>([]);
const [paging, setPaging] = useState<Paging>({} 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<void> => {
return new Promise<void>((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<void> => {
return new Promise<void>((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<Operation>,
triggerIngestion?: boolean
): Promise<void> => {
return new Promise<void>((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 ? (
<Loader />
) : (
<Ingestion
addIngestion={addIngestionWorkflowHandler}
deleteIngestion={deleteIngestionById}
ingestionList={ingestions}
paging={paging}
pagingHandler={pagingHandler}
serviceList={serviceList}
triggerIngestion={triggerIngestionById}
updateIngestion={updateIngestionById}
/>
)}
</>
);
};
export default IngestionPage;

View File

@ -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}
/>
<Route exact component={IngestionPage} path={ROUTES.INGESTION} />
<Redirect to={ROUTES.NOT_FOUND} />
</Switch>

View File

@ -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 {

View File

@ -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;
}

View File

@ -28,6 +28,7 @@ import {
import {
DashboardServiceType,
DatabaseServiceType,
IngestionType,
MessagingServiceType,
PipelineServiceType,
} from '../enums/service.enum';
@ -249,3 +250,54 @@ export const getTotalEntityCountByService = (buckets: Array<Bucket> = []) => {
return entityCounts;
};
export const getIngestionTypeList = (
serviceType: string
): Array<string> | undefined => {
let ingestionType: Array<string> | 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;
};

View File

@ -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<Props> = ({
@ -435,6 +437,10 @@ const SVGIcons: FunctionComponent<Props> = ({
case Icons.VERSION_WHITE:
IconComponent = IconVersionWhite;
break;
case Icons.ICON_DEPLOY:
IconComponent = IconDeploy;
break;
default:

View File

@ -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,
},