diff --git a/package-lock.json b/package-lock.json index 890d39c78a..ee151c6301 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "react-hotkeys-hook": "^5.2.1", "react-i18next": "^16.5.0", "react-icons": "^5.5.0", + "react-js-cron": "^5.0.1", "react-redux": "^9.2.0", "react-router": "^7.9.5", "react-select": "^5.10.2", @@ -79,6 +80,115 @@ "node": ">=6.0.0" } }, + "node_modules/@ant-design/colors": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", + "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz", + "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/cssinjs/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@ant-design/cssinjs/node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "license": "MIT", @@ -477,6 +587,12 @@ } } }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", "license": "MIT", @@ -1214,6 +1330,18 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "license": "MIT" @@ -1802,6 +1930,155 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz", + "integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@redux-devtools/extension": { "version": "3.3.0", "dev": true, @@ -3194,6 +3471,72 @@ "node": ">=4" } }, + "node_modules/antd": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.1.tgz", + "integrity": "sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ant-design/colors": "^7.2.0", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.2.6", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.2.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.7", + "rc-slider": "~11.1.8", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.50.4", + "rc-tabs": "~15.6.1", + "rc-textarea": "~1.10.0", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.9.0", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -3448,6 +3791,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3663,6 +4014,12 @@ "node": ">= 0.8" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -3679,6 +4036,15 @@ "node": ">=18" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "license": "MIT", @@ -3789,6 +4155,13 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "peer": true + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -5477,6 +5850,15 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -6102,6 +6484,618 @@ "version": "4.0.3", "license": "MIT" }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", + "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", + "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.50.5", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz", + "integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.6.1.tgz", + "integrity": "sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.0.tgz", + "integrity": "sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.0.tgz", + "integrity": "sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/rc-virtual-list": { + "version": "3.18.6", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.18.6.tgz", + "integrity": "sha512-TQ5SsutL3McvWmmxqQtMIbfeoE3dGjJrRSfKekgby7WQMpPIFvv4ghytp5Z0s3D8Nik9i9YNOCqHBfk86AwgAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -6206,6 +7200,17 @@ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT" }, + "node_modules/react-js-cron": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-js-cron/-/react-js-cron-5.2.0.tgz", + "integrity": "sha512-+Mxm3cS7qmIoAIz7NVY27jvsJKNZ4tx+H/nNtMUJW4DcKR7jUIL1GP0jOD79K5j86Dq8jvShKJMh30+c8bmVtA==", + "license": "MIT", + "peerDependencies": { + "antd": ">=5.8.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/react-redux": { "version": "9.2.0", "license": "MIT", @@ -6401,6 +7406,12 @@ "version": "5.1.1", "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "license": "MIT", @@ -6573,6 +7584,15 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "6.3.1", "dev": true, @@ -6733,6 +7753,17 @@ "tslib": "^2.0.3" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -6741,6 +7772,18 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "dev": true, @@ -6751,6 +7794,12 @@ "dev": true, "license": "MIT" }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "dev": true, @@ -6901,6 +7950,39 @@ "version": "6.2.0", "license": "MIT" }, + "node_modules/terser": { + "version": "5.17.3", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, "node_modules/tiny-case": { "version": "1.0.3", "license": "MIT" @@ -7016,6 +8098,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/toposort": { "version": "2.0.2", "license": "MIT" diff --git a/package.json b/package.json index c5d0868350..8677fc3b62 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-hotkeys-hook": "^5.2.1", "react-i18next": "^16.5.0", "react-icons": "^5.5.0", + "react-js-cron": "^5.0.1", "react-redux": "^9.2.0", "react-router": "^7.9.5", "react-select": "^5.10.2", diff --git a/src/App.tsx b/src/App.tsx index 81c18f7daa..673a07f7c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import Acls from "./components/users/Acls"; import About from "./components/About"; import { useAppDispatch } from "./store"; import { fetchOcVersion, fetchUserInfo } from "./slices/userInfoSlice"; +import LifeCyclePolicies from "./components/events/LifeCyclePolicies"; import { subscribeToAuthEvents } from "./utils/broadcastSync"; import { useTableFilterStateValidation } from "./hooks/useTableFilterStateValidation"; @@ -47,6 +48,8 @@ function App() { } /> + } /> + } /> } /> diff --git a/src/components/events/LifeCyclePolicies.tsx b/src/components/events/LifeCyclePolicies.tsx new file mode 100644 index 0000000000..2143a9e4cb --- /dev/null +++ b/src/components/events/LifeCyclePolicies.tsx @@ -0,0 +1,121 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TableFilters from "../shared/TableFilters"; +import Table from "../shared/Table"; +import Notifications from "../shared/Notifications"; +import { loadLifeCyclePoliciesIntoTable } from "../../thunks/tableThunks"; +import { fetchFilters } from "../../slices/tableFilterSlice"; +import Header from "../Header"; +import NavBar from "../NavBar"; +import MainView from "../MainView"; +import Footer from "../Footer"; +import { useAppDispatch, useAppSelector } from "../../store"; +import { AsyncThunk } from "@reduxjs/toolkit"; +import { getTotalLifeCyclePolicies } from "../../selectors/lifeCycleSelectors"; +import { fetchLifeCyclePolicies } from "../../slices/lifeCycleSlice"; +import { lifeCyclePoliciesTemplateMap } from "../../configs/tableConfigs/lifeCyclePoliciesTableMap"; +import { fetchLifeCyclePolicyActions, fetchLifeCyclePolicyTargetTypes, fetchLifeCyclePolicyTimings } from "../../slices/lifeCycleDetailsSlice"; +import { ModalHandle } from "../shared/modals/Modal"; +import { eventsLinks } from "./partials/EventsNavigation"; +import { resetTableProperties } from "../../slices/tableSlice"; +import LifeCyclePolicyDetailsModal from "./partials/modals/LifeCyclePolicyDetailsModal"; + +/** + * This component renders the table view of policies + */ +const LifeCyclePolicies = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [displayNavigation, setNavigation] = useState(false); + const newPolicyModalRef = useRef(null); + + const policiesTotal = useAppSelector(state => getTotalLifeCyclePolicies(state)); + + useEffect(() => { + // State variable for interrupting the load function + let allowLoadIntoTable = true; + + // Clear table of previous data + dispatch(resetTableProperties()); + + dispatch(fetchFilters("lifeCyclePolicies")); + + // Load policies on mount + const loadLifeCyclePolicies = async () => { + // Fetching policies from server + await dispatch(fetchLifeCyclePolicies()); + + // Load policies into table + if (allowLoadIntoTable) { + dispatch(loadLifeCyclePoliciesIntoTable()); + } + }; + loadLifeCyclePolicies(); + + // Fetch policies repeatedly + const fetchInterval = setInterval(loadLifeCyclePolicies, 5000); + + return () => { + allowLoadIntoTable = false; + clearInterval(fetchInterval); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const showNewPolicyModal = async () => { + await dispatch(fetchLifeCyclePolicyActions()); + await dispatch(fetchLifeCyclePolicyTargetTypes()); + await dispatch(fetchLifeCyclePolicyTimings()); + + newPolicyModalRef.current?.open(); + }; + + return ( + <> +
+ + + + + {/* Include notifications component */} + + +
+ {/* Include filters component */} + {/* LifeCycle policies are not indexed, can't search or filter them */} + {/* But if we don't include this component, the policies won't load on page load, because the first + fetch request we send to the backend contains invalid params >.> */} + } + loadResourceIntoTable={loadLifeCyclePoliciesIntoTable} + resource={"lifeCyclePolicies"} + /> + +

{t("LIFECYCLE.POLICIES.TABLE.CAPTION")}

+

{t("TABLE_SUMMARY", { numberOfRows: policiesTotal })}

+
+ {/* Include table component */} + + +
+ + {/* Include table modal */} + + + ); +}; + +export default LifeCyclePolicies; diff --git a/src/components/events/partials/EventsNavigation.tsx b/src/components/events/partials/EventsNavigation.tsx index 5c21a230ec..45d0eac8e9 100644 --- a/src/components/events/partials/EventsNavigation.tsx +++ b/src/components/events/partials/EventsNavigation.tsx @@ -18,4 +18,9 @@ export const eventsLinks: { accessRole: "ROLE_UI_SERIES_VIEW", text: "EVENTS.EVENTS.NAVIGATION.SERIES", }, + { + path: "/events/lifeCyclePolicies", + accessRole: "ROLE_UI_LIFECYCLEPOLICIES_VIEW", + text: "LIFECYCLE.NAVIGATION.POLICIES", + }, ]; diff --git a/src/components/events/partials/LifeCyclePolicyActionCell.tsx b/src/components/events/partials/LifeCyclePolicyActionCell.tsx new file mode 100644 index 0000000000..357e41ed69 --- /dev/null +++ b/src/components/events/partials/LifeCyclePolicyActionCell.tsx @@ -0,0 +1,54 @@ +import { useAppDispatch } from "../../../store"; +import { deleteLifeCyclePolicy, LifeCyclePolicy } from "../../../slices/lifeCycleSlice"; +import { fetchLifeCyclePolicyDetails, openModal } from "../../../slices/lifeCycleDetailsSlice"; +import ButtonLikeAnchor from "../../shared/ButtonLikeAnchor"; +import { LuFileText } from "react-icons/lu"; +import { ActionCellDelete } from "../../shared/ActionCellDelete"; + +/** + * This component renders the title cells of series in the table view + */ +const LifeCyclePolicyActionCell = ({ + row, +}: { + row: LifeCyclePolicy +}) => { + const dispatch = useAppDispatch(); + + const showLifeCyclePolicyDetails = async () => { + await dispatch(fetchLifeCyclePolicyDetails(row.id)); + + dispatch(openModal(row)); + }; + + const deletingPolicy = (id: string) => { + dispatch(deleteLifeCyclePolicy(id)); + }; + + return ( + <> + {/* view details location/recording */} + showLifeCyclePolicyDetails()} + className={"action-cell-button"} + editAccessRole={"ROLE_UI_LIFECYCLEPOLICY_DETAILS_VIEW"} + // tooltipText={"LIFECYCLE.POLICIES.TABLE.TOOLTIP.DETAILS"} // Disabled due to performance concerns + > + + + + + {/* delete policy */} + + + ); +}; + +export default LifeCyclePolicyActionCell; diff --git a/src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx b/src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx new file mode 100644 index 0000000000..358c5c131e --- /dev/null +++ b/src/components/events/partials/LifeCyclePolicyIsActiveCell.tsx @@ -0,0 +1,21 @@ +import { LifeCyclePolicy } from "../../../slices/lifeCycleSlice"; + +/** + * This component renders the maintenance cells of servers in the table view + */ +const LifeCyclePolicyIsActiveCell = ({ + row, +}: { + row: LifeCyclePolicy +}) => { + + return ( + + ); +}; + +export default LifeCyclePolicyIsActiveCell; diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsLifeCyclePolicy.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsLifeCyclePolicy.tsx new file mode 100644 index 0000000000..7b15f69ab3 --- /dev/null +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsLifeCyclePolicy.tsx @@ -0,0 +1,111 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { getLifeCyclePoliciesForEvent } from "../../../../selectors/eventDetailsSelectors"; +import { fetchEventLifeCyclePolicies } from "../../../../slices/eventDetailsSlice"; +import ModalContentTable from "../../../shared/modals/ModalContentTable"; +import Notifications from "../../../shared/Notifications"; +import ButtonLikeAnchor from "../../../shared/ButtonLikeAnchor"; +import { LuChevronRight } from "react-icons/lu"; +import { useNavigate } from "react-router"; +import { fetchLifeCyclePolicyDetails, openModal } from "../../../../slices/lifeCycleDetailsSlice"; +import { LifeCyclePolicy } from "../../../../slices/lifeCycleSlice"; + + +/** + * This component shows lifecycle policies that would affect the event + */ +const EventDetailsLifeCyclePolicy = ({ + eventId, +}: { + eventId: string, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const policies = useAppSelector(state => getLifeCyclePoliciesForEvent(state)); + + useEffect(() => { + dispatch(fetchEventLifeCyclePolicies(eventId)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const openPolicyDetails = async (policy: LifeCyclePolicy) => { + await dispatch(fetchLifeCyclePolicyDetails(policy.id)); + dispatch(openModal(policy)); + navigate("/events/lifeCyclePolicies"); + }; + + return ( + } + > + {/* Disclaimer */} +
+
+ {t("EVENTS.EVENTS.DETAILS.LIFECYCLEPOLICIES.DISCLAIMER.TITLE")} +
+
+ {t("EVENTS.EVENTS.DETAILS.LIFECYCLEPOLICIES.DISCLAIMER.MESSAGE")} +
+
+ +
+ { + /* No policies message */ + policies.length === 0 && ( +
+ + + +
+ {t("EVENTS.EVENTS.DETAILS.LIFECYCLEPOLICIES.EMPTY")} +
+ ) + } + + { policies.length !== 0 && ( +
+ + <> + + + + + + + { + policies.map((policy, key) => ( + + + + {/* link to 'Details' sub-Tab */} + + + )) + } + + +
+ {t("EVENTS.EVENTS.DETAILS.LIFECYCLEPOLICIES.TABLE_TITLE")} + +
+ {policy.title} + + openPolicyDetails(policy)} + > + {t("EVENTS.EVENTS.DETAILS.MEDIA.DETAILS")} + + +
+
+ )} + + + ); +}; + +export default EventDetailsLifeCyclePolicy; diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx index 20dad74e06..c6befa37b0 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx @@ -187,6 +187,7 @@ const EventDetailsWorkflowSchedulingTab = ({
{hasCurrentAgentAccess() && isRoleWorkflowEdit && + formik.values.configuration && !!workflowConfiguration && !!workflowConfiguration.workflowId && (
)} diff --git a/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyAccessTab.tsx b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyAccessTab.tsx new file mode 100644 index 0000000000..e375ee96fd --- /dev/null +++ b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyAccessTab.tsx @@ -0,0 +1,56 @@ +import { useEffect } from "react"; +import ResourceDetailsAccessPolicyTab from "../../../shared/modals/ResourceDetailsAccessPolicyTab"; +import { removeNotificationWizardForm } from "../../../../slices/notificationSlice"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { getLifeCyclePolicyDetailsAcl } from "../../../../selectors/lifeCycleDetailsSelectors"; +import { fetchLifeCyclePolicyDetailsAcls, updateLifeCyclePolicyAccess } from "../../../../slices/lifeCycleDetailsSlice"; +import { ParseKeys } from "i18next"; + +/** + * This component manages the access policy tab of the series details modal + */ +const LifeCyclePolicyDetailsAccessTab = ({ + seriesId, + header, + policyChanged, + setPolicyChanged, +}: { + seriesId: string, + header: ParseKeys, + policyChanged: boolean, + setPolicyChanged: (value: boolean) => void, +}) => { + const dispatch = useAppDispatch(); + + const acl = useAppSelector(state => getLifeCyclePolicyDetailsAcl(state)); + + useEffect(() => { + dispatch(removeNotificationWizardForm()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +}; + +export default LifeCyclePolicyDetailsAccessTab; diff --git a/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyGeneralTab.tsx b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyGeneralTab.tsx new file mode 100644 index 0000000000..5e1ba8b7dc --- /dev/null +++ b/src/components/events/partials/ModalTabsAndPages/LifeCyclePolicyGeneralTab.tsx @@ -0,0 +1,188 @@ +import { useTranslation } from "react-i18next"; +import { LifeCyclePolicy, TargetFilter } from "../../../../slices/lifeCycleSlice"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { Formik, FormikProps } from "formik"; +import Notifications from "../../../shared/Notifications"; +import cn from "classnames"; +import { ConfigurationPanelField } from "../../../../slices/workflowSlice"; +import { updateLifeCyclePolicy } from "../../../../slices/lifeCycleDetailsSlice"; +import LifeCyclePolicyGeneralFields from "../wizards/LifeCyclePolicyGeneralFields"; +import { hasAccess } from "../../../../utils/utils"; +import { getUserInformation } from "../../../../selectors/userInfoSelectors"; +import { LifeCyclePolicySchema } from "../../../../utils/validate"; +import _ from "lodash"; +import { parseTargetFiltersForEditing, parseTargetFiltersForSubmit } from "../../../../utils/lifeCycleUtils"; + +/** + * This component renders details about a recording/capture agent + */ +const LifeCyclePolicyGeneralTab = ({ + policy, +}: { + policy: LifeCyclePolicy +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const user = useAppSelector(state => getUserInformation(state)); + + const handleSubmit = (values: LifeCyclePolicy & { + workflowParameters: ConfigurationPanelField[], + targetFiltersTransformed: { [key: string]: (TargetFilter & { filter: string })[] } + }) => { + + // Parse filters + const targetFilters: typeof values["targetFilters"] = parseTargetFiltersForSubmit(values.targetFiltersTransformed); + + // TODO: Improve workflowParameters rendering + // Parse action parameters + // const workflowParameters: { [key: string]: unknown} = {}; + // for (const field of values.workflowParameters) { + // if (field.fieldset && field.fieldset.length > 0) { + // workflowParameters[field.fieldset[0].name] = field.fieldset[0].value + // } + // } + // const newActionParameters = { + // ...values.actionParameters, + // workflowParameters + // } + + const newValues = { + ...values, + targetFilters: targetFilters, + // actionParameters: newActionParameters, + }; + // values.actionParameters["workflowParameters"] = JSON.stringify(workflowParameters); + + dispatch(updateLifeCyclePolicy(newValues)); + }; + + // set current values of metadata fields as initial values + const getInitialValues = (policy: LifeCyclePolicy) => { + const initialValues: LifeCyclePolicy & { + workflowParameters: ConfigurationPanelField[], + targetFiltersTransformed: { [key: string]: (TargetFilter & { filter: string })[] } + } = { + workflowParameters: [], + targetFiltersTransformed: {}, + ...policy, + }; + + // Access policies are handled in a different tab + // Remove them here, else they will delete the ACL due to their formatting + // @ts-expect-error: TODO: Find a typesafe (or straight up better) way to do this + delete initialValues.accessControlEntries; + + // Transform filters into something more editable + const targetFiltersTransformed = { + "dublincore/episode": [], + ...parseTargetFiltersForEditing(policy.targetFilters), + }; + // for (const key in policy.targetFilters) { + // targetFiltersArray.push({ + // filter: key, + // ...policy.targetFilters[key], + // }); + // } + + // TODO: Improve workflowParameters rendering + // Parse action parameters + // const configPanelFields: ConfigurationPanelField[] = [] + // const workflowParameters = JSON.parse(policy.actionParameters["workflowParameters"] as string) + // Object.entries(workflowParameters).forEach(([key, value]) => { + // configPanelFields.push({ + // fieldset: [{ + // name: key, + // value: value, + // defaultValue: value, + // type: "text", + // checked: false, + // label: key, + // }] + // }); + // }); + + initialValues.targetFiltersTransformed = targetFiltersTransformed; + // initialValues.workflowParameters = configPanelFields; + + return initialValues; + }; + + const checkValidity = (formik: FormikProps) => { + if (formik.dirty && formik.isValid && hasAccess("ROLE_UI_LIFECYCLEPOLICY_DETAILS_GENERAL_EDIT", user)) { + // check if user provided values differ from initial ones + return !_.isEqual(formik.values, formik.initialValues); + } else { + return false; + } + }; + + return ( + // initialize form + handleSubmit(values)} + > + {formik => ( + <> +
+
+ +
+ {/*
+
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.CAPTION")}
*/} + + {/* Render fields */} + + +
+
+ {t("LIFECYCLE.POLICIES.DETAILS.GENERAL.NOTE.TITLE")} +
+
+ {t("LIFECYCLE.POLICIES.DETAILS.GENERAL.NOTE.MESSAGE")} +
+
+ + {formik.dirty && ( + <> + {/* Render buttons for updating metadata */} +
+ + +
+ +
+ + )} + {/*
*/} +
+
+
+ + )} + + ); +}; + +export default LifeCyclePolicyGeneralTab; diff --git a/src/components/events/partials/ModalTabsAndPages/NewLifeCyclePolicyGeneralPage.tsx b/src/components/events/partials/ModalTabsAndPages/NewLifeCyclePolicyGeneralPage.tsx new file mode 100644 index 0000000000..06aa64f775 --- /dev/null +++ b/src/components/events/partials/ModalTabsAndPages/NewLifeCyclePolicyGeneralPage.tsx @@ -0,0 +1,37 @@ +import { FormikProps } from "formik"; +import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons"; +import LifeCyclePolicyGeneralFields from "../wizards/LifeCyclePolicyGeneralFields"; +import { LifeCyclePolicy, TargetFilter } from "../../../../slices/lifeCycleSlice"; + +/** + * This component renders the metadata page for new events and series in the wizards. + */ +const NewLifeCyclePolicyGeneralPage = ({ + formik, + nextPage, +}: { + formik: FormikProps, + nextPage: (values: T) => void, +}) => { + + return ( + <> +
+
+
+ {/* Table view containing input fields for metadata */} + +
+
+
+ + {/* Button for navigation to next page */} + + + ); +}; + +export default NewLifeCyclePolicyGeneralPage; diff --git a/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx b/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx index 5057995381..2627910f49 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx @@ -17,6 +17,7 @@ import ModalContentTable from "../../../shared/modals/ModalContentTable"; interface RequiredFormProps { sourceMode: string, processingWorkflow: string, + configuration: { [key: string]: any }, } const NewProcessingPage = ({ @@ -115,8 +116,8 @@ const NewProcessingPage = ({ ) : null}
diff --git a/src/components/events/partials/modals/EventDetails.tsx b/src/components/events/partials/modals/EventDetails.tsx index 6e190df462..2380f55fcc 100644 --- a/src/components/events/partials/modals/EventDetails.tsx +++ b/src/components/events/partials/modals/EventDetails.tsx @@ -51,6 +51,7 @@ import { NOTIFICATION_CONTEXT } from "../../../../configs/modalConfig"; import { unwrapResult } from "@reduxjs/toolkit"; import { ParseKeys } from "i18next"; import EventDetailsWorkflowSchedulingTab from "../ModalTabsAndPages/EventDetailsWorkflowSchedulingTab"; +import EventDetailsLifeCyclePolicy from "../ModalTabsAndPages/EventDetailsLifeCyclePolicy"; import { useHotkeys } from "react-hotkeys-hook"; import { availableHotkeys } from "../../../../configs/hotkeysConfig"; @@ -65,6 +66,7 @@ export enum EventDetailsPage { Comments, Tobira, Statistics, + LifeCyclePolicies, } export type WorkflowTabHierarchy = "workflows" | "workflow-details" | "workflow-operations" | "workflow-operation-details" | "errors-and-warnings" | "workflow-error-details" @@ -219,6 +221,12 @@ const EventDetails = ({ page: EventDetailsPage.Statistics, hidden: !hasStatistics, }, + { + tabNameTranslation: "EVENTS.EVENTS.DETAILS.TABS.LIFECYCLEPOLICIES", + accessRole: "ROLE_UI_EVENTS_DETAILS_LIFECYCLEPOLICIES_VIEW", + name: "lifecyclepolicies", + page: EventDetailsPage.LifeCyclePolicies, + }, ]; const openTab = (tabNr: EventDetailsPage) => { @@ -372,6 +380,11 @@ const EventDetails = ({ header={tabs[page].bodyHeaderTranslation ?? "EVENTS.EVENTS.DETAILS.STATISTICS.CAPTION"} /> )} + {page === EventDetailsPage.LifeCyclePolicies && ( + + )}
); diff --git a/src/components/events/partials/modals/LifeCyclePolicyDetails.tsx b/src/components/events/partials/modals/LifeCyclePolicyDetails.tsx new file mode 100644 index 0000000000..cd3d871a4e --- /dev/null +++ b/src/components/events/partials/modals/LifeCyclePolicyDetails.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; +import ModalNavigation from "../../../shared/modals/ModalNavigation"; +import { getLifeCyclePolicyDetails } from "../../../../selectors/lifeCycleDetailsSelectors"; +import LifeCyclePolicyGeneralTab from "../ModalTabsAndPages/LifeCyclePolicyGeneralTab"; +import LifeCyclePolicyDetailsAccessTab from "../ModalTabsAndPages/LifeCyclePolicyAccessTab"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { removeNotificationWizardForm } from "../../../../slices/notificationSlice"; +import { fetchLifeCyclePolicyActions, fetchLifeCyclePolicyTargetTypes, fetchLifeCyclePolicyTimings } from "../../../../slices/lifeCycleDetailsSlice"; +import { ParseKeys } from "i18next"; + +/** + * This component manages the tabs of the series details modal + */ +const LifeCyclePolicyDetails = ({ + policyId, + policyChanged, + setPolicyChanged, +}: { + policyId: string + policyChanged: boolean + setPolicyChanged: (policyChanged: boolean) => void +}) => { + const [page, setPage] = useState(0); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(removeNotificationWizardForm()); + dispatch(fetchLifeCyclePolicyActions()); + dispatch(fetchLifeCyclePolicyTargetTypes()); + dispatch(fetchLifeCyclePolicyTimings()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const policy = useAppSelector(state => getLifeCyclePolicyDetails(state)); + + // information about tabs + const tabs: { + tabTranslation: ParseKeys, + accessRole: string, + name: string, + }[] = [ + { + tabTranslation: "LIFECYCLE.POLICIES.DETAILS.TAB.GENERAL", + accessRole: "ROLE_UI_LIFECYCLEPOLICIES_DETAILS_GENERAL_VIEW", + name: "general", + }, + { + tabTranslation: "LIFECYCLE.POLICIES.DETAILS.TAB.ACCESSPOLICIES", + accessRole: "ROLE_UI_LIFECYCLEPOLICIES_DETAILS_ACCESSPOLICIES_VIEW", + name: "Access Policies", + }, + ]; + + const openTab = (tabNr: number) => { + setPage(tabNr); + }; + + return ( + <> + {/* Navigation */} + + +
+ {page === 0 && } + {page === 1 && + + } +
+ + ); +}; + +export default LifeCyclePolicyDetails; diff --git a/src/components/events/partials/modals/LifeCyclePolicyDetailsModal.tsx b/src/components/events/partials/modals/LifeCyclePolicyDetailsModal.tsx new file mode 100644 index 0000000000..fccb476353 --- /dev/null +++ b/src/components/events/partials/modals/LifeCyclePolicyDetailsModal.tsx @@ -0,0 +1,68 @@ +import { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { removeNotificationWizardForm } from "../../../../slices/notificationSlice"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { Modal } from "../../../shared/modals/Modal"; +import { confirmUnsaved } from "../../../../utils/utils"; +import { FormikProps } from "formik"; +import LifeCyclePolicyDetails from "./LifeCyclePolicyDetails"; +import { getModalLifeCyclePolicy, showModal } from "../../../../selectors/lifeCycleDetailsSelectors"; +import { setModalLifeCyclePolicy, setShowModal } from "../../../../slices/lifeCycleDetailsSlice"; + +/** + * This component renders the modal for displaying lifecycle policy details + */ +const LifeCyclePolicyDetailsModal = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + // tracks, whether the policies are different to the initial value + const [policyChanged, setPolicyChanged] = useState(false); + const formikRef = useRef>(null); + + const displayDetailsModal = useAppSelector(state => showModal(state)); + const policy = useAppSelector(state => getModalLifeCyclePolicy(state))!; + + const hideModal = () => { + dispatch(setModalLifeCyclePolicy(null)); + dispatch(setShowModal(false)); + }; + + const close = () => { + let isUnsavedChanges = false; + isUnsavedChanges = policyChanged; + if (formikRef.current && formikRef.current.dirty !== undefined && formikRef.current.dirty) { + isUnsavedChanges = true; + } + + if (!isUnsavedChanges || confirmUnsaved(t)) { + setPolicyChanged(false); + dispatch(removeNotificationWizardForm()); + hideModal(); + return true; + } + return false; + }; + + return ( + <> + {displayDetailsModal && + + setPolicyChanged(value)} + /> + + } + + ); +}; + +export default LifeCyclePolicyDetailsModal; diff --git a/src/components/events/partials/wizards/LifeCyclePolicyGeneralFields.tsx b/src/components/events/partials/wizards/LifeCyclePolicyGeneralFields.tsx new file mode 100644 index 0000000000..24a83d0e6e --- /dev/null +++ b/src/components/events/partials/wizards/LifeCyclePolicyGeneralFields.tsx @@ -0,0 +1,584 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { FieldArray, FieldProps, FormikProps } from "formik"; +import { Field } from "../../../shared/Field"; +import RenderField from "../../../shared/wizard/RenderField"; +import { ALL_TARGET_FILTER_TYPES, LifeCyclePolicy, TargetFilter } from "../../../../slices/lifeCycleSlice"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { getLifeCyclePolicyActions, getLifeCyclePolicyTargetTypes, getLifeCyclePolicyTimings } from "../../../../selectors/lifeCycleDetailsSelectors"; +import DropDown from "../../../shared/DropDown"; +import { getEventMetadata } from "../../../../selectors/eventSelectors"; +import { fetchEventMetadata } from "../../../../slices/eventSlice"; +import { formatPolicyActionsForDropdown, formatWorkflowsForDropdown } from "../../../../utils/dropDownUtils"; +import { getWorkflowDef } from "../../../../selectors/workflowSelectors"; +import { fetchWorkflowDef } from "../../../../slices/workflowSlice"; +import RenderWorkflowConfig, { Configuration } from "./RenderWorkflowConfig"; +import { setDefaultConfig } from "../../../../utils/workflowPanelUtils"; +import ButtonLikeAnchor from "../../../shared/ButtonLikeAnchor"; +import { LuCircleX } from "react-icons/lu"; + +/** + * This component renders the metadata page for new events and series in the wizards. + */ +// interface RequiredFormProps { +// sourceMode: string, +// processingWorkflow: string, +// } +type EventFilterOption = { + id: string, + type: string, + collection?: unknown +} + +const LifeCyclePolicyGeneralFields = ({ + formik, + isNew, +}: { + formik: FormikProps, + isNew: boolean +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const actions = useAppSelector(state => getLifeCyclePolicyActions(state)); + const targetTypes = useAppSelector(state => getLifeCyclePolicyTargetTypes(state)); + const timings = useAppSelector(state => getLifeCyclePolicyTimings(state)); + const metadataFields = useAppSelector(state => getEventMetadata(state)); + + useEffect(() => { + dispatch(fetchEventMetadata()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const ADDITIONAL_TARGET_FILTER_KEYS_EVENTS = [ + { + id: "series_name", + type: "text", + collection: undefined, + }, + { + id: "presenter", + type: "text", + collection: undefined, + }, + { + id: "start_date", + type: "date", + collection: undefined, + }, + { + id: "end_date", + type: "date", + collection: undefined, + }, + { + id: "created", + type: "date", + collection: undefined, + }, + { + id: "source", + type: "text", + collection: undefined, + }, + { + id: "rights", + type: "text", + collection: undefined, + }, + { + id: "location", + type: "text", + collection: undefined, + }, + ]; + + const eventFilterOptions: EventFilterOption[] = []; + for (const field of metadataFields.fields) { + eventFilterOptions.push(field); + } + for (const field of ADDITIONAL_TARGET_FILTER_KEYS_EVENTS) { + eventFilterOptions.push(field); + } + + const createTargetFilter = (): TargetFilter => { + return { + value: "", + type: "SEARCH", + must: true, + }; + }; + + const filterOptions = (targetType: string) => { + switch (targetType) { + case "EVENT": + return eventFilterOptions; + default: + return []; + } + }; + + const filterTargetTypesByFilter = (filter: string) => { + const event = eventFilterOptions.find(event => event.id === filter); + + if (!event) { + return ALL_TARGET_FILTER_TYPES; + } + if (event.type.includes("text")) { + return ["SEARCH", "WILDCARD"]; + } + if (event.type.includes("date")) { + return ["GREATER_THAN", "LESS_THAN"]; + } + return ALL_TARGET_FILTER_TYPES; + }; + + + return ( + <> +
+
{t("LIFECYCLE.POLICIES.NEW.GENERAL.CAPTION")}
+ + + + + + + + + + + + {!isNew && + + + + + } + + + + + + + + + + + + + {formik.values.timing === "SPECIFIC_DATE" && + + + + + } + {formik.values.timing === "REPEATING" && + + + + + } + {!isNew && + + + + + } + + +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TITLE")}* + +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ISACTIVE")}* + +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ISCREATEDFROMCONFIG")} + +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETTYPE")}* + ({ value: element, name: element })), + id: "language", + }} + component={RenderField} + /> +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TIMING")}* + ({ value: element, name: element })), + id: "language", + }} + component={RenderField} + /> +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ACTION")}* + ({ value: element, name: element })), + id: "language", + }} + component={RenderField} + /> +
+ {t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ACTIONDATE")} + {formik.values.timing === "SPECIFIC_DATE" && *} + + +
+ {t("LIFECYCLE.POLICIES.DETAILS.GENERAL.CRONTRIGGER")} + {formik.values.timing === "REPEATING" && *} + + +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ID")} + {formik.values.id} +
+
+ +
+ + + {/* Target Filters like the ACLs + Can we make "key" a dropdown? + Type of "Value" should depend on key, e.g. for key "start_date" show a date picker + */} +
+
+ { t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.CAPTION") } +
+ + + {/* column headers */} + + + + + + + + + + + + {Object.entries(formik.values.targetFiltersTransformed).map(([outerKey, filters]) => { + if (outerKey !== "dublincore/episode") { return null; } + + return ( + ( + <> + {Object.entries(filters).map(([key, filter], index) => { + // Get available filter options + const availableFilterOptions = filterOptions(formik.values.targetType); + + // Derive available type options based on selected filter + const dependentTypeOptions = filterTargetTypesByFilter(filter.filter); + + return ( + + + + + + + + ); + })} + + + + + )} + /> + ); + })} + +
+ { t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.FILTER") } + + { t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.VALUE") } + + { t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.TYPE") } + + { t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.MUST") } + + { t("EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.ACTION") } +
+ e.id)} + creatable={true} + clearFieldName={`targetFiltersTransformed.${outerKey}.${key}.value`} + component={DropdownField} + onChangeOverride={(element: { value: string; label: string } | null) => { + formik.setFieldValue(`targetFiltersTransformed.${outerKey}.${key}.value`, undefined); + formik.setFieldValue(`targetFiltersTransformed.${outerKey}.${key}.filter`, element?.value ?? undefined); + // Reset type when filter changes + formik.setFieldValue(`targetFiltersTransformed.${outerKey}.${key}.type`, undefined); + }} + /> + + + + + + + + arrayHelpers.remove(index)} + className="action-cell-button remove" + > + + +
+ + arrayHelpers.push(createTargetFilter()) + } + className="button-like-anchor" + > + +{" "} + {t( + "LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.NEW", + )} + +
+
+ + {formik.values.action === "START_WORKFLOW" && + + } + + ); +}; + + +export default LifeCyclePolicyGeneralFields; + +const DropdownField = ({ + field, + form: { setFieldValue }, + value, + values, + clearFieldName, + creatable = false, + onChangeOverride, +}: { + field: FieldProps["field"] + form: FieldProps["form"] + value: string, + values: string[] + clearFieldName: string + creatable: boolean + onChangeOverride?: (element: { value: string; label: string } | null) => void +}) => { + const { t } = useTranslation(); + + const handleChange = (element: { value: string; label: string } | null) => { + if (onChangeOverride) { + // call the override function if provided + onChangeOverride(element); + } else { + // default behavior + setFieldValue(clearFieldName, undefined); + if (element) { + setFieldValue(field.name, element.value); + } + } + }; + + return ( + + ); +}; + +const getTargetFilterRenderType = (filterName: string, targetFilterOptions: { id: string, type: string, collection?: unknown }[]) => { + const option = targetFilterOptions.find(e => e.id === filterName); + if (option === undefined) { + return "text"; + } + // Simplify types like "long_text" or "mixed_text" + if (option.type.includes("text")) { + return "text"; + } + return option.type; +}; + +const getTargetFilterRenderCollection = (filterName: string, targetFilterOptions: { id: string, type: string, collection?: unknown }[]) => { + const option = targetFilterOptions.find(e => e.id === filterName); + return option !== undefined ? option.collection : undefined; +}; + +const WorkflowSelector = ({ + formik, +}: { + formik: FormikProps, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const workflowDef = useAppSelector(state => getWorkflowDef(state)); + // const lol = JSON.parse(formik.values.actionParameters.workflowParameters) + + useEffect(() => { + // Load workflow definitions for selecting + dispatch(fetchWorkflowDef("tasks")); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const setDefaultValues = (value: string) => { + const workflowId = value; + // fill values with default configuration of chosen workflow + const defaultConfiguration = setDefaultConfig(workflowDef, workflowId); + + // set default configuration in formik + formik.setFieldValue("actionParameters.workflowParameters", defaultConfiguration); + // set chosen workflow in formik + formik.setFieldValue("actionParameters.workflowId", workflowId); + }; + + return ( +
+
+ {t("EVENTS.EVENTS.NEW.PROCESSING.SELECT_WORKFLOW")} +
+
+ {workflowDef.length > 0 ? ( +
+ + formik.values.actionParameters.workflowId === workflow.id, + )?.title ?? "" + } + options={formatWorkflowsForDropdown(workflowDef)} + required={true} + handleChange={element => { + if (element) { + setDefaultValues(element.value as string); + } + }} + placeholder={t( + "EVENTS.EVENTS.NEW.PROCESSING.SELECT_WORKFLOW", + )} + customCSS={{ width: "100%" }} + /> +
+ ) : ( + + {t("EVENTS.EVENTS.NEW.PROCESSING.SELECT_WORKFLOW_EMPTY")} + + )} + + {/* Configuration panel of selected workflow */} +
+
+ {formik.values.actionParameters.workflowId ? ( + + ) : null} +
+
+
+
+ ); +}; diff --git a/src/components/events/partials/wizards/NewLifeCyclePolicySummary.tsx b/src/components/events/partials/wizards/NewLifeCyclePolicySummary.tsx new file mode 100644 index 0000000000..a07f92a52a --- /dev/null +++ b/src/components/events/partials/wizards/NewLifeCyclePolicySummary.tsx @@ -0,0 +1,124 @@ +import { useTranslation } from "react-i18next"; +import AccessSummaryTable from "./summaryTables/AccessSummaryTable"; +import WizardNavigationButtons from "../../../shared/wizard/WizardNavigationButtons"; +import { FormikProps } from "formik"; +import { renderValidDate } from "../../../../utils/dateUtils"; +import { initialFormValuesNewLifeCyclePolicy } from "../../../../configs/modalConfig"; + +/** + * This component renders the summary page for new series in the new series wizard. + */ +// interface RequiredFormProps { + +// } + +const NewLifeCyclePolicySummary = ({ + formik, + previousPage, +}: { + formik: FormikProps, + previousPage: (values: T, twoPagesBack?: boolean) => void, +}) => { + const { t } = useTranslation(); + + return ( + <> +
+
+
+ +
+
{t("LIFECYCLE.POLICIES.NEW.CAPTION")}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )) + ))} + + + + + + {/* @ts-expect-error: Potentially unknown */} + + + + + + ))} + + + } + + + +
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TITLE")}{formik.values.title}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ISACTIVE")}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETTYPE")}{formik.values.targetType}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TIMING")}{formik.values.timing}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ACTION")}{formik.values.action}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ACTIONDATE")}{t("dateFormats.dateTime.medium", { dateTime: renderValidDate(formik.values.actionDate) })}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.CRONTRIGGER")}{formik.values.cronTrigger}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.TARGETFILTERS.CAPTION")} + {Object.entries(formik.values.targetFiltersTransformed) + .filter(([outerKey]) => outerKey === "dublincore/episode") + .map(([_outerKey, filters]) => ( + filters.map((filter, key) => ( +
{filter.filter}{filter.value.toString()}{filter.type}{filter.must.toString()}
{t("LIFECYCLE.POLICIES.DETAILS.GENERAL.ACTIONPARAMETERS.CAPTION")} + {formik.values.action === "START_WORKFLOW" && +
{formik.values.actionParameters.workflowId} + {/* @ts-expect-error: Potentially unknown */} + {Object.entries(formik.values.actionParameters.workflowParameters).map(([key, value]) => ( +
{key}{String(value)}
+
+
+ {/* Summary access configuration */} + + +
+
+
+ + {/* Button for navigation to next page and previous page */} + + + ); +}; + +export default NewLifeCyclePolicySummary; diff --git a/src/components/events/partials/wizards/NewLifeCyclePolicyWizard.tsx b/src/components/events/partials/wizards/NewLifeCyclePolicyWizard.tsx new file mode 100644 index 0000000000..050b75e7f0 --- /dev/null +++ b/src/components/events/partials/wizards/NewLifeCyclePolicyWizard.tsx @@ -0,0 +1,181 @@ +import { useEffect, useState } from "react"; +import { Formik } from "formik"; +import NewAccessPage from "../ModalTabsAndPages/NewAccessPage"; +import WizardStepper from "../../../shared/wizard/WizardStepper"; +import { useAppDispatch, useAppSelector } from "../../../../store"; +import { getUserInformation } from "../../../../selectors/userInfoSelectors"; +import { UserInfoState } from "../../../../slices/userInfoSlice"; +import { postNewLifeCyclePolicy } from "../../../../slices/lifeCycleSlice"; +import NewLifeCyclePolicyGeneralPage from "../ModalTabsAndPages/NewLifeCyclePolicyGeneralPage"; +import NewLifeCyclePolicySummary from "./NewLifeCyclePolicySummary"; +import { LifeCyclePolicySchema } from "../../../../utils/validate"; +import { initialFormValuesNewLifeCyclePolicy } from "../../../../configs/modalConfig"; +import { parseTargetFiltersForSubmit } from "../../../../utils/lifeCycleUtils"; +import { ParseKeys } from "i18next"; + +/** + * This component manages the pages of the new event wizard and the submission of values + */ +const NewLifeCyclePolicyWizard = ({ + close, +}: { + close: () => void +}) => { + const dispatch = useAppDispatch(); + + const user = useAppSelector(state => getUserInformation(state)); + + const initialValues = getInitialValues(user); + + const [page, setPage] = useState(0); + const [snapshot, setSnapshot] = useState(initialValues); + const [pageCompleted, setPageCompleted] = useState<{ [key: number]: boolean }>({}); + + // Caption of steps used by Stepper + const steps: { + translation: ParseKeys, + name: string, + hidden: boolean, + }[] = [ + { + translation: "LIFECYCLE.POLICIES.NEW.GENERAL.CAPTION", + name: "general", + hidden: false, + }, + { + translation: "EVENTS.EVENTS.NEW.ACCESS.CAPTION", + name: "access", + hidden: false, + }, + { + translation: "EVENTS.EVENTS.NEW.SUMMARY.CAPTION", + name: "summary", + hidden: false, + }, + ]; + + const nextPage = (values: typeof initialValues) => { + setSnapshot(values); + + // set page as completely filled out + const updatedPageCompleted = pageCompleted; + updatedPageCompleted[page] = true; + setPageCompleted(updatedPageCompleted); + + let newPage = page; + do { + newPage = newPage + 1; + } while (steps[newPage] && steps[newPage].hidden); + if (steps[newPage]) { + setPage(newPage); + } + }; + + const previousPage = (values: typeof initialValues) => { + setSnapshot(values); + + let newPage = page; + do { + newPage = newPage - 1; + } while (steps[newPage] && steps[newPage].hidden); + if (steps[newPage]) { + setPage(newPage); + } + }; + + const handleSubmit = (values: typeof initialValues) => { + const fixedValues = { + ...values, + targetFilters: parseTargetFiltersForSubmit(values.targetFiltersTransformed), + accessControlEntries: values.policies, + }; + + const response = dispatch(postNewLifeCyclePolicy(fixedValues)); + console.info(response); + close(); + }; + + return ( + <> + handleSubmit(values)} + > + {/* Render wizard pages depending on current value of page variable */} + {formik => { + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + formik.validateForm(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + return ( + <> + {/* Stepper that shows each step of wizard as header */} + +
+ {page === 0 && ( + + )} + {page === 1 && ( + + )} + {page === 2 && ( + + )} +
+ + ); + }} +
+ + ); +}; + +// Transform all initial values needed from information provided by backend +const getInitialValues = ( + user: UserInfoState, +) => { + const initialValues = initialFormValuesNewLifeCyclePolicy; + + initialValues["policies"] = [ + { + role: user.userRole, + read: true, + write: true, + actions: [], + user: user.user, + }, + ]; + + return initialValues; +}; + +export default NewLifeCyclePolicyWizard; diff --git a/src/components/events/partials/wizards/RenderWorkflowConfig.tsx b/src/components/events/partials/wizards/RenderWorkflowConfig.tsx index 1584f761f6..fd99cbe38f 100644 --- a/src/components/events/partials/wizards/RenderWorkflowConfig.tsx +++ b/src/components/events/partials/wizards/RenderWorkflowConfig.tsx @@ -1,28 +1,27 @@ import React from "react"; import { v4 as uuidv4 } from "uuid"; -import { FormikProps } from "formik"; import { Field } from "../../../shared/Field"; import { getWorkflowDefById, } from "../../../../selectors/workflowSelectors"; import { useAppSelector } from "../../../../store"; -import { FieldSetField } from "../../../../slices/workflowSlice"; +import { ConfigurationPanelField, FieldSetField } from "../../../../slices/workflowSlice"; /** * This component renders the configuration panel for the selected workflow in the processing step of the new event * wizard chosen via dropdown. */ -interface RequiredFormProps { - configuration?: { [key: string]: any } -} +export type Configuration = { [key: string]: any } -const RenderWorkflowConfig = ({ +const RenderWorkflowConfig = ({ workflowId, - formik, + configuration, + configurationName, displayDescription, }: { workflowId: string - formik: FormikProps + configuration: Configuration + configurationName: string displayDescription?: boolean }) => { @@ -32,13 +31,35 @@ const RenderWorkflowConfig = ({ const configPanel = !!workflowDef && workflowDef.configurationPanelJson ? workflowDef.configurationPanelJson : []; - const description = !!workflowDef && workflowDef.description + const description = !!workflowDef && workflowDef.description && !displayDescription ? workflowDef.description : ""; + return ( + + ); +}; + +export const WorkflowConfig = ({ + configuration, + configurationName, + configPanel, + description, +}: { + configuration: Configuration + configurationName: string + configPanel: string | ConfigurationPanelField[] + description: string +}) => { + return ( <> - {displayDescription && description.length > 0 && ( + {description.length > 0 && (
{description.trim()}
@@ -57,7 +78,7 @@ const RenderWorkflowConfig = ({ )}
    {configOption.fieldset?.map((field, keys) => - renderInputByType(field, keys, formik), + renderInputByType(field, keys, configuration, configurationName), )}
@@ -70,44 +91,45 @@ const RenderWorkflowConfig = ({ }; // render input depending on field type -const renderInputByType = ( +const renderInputByType = ( field: FieldSetField, key: React.Key | null | undefined, - formik: FormikProps, + configuration: Configuration, + configurationName: string, ) => { switch (field.type) { case "checkbox": - return ; + return ; case "radio": - return ; + return ; case "number": - return ; + return ; case "text": - return ; + return ; case "datetime-local": - return ; + return ; default: return ""; } }; -const RenderDatetimeLocal = ( - { field, formik } : { field: FieldSetField, formik: FormikProps }) => { - return ; +const RenderDatetimeLocal = ( + { field, configuration, configurationName } : { field: FieldSetField, configuration: Configuration, configurationName: string }) => { + return ; }; -const RenderCheckbox = ( - { field, formik } : { field: FieldSetField, formik: FormikProps }) => { - return ; +const RenderCheckbox = ( + { field, configuration, configurationName } : { field: FieldSetField, configuration: Configuration, configurationName: string }) => { + return ; }; -const RenderRadio = ( - { field, formik } : { field: FieldSetField, formik: FormikProps }) => { - return ; +const RenderRadio = ( + { field, configuration, configurationName } : { field: FieldSetField, configuration: Configuration, configurationName: string }) => { + return ; }; -const RenderNumber = ( - { field, formik } : { field: FieldSetField, formik: FormikProps }) => { +const RenderNumber = ( + { field, configuration, configurationName } : { field: FieldSetField, configuration: Configuration, configurationName: string }) => { // validate that value of number is between max and min const validate = (value: string) => { let error; @@ -117,26 +139,30 @@ const RenderNumber = ( return error; }; - return ; + return ; }; -const RenderText = ({ +const RenderText = ({ field, - formik, + configuration, + configurationName, }: { field: FieldSetField, - formik: FormikProps, + configuration: Configuration, + configurationName: string, }) => { - return ; + return ; }; -const RenderField = ({ +const RenderField = ({ field, - formik, + configuration, + configurationName, validate = undefined, }: { field: FieldSetField, - formik: FormikProps, + configuration: Configuration, + configurationName: string, validate?: (value: any) => string | undefined, }) => { // id used for Field and label @@ -150,7 +176,7 @@ const RenderField = ({ defaultValue={field.defaultValue} validate={validate} className="configField" - name={"configuration." + field.name} + name={configurationName + "." + field.name} disabled={disabled} type={field.type} min={field.min} @@ -165,9 +191,9 @@ const RenderField = ({ {/* if input has an additional fieldset or further configuration inputs then render again by input type*/} - {!!field.fieldset && !!formik.values.configuration && !!formik.values.configuration[field.name] && ( + {!!field.fieldset && !!configuration && !!configuration[field.name] && (
    - {field.fieldset?.map((f, keys) => renderInputByType(f, keys, formik))} + {field.fieldset?.map((f, keys) => renderInputByType(f, keys, configuration, configurationName))}
)} diff --git a/src/components/shared/ConfirmModal.tsx b/src/components/shared/ConfirmModal.tsx index 200bcf6e46..91f758002b 100644 --- a/src/components/shared/ConfirmModal.tsx +++ b/src/components/shared/ConfirmModal.tsx @@ -5,7 +5,7 @@ import { NotificationComponent } from "./Notifications"; import { ParseKeys } from "i18next"; import BaseButton from "./BaseButton"; -export type ResourceType = "EVENT" | "SERIES" | "LOCATION" | "USER" | "GROUP" | "ACL" | "THEME" | "TOBIRA_PATH"; +export type ResourceType = "EVENT" | "SERIES" | "LOCATION" | "USER" | "GROUP" | "ACL" | "THEME" | "TOBIRA_PATH" | "LIFECYCLE_POLICY"; const ConfirmModal = ({ close, diff --git a/src/components/shared/MainNav.tsx b/src/components/shared/MainNav.tsx index 6d126f647d..691988c5cc 100644 --- a/src/components/shared/MainNav.tsx +++ b/src/components/shared/MainNav.tsx @@ -92,6 +92,12 @@ const MainNav = ({ tooltipTitle: "NAV.EVENTS.TITLE", Icon: LuCalendarCheck, }, + { + path: "/events/lifeCyclePolicies", + accessRole: "ROLE_UI_LIFECYCLEPOLICIES_VIEW", + tooltipTitle: "NAV.EVENTS.TITLE", + Icon: LuCalendarCheck, + }, ], }, "recordings": { diff --git a/src/components/shared/NewResourceModal.tsx b/src/components/shared/NewResourceModal.tsx index 488931b7f1..610c26dfb2 100644 --- a/src/components/shared/NewResourceModal.tsx +++ b/src/components/shared/NewResourceModal.tsx @@ -6,6 +6,7 @@ import NewThemeWizard from "../configuration/partials/wizard/NewThemeWizard"; import NewAclWizard from "../users/partials/wizard/NewAclWizard"; import NewGroupWizard from "../users/partials/wizard/NewGroupWizard"; import NewUserWizard from "../users/partials/wizard/NewUserWizard"; +import NewLifeCyclePolicyWizard from "../events/partials/wizards/NewLifeCyclePolicyWizard"; import { Modal, ModalHandle } from "./modals/Modal"; /** @@ -17,16 +18,17 @@ export type NewResource = | "user" | "group" | "acl" - | "themes"; + | "themes" + | "lifecyclepolicy"; const NewResourceModal = ({ handleClose, resource, modalRef, }: { - handleClose: () => void; - resource: "events" | "series" | "user" | "group" | "acl" | "themes"; - modalRef: React.RefObject; + handleClose: () => void, + resource: NewResource + modalRef: React.RefObject }) => { const { t } = useTranslation(); @@ -48,6 +50,8 @@ const NewResourceModal = ({ return t("USERS.GROUPS.NEW.CAPTION"); case "user": return t("USERS.USERS.DETAILS.NEWCAPTION"); + case "lifecyclepolicy": + return t("LIFECYCLE.POLICIES.NEW.CAPTION"); } }; @@ -56,6 +60,7 @@ const NewResourceModal = ({ header={headerText()} classId="add-event-modal" // initialFocus={"#firstField"} + focusTrapActive={resource === "lifecyclepolicy" ? false : true} ref={modalRef} > {resource === "events" && ( @@ -82,6 +87,10 @@ const NewResourceModal = ({ // New User Wizard )} + {resource === "lifecyclepolicy" && ( + // New LifeCyclePolicy Wizard + + )} ); }; diff --git a/src/components/shared/modals/Modal.tsx b/src/components/shared/modals/Modal.tsx index 3f9e471d65..f117c4e560 100644 --- a/src/components/shared/modals/Modal.tsx +++ b/src/components/shared/modals/Modal.tsx @@ -23,6 +23,7 @@ export type ModalProps = { header: string; classId: string; className?: string; + focusTrapActive?: boolean; // Deactive focus trap, because it clashes with react-js-cron (can't click on dropdown elements) }; export type ModalHandle = { @@ -33,7 +34,7 @@ export type ModalHandle = { export const Modal = forwardRef>( ( - { open = false, closeCallback, header, classId, className, children }, + { open = false, closeCallback, header, classId, className, children, focusTrapActive = true }, ref, ) => { const { t } = useTranslation(); @@ -69,7 +70,9 @@ export const Modal = forwardRef>( return ReactDOM.createPortal( isOpen && - +
)} + {metadataField.type === "cron" && ( + + )}
{!focused && showCheck && ( { return { return { label: item.label ?? item.name, value: item.value, order: item.order }; })} text={text} form={form} - options={ - metadataField.collection - ? metadataField.collection.map(item => ({ - label: item.label ?? item.name, - value: item.value, - order: item.order, - })) - : [] - } isFirstField={isFirstField} focused={focused} setFocused={setFocused} @@ -327,6 +350,38 @@ const EditableSingleValueTime = ({ ); }; +const EditableCronValue = ({ + field, + form: { setFieldValue }, +} : { + field: FieldProps["field"] + form: FieldProps["form"] +}) => { + + return ( +
+ setFieldValue(field.name, value)} + /> +
+ ); + //
setEditMode(true)} className="show-edit"> + // {text || ""} + //
+ // + // {showCheck && ( + // + // )} + //
+ //
+}; + /** * Special case for series. Uses an async selector to fetch options. * diff --git a/src/components/shared/wizard/RenderMultiField.tsx b/src/components/shared/wizard/RenderMultiField.tsx index 7cd94f5125..164eed98b3 100644 --- a/src/components/shared/wizard/RenderMultiField.tsx +++ b/src/components/shared/wizard/RenderMultiField.tsx @@ -30,7 +30,7 @@ const RenderMultiField = ({ // Temporary storage for value user currently types in const [inputValue, setInputValue] = useState(""); - const fieldValue = [...field.value as string[]]; + const fieldValue = field.value ? [...field.value as string[]] : []; // Handle change of value user currently types in const handleChange = (e: React.ChangeEvent) => { diff --git a/src/configs/modalConfig.ts b/src/configs/modalConfig.ts index d31ae4a1bd..a07e8a49af 100644 --- a/src/configs/modalConfig.ts +++ b/src/configs/modalConfig.ts @@ -5,6 +5,7 @@ import { TobiraPage } from "../slices/seriesSlice"; import { initArray } from "../utils/utils"; import { EditedEvents, Event, UploadAssetsTrack } from "../slices/eventSlice"; import { Role } from "../slices/aclSlice"; +import { TargetFilter } from "../slices/lifeCycleSlice"; import { ParseKeys } from "i18next"; import { UserRole } from "../slices/userSlice"; @@ -217,3 +218,37 @@ export const initialFormValuesEditScheduledEvents: { editedEvents: [], changedEvents: [], }; + +export const initialFormValuesNewLifeCyclePolicy: { + title: string, + isActive: boolean, + isCreatedFromConfig: boolean, + targetType: string, + timing: string, + action: string, + actionDate: string, + cronTrigger: string, + actionParameters: { [key: string]: unknown } + policies: TransformedAcl[] + targetFiltersTransformed: { [key: string]: (TargetFilter & { filter: string })[] }, +} = { + title: "", + isActive: true, + isCreatedFromConfig: false, + targetType: "EVENT", + timing: "SPECIFIC_DATE", + action: "START_WORKFLOW", + actionDate: "", + cronTrigger: "", + actionParameters: { + // workflowId: "noop", + // workflowParameters: "{\"straightToPublishing\": true}", + workflowId: "", + workflowParameters: { }, + }, + + policies: [], + targetFiltersTransformed: { + "dublincore/episode": [], + }, +}; diff --git a/src/configs/tableConfigs/lifeCyclePoliciesTableConfig.ts b/src/configs/tableConfigs/lifeCyclePoliciesTableConfig.ts new file mode 100644 index 0000000000..71a47c8a8e --- /dev/null +++ b/src/configs/tableConfigs/lifeCyclePoliciesTableConfig.ts @@ -0,0 +1,30 @@ +import { TableConfig } from "./aclsTableConfig"; + +export const lifeCyclePolicyTableConfig: TableConfig = { + columns: [ + { + name: "title", + label: "LIFECYCLE.POLICIES.TABLE.TITLE", + sortable: true, + }, + { + template: "LifeCyclePolicyIsActiveCell", + name: "isActive", + label: "LIFECYCLE.POLICIES.TABLE.ISACTIVE", + }, + { + name: "timing", + label: "LIFECYCLE.POLICIES.TABLE.TIMING", + sortable: true, + }, + { + template: "LifeCyclePolicyActionCell", + name: "actions", + label: "LIFECYCLE.POLICIES.TABLE.ACTION", + }, + ], + caption: "TABLE.CAPTION", + resource: "lifeCyclePolicies", + category: "events", + multiSelect: false, +}; diff --git a/src/configs/tableConfigs/lifeCyclePoliciesTableMap.ts b/src/configs/tableConfigs/lifeCyclePoliciesTableMap.ts new file mode 100644 index 0000000000..7a460a6363 --- /dev/null +++ b/src/configs/tableConfigs/lifeCyclePoliciesTableMap.ts @@ -0,0 +1,11 @@ +import LifeCyclePolicyActionCell from "../../components/events/partials/LifeCyclePolicyActionCell"; +import LifeCyclePolicyIsActiveCell from "../../components/events/partials/LifeCyclePolicyIsActiveCell"; + +/** + * This map contains the mapping between the template strings above and the corresponding react component. + * This helps to render different templates of cells more dynamically + */ +export const lifeCyclePoliciesTemplateMap = { + LifeCyclePolicyIsActiveCell: LifeCyclePolicyIsActiveCell, + LifeCyclePolicyActionCell: LifeCyclePolicyActionCell, +}; diff --git a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json index fdb920c6f8..b60210ae6c 100644 --- a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json +++ b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json @@ -122,6 +122,7 @@ "USER": "The following user will be deleted", "THEME": "The following theme will be deleted", "LOCATION": "The following location will be deleted", + "LIFECYCLE_POLICY": "The following lifeCycle policy will be deleted", "TOBIRA_PATH": "The series will be removed from the following path in Tobira:" }, "NAME": "Name" @@ -203,6 +204,10 @@ "EVENTS_NOT_DELETED_NOT_AUTHORIZED": "The event(s) could not be deleted, because you don't have the permission to do so.", "SERIES_DELETED": "The series has been deleted", "SERIES_NOT_DELETED": "The series could not be deleted", + "LIFECYCLE_POLICY_ADDED": "The lifeCycle policy has been created", + "LIFECYCLE_POLICY_NOT_SAVED": "The lifeCycle policy could not be saved", + "LIFECYCLE_POLICY_DELETED": "The lifeCycle policy has been deleted", + "LIFECYCLE_POLICY_NOT_DELETED": "The lifeCycle policy could not be deleted", "LOCATION_DELETED": "The location has been deleted", "LOCATION_NOT_DELETED": "The location could not be deleted", "LOCATION_NOT_DELETED_NOT_AUTHORIZED": "The location could not be deleted, because you don't have the permission to do so.", @@ -703,7 +708,8 @@ "ACCESS": "Access policy", "COMMENTS": "Comments", "STATISTICS": "Statistics", - "TOBIRA": "Tobira" + "TOBIRA": "Tobira", + "LIFECYCLEPOLICIES": "LifeCycle Policies" }, "PUBLICATIONS": { "CAPTION": "Publications", @@ -1106,6 +1112,15 @@ "TECHNICAL_DETAILS": "Technical details", "OPERATION": "Operation the error occured in" } + }, + "LIFECYCLEPOLICIES": { + "EMPTY": "No policies found", + "TABLE_TITLE": "LifeCycle Policies", + "TITLE": "Policy title", + "DISCLAIMER": { + "TITLE": "Important note", + "MESSAGE": "The list contains lifecycle policies that would affect this event, were the policies to be executed right now. Since policies only decide which events they affect on the moment of their execution, this list cannot claim to be accurate. A policy appearing in the list below is no guarantee that the policy will affect this event." + } } } }, @@ -1281,6 +1296,77 @@ } } }, + "LIFECYCLE": { + "NAVIGATION": { + "POLICIES": "LifeCycle Policies" + }, + "POLICIES": { + "TABLE": { + "ACTION": "Actions", + "CAPTION": "LifeCycle Policies", + "ISACTIVE": "Active", + "TIMING": "Timing", + "TITLE": "Title", + "TOOLTIP": { + "DETAILS": "LifeCycle Policy Details", + "DELETE": "Delete LifeCycle Policy" + }, + "ADD_POLICY": "Add LifeCycle Policy" + }, + "NEW": { + "CAPTION": "Create LifeCycle Policy", + "GENERAL": { + "CAPTION": "General" + } + }, + "DETAILS": { + "HEADER": "LifeCycle Policy Details", + "GENERAL": { + "ACTION": "Action", + "ACTIONDATE": "Action Date", + "ACTIONPARAMETERS": { + "CAPTION": "Action Parameters", + "WORKFLOW_ID": "Workflow ID", + "WORKFLOW_PARAMETERS": "Workflow Parameters" + }, + "CAPTION": "LifeCycle Policy Details", + "CRONTRIGGER": "Cron Trigger", + "ID": "Identifier", + "ISACTIVE": "Active", + "ISCREATEDFROMCONFIG": "Created from Config", + "TARGETTYPE": "Target Type", + "TARGETFILTERS": { + "CAPTION": "Target Filters", + "FILTER": "Filter", + "VALUE": "Value", + "TYPE": "Type", + "MUST": "Must", + "NEW": "New Filter" + }, + "TIMING": "Timing", + "TITLE": "Title", + "NOTE": { + "TITLE": "A note on editing LifeCycle Policies", + "MESSAGE": "The same policy cannot affect the same target multiple times. Depending on your goals, creating a new policy may be required." + } + }, + "ACCESS": { + "LABEL": "Select a template", + "DESCRIPTION": "At least one role with Read and Write permissions is required.", + "NON_USER_ROLES": "Roles and Groups authorized for the policy", + "ROLE": "Role", + "NEW": "New policy", + "USER": "User", + "USERS": "Users who are authorized for the policy", + "NEW_USER": "New user" + }, + "TAB": { + "GENERAL": "General", + "ACCESSPOLICIES": "Access Policies" + } + } + } + }, "RECORDINGS": { "NAVIGATION": { "LOCATIONS": "Locations" diff --git a/src/selectors/eventDetailsSelectors.ts b/src/selectors/eventDetailsSelectors.ts index 5ca19b608d..993e9cc777 100644 --- a/src/selectors/eventDetailsSelectors.ts +++ b/src/selectors/eventDetailsSelectors.ts @@ -193,3 +193,6 @@ export const hasStatisticsError = (state: RootState) => state.eventDetails.hasStatisticsError; export const isFetchingStatistics = (state: RootState) => state.eventDetails.statusStatistics === "loading"; + +export const getLifeCyclePoliciesForEvent = (state: RootState) => + state.eventDetails.lifeCyclePolicies; diff --git a/src/selectors/lifeCycleDetailsSelectors.ts b/src/selectors/lifeCycleDetailsSelectors.ts new file mode 100644 index 0000000000..6db657af6d --- /dev/null +++ b/src/selectors/lifeCycleDetailsSelectors.ts @@ -0,0 +1,14 @@ +import { RootState } from "../store"; + +/** + * This file contains selectors regarding details of a certain lifeCyclePolicy/capture agent + */ +/* selectors for modal */ +export const showModal = (state: RootState) => state.lifeCyclePolicyDetails.modal.show; +export const getModalLifeCyclePolicy = (state: RootState) => state.lifeCyclePolicyDetails.modal.policy; + +export const getLifeCyclePolicyDetails = (state: RootState) => state.lifeCyclePolicyDetails; +export const getLifeCyclePolicyDetailsAcl = (state: RootState) => state.lifeCyclePolicyDetails.accessControlEntries; +export const getLifeCyclePolicyActions = (state: RootState) => state.lifeCyclePolicyDetails.actionsEnum; +export const getLifeCyclePolicyTargetTypes = (state: RootState) => state.lifeCyclePolicyDetails.targetTypesEnum; +export const getLifeCyclePolicyTimings = (state: RootState) => state.lifeCyclePolicyDetails.timingsEnum; diff --git a/src/selectors/lifeCycleSelectors.ts b/src/selectors/lifeCycleSelectors.ts new file mode 100644 index 0000000000..fadb641e3d --- /dev/null +++ b/src/selectors/lifeCycleSelectors.ts @@ -0,0 +1,7 @@ +import { RootState } from "../store"; + +/** + * This file contains selectors regarding acls + */ +export const getLifeCyclePolicies = (state: RootState) => state.lifeCycle.results; +export const getTotalLifeCyclePolicies = (state: RootState) => state.lifeCycle.total; diff --git a/src/slices/eventDetailsSlice.ts b/src/slices/eventDetailsSlice.ts index 879ce760d2..a435b176b2 100644 --- a/src/slices/eventDetailsSlice.ts +++ b/src/slices/eventDetailsSlice.ts @@ -38,6 +38,7 @@ import { Ace } from "./aclSlice"; import { setTobiraTabHierarchy, TobiraData } from "./seriesDetailsSlice"; import { handleTobiraError } from "./shared/tobiraErrors"; import camelcaseKeys from "camelcase-keys"; +import { LifeCyclePolicy } from "./lifeCycleSlice"; // Contains the navigation logic for the modal type EventDetailsModal = { @@ -218,6 +219,8 @@ type EventDetailsState = { errorStatisticsValue: SerializedError | null, statusTobiraData: "uninitialized" | "loading" | "succeeded" | "failed", errorTobiraData: SerializedError | null, + statusLifeCyclePolicies: "uninitialized" | "loading" | "succeeded" | "failed", + errorLifeCyclePolicies: SerializedError | null, eventId: string, modal: EventDetailsModal, metadata: MetadataCatalog, @@ -377,6 +380,7 @@ type EventDetailsState = { statistics: Statistics[], hasStatisticsError: boolean, tobiraData: TobiraData, + lifeCyclePolicies: LifeCyclePolicy[] } // Initial state of event details in redux store @@ -441,6 +445,8 @@ const initialState: EventDetailsState = { errorStatisticsValue: null, statusTobiraData: "uninitialized", errorTobiraData: null, + statusLifeCyclePolicies: "uninitialized", + errorLifeCyclePolicies: null, eventId: "", modal: { show: false, @@ -609,6 +615,7 @@ const initialState: EventDetailsState = { id: "", hostPages: [], }, + lifeCyclePolicies: [], }; @@ -1573,6 +1580,13 @@ export const fetchEventStatisticsValueUpdate = createAppAsyncThunk("eventDetails ); }); +export const fetchEventLifeCyclePolicies = createAppAsyncThunk("eventDetails/fetchLifeCyclePolicies", async (eventId: Event["id"]) => { + const data = await axios.get( + `/api/lifecyclemanagement/policiesForEvent/${eventId}`, + ); + return data.data; +}); + export const updateMetadata = createAppAsyncThunk("eventDetails/updateMetadata", async (params: { id: Event["id"], values: { [key: string]: MetadataCatalog["fields"][0]["value"] } @@ -2546,6 +2560,21 @@ const eventDetailsSlice = createSlice({ .addCase(fetchEventDetailsTobira.rejected, (state, action) => { state.statusTobiraData = "failed"; state.errorTobiraData = action.error; + }) + // fetch lifecycle + .addCase(fetchEventLifeCyclePolicies.pending, state => { + state.statusLifeCyclePolicies = "loading"; + }) + .addCase(fetchEventLifeCyclePolicies.fulfilled, (state, action: PayloadAction< + EventDetailsState["lifeCyclePolicies"] + >) => { + state.statusLifeCyclePolicies = "succeeded"; + state.lifeCyclePolicies = action.payload; + state.errorLifeCyclePolicies = null; + }) + .addCase(fetchEventLifeCyclePolicies.rejected, (state, action) => { + state.statusLifeCyclePolicies = "failed"; + state.errorLifeCyclePolicies = action.error; }); }, }); diff --git a/src/slices/lifeCycleDetailsSlice.ts b/src/slices/lifeCycleDetailsSlice.ts new file mode 100644 index 0000000000..47fc9048a2 --- /dev/null +++ b/src/slices/lifeCycleDetailsSlice.ts @@ -0,0 +1,278 @@ +import { PayloadAction, SerializedError, createSlice } from "@reduxjs/toolkit"; +import axios from "axios"; +import { createAppAsyncThunk } from "../createAsyncThunkWithTypes"; +import { LifeCyclePolicy } from "./lifeCycleSlice"; +import { TransformedAcl } from "./aclDetailsSlice"; +import { createPolicy } from "../utils/resourceUtils"; +import { Ace } from "./aclSlice"; +import { addNotification } from "./notificationSlice"; +import { AppDispatch } from "../store"; + + +/** + * This file contains redux reducer for actions affecting the state of a lifeCyclePolicy/capture agent + */ +type LifeCyclePolicyDetailsModal = { + show: boolean, + policy: { id: string, title: string } | null, +} + +interface LifeCyclePolicyDetailsState extends LifeCyclePolicy { + statusLifeCyclePolicyDetails: "uninitialized" | "loading" | "succeeded" | "failed", + errorLifeCyclePolicyDetails: SerializedError | null, + + modal: LifeCyclePolicyDetailsModal, + actionsEnum: string[], + targetTypesEnum: string[], + timingsEnum: string[], +} + +// Initial state of lifeCyclePolicy details in redux store +const initialState: LifeCyclePolicyDetailsState = { + statusLifeCyclePolicyDetails: "uninitialized", + errorLifeCyclePolicyDetails: null, + modal: { + show: false, + policy: null, + }, + actionParameters: {}, + timing: "SPECIFIC_DATE", + action: "START_WORKFLOW", + targetType: "EVENT", + id: "", + title: "", + isActive: false, + isCreatedFromConfig: false, + actionDate: "", + cronTrigger: "", + targetFilters: {}, + accessControlEntries: [], + + actionsEnum: [], + targetTypesEnum: [], + timingsEnum: [], +}; + +// fetch details of certain lifeCyclePolicy from server +export const fetchLifeCyclePolicyDetails = createAppAsyncThunk("lifeCyclePolicyDetails/fetchLifeCyclePolicyDetails", async (id: string) => { + type ReturnType = LifeCyclePolicy & { actionParameters: string, targetFilters: string, accessControlEntries: { + id: number, + allow: boolean, + role: string, + action: string, + }[] } + const res = await axios.get(`/api/lifecyclemanagement/policies/${id}`); + const data = res.data; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + data.actionParameters = JSON.parse(data.actionParameters); + if (data.action === "START_WORKFLOW") { + data.actionParameters.workflowParameters = JSON.parse(data.actionParameters.workflowParameters as string); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + data.targetFilters = JSON.parse(data.targetFilters); + + const accessPolicies : { + id: number, + allow: boolean, + role: string, + action: string, + }[] = data.accessControlEntries; + let acls: TransformedAcl[] = []; + + const json = accessPolicies; + const newPolicies: { [key: string]: TransformedAcl } = {}; + const policyRoles: string[] = []; + for (let i = 0; i < json.length; i++) { + const policy: Ace = json[i]; + if (!newPolicies[policy.role]) { + newPolicies[policy.role] = createPolicy(policy.role); + policyRoles.push(policy.role); + } + if (policy.action === "read" || policy.action === "write") { + newPolicies[policy.role][policy.action] = policy.allow; + } else if (policy.allow === true) { // || policy.allow === "true") { + newPolicies[policy.role].actions.push(policy.action); + } + } + acls = policyRoles.map(role => newPolicies[role]); + + const result = { + ...data, + accessControlEntries: acls, + }; + + return result; +}); + +export const fetchLifeCyclePolicyActions = createAppAsyncThunk("lifeCyclePolicyDetails/fetchLifeCyclePolicyActions", async () => { + const res = await axios.get("/api/lifecyclemanagement/policies/actions"); + const data = res.data; + + return data; +}); + +export const fetchLifeCyclePolicyTargetTypes = createAppAsyncThunk("lifeCyclePolicyDetails/fetchLifeCyclePolicyTargetTypes", async () => { + const res = await axios.get("/api/lifecyclemanagement/policies/targettypes"); + const data = res.data; + + return data; +}); + +export const fetchLifeCyclePolicyTimings = createAppAsyncThunk("lifeCyclePolicyDetails/fetchLifeCyclePolicyTimings", async () => { + const res = await axios.get("/api/lifecyclemanagement/policies/timings"); + const data = res.data; + + return data; +}); + +// Dummy function for compatability +// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars +export const fetchLifeCyclePolicyDetailsAcls = createAppAsyncThunk("lifeCyclePolicyDetails/fetchLifeCyclePolicyDetailsAcls", async (id: string, { getState }) => { + const state = getState(); + return state.lifeCyclePolicyDetails.accessControlEntries; +}); + +// Dummy function for compatability +export const updateLifeCyclePolicyAccess = createAppAsyncThunk("lifeCyclePolicyDetails/fetchLifeCyclePolicyDetailsAcls", async (params: { + id: string, + policies: { acl: { ace: Ace[] } } +}, { dispatch }) => { + const { id, policies } = params; + + const data = new URLSearchParams(); + data.append("accessControlEntries", JSON.stringify(policies.acl.ace)); + + await axios.put(`/api/lifecyclemanagement/policies/${id}`, data) + .then(response => { + console.info(response); + dispatch(addNotification({ type: "success", key: "LIFECYCLE_POLICY_ADDED" })); + return true; + }) + .catch(response => { + console.error(response); + dispatch(addNotification({ type: "error", key: "LIFECYCLEPOLICY_NOT_SAVED" })); + return false; + }); +}); + +export const updateLifeCyclePolicy = createAppAsyncThunk("lifeCyclePolicyDetails/updateLifeCyclePolicy", async (policy: LifeCyclePolicy, { dispatch }) => { + const data = new URLSearchParams(); + + Object.entries(policy).forEach(([key, value]) => { + let stringified = value; + if (stringified instanceof Date) { + stringified = stringified.toJSON(); + } else if (stringified === Object(stringified)) { + stringified = JSON.stringify(stringified); + } + // @ts-expect-error: ??? + data.append(key, stringified); + }); + + await axios.put(`/api/lifecyclemanagement/policies/${policy.id}`, data) + .then(response => { + console.info(response); + dispatch(addNotification({ type: "success", key: "LIFECYCLE_POLICY_ADDED" })); + }) + .catch(response => { + console.error(response); + dispatch(addNotification({ type: "error", key: "LIFECYCLEPOLICY_NOT_SAVED" })); + }); +}); + +/** + * Open details modal externally + * + * @param page modal page + * @param policy policy to show + */ +export const openModal = ( + policy: LifeCyclePolicyDetailsModal["policy"], +) => (dispatch: AppDispatch) => { + dispatch(setModalLifeCyclePolicy(policy)); + dispatch(setShowModal(true)); +}; + +const lifeCyclePolicyDetailsSlice = createSlice({ + name: "lifeCyclePolicyDetails", + initialState, + reducers: { + setShowModal(state, action: PayloadAction< + LifeCyclePolicyDetailsState["modal"]["show"] + >) { + state.modal.show = action.payload; + }, + setModalLifeCyclePolicy(state, action: PayloadAction< + LifeCyclePolicyDetailsState["modal"]["policy"] + >) { + state.modal.policy = action.payload; + }, + }, + // These are used for thunks + extraReducers: builder => { + builder + .addCase(fetchLifeCyclePolicyDetails.pending, state => { + state.statusLifeCyclePolicyDetails = "loading"; + }) + .addCase(fetchLifeCyclePolicyDetails.fulfilled, (state, action: PayloadAction<{ + actionParameters: LifeCyclePolicyDetailsState["actionParameters"], + timing: LifeCyclePolicyDetailsState["timing"], + action: LifeCyclePolicyDetailsState["action"], + targetType: LifeCyclePolicyDetailsState["targetType"], + id: LifeCyclePolicyDetailsState["id"], + title: LifeCyclePolicyDetailsState["title"], + isActive: LifeCyclePolicyDetailsState["isActive"], + isCreatedFromConfig: LifeCyclePolicyDetailsState["isCreatedFromConfig"], + actionDate: LifeCyclePolicyDetailsState["actionDate"], + cronTrigger: LifeCyclePolicyDetailsState["cronTrigger"], + targetFilters: LifeCyclePolicyDetailsState["targetFilters"], + accessControlEntries: LifeCyclePolicyDetailsState["accessControlEntries"], + }>) => { + state.statusLifeCyclePolicyDetails = "succeeded"; + const lifeCyclePolicyDetails = action.payload; + state.actionParameters = lifeCyclePolicyDetails.actionParameters; + state.timing = lifeCyclePolicyDetails.timing; + state.action = lifeCyclePolicyDetails.action; + state.targetType = lifeCyclePolicyDetails.targetType; + state.id = lifeCyclePolicyDetails.id; + state.title = lifeCyclePolicyDetails.title; + state.isActive = lifeCyclePolicyDetails.isActive; + state.isCreatedFromConfig = lifeCyclePolicyDetails.isCreatedFromConfig; + state.actionDate = lifeCyclePolicyDetails.actionDate; + state.cronTrigger = lifeCyclePolicyDetails.cronTrigger; + state.targetFilters = lifeCyclePolicyDetails.targetFilters; + state.accessControlEntries = lifeCyclePolicyDetails.accessControlEntries; + }) + .addCase(fetchLifeCyclePolicyDetails.rejected, (state, action) => { + state.statusLifeCyclePolicyDetails = "failed"; + state.errorLifeCyclePolicyDetails = action.error; + }) + .addCase(fetchLifeCyclePolicyActions.fulfilled, (state, action: PayloadAction< + LifeCyclePolicyDetailsState["actionsEnum"] + >) => { + const actionsEnum = action.payload; + state.actionsEnum = actionsEnum; + }) + .addCase(fetchLifeCyclePolicyTargetTypes.fulfilled, (state, action: PayloadAction< + LifeCyclePolicyDetailsState["actionsEnum"] + >) => { + const targetTypesEnum = action.payload; + state.targetTypesEnum = targetTypesEnum; + }) + .addCase(fetchLifeCyclePolicyTimings.fulfilled, (state, action: PayloadAction< + LifeCyclePolicyDetailsState["actionsEnum"] + >) => { + const timingsEnum = action.payload; + state.timingsEnum = timingsEnum; + }); + }, +}); + +export const { + setShowModal, + setModalLifeCyclePolicy, +} = lifeCyclePolicyDetailsSlice.actions; + +// Export the slice reducer as the default export +export default lifeCyclePolicyDetailsSlice.reducer; diff --git a/src/slices/lifeCycleSlice.ts b/src/slices/lifeCycleSlice.ts new file mode 100644 index 0000000000..ed99509dc1 --- /dev/null +++ b/src/slices/lifeCycleSlice.ts @@ -0,0 +1,186 @@ +import { PayloadAction, SerializedError, createSlice } from "@reduxjs/toolkit"; +import { TableConfig } from "../configs/tableConfigs/aclsTableConfig"; +import { lifeCyclePolicyTableConfig } from "../configs/tableConfigs/lifeCyclePoliciesTableConfig"; +import axios from "axios"; +import { getURLParams, prepareAccessPolicyRulesForPost } from "../utils/resourceUtils"; +import { createAppAsyncThunk } from "../createAsyncThunkWithTypes"; +import { TransformedAcl } from "./aclDetailsSlice"; +import { addNotification } from "./notificationSlice"; + +// type LifeCyclePolicyTiming = "SPECIFIC_DATE" | "REPEATING" | "ALWAYS"; +// type LifeCyclePolicyAction = "START_WORKFLOW" +// type LifeCyclePolicyTargetType = "EVENT" +export type CommonMetadataCatalogFlavor = "dublincore/episode"; // TODO: Get this from the backend +export type TargetFilter = { + value: string | string[], + type: TargetFiltersType, + must: boolean +} +export const ALL_TARGET_FILTER_TYPES = ["SEARCH", "WILDCARD", "GREATER_THAN", "LESS_THAN"] as const; +type TargetFilterTypesTuple = typeof ALL_TARGET_FILTER_TYPES; +type TargetFiltersType = TargetFilterTypesTuple[number]; + +export type LifeCyclePolicy = { + actionParameters: { [key: string]: unknown }, // JSON. Variable, depends on action1 + timing: string, + action: string, + targetType: string, + id: string, + title: string, + isActive: boolean, + isCreatedFromConfig: boolean, + actionDate: string, // Date + cronTrigger: string, + targetFilters: { [key: string]: { [key: string]: TargetFilter } }, + accessControlEntries: TransformedAcl[] +} + +type LifeCycleState = { + status: "uninitialized" | "loading" | "succeeded" | "failed", + error: SerializedError | null, + results: LifeCyclePolicy[], + columns: TableConfig["columns"], + total: number, + offset: number, + limit: number, +}; + +// Fill columns initially with columns defined in aclsTableConfig +const initialColumns = lifeCyclePolicyTableConfig.columns.map(column => ({ + ...column, + deactivated: false, +})); + +// Initial state of acls in redux store +const initialState: LifeCycleState = { + status: "uninitialized", + error: null, + results: [], + columns: initialColumns, + total: 0, + offset: 0, + limit: 0, +}; + +type FetchLifeCyclePolicies = { + total: number, + offset: number, + limit: number, + results: LifeCyclePolicy[] +} + +export const fetchLifeCyclePolicies = createAppAsyncThunk("lifeCycle/fetchLifeCyclePolicies", async (_, { getState }) => { + const state = getState(); + const params = getURLParams(state, "lifeCyclePolicies"); + const res = await axios.get("/api/lifecyclemanagement/policies", { params: params }); + return res.data; +}); + +export const postNewLifeCyclePolicy = createAppAsyncThunk("lifeCycle/postNewLifeCyclePolicy", async ( + policy: { + actionParameters: { [key: string]: unknown }, + timing: string, + action: string, + targetType: string, + title: string, + isActive: boolean, + actionDate: string, + cronTrigger: string, + targetFilters: { [key: string]: { [key: string]: TargetFilter } }, + accessControlEntries: TransformedAcl[] + }, + { dispatch }, +) => { + const data = new URLSearchParams(); + + // Format filter collections + // for (const filterName in policy.targetFilters) { + // // policy.targetFilters[filterName] + // if (hasOwnProperty(TARGET_FILTER_KEYS_EVENTS, filterName) + // && TARGET_FILTER_KEYS_EVENTS[filterName].collection) { + // policy.targetFilters[filterName].value = policy.targetFilters[filterName].value.toString() + // } + // } + + // Stringify + Object.entries(policy).forEach(([key, value]) => { + let stringified = value; + if (stringified instanceof Date) { + stringified = stringified.toJSON(); + } else if (stringified === Object(stringified)) { + stringified = JSON.stringify(stringified); + } + // @ts-expect-error: ??? + data.append(key, stringified); + }); + + data.delete("accessControlEntries"); + data.append("accessControlEntries", JSON.stringify(prepareAccessPolicyRulesForPost(policy.accessControlEntries).acl.ace)); + + await axios.post("/api/lifecyclemanagement/policies", data) + .then(res => { + console.info(res); + dispatch(addNotification({ type: "success", key: "LIFECYCLE_POLICY_ADDED" })); + }) + .catch(res => { + console.error(res); + dispatch(addNotification({ type: "error", key: "LIFECYCLE_POLICY_NOT_SAVED" })); + }); +}); + +export const deleteLifeCyclePolicy = createAppAsyncThunk("lifeCycle/fetchLifeCyclePolicies", async (id: string, { dispatch }) => { + await axios + .delete(`/api/lifecyclemanagement/policies/${id}`) + .then(res => { + console.info(res); + dispatch(addNotification({ type: "success", key: "LIFECYCLE_POLICY_DELETED" })); + }) + .catch(res => { + console.error(res); + dispatch(addNotification({ type: "error", key: "LIFECYCLE_POLICY_NOT_DELETED" })); + }); +}); + +const lifeCycleSlice = createSlice({ + name: "lifeCycle", + initialState, + reducers: { + setLifeCycleColumns(state, action: PayloadAction< + LifeCycleState["columns"] + >) { + state.columns = action.payload; + }, + }, + // These are used for thunks + extraReducers: builder => { + builder + .addCase(fetchLifeCyclePolicies.pending, state => { + state.status = "loading"; + }) + // Pass the generated action creators to `.addCase()` + .addCase(fetchLifeCyclePolicies.fulfilled, (state, action: PayloadAction<{ + total: LifeCycleState["total"], + limit: LifeCycleState["limit"], + offset: LifeCycleState["offset"], + results: LifeCycleState["results"], + }>) => { + // Same "mutating" update syntax thanks to Immer + state.status = "succeeded"; + const policies = action.payload; + state.total = policies.total; + state.limit = policies.limit; + state.offset = policies.offset; + state.results = policies.results; + }) + .addCase(fetchLifeCyclePolicies.rejected, (state, action) => { + state.status = "failed"; + state.results = []; + state.error = action.error; + }); + }, +}); + +export const { setLifeCycleColumns } = lifeCycleSlice.actions; + +// Export the slice reducer as the default export +export default lifeCycleSlice.reducer; diff --git a/src/slices/tableSlice.ts b/src/slices/tableSlice.ts index 3dbe3c2c5c..bcea57f1c4 100644 --- a/src/slices/tableSlice.ts +++ b/src/slices/tableSlice.ts @@ -10,6 +10,7 @@ import { AclResult } from "./aclSlice"; import { ThemeDetailsType } from "./themeSlice"; import { Series } from "./seriesSlice"; import { Event } from "./eventSlice"; +import { LifeCyclePolicy } from "./lifeCycleSlice"; import { eventsTableConfig } from "../configs/tableConfigs/eventsTableConfig"; import { seriesTableConfig } from "../configs/tableConfigs/seriesTableConfig"; import { recordingsTableConfig } from "../configs/tableConfigs/recordingsTableConfig"; @@ -19,6 +20,7 @@ import { servicesTableConfig } from "../configs/tableConfigs/servicesTableConfig import { usersTableConfig } from "../configs/tableConfigs/usersTableConfig"; import { groupsTableConfig } from "../configs/tableConfigs/groupsTableConfig"; import { themesTableConfig } from "../configs/tableConfigs/themesTableConfig"; +import { lifeCyclePolicyTableConfig } from "../configs/tableConfigs/lifeCyclePoliciesTableConfig"; import { RootState } from "../store"; /* @@ -69,11 +71,11 @@ export function isRowSelectable(row: Row) { return false; } -export function isEvent(row: Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Event { +export function isEvent(row: Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy): row is Event { return (row as Event).event_status !== undefined; } -export function isSeries(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Series { +export function isSeries(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy): row is Series { return (row as Series).organizers !== undefined; } @@ -81,13 +83,13 @@ export function isSeries(row: Row | Event | Series | Recording | Server | Job | export type Row = { id: string, // For use with entityAdapter. Directly taken from event/series etc. if available selected: boolean // If the row was marked in the ui by the user -} & (Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType) +} & (Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy) export type SubmitRow = { selected: boolean -} & (Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType) +} & (Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType | LifeCyclePolicy) -export type Resource = "events" | "series" | "recordings" | "jobs" | "servers" | "services" | "users" | "groups" | "acls" | "themes" +export type Resource = "events" | "series" | "recordings" | "jobs" | "servers" | "services" | "users" | "groups" | "acls" | "themes" | "lifeCyclePolicies" export type ReverseOptions = "ASC" | "DESC" @@ -143,6 +145,7 @@ const initialState: TableState = { groups: groupsTableConfig.multiSelect, acls: aclsTableConfig.multiSelect, themes: themesTableConfig.multiSelect, + lifeCyclePolicies: lifeCyclePolicyTableConfig.multiSelect, }, resource: "events", pages: [], @@ -158,6 +161,7 @@ const initialState: TableState = { groups: "name", acls: "name", themes: "name", + lifeCyclePolicies: "title", }, predicate: "", reverse: { @@ -171,6 +175,7 @@ const initialState: TableState = { groups: "ASC", acls: "ASC", themes: "ASC", + lifeCyclePolicies: "ASC", }, rows: rowsAdapter.getInitialState(), maxLabel: "", diff --git a/src/slices/workflowSlice.ts b/src/slices/workflowSlice.ts index 784ee13dea..c730954201 100644 --- a/src/slices/workflowSlice.ts +++ b/src/slices/workflowSlice.ts @@ -18,7 +18,7 @@ export type FieldSetField = { [key: string]: unknown } -type ConfigurationPanelField = { +export type ConfigurationPanelField = { // We could potentially specify 'fieldset' more, but I cannot find a definition // for which key value pairs are allowed fieldset?: FieldSetField[] // Values can be anything diff --git a/src/store.ts b/src/store.ts index 76a3f65d9f..3c116916da 100644 --- a/src/store.ts +++ b/src/store.ts @@ -6,6 +6,7 @@ import tableFilterProfiles from "./slices/tableFilterProfilesSlice"; import events from "./slices/eventSlice"; import table from "./slices/tableSlice"; import series from "./slices/seriesSlice"; +import lifeCycle from "./slices/lifeCycleSlice"; import recordings from "./slices/recordingSlice"; import jobs from "./slices/jobSlice"; import servers from "./slices/serverSlice"; @@ -19,6 +20,7 @@ import notifications from "./slices/notificationSlice"; import workflows from "./slices/workflowSlice"; import eventDetails from "./slices/eventDetailsSlice"; import seriesDetails from "./slices/seriesDetailsSlice"; +import lifeCyclePolicyDetails from "./slices/lifeCycleDetailsSlice"; import userDetails from "./slices/userDetailsSlice"; import recordingDetails from "./slices/recordingDetailsSlice"; import groupDetails from "./slices/groupDetailsSlice"; @@ -38,6 +40,7 @@ import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2"; const tableFilterProfilesPersistConfig = { key: "tableFilterProfiles", storage, whitelist: ["profiles"] }; const eventsPersistConfig = { key: "events", storage, whitelist: ["columns"] }; const seriesPersistConfig = { key: "series", storage, whitelist: ["columns"] }; +const lifeCyclePersistConfig = { key: "lifeCycle", storage, whitelist: ["columns"] }; const tablePersistConfig = { key: "table", storage, whitelist: ["pagination", "sortBy", "reverse"] }; const recordingsPersistConfig = { key: "recordings", storage, whitelist: ["columns"] }; const jobsPersistConfig = { key: "jobs", storage, whitelist: ["columns"] }; @@ -54,6 +57,7 @@ const reducers = combineReducers({ tableFilterProfiles: persistReducer(tableFilterProfilesPersistConfig, tableFilterProfiles), events: persistReducer(eventsPersistConfig, events), series: persistReducer(seriesPersistConfig, series), + lifeCycle: persistReducer(lifeCyclePersistConfig, lifeCycle), table: persistReducer(tablePersistConfig, table), recordings: persistReducer(recordingsPersistConfig, recordings), jobs: persistReducer(jobsPersistConfig, jobs), @@ -69,6 +73,7 @@ const reducers = combineReducers({ eventDetails, themeDetails, seriesDetails, + lifeCyclePolicyDetails, recordingDetails, userDetails, groupDetails, diff --git a/src/styles/main.scss b/src/styles/main.scss index 75fce48791..902b804df0 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -161,3 +161,8 @@ input.disabled, select.disabled { visibility: visible !important; text-align: left; } + +// React-JS-Cron +.my-project-cron-select-dropdown { + z-index: 10000; +} diff --git a/src/thunks/tableThunks.ts b/src/thunks/tableThunks.ts index f3d78dfba5..38a142fe89 100644 --- a/src/thunks/tableThunks.ts +++ b/src/thunks/tableThunks.ts @@ -35,6 +35,7 @@ import { fetchRecordings, setRecordingsColumns } from "../slices/recordingSlice" import { setGroupColumns } from "../slices/groupSlice"; import { fetchAcls, setAclColumns } from "../slices/aclSlice"; import { AppDispatch, AppThunk, RootState } from "../store"; +import { fetchLifeCyclePolicies, setLifeCycleColumns } from "../slices/lifeCycleSlice"; /** * This file contains methods/thunks used to manage the table in the main view and its state changes @@ -121,6 +122,30 @@ export const loadSeriesIntoTable = (): AppThunk => (dispatch, getState) => { dispatch(loadResourceIntoTable(tableData)); }; +export const loadLifeCyclePoliciesIntoTable = (): AppThunk => (dispatch, getState) => { + const { lifeCycle, table } = getState(); + const pagination = table.pagination; + const resource = lifeCycle.results; + const total = lifeCycle.total; + + const pages = calculatePages(total / pagination.limit, pagination.offset); + + const tableData = { + resource: "lifeCyclePolicies" as const, + rows: resource.map(obj => { + return { ...obj, selected: false }; + }), + columns: lifeCycle.columns, + multiSelect: table.multiSelect["lifeCyclePolicies"], + pages: pages, + sortBy: table.sortBy["lifeCyclePolicies"], + reverse: table.reverse["lifeCyclePolicies"], + totalItems: total, + }; + + dispatch(loadResourceIntoTable(tableData)); +}; + export const loadRecordingsIntoTable = (): AppThunk => (dispatch, getState) => { const { recordings, table } = getState(); const pagination = table.pagination; @@ -338,6 +363,11 @@ export const goToPage = (pageNumber: number) => async (dispatch: AppDispatch, ge dispatch(loadSeriesIntoTable()); break; } + case "lifeCyclePolicies": { + await dispatch(fetchLifeCyclePolicies()); + dispatch(loadLifeCyclePoliciesIntoTable()); + break; + } case "recordings": { await dispatch(fetchRecordings()); dispatch(loadRecordingsIntoTable()); @@ -406,6 +436,11 @@ export const updatePages = () => async (dispatch: AppDispatch, getState: () => R dispatch(loadSeriesIntoTable()); break; } + case "lifeCyclePolicies": { + await dispatch(fetchLifeCyclePolicies()); + dispatch(loadLifeCyclePoliciesIntoTable()); + break; + } case "recordings": { await dispatch(fetchRecordings()); dispatch(loadRecordingsIntoTable()); @@ -515,6 +550,11 @@ export const changeColumnSelection = (updatedColumns: TableConfig["columns"]) => dispatch(loadSeriesIntoTable()); break; } + case "lifeCyclePolicies": { + dispatch(setLifeCycleColumns(updatedColumns)); + dispatch(loadLifeCyclePoliciesIntoTable()); + break; + } case "recordings": { dispatch(setRecordingsColumns(updatedColumns)); dispatch(loadRecordingsIntoTable()); diff --git a/src/utils/dropDownUtils.ts b/src/utils/dropDownUtils.ts index bad70de163..5338f779c4 100644 --- a/src/utils/dropDownUtils.ts +++ b/src/utils/dropDownUtils.ts @@ -19,3 +19,7 @@ export const formatWorkflowsForDropdown = (workflows: Workflow[]) => { export const formatAclTemplatesForDropdown = (templates: { id: string, value: string }[]) => { return templates.map(template => ({ label: template.value, value: template.id })); }; + +export const formatPolicyActionsForDropdown = (policieActions: string[]) => { + return policieActions.map(action => ({ label: action, value: action })); +}; diff --git a/src/utils/lifeCycleUtils.ts b/src/utils/lifeCycleUtils.ts new file mode 100644 index 0000000000..7de6597fcb --- /dev/null +++ b/src/utils/lifeCycleUtils.ts @@ -0,0 +1,38 @@ +import { TargetFilter } from "../slices/lifeCycleSlice"; + +export function parseTargetFiltersForSubmit( + transformed: { [key: string]: (TargetFilter & { filter: string })[] }, +): { [key: string]: { [key: string]: TargetFilter } } { + const result: { [key: string]: { [key: string]: TargetFilter } } = {}; + + for (const outerKey in transformed) { + const list = transformed[outerKey]; + const innerMap: { [key: string]: TargetFilter } = {}; + + for (const item of list) { + const { filter, ...rest } = item; + innerMap[filter] = rest; + } + + result[outerKey] = innerMap; + } + + return result; +} + +export function parseTargetFiltersForEditing( + targetFilters: { [key: string]: { [key: string]: TargetFilter } }, +): { [key: string]: (TargetFilter & { filter: string })[] } { + const result: { [key: string]: (TargetFilter & { filter: string })[] } = {}; + + for (const outerKey in targetFilters) { + const innerMap = targetFilters[outerKey]; + + result[outerKey] = Object.entries(innerMap).map(([innerKey, filterObj]) => ({ + ...filterObj, + filter: innerKey, + })); + } + + return result; +}; diff --git a/src/utils/resourceUtils.ts b/src/utils/resourceUtils.ts index f0143baa79..7213d4491e 100644 --- a/src/utils/resourceUtils.ts +++ b/src/utils/resourceUtils.ts @@ -304,7 +304,7 @@ export const getMetadataCollectionFieldName = ( return name.label ? t(name.label) : ""; } - return collectionField ? t(collectionField.name as ParseKeys) : ""; + return collectionField ? t(collectionField.name as ParseKeys, { nsSeparator: false }) : ""; } return ""; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 178e6d5847..0a30e090d3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -148,6 +148,16 @@ export const translateOverrideFallback = (asset: UploadOption, t: TFunction, suf return result; }; +/** + * (Hopefully) Typesafe way of checking if an object has a specific property + */ +export function hasOwnProperty( + obj: O, + key: K, +): obj is O & Record { + return Object.prototype.hasOwnProperty.call(obj, key); +} + /** * Have the browser show a warning dialog for unsaved changes */ diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 15ce8cbdf6..f83b1cec45 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -249,3 +249,96 @@ export const EditGroupSchema = Yup.object().shape({ export const AdopterRegistrationSchema = Yup.object().shape({ email: Yup.string().email(), }); + +/** + * Validation Schema used in lifecycle policy modal + */ +const filterItemSchema = Yup.object({ + filter: Yup.string().required("Required"), + value: Yup.mixed().required("Required"), + type: Yup.string().required("Required"), + must: Yup.boolean(), +}); + +const targetFiltersSchema = Yup.object() + .test( + "has-at-least-one-array", + "At least one filter group is required", + function (obj) { + if (!obj || typeof obj !== "object") { return false; } + return Object.keys(obj).length > 0; + }, + ) + .test( + "has-at-least-one-item", + "At least one filter must be defined", + function (obj) { + if (!obj || typeof obj !== "object") { return false; } + + let total = 0; + + for (const key of Object.keys(obj)) { + const arr = (obj as Record)[key]; + if (Array.isArray(arr)) { + total += arr.length; + } + } + + return total > 0; + }, + ) + .test( + "validate-items", + "Invalid filter structure", + function (obj) { + if (!obj || typeof obj !== "object") { return false; } + + for (const key of Object.keys(obj)) { + const arr = (obj as Record)[key]; + + if (!Array.isArray(arr)) { + return this.createError({ + message: `Filter "${key}" must be an array`, + }); + } + + for (const item of arr) { + try { + filterItemSchema.validateSync(item); + } catch (err) { + return this.createError({ message: (err as Error).message }); + } + } + } + + return true; + }, + ); + +export const LifeCyclePolicySchema = [ + Yup.object().shape({ + title: Yup.string().required("Required"), + isActive: Yup.bool().required("Required"), + targetType: Yup.string().required("Required"), + timing: Yup.string().required("Required"), + action: Yup.string().required("Required"), + actionParameters: Yup.object().shape({ + // Property only required if it actually exists on the object + workflowId: Yup.string().required("Required"), + // workflowId: Yup.string().when("workflowId", { + // is: (exists: any) => !!exists, + // then: Yup.string().required("Required"), + // }), + }), + actionDate: Yup.date().when("timing", { + is: (timing: string) => timing === "SPECIFIC_DATE", + then: () => Yup.date().required("Required"), + }), + cronTrigger: Yup.string().when("timing", { + is: (timing: string) => timing === "REPEATING", + then: () => Yup.string().required("Required"), + }), + targetFiltersTransformed: targetFiltersSchema, + }), +]; +