diff --git a/package-lock.json b/package-lock.json index 2a646992..00a48b85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@angular/core": "^17.2.3", "@angular/elements": "^17.2.3", "@angular/forms": "^17.2.3", + "@angular/material": "^17.2.1", "@angular/platform-browser": "^17.2.3", "@angular/platform-browser-dynamic": "^17.2.3", "@angular/router": "^17.2.3", @@ -1787,6 +1788,77 @@ "node": "^18.13.0 || >=20.9.0" } }, + "node_modules/@angular/material": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.2.1.tgz", + "integrity": "sha512-NLQJkX4XiwIm32dGdNseoc+ARn6JvuB2xMY5XfWTtjJBbQaPk5sIvjH4wsAEeYqDKtZbRCjxGwRz0K1djyaVqQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/auto-init": "15.0.0-canary.7f224ddd4.0", + "@material/banner": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/card": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/chips": "15.0.0-canary.7f224ddd4.0", + "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", + "@material/data-table": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dialog": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/drawer": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/fab": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/form-field": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/image-list": "15.0.0-canary.7f224ddd4.0", + "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/radio": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/slider": "15.0.0-canary.7f224ddd4.0", + "@material/snackbar": "15.0.0-canary.7f224ddd4.0", + "@material/switch": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/textfield": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tooltip": "15.0.0-canary.7f224ddd4.0", + "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^17.0.0 || ^18.0.0", + "@angular/cdk": "17.2.1", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@angular/platform-browser": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.2.3.tgz", @@ -4524,6 +4596,1096 @@ "node": ">= 0.4" } }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/animation/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", + "license": "MIT", + "dependencies": { + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/form-field/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/menu/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", + "license": "MIT", + "dependencies": { + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/tab/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", + "license": "MIT", + "dependencies": { + "@material/elevation": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tooltip/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@ngtools/webpack": { "version": "17.2.2", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.2.2.tgz", @@ -14925,6 +16087,12 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==", + "license": "Apache-2.0" + }, "node_modules/sass": { "version": "1.64.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", diff --git a/package.json b/package.json index dfc71054..50d301a3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@angular/core": "^17.2.3", "@angular/elements": "^17.2.3", "@angular/forms": "^17.2.3", + "@angular/material": "^17.2.1", "@angular/platform-browser": "^17.2.3", "@angular/platform-browser-dynamic": "^17.2.3", "@angular/router": "^17.2.3", @@ -59,6 +60,16 @@ "ngx-markdown": "^17.1.1", "ngx-plyr": "^4.0.1", "prismjs": "1.29.0", + "rete": "^2.0.3", + "rete-angular-plugin": "^2.1.1", + "rete-area-plugin": "^2.0.4", + "rete-auto-arrange-plugin": "^2.0.1", + "rete-connection-path-plugin": "^2.0.3", + "rete-connection-plugin": "^2.0.1", + "rete-context-menu-plugin": "^2.0.3", + "rete-engine": "^2.0.1", + "rete-readonly-plugin": "^2.0.1", + "rete-render-utils": "^2.0.2", "rxjs": "^7.8.1", "rxjs-compat": "^6.5.5", "sass": "^1.26.3", @@ -67,17 +78,7 @@ "ts-helpers": "^1.1.2", "ts-md5": "^1.2.7", "uuid": "^9.0.0", - "zone.js": "~0.14.4", - "rete": "^2.0.3", - "rete-angular-plugin": "^2.1.1", - "rete-connection-plugin": "^2.0.1", - "rete-auto-arrange-plugin": "^2.0.1", - "rete-area-plugin": "^2.0.4", - "rete-render-utils": "^2.0.2", - "rete-readonly-plugin": "^2.0.1", - "rete-connection-path-plugin": "^2.0.3", - "rete-context-menu-plugin": "^2.0.3", - "rete-engine": "^2.0.1" + "zone.js": "~0.14.4" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.1", @@ -86,6 +87,7 @@ "@angular/compiler-cli": "^17.2.3", "@angular/language-service": "^17.2.3", "@ngtools/webpack": "^17.2.2", + "@types/d3-shape": "^3.1.6", "@types/hammerjs": "^2.0.36", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "^2.0.8", @@ -108,11 +110,10 @@ "tslint": "~6.1.0", "typescript": "^5.3.3", "webpack": "^5.54.0", - "webpack-bundle-analyzer": "^4.9.0", - "@types/d3-shape": "^3.1.6" + "webpack-bundle-analyzer": "^4.9.0" }, "engines": { "node": ">= 16.3.0", "npm": ">= 5.6.0" } -} +} \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 9d471aad..6f4c4116 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,6 +4,7 @@ import {DefaultLayoutComponent} from './containers/default-layout'; import {P404Component} from './views/error/404.component'; import {P500Component} from './views/error/500.component'; import {LoginComponent} from './views/login/login.component'; +import {TableSelectionDialogComponent} from './components/table-selection-dialog/table-selection-dialog.component'; export const routes: Routes = [ @@ -28,6 +29,13 @@ export const routes: Routes = [ title: 'Login Page' } }, + { + path: 'table-selection', + component: TableSelectionDialogComponent, + data: { + title: 'Table Selection' + } + }, { path: '', pathMatch: 'full', diff --git a/src/app/components/delete-confirm/delete-confirm.component.html b/src/app/components/delete-confirm/delete-confirm.component.html index e02e5c99..c6a49a4e 100644 --- a/src/app/components/delete-confirm/delete-confirm.component.html +++ b/src/app/components/delete-confirm/delete-confirm.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/components/table-selection-dialog/table-selection-dialog.component.html b/src/app/components/table-selection-dialog/table-selection-dialog.component.html new file mode 100644 index 00000000..e6992d72 --- /dev/null +++ b/src/app/components/table-selection-dialog/table-selection-dialog.component.html @@ -0,0 +1,59 @@ + + + Tabellen auswählen + + + 📦 {{ db.name }} + + 📁 {{ schema.name }} + + + + 📄 {{ db.name }}.{{ schema.name }}.{{ table.name }} + + + + + + 🔹 {{ attr.name }} : {{ attr.type }} + + + + + + + + + + Schließen + Ausgewählte anzeigen + Sende Metadaten + + + + + + + + {{ name }} + + + + + {{ key }} + + + + + + {{ row[key] }} + + + + + + + + + + diff --git a/src/app/components/table-selection-dialog/table-selection-dialog.component.scss b/src/app/components/table-selection-dialog/table-selection-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/table-selection-dialog/table-selection-dialog.component.spec.ts b/src/app/components/table-selection-dialog/table-selection-dialog.component.spec.ts new file mode 100644 index 00000000..5558c4a5 --- /dev/null +++ b/src/app/components/table-selection-dialog/table-selection-dialog.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TableSelectionDialogComponent } from './table-selection-dialog.component'; + +// @ts-ignore +describe('TableSelectionDialogComponent', () => { + let component: TableSelectionDialogComponent; + let fixture: ComponentFixture; + + // @ts-ignore + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TableSelectionDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TableSelectionDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // @ts-ignore + it('should create', () => { + // @ts-ignore + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/table-selection-dialog/table-selection-dialog.component.ts b/src/app/components/table-selection-dialog/table-selection-dialog.component.ts new file mode 100644 index 00000000..13f13834 --- /dev/null +++ b/src/app/components/table-selection-dialog/table-selection-dialog.component.ts @@ -0,0 +1,188 @@ +import {Component, inject} from '@angular/core'; +import {DatabaseInfo, SchemaInfo, TableInfo} from '../../models/databaseInfo.model'; +import {FormsModule} from '@angular/forms'; +import {NgForOf, NgIf} from '@angular/common'; +import {CrudService} from '../../services/crud.service'; +import {AdapterModel, AdapterType, PolyMap} from '../../views/adapters/adapter.model'; +import {DeployMode} from '../../models/catalog.model'; + +@Component({ + selector: 'app-table-selection-dialog', + imports: [ + FormsModule, + NgForOf, + NgIf + ], + templateUrl: './table-selection-dialog.component.html', + standalone: true, + styleUrl: './table-selection-dialog.component.scss' +}) +export class TableSelectionDialogComponent { + + private readonly _crud = inject(CrudService); + + data: DatabaseInfo[] = []; + selectedMetadata: string[] = []; + adapter: AdapterModel; + + tablePreviewName = ''; + tablePreview: any[] = []; + previewKeys: string[] = []; + + tablePreviewAll: { [tableName: string]: any[] } = {}; + + + close(): void { + window.close(); + } + + ngOnInit(): void { + const rawSettings = localStorage.getItem('adapterSettings'); + const metaRaw = localStorage.getItem('metaRoot'); + const previewRaw = localStorage.getItem('preview'); + console.log(metaRaw); + console.log(previewRaw); + + const settingsObj = rawSettings ? JSON.parse(rawSettings) as Record : {}; + const adapterSettings = new PolyMap(); + for (const [k, v] of Object.entries(settingsObj)) { + adapterSettings.set(k, v as string); + } + + if (!metaRaw || !previewRaw) { + console.error('No meta or preview data found'); + return; + } + + let rootNode: any = JSON.parse(metaRaw); + if (typeof rootNode === 'string') { + rootNode = JSON.parse(rootNode); + } + + let preview: any = JSON.parse(previewRaw); + if (typeof preview === 'string') { + preview = JSON.parse(preview); + } + + this.tablePreviewAll = preview; + const [name, rawRows] = Object.entries(preview)[0]; + const rows = rawRows as any[]; + + this.tablePreviewName = name; + this.tablePreview = rows; + this.previewKeys = rows.length > 0 ? Object.keys(rows[0]) : []; + + + this.data = this.buildDatabaseInfo(rootNode, preview); + + const infoRaw = localStorage.getItem('adapterInfo'); + const info = infoRaw ? JSON.parse(infoRaw) as Partial<{ + uniqueName: string; + adapterName: string; + type: AdapterType; + mode: DeployMode; + persistent: boolean; + }> : {}; + + this.adapter = new AdapterModel( + info.uniqueName ?? adapterSettings.get('uniqueName') ?? 'adapter_' + Date.now(), + info.adapterName ?? 'UNKNOWN', + adapterSettings, + info.persistent ?? true, + info.type ?? AdapterType.SOURCE, + info.mode ?? DeployMode.REMOTE + ); + } + + private buildDatabaseInfo(root: any, preview: any): DatabaseInfo[] { + const db: DatabaseInfo = {name: root.name, schemas: []}; + for (const schemaNode of root.children ?? []) { + + const schema: SchemaInfo = {name: schemaNode.name, tables: []}; + + for (const tableNode of schemaNode.children ?? []) { + + const tableKey = `${schemaNode.name}.${tableNode.name}`; + const sampleRows = preview[tableKey] ?? []; + + const table: TableInfo = {name: tableNode.name, attributes: []}; + + for (const colNode of tableNode.children ?? []) { + const colName = colNode.name; + + table.attributes.push({ + name: colName, + type: colNode.properties?.type ?? '', + selected: false, + sampleValues: sampleRows.slice(0, 5).map((r: any) => r[colName]) + }); + } + schema.tables.push(table); + } + db.schemas.push(schema); + } + this.data = [db]; + return [db]; + } + + getSelectedAttributeMetadata(): string[] { + const selected: string[] = []; + + for (const db of this.data) { + for (const schema of db.schemas) { + for (const table of schema.tables) { + for (const attr of table.attributes) { + if (attr.selected) { + selected.push( + `${db.name}.${schema.name}.${table.name}.${attr.name} : ${attr.type}` + ); + } + } + } + } + } + + return selected; + } + + showSelectedMetadata(): void { + this.selectedMetadata = this.getSelectedAttributeMetadata(); + console.log(this.selectedMetadata); + } + + getKeys(rows: any[]): string[] { + return rows.length > 0 ? Object.keys(rows[0]) : []; + } + + getTableNames(): string[] { + return Object.keys(this.tablePreviewAll); + } + + + sendMetadataInfos(): void { + + + (this.adapter as any).metadata = this.selectedMetadata; + + const formdata = new FormData(); + formdata.set('body', JSON.stringify(this.adapter)); + + formdata.forEach((value, key) => { + console.log(`${key}: ${value}`); + }); + + this._crud.createAdapter(this.adapter, formdata).subscribe({ + next: (res) => { + console.log('Adapter + Metadaten erfolgreich gesendet', res); + alert('Daten erfolgreich gesendet.'); + }, + error: (err) => { + console.error(err); + alert('Fehler beim Senden!'); + } + }); + } + + +} + diff --git a/src/app/models/databaseInfo.model.ts b/src/app/models/databaseInfo.model.ts new file mode 100644 index 00000000..f2e6182e --- /dev/null +++ b/src/app/models/databaseInfo.model.ts @@ -0,0 +1,21 @@ +export interface DatabaseInfo { + name: string; + schemas: SchemaInfo[]; +} + +export interface TableInfo { + name: string; + attributes: AttributeInfo[]; +} + +export interface SchemaInfo { + name: string; + tables: TableInfo[]; +} + +export interface AttributeInfo { + name: String; + type: String; + sampleValues?: string[]; + selected?: boolean; +} diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index 1c0717c2..e429367d 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -210,6 +210,28 @@ export class EditCollectionRequest { } } +/** + * Request for dealing with the data preview before deploying an adapter. + */ +export class PreviewRequest { + adapterName: string; + adapterType: string; + settings: { [key: string]: string }; + limit: number; + uniqueName?: string; + + constructor(adapterName: string, adapterType: string, settings: { + [key: string]: string + }, limit: number, uniqueName: string) { + this.adapterName = adapterName; + this.adapterType = adapterType; + this.settings = settings; + this.limit = limit; + this.uniqueName = uniqueName; + } +} + + /** * Request to drop or create a constraint of a table */ diff --git a/src/app/services/catalog.service.ts b/src/app/services/catalog.service.ts index d7799f1b..676019c2 100644 --- a/src/app/services/catalog.service.ts +++ b/src/app/services/catalog.service.ts @@ -154,7 +154,7 @@ export class CatalogService { for (let idEntity of idEntities) { const id = extract(idEntity); if (!map.has(id)) { - map.set(id, []) + map.set(id, []); } map.set(id, [idEntity, ...map.get(id)]); } @@ -289,7 +289,7 @@ export class CatalogService { private getNamespaceIcon(dataModel: DataModel): string { if (!this.assets) { this.updateIfNecessary(); - return ""; + return ''; } switch (dataModel) { case DataModel.DOCUMENT: @@ -370,6 +370,21 @@ export class CatalogService { }); } + updateAdapterFlag(uniqueName: string, delta: Partial): void { + this.adapters.update(oldMap => { + const newMap = new Map(oldMap); + + for (const [id, adapter] of oldMap.entries()) { + if (adapter.name === uniqueName) { + newMap.set(id, { ...adapter, ...delta }); + break; + } + } + + return newMap; + }); + } + getAdapterTemplate(adapterName: string, type: AdapterType): AdapterTemplateModel { return this.adapterTemplates().get(adapterName + '_' + type); } diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index 7c5365a8..c21848d9 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -5,7 +5,25 @@ import {EntityMeta, IndexModel, ModifyPartitionRequest, PartitionFunctionModel, RelationalResult } from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; -import {ColumnRequest, ConstraintRequest, DataModel, DeleteRequest, EditCollectionRequest, EditTableRequest, EntityRequest, ExploreTable, GraphRequest, MaterializedRequest, Method, MonitoringRequest, Namespace, PolyAlgRequest, QueryRequest, StatisticRequest} from '../models/ui-request.model'; +import { + ColumnRequest, + ConstraintRequest, + DataModel, + DeleteRequest, + EditCollectionRequest, + EditTableRequest, + EntityRequest, + ExploreTable, + GraphRequest, + MaterializedRequest, + Method, + MonitoringRequest, + Namespace, + PolyAlgRequest, + PreviewRequest, + QueryRequest, + StatisticRequest +} from '../models/ui-request.model'; import {AutoDockerResult, AutoDockerStatus, CreateDockerResponse, DockerInstanceInfo, DockerSettings, @@ -22,13 +40,14 @@ import {Observable} from 'rxjs'; import {PolyAlgRegistry} from '../components/polyalg/models/polyalg-registry'; import {PlanNode} from '../components/polyalg/models/polyalg-plan.model'; import {PlanType} from '../models/information-page.model'; +import {MetadataStatusResponse} from './metadata-polling.service'; @Injectable({ providedIn: 'root' }) export class CrudService { - private readonly _http = inject(HttpClient); + readonly _http = inject(HttpClient); private readonly _settings = inject(WebuiSettingsService); private enabledPlugins: [string] = null; @@ -43,7 +62,7 @@ export class CrudService { public connected = false; private reconnected = new EventEmitter(); - private httpUrl = this._settings.getConnection('crud.rest'); + httpUrl = this._settings.getConnection('crud.rest'); private httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; private httpOptionsPart = {headers: new HttpHeaders({'Content-Type': 'multipart/form-data'})}; private socket; @@ -257,6 +276,48 @@ export class CrudService { return this._http.post(`${this.httpUrl}/dropConstraint`, request, this.httpOptions); } + /** + * Get the preview for an adapter. + */ + previewTable(request: PreviewRequest) { + return this._http.post(`${this.httpUrl}/previewTable`, request, this.httpOptions); + alert('Methode wird aufgerufen'); + } + + /** + * Get the preview for an adapter with forms + */ + previewTableForm(request: PreviewRequest, formdata: FormData) { + formdata.set('body', JSON.stringify(request)); + console.log('Daten, die als Preview geschickt werden'); + formdata.forEach((value, key) => { + console.log(`${key}: ${value}`); + }); + return this._http.post(`${this.httpUrl}/previewTable`, formdata); + alert('Methode wird aufgerufen'); + } + + metadataStatus(request: string) { + return this._http.get(`${this.httpUrl}/metadataStatus/${request}`, this.httpOptions); + } + + metadataChanges(request: string) { + return this._http.get(`${this.httpUrl}/metadataChange/${request}`, this.httpOptions); + } + + metadataAck(body: { uniqueName: string, addedPaths: string[], removedPaths: string[] }) { + console.log(body); + return this._http.post(`${this.httpUrl}/metadataAck/${body.uniqueName}`, body, this.httpOptions); + } + + metadataConfiguration(request: string) { + return this._http.get(`${this.httpUrl}/metadataConfiguration/${request}`, this.httpOptions); + } + + setMetaConfiguration(body: {uniqueName: string, selected: string[]}) { + return this._http.post(`${this.httpUrl}/setMetaConfig`, body, this.httpOptions); + } + /** * Add a primary key to a table */ @@ -486,6 +547,11 @@ export class CrudService { return this._http.post(`${this.httpUrl}/updateAdapterSettings`, adapter); } + updateAdapterSettingsForm(fd: FormData) { + return this._http.post(`${this.httpUrl}/updateAdapterSettingsForm`, fd); + } + + getSources() { return this._http.get(`${this.httpUrl}/getSources`); } @@ -500,9 +566,19 @@ export class CrudService { createAdapter(adapter: AdapterModel, formdata: FormData) { formdata.set('body', JSON.stringify(adapter)); + console.log('Adaptersettings vor der Creation'); + formdata.forEach((value, key) => { + console.log(`${key}: ${value}`); + + }); return this._http.post(`${this.httpUrl}/createAdapter`, formdata); } + sendSelectedMetadata(request: string[]) { + console.log(request); + return this._http.post(`${this.httpUrl}/sendSelectedMetadata`, request, this.httpOptions); + } + pathAccess(req: PathAccessRequest) { return this._http.post(`${this.httpUrl}/pathAccess`, req); diff --git a/src/app/services/metadata-polling.service.ts b/src/app/services/metadata-polling.service.ts new file mode 100644 index 00000000..96fa82fc --- /dev/null +++ b/src/app/services/metadata-polling.service.ts @@ -0,0 +1,54 @@ +// src/app/services/metadata-polling.service.ts +import {inject, Injectable} from '@angular/core'; +import {CrudService} from './crud.service'; +import {timer, Subscription, of, switchMap} from 'rxjs'; + +import {CatalogService} from './catalog.service'; +import {AdapterModel} from '../views/adapters/adapter.model'; + +export interface MetadataStatusResponse { + changed: 'OK' | 'WARNING' | 'CRITICAL' | null; +} + +@Injectable({providedIn: 'root'}) +export class MetadataPollingService { + + private readonly _crud = inject(CrudService); + private readonly _catalog = inject(CatalogService); + + private readonly intervalMs = 30_000; + private timerSub: Subscription | null = null; + + + start(): void { + if (this.timerSub) { + return; + } + + this.timerSub = timer(0, this.intervalMs).pipe( + switchMap(() => of(this._catalog.getSources())) + ).subscribe(sourceList => { + sourceList.forEach(source => this.checkSource(source)); + }); + } + + stop(): void { + this.timerSub?.unsubscribe(); + this.timerSub = null; + } + + + private checkSource(adapter: AdapterModel): void { + this._crud.metadataStatus(adapter.name).subscribe({ + next: (res: Object) => { + const casted = res as MetadataStatusResponse; + console.log('casted', casted); + this._catalog.updateAdapterFlag(adapter.name, { + metadataStatus: casted.changed + }); + }, + error: err => console.error('[metadata-poll]', err) + }); + } + +} diff --git a/src/app/services/preview-navigation.service.ts b/src/app/services/preview-navigation.service.ts new file mode 100644 index 00000000..bdbf659b --- /dev/null +++ b/src/app/services/preview-navigation.service.ts @@ -0,0 +1,43 @@ +import {Injectable} from '@angular/core'; +import {AbstractNode, ChangeLogView} from '../views/preview-selection/models/metadataTree.model'; +import {AdapterModel, AdapterType, PolyMap} from '../views/adapters/adapter.model'; +import {DeployMode} from '../models/catalog.model'; + + +export type PreviewMode = 'deploy' | 'change' | 'config'; + +export interface PreviewContext { + mode: PreviewMode; + adapter: AdapterModel; + metadata: AbstractNode; + preview: Record | any[]; + formData: FormData; + files: Map; + adapterSettings: PolyMap; + adapterInfo: Partial<{ + uniqueName: string; + adapterName: string; + type: AdapterType; + mode: DeployMode; + persistent: boolean; + }>; + changeLog?: ChangeLogView[]; +} + +@Injectable({providedIn: 'root'}) +export class PreviewNavigationService { + + private _ctx: PreviewContext | null = null; + + setContext(ctx: PreviewContext): void { + this._ctx = ctx; + } + + get context(): PreviewContext | null { + return this._ctx; + } + + clear(): void { + this._ctx = null; + } +} diff --git a/src/app/services/schema-discovery.service.ts b/src/app/services/schema-discovery.service.ts new file mode 100644 index 00000000..f49b04a6 --- /dev/null +++ b/src/app/services/schema-discovery.service.ts @@ -0,0 +1,47 @@ +import {Component, Inject, Injectable} from '@angular/core'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {CommonModule} from '@angular/common'; +import {HttpClientModule} from '@angular/common/http'; +import { RouterOutlet } from '@angular/router'; +import {DatabaseInfo, TableInfo} from '../models/databaseInfo.model'; +import {FormsModule} from '@angular/forms'; +import {TableSelectionDialogComponent} from '../components/table-selection-dialog/table-selection-dialog.component'; +import {MatDialog} from '@angular/material/dialog'; +import {PreviewRequest} from '../models/ui-request.model'; +import {WebSocket} from './webSocket'; + +@Injectable({ + providedIn: 'root' +}) +export class SchemaDiscoveryService { + title = 'schemaui'; + message = ''; + databaseList: DatabaseInfo[] = []; + + constructor(private http: HttpClient, private dialog: MatDialog) { + } + + makeRequest() { + this.message = 'Button geklickt !!!'; + } + + openTableDialogs(): void { + this.http.post('http://127.0.0.1:7659/confirm', {}) + .subscribe({ + next: (data) => { + alert('Nachricht angekommen.'); + console.log('Dateien: ', data); + localStorage.setItem('databaseInfo', JSON.stringify(data)); + window.open('/#/table-selection', 'popup', 'width=1000, height=700'); + }, + error: (err) => { + console.error('Fehler beim Abrufen der Datenbankstruktur:', err); + alert('Nachricht nicht angekommen !!!'); + } + }); + } + + openTableDialog(): void { + + } +} diff --git a/src/app/views/adapters/adapter.model.ts b/src/app/views/adapters/adapter.model.ts index 04e126c5..4ecd21cc 100644 --- a/src/app/views/adapters/adapter.model.ts +++ b/src/app/views/adapters/adapter.model.ts @@ -8,14 +8,20 @@ export class AdapterModel extends IdEntity { readonly type: AdapterType; readonly mode: DeployMode; indexMethods: IndexMethodModel[]; + readonly metadata?: string[]; + metadataStatus?: 'OK' | 'WARNING' | 'CRITICAL' | null; + columnAliases?: Map; - constructor(uniqueName: string, adapterName: string, settings: Map, persistent: boolean, type: AdapterType, deployMode: DeployMode) { + constructor(uniqueName: string, adapterName: string, settings: Map, persistent: boolean, type: AdapterType, deployMode: DeployMode, metadata?: string[], metadataStatus?: 'OK' | 'WARNING' | 'CRITICAL' | null, aliase?: Map) { super(-1, uniqueName); this.adapterName = adapterName; this.settings = settings; this.persistent = persistent; this.type = type; this.mode = deployMode; + this.metadata = metadata; + this.metadataStatus = metadataStatus; + this.columnAliases = aliase; } } diff --git a/src/app/views/adapters/adapters.component.html b/src/app/views/adapters/adapters.component.html index c122415e..fde9d4d0 100644 --- a/src/app/views/adapters/adapters.component.html +++ b/src/app/views/adapters/adapters.component.html @@ -74,8 +74,20 @@ Adapters - + ❗ + + + + + + Configure @@ -164,18 +176,11 @@ {{ activeMode() ? "Settings" : "Deployment Mode" }} - test - {{ control.key }} - - - {{ fileLabel }} - - - required + + {{ control.key }} + @@ -269,12 +274,13 @@ {{ activeMode() ? "Settings" : "Deployment Mode" }} Close - Save + Deploy diff --git a/src/app/views/adapters/adapters.component.ts b/src/app/views/adapters/adapters.component.ts index 4678edc7..f462ad49 100644 --- a/src/app/views/adapters/adapters.component.ts +++ b/src/app/views/adapters/adapters.component.ts @@ -14,6 +14,7 @@ import {CrudService} from '../../services/crud.service'; import {ActivatedRoute, Router} from '@angular/router'; import {AdapterModel, AdapterType, PolyMap} from './adapter.model'; import {ToasterService} from '../../components/toast-exposer/toaster.service'; +import {MetadataPollingService} from '../../services/metadata-polling.service'; import { AbstractControl, FormGroup, @@ -23,15 +24,19 @@ import { ValidatorFn, Validators } from '@angular/forms'; -import {PathAccessRequest, RelationalResult} from '../../components/data-view/models/result-set.model'; +import { + PathAccessRequest, + RelationalResult +} from '../../components/data-view/models/result-set.model'; +import {AbstractNode, ChangeLogView, PreviewResult} from '../preview-selection/models/metadataTree.model'; +import {PreviewRequest} from '../../models/ui-request.model'; +import {Node, reviveTree} from '../preview-selection/preview-selection.component'; import {Subscription} from 'rxjs'; import {CatalogService} from '../../services/catalog.service'; -import { - AdapterSettingModel, - AdapterTemplateModel, - DeployMode -} from '../../models/catalog.model'; +import {AdapterSettingModel, AdapterTemplateModel, DeployMode} from '../../models/catalog.model'; import {LeftSidebarService} from '../../components/left-sidebar/left-sidebar.service'; +import {SchemaDiscoveryService} from '../../services/schema-discovery.service'; +import {PreviewMode, PreviewNavigationService} from '../../services/preview-navigation.service'; @Component({ selector: 'app-adapters', @@ -47,8 +52,10 @@ export class AdaptersComponent implements OnInit, OnDestroy { private readonly _fb = inject(UntypedFormBuilder); private readonly _catalog = inject(CatalogService); private readonly _left = inject(LeftSidebarService); + private readonly _metaPolling = inject(MetadataPollingService); - constructor(private injector: Injector) { + constructor(private injector: Injector, + private schemaDiscoveryService: SchemaDiscoveryService) { this.availableAdapters = computed(() => { return this._catalog.getAdapterTemplates().filter(a => a.adapterType === this.getMatchingAdapterType()); }); @@ -69,6 +76,7 @@ export class AdaptersComponent implements OnInit, OnDestroy { readonly availableAdapters: Signal; readonly currentRoute: WritableSignal = signal(null); private subscriptions = new Subscription(); + private nav: PreviewNavigationService = inject(PreviewNavigationService); readonly adapter: WritableSignal = signal(null); editingAdapterForm: FormGroup; @@ -104,6 +112,10 @@ export class AdaptersComponent implements OnInit, OnDestroy { private readonly files = new Map(); + pendingDeploy!: AdapterModel; + pendingFormData!: FormData; + metadataSelection = false; + readonly positionOrder = () => { return (a, b) => { @@ -123,10 +135,12 @@ export class AdaptersComponent implements OnInit, OnDestroy { this.currentRoute.set(params['action']); }); this.subscriptions.add(sub); + this._metaPolling.start(); } ngOnDestroy() { this.subscriptions.unsubscribe(); + this._metaPolling.stop(); } subscribeActiveChange() { @@ -194,6 +208,20 @@ export class AdaptersComponent implements OnInit, OnDestroy { } initAdapterSettingsConfigureModal(adapter: AdapterModel) { + if (adapter.type === AdapterType.SOURCE) { + const uniqueName = adapter.name.toLowerCase(); + this.getMetaConfiguration(uniqueName); + } else { + const allSettings = this._catalog.getAdapterTemplate(adapter.adapterName, adapter.type); + const current = Adapter.from(allSettings, adapter, Task.CHANGE); + this.adapter.set(current); + this.activeMode.set(current.modes[0]); + this.handshaking = false; + this.modalActive = true; + } + } + + changeAdapterSettings(adapter: AdapterModel) { const allSettings = this._catalog.getAdapterTemplate(adapter.adapterName, adapter.type); const current = Adapter.from(allSettings, adapter, Task.CHANGE); @@ -204,6 +232,169 @@ export class AdaptersComponent implements OnInit, OnDestroy { this.modalActive = true; } + private getMetaConfiguration(uniqueName: string) { + this._crud.metadataConfiguration(uniqueName).subscribe({ + next: (preview: PreviewResult) => { + console.log(preview); + this.openPreview( + 'config', + null, + preview.preview, + typeof preview.metadata === 'string' + ? JSON.parse(preview.metadata) + : preview.metadata, + null, + null, + null, + { + uniqueName: uniqueName + }, + preview.history + ); + } + } + ); + } + + + private openPreview( + mode: PreviewMode, + adapter: AdapterModel, + preview: Record | any[], + metaRoot: AbstractNode, + fd: FormData, + files: Map, + adapterSettings: PolyMap, + adapterInfo: Partial<{ + uniqueName: string; + adapterName: string; + type: AdapterType; + mode: DeployMode; + persistent: boolean; + }>, + changeLog?: ChangeLogView[] + ) { + this.nav.setContext({ + mode: mode, + adapter, + metadata: metaRoot, + preview, + formData: fd, + files, + adapterSettings, + adapterInfo, + changeLog + }); + + this._router.navigate(['/views/preview-selection']); + } + + previewAndDeploy(): void { + + // Create AdapterModel and use it later for deploy. + const deploy = new AdapterModel( + this.editingAvailableAdapterForm.controls['uniqueName'].value, + this.adapter().adapterName, + new PolyMap(), + this.adapter().persistent, + this.adapter().type, + this.activeMode() + ); + + // If settings consists directory, append InpuStream + const fd = new FormData(); + + for (const [k, v] of Object.entries(this.editingAvailableAdapterForm.controls)) { + const setting = this.getAdapterSetting(k); + if (!setting) { + continue; + } + + let value = (v as AbstractControl).value; + + if (setting.template.type.toLowerCase() === 'directory') { + const first = setting.template.fileNames?.[0]; + if (first) { + value = JSON.stringify(first); + fd.append(first, this.files.get(first)); + } + } + + deploy.settings.set(k, value); + } + fd.set('body', JSON.stringify(deploy)); + + // Preview-Request for temporal adapter deploy. + const previewReq = new PreviewRequest( + deploy.adapterName, + deploy.type, + Object.fromEntries(deploy.settings), + 10, + deploy.name + ); + + this._crud.previewTableForm(previewReq, fd).subscribe({ + next: (preview: PreviewResult) => { + const settingsObj = Object.fromEntries( + Object.entries(this.editingAvailableAdapterForm.controls) + .map(([key, ctrl]) => [key, (ctrl as AbstractControl).value]) + ); + + const metaRoot = reviveTree( + typeof preview.metadata === 'string' + ? JSON.parse(preview.metadata) + : preview.metadata + ); + + this.openPreview( + 'deploy', + deploy, + preview.preview, + metaRoot, + fd, + this.files, + new PolyMap(Object.entries(settingsObj)), + { + uniqueName: deploy.name, + adapterName: deploy.adapterName, + type: deploy.type, + mode: deploy.mode, + persistent: deploy.persistent + } + ); + + this._toast.success('Preview loaded successfully.'); + }, + error: (err) => { + this._toast.error('Error getting preview!'); + } + }); + } + + previewAndChange(uniqueName: string): void { + this._crud.metadataChanges(uniqueName).subscribe({ + next: (preview: PreviewResult) => { + const node = typeof preview.metadata === 'string' ? JSON.parse(preview.metadata) : preview.metadata; + + this.openPreview( + 'change', + null, + preview.preview, + typeof preview.metadata === 'string' + ? JSON.parse(preview.metadata) + : preview.metadata, + null, + null, + null, + { + uniqueName: uniqueName + } + ); + } + }); + } + + saveAdapterSettings() { const adapter = this.adapter; adapter.settings = {}; @@ -231,6 +422,88 @@ export class AdaptersComponent implements OnInit, OnDestroy { }); } + + updateAdapterSettings() { + const form = this.editingAdapterForm; + if (!form) { + this._toast.error('No editing adapter form available!'); + return; + } + + const adapter = this.adapter(); + const fd = new FormData(); + let hasUpload = false; + + // Collect new settings. + const mergedObj: Record = {}; + adapter.settings.forEach((ms, key) => { + mergedObj[key] = ms.current ?? ''; + }); + + for (const [key, ctrl] of Object.entries(form.controls)) { + if (key === 'uniqueName') { + continue; + } + + const val = (ctrl as AbstractControl).value ?? ''; + const meta = this.getAdapterSetting(key); + + if (meta?.template?.type?.toLowerCase() === 'directory') { + const first = meta.template.fileNames?.[0]; + if (first) { + hasUpload = true; + mergedObj[key] = JSON.stringify(first); + fd.append(first, this.files.get(first)); + } + } else { + mergedObj[key] = val; + } + } + + const poly = new PolyMap(Object.entries(mergedObj) as [string, string][]); + + + // Create AdapterModel and PreviewRequest depending on if settings changes or reupload. + const updated = new AdapterModel( + adapter.uniqueName, + adapter.adapterName, + poly, + adapter.persistent, + adapter.type, + adapter.mode + ); + + + const updatedForm = new PreviewRequest( + adapter.adapterName, + adapter.type, + mergedObj, + 10, + adapter.uniqueName + ); + + if (hasUpload) { + fd.set('body', JSON.stringify(updatedForm)); + this._crud.updateAdapterSettingsForm(fd).subscribe(this.handleResult); + } else { + this._crud.updateAdapterSettings(updated).subscribe(this.handleResult); + } + } + + private handleResult = { + next: res => { + res.error + ? this._toast.exception(res) + : this._toast.success('Updated adapter settings'); + this.modalActive = false; + }, + error: err => { + this._toast.error('Could not update adapter settings'); + console.error(err); + } + }; + + getDefaultUniqueName(): string { if (this.adapter !== undefined) { const base = this.adapter().adapterName.toLowerCase(); // + "_"; // TODO: re-enable underscores when graph namespaces work with it @@ -282,10 +555,10 @@ export class AdaptersComponent implements OnInit, OnDestroy { const fileNames = []; const setting = this.getAdapterSetting(key); setting.template.fileNames = []; - for (let file of files) { + for (const file of files) { fileNames.push(file.name); this.files.set(file.name, file); - setting.template.fileNames.push(file.name) + setting.template.fileNames.push(file.name); } } @@ -350,10 +623,10 @@ export class AdaptersComponent implements OnInit, OnDestroy { setting.current = null; } - if (setting.template.type.toLowerCase() === "directory") { + if (setting.template.type.toLowerCase() === 'directory') { const fileNames = []; - for (let fileName of setting.template.fileNames) { + for (const fileName of setting.template.fileNames) { fd.append(fileName, this.files.get(fileName)); } setting.current = JSON.stringify(setting.template.fileNames); @@ -407,7 +680,7 @@ export class AdaptersComponent implements OnInit, OnDestroy { } private startDeploying(deploy: AdapterModel, formdata: FormData) { - console.log(deploy) + console.log(deploy); this.deploying = true; this._crud.createAdapter(deploy, formdata).subscribe({ next: (result: RelationalResult) => { @@ -594,6 +867,7 @@ export class AdaptersComponent implements OnInit, OnDestroy { return true; } + protected readonly alert = alert; } // see https://angular.io/guide/form-validation#custom-validators diff --git a/src/app/views/doc-card/doc-card.component.html b/src/app/views/doc-card/doc-card.component.html new file mode 100644 index 00000000..35417726 --- /dev/null +++ b/src/app/views/doc-card/doc-card.component.html @@ -0,0 +1,36 @@ + + + + {{ prettifyKey(node.name) }} + + + + + + + + + {{ prettifyKey(row.k) }} : + + + {{ row.v }} + + + + [ … ] + { … } + + + + + + + + + + + diff --git a/src/app/views/doc-card/doc-card.component.scss b/src/app/views/doc-card/doc-card.component.scss new file mode 100644 index 00000000..780edddf --- /dev/null +++ b/src/app/views/doc-card/doc-card.component.scss @@ -0,0 +1,31 @@ +.kvRow { + line-height: 1.4rem; +} + +.kvRow b { + font-weight: 500; + margin-right: .25rem; +} + +mat-card { + width: 260px; + min-height: 180px; +} + +.cardGrid { + display: grid; + grid-template-columns: repeat(auto-fill, 260px); + gap: 1rem 1.25rem; +} + +/* doc-card.component.scss */ +.nestedCard { + margin-left: .75rem; + margin-top: .25rem; + box-shadow: none; + border-left: 2px solid #e0e0e0; +} + +.kvRow { + margin-bottom: .125rem; +} diff --git a/src/app/views/doc-card/doc-card.component.ts b/src/app/views/doc-card/doc-card.component.ts new file mode 100644 index 00000000..3bb19876 --- /dev/null +++ b/src/app/views/doc-card/doc-card.component.ts @@ -0,0 +1,53 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {MatCheckbox, MatCheckboxModule} from '@angular/material/checkbox'; +import {FormsModule} from '@angular/forms'; +import {MatCard, MatCardContent, MatCardModule, MatCardTitle} from '@angular/material/card'; +import {AbstractNode} from '../preview-selection/models/metadataTree.model'; +import {NgIf, NgForOf} from '@angular/common'; + + +@Component({ + selector: 'app-doc-card', + standalone: true, + imports: [ + MatCheckbox, + FormsModule, + MatCardContent, + MatCardTitle, + MatCard, + NgIf, + NgForOf, + MatCardModule, + MatCheckboxModule + ], + templateUrl: './doc-card.component.html', + styleUrl: './doc-card.component.scss' +}) +export class DocCardComponent { + @Input() node!: AbstractNode; + @Input() nested = false; + @Output() toggle = new EventEmitter(); + + + get kvRows(): { k: string; v: any; type: string }[] { + if (!this.node.children?.length) { + return []; + } + + return this.node.children.map(c => ({ + k: c.name, + v: c.properties?.sample, + type: c.type + })); + } + + + get subNodes(): AbstractNode[] { + return this.node.children?.filter(c => c.children?.length) ?? []; + } + + prettifyKey(k: string): string { + return k.replace(/^idx\d+:?/, '').replace(/_/g, ' '); + } +} + diff --git a/src/app/views/preview-selection/metadata-tree/metadata-tree.component.html b/src/app/views/preview-selection/metadata-tree/metadata-tree.component.html new file mode 100644 index 00000000..364e7ff0 --- /dev/null +++ b/src/app/views/preview-selection/metadata-tree/metadata-tree.component.html @@ -0,0 +1,45 @@ + + + + + {{ node.name }} + + : {{ node.properties.sample }} + + + + + {{ node.name }} + + + + + + + + + + + + + + diff --git a/src/app/views/preview-selection/metadata-tree/metadata-tree.component.scss b/src/app/views/preview-selection/metadata-tree/metadata-tree.component.scss new file mode 100644 index 00000000..c522edac --- /dev/null +++ b/src/app/views/preview-selection/metadata-tree/metadata-tree.component.scss @@ -0,0 +1,13 @@ +.aliases { + margin-left: 30px; +} + +.node-added { + color: #7FFF00; + font-weight: 600; +} + +.node-removed { + color: #FF2400; + text-decoration: line-through; +} diff --git a/src/app/views/preview-selection/metadata-tree/metadata-tree.component.ts b/src/app/views/preview-selection/metadata-tree/metadata-tree.component.ts new file mode 100644 index 00000000..41a5d63c --- /dev/null +++ b/src/app/views/preview-selection/metadata-tree/metadata-tree.component.ts @@ -0,0 +1,107 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Node} from '../preview-selection.component'; +import {FormsModule} from '@angular/forms'; +import {AbstractNode} from '../models/metadataTree.model'; + +@Component({ + selector: 'app-metadata-tree', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './metadata-tree.component.html', + styleUrl: './metadata-tree.component.scss' +}) +export class MetadataTreeComponent { + @Input() node!: AbstractNode; + @Input() path = ''; + @Output() columnToggle = new EventEmitter<{ fullKey: string; checked: boolean; diff?; type? }>(); + @Output() autoSelectRemoved = new EventEmitter(); + + ngOnInit(): void { + const removed = new Set(); + this.collectRemoved(this.node, [], removed); + if (removed.size) { + this.autoSelectRemoved.emit([...removed]); + } + } + + // Automatically collect removed metadata. + private collectRemoved(n: AbstractNode, path: string[], out: Set): void { + const next = [...path, n.name]; + if ((n as any).type === 'ghost') { + out.add(next.join('.')); + } + n.children?.forEach(c => this.collectRemoved(c, next, out)); + } + + // (Un)selecting metadata. + toggleColumn(fullKey: string, checked: boolean, diff?: any, type?: any) { + this.columnToggle.emit({fullKey, checked, diff, type}); + } + + onNodeToggle(checked: boolean): void { + if (this.isRemovedOrGhost(this.node)) { + (this.node as any).isSelected = false; + return; + } + const parts = this.path ? this.path.split('.') : []; + this.toggleRecursive(this.node, parts, checked); + } + + private toggleRecursive(n: AbstractNode, pathParts: string[], checked: boolean): void { + if (this.isRemovedOrGhost(n)) { + (n as any).isSelected = false; + return; + } + + (n as any).isSelected = checked; + + const next = [...pathParts, n.name]; + const isLeaf = !n.children || n.children.length === 0; + + if (isLeaf && (n as any).type === 'column') { + const fullKey = next.join('.'); + const diff = (n as any).properties?.['diff']; + this.columnToggle.emit({fullKey, checked, diff, type: 'column'}); + return; + } + n.children?.forEach(c => this.toggleRecursive(c, next, checked)); + } + + // TODO Alias names could be display with the physical name + getAlias(node: AbstractNode): string { + return (node as any).properties?.['alias'] ?? ''; + } + + setAlias(node: AbstractNode, value: string): void { + const trimmed = (value ?? '').trim(); + if (!trimmed) { + if ((node as any).properties) { + delete (node as any).properties['alias']; + } + return; + } + (node as any).properties = (node as any).properties ?? {}; + (node as any).properties['alias'] = trimmed; + } + + // In case when a metadata tree occurs from the observer. + // Correctly displaying added or removed metadata. + getNodeClass(node: AbstractNode): string { + const props = (node as any).properties || {}; + if (props['diff'] === 'ADDED') { + return 'node-added'; + } + if (props['diff'] === 'REMOVED' || (node as any).type === 'ghost') { + return 'node-removed'; + } + return ''; + } + + get isRemovedOrGhost() { + return (node: AbstractNode) => { + const props = (node as any).properties || {}; + return props['diff'] === 'REMOVED' || (node as any).type === 'ghost'; + }; + } +} diff --git a/src/app/views/preview-selection/models/metadataTree.model.ts b/src/app/views/preview-selection/models/metadataTree.model.ts new file mode 100644 index 00000000..5239e97e --- /dev/null +++ b/src/app/views/preview-selection/models/metadataTree.model.ts @@ -0,0 +1,80 @@ +/** + * AbstractNode interface with general implementation. + */ + +export class Node implements AbstractNode { + type: string; + name: string; + children: AbstractNode[] = []; + properties: { [key: string]: any } = {}; + isSelected?: boolean; + + jsonPath?: string; + cardCandidate?: boolean; + valueType?: string; + + + constructor(type: string, name: string) { + this.type = type; + this.name = name; + } + + addChild(node: AbstractNode): void { + this.children.push(node); + } + + addProperty(key: string, value: any): void { + this.properties[key] = value; + } + + getProperty(key: string): string { + return this.properties[key]; + } +} + + +export interface ColumnToggleEvent { + fullKey: string; + checked: boolean; + diff?: 'ADDED' | 'REMOVED'; + type?: 'ghost' | 'table' | 'column'; +} + +export interface AbstractNode { + type: string; + name: string; + children?: AbstractNode[]; + properties?: { [key: string]: any }; + isSelected?: boolean; + jsonPath?: string; + cardCandidate?: boolean; + valueType?: string; + + addChild(node: AbstractNode): void; + + addProperty(key: string, value: any): void; + + getProperty(key: string): string | null; +} + +/** + * model for handling preview data + */ +export class PreviewResult { + metadata: string; + preview: any[]; + history?: ChangeLogView[]; +} + +export interface ChangeLogView { + adapterName: string; + timestamp: string; + severity: ChangeStatus; + messages: string[]; +} + +export enum ChangeStatus { + CRITICAL = 'CRITICAL', + WARNING = 'WARNING', + OK = 'OK' +} diff --git a/src/app/views/preview-selection/preview-selection.component.html b/src/app/views/preview-selection/preview-selection.component.html new file mode 100644 index 00000000..bf2d9a2a --- /dev/null +++ b/src/app/views/preview-selection/preview-selection.component.html @@ -0,0 +1,84 @@ + + + + Preview Selection + + + + + + + + + + + + + + + + + + + + Preview + + + + {{ tableName }} + + + + {{ col }} + + + + {{ row[col] }} + + + + + + + + + + + + + + + + + {{ entry.timestamp }} — {{ entry.severity }} + + + {{ entry.messages[0] || '' }} + + + {{ entry.messages | json }} + + + + + + + + + + diff --git a/src/app/views/preview-selection/preview-selection.component.scss b/src/app/views/preview-selection/preview-selection.component.scss new file mode 100644 index 00000000..664623e0 --- /dev/null +++ b/src/app/views/preview-selection/preview-selection.component.scss @@ -0,0 +1,131 @@ +html, body { + margin: 0; + padding: 0; +} + +.preview-container { + position: fixed; + top: 64px; + left: 0; + width: 100vw; + height: calc(100vh - 130px); + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + background: #fff; + padding: 1rem; + box-shadow: 0 1px 4px rgba(0, 0, 0, .1); + z-index: 2; + + h2 { + margin: 0; + } + + .btn-circle { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + background: #1976d2; + color: #fff; + border: none; + cursor: pointer; + + &:hover { + background: #0d47a1; + } + } +} + +.main-wrapper { + flex: 1; + width: 100%; + display: flex; + overflow: hidden; +} + +.main-content { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; +} + +.sidebar { + width: 280px; + background: #022E3D; + color: #fff; + padding: .5rem; + overflow-y: auto; + transition: width .3s ease; + + &.collapsed { + width: 0; + padding: 0; + overflow: hidden; + } + + ::ng-deep .mat-card-title, + ::ng-deep .mat-card-content, + ::ng-deep .mat-checkbox-label { + color: #fff !important; + } + + .meta-card { + background: transparent; + box-shadow: none; + height: 100%; + } +} + +.preview { + flex: 1; + background: #fff; + padding: 1rem; + display: flex; + flex-direction: column; + overflow-y: auto; + + .preview-card { + margin-bottom: .5rem; + box-shadow: none; + } + + .preview-table-area { + flex: 1; + overflow-y: auto; + } + + .log-toggle { + max-height: 40px; + overflow: hidden; + transition: max-height .3s ease; + + &.expanded { + max-height: 250px; + } + + mat-expansion-panel { + background: #f9f9f9; + } + } +} + +.actions { + width: 100%; + background: #fff; + padding: .5rem 1rem; + text-align: right; + + button { + margin-left: .5rem; + } +} diff --git a/src/app/views/preview-selection/preview-selection.component.ts b/src/app/views/preview-selection/preview-selection.component.ts new file mode 100644 index 00000000..534e2c00 --- /dev/null +++ b/src/app/views/preview-selection/preview-selection.component.ts @@ -0,0 +1,361 @@ +import {Component, inject} from '@angular/core'; +import {ButtonCloseDirective, ButtonDirective, ColComponent, RowComponent} from '@coreui/angular'; +import {Router, RouterLink} from '@angular/router'; +import {AbstractNode, ChangeLogView, ChangeStatus} from './models/metadataTree.model'; +import {ColumnToggleEvent} from './models/metadataTree.model'; +import {CommonModule} from '@angular/common'; +import {MetadataTreeComponent} from './metadata-tree/metadata-tree.component'; +import {AdapterModel, AdapterType, PolyMap} from '../adapters/adapter.model'; +import {DeployMode} from '../../models/catalog.model'; +import {CrudService} from '../../services/crud.service'; +import {PreviewNavigationService} from '../../services/preview-navigation.service'; +import {DocCardComponent} from '../doc-card/doc-card.component'; +import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; +import {MatTab, MatTabGroup} from '@angular/material/tabs'; +import {MatIcon} from '@angular/material/icon'; +import { + MatAccordion, + MatExpansionPanel, + MatExpansionPanelDescription, MatExpansionPanelHeader, + MatExpansionPanelTitle +} from '@angular/material/expansion'; +import {ToasterService} from '../../components/toast-exposer/toaster.service'; + +@Component({ + selector: 'app-preview-selection', + standalone: true, + imports: [ + ButtonDirective, + CommonModule, + MetadataTreeComponent, + MatCard, + MatCardHeader, + MatCardTitle, + MatCardContent, + MatAccordion, + MatExpansionPanel, + MatExpansionPanelTitle, + MatExpansionPanelDescription, + MatExpansionPanelHeader + ], + templateUrl: './preview-selection.component.html', + styleUrl: './preview-selection.component.scss' +}) +export class PreviewSelectionComponent { + + private readonly _nav = inject(PreviewNavigationService); + private readonly _crud = inject(CrudService); + private readonly _router = inject(Router); + private readonly _toast = inject(ToasterService); + + private ctx = null; + mode!: null; + + formData: FormData = new FormData(); + pendingFiles!: Map; + adapter: AdapterModel; + + + metadata: AbstractNode = null; + preview: Record | any[] = {}; + adapterInfo: string = null; + cards: AbstractNode[] = []; + + + selected: Set = new Set(); + + added: Set = new Set(); + removed: Set = new Set(); + changeLog: ChangeLogView[] = []; + readonly ChangeStatus = ChangeStatus; + + showSaveButton = false; + showMetadata = true; + showLogs = false; + ready = false; + + ngOnInit() { + this.ctx = this._nav.context; + + if (!this.ctx) { + console.error('Preview-context not found !'); + return; + } + + this.changeLog = this.ctx.changeLog ?? []; + console.log(this.changeLog); + this.mode = this.ctx.mode; + console.log(this.mode); + + if (this.mode === 'deploy') { + this.formData = this.ctx.formData; + this.pendingFiles = this.ctx.files; + this.metadata = this.ctx.metadata; + this.preview = this.ctx.preview; + this.adapter = this.ctx.adapter; + } else { + this.metadata = this.ctx.metadata; + this.preview = this.ctx.preview; + this.adapterInfo = this.ctx.adapterInfo.uniqueName; + console.log(this.metadata); + } + + this.collectSelected(this.metadata); + + + if (this.isDocumentAdapter) { + this.cards = []; + this.cards = this.findCards(this.metadata); + } + + + this.ready = true; + + } + + close(): void { + this.showSaveButton = false; + this._router.navigate(['/views/adapters']); + } + + + collectSelected(node: AbstractNode, path: string[] = []) { + const fullPath = [...path, node.name]; + if (node.isSelected) { + this.selected.add(fullPath.join('.')); + } + node.children?.forEach(c => this.collectSelected(c, fullPath)); + } + + getPreviewKeys(): string[] { + return Object.keys(this.preview); + } + + getPreviewColumns(tableName: string): string[] { + const rows = this.preview[tableName]; + return rows && rows.length ? Object.keys(rows[0]) : []; + } + + onColumnToggle(e: ColumnToggleEvent) { + this.makeSaveButtonVisible(); + console.log(e.diff); + if (e.diff === 'ADDED') { + e.checked ? this.added.add(e.fullKey) : this.added.delete(e.fullKey); + console.log(this.added); + } else { + e.checked ? this.selected.add(e.fullKey) : this.selected.delete(e.fullKey); + console.log(this.selected); + } + } + + onAutoSelect(paths: string[]): void { + paths.forEach(p => this.removed.add(p)); + console.log(this.removed); + } + + private collectCards(n: AbstractNode) { + console.log(n); + if (n.cardCandidate === true) { + this.cards.push(n); + } + n.children?.forEach(c => this.collectCards(c)); + } + + + sendAck(): void { + const addedPaths: string[] = Array.from(this.added); + const removedPaths: string[] = Array.from(this.removed); + + + const payload = { + uniqueName: this.adapterInfo, + addedPaths: Array.from(addedPaths), + removedPaths: Array.from(removedPaths) + }; + console.log(payload.addedPaths); + console.log(payload.removedPaths); + + + this._crud.metadataAck(payload).subscribe( + { + next: () => { + this._toast.success('Successfully acknowledged!'); + this.close(); + }, + error: err => { + this._toast.error('Error during acknowledgement!'); + this.close(); + } + } + ); + } + + sendConfigChange(): void { + const selected: string[] = Array.from(this.selected); + + const payload = { + uniqueName: this.adapterInfo, + selected: selected + }; + + + console.log(selected); + this._crud.setMetaConfiguration(payload).subscribe({ + next: () => { + this._toast.success('Config changed successfully!'); + this.close(); + }, + error: err => { + this._toast.error('Config changed failed!'); + this.close(); + } + }); + } + + makeSaveButtonVisible(): void { + if (this.showSaveButton === false) { + this.showSaveButton = true; + } + } + + get isDocumentAdapter(): boolean { + return this.adapter?.adapterName.toLowerCase() === 'json'; + } + + onCardToggle(node: Node) { + (node as any).isSelected = !(node as any).isSelected; + console.log('Card toggled:', node.name, 'selected?', (node as any).isSelected); + if ((node as any).isSelected) { + this.selected.add(node.name); + } else { + this.selected.delete(node.name); + } + console.log(this.selected); + } + + private findCards(root: AbstractNode): AbstractNode[] { + const out: AbstractNode[] = []; + + function walk(n: AbstractNode) { + if ((n as any).cardCandidate) { + out.push(n as AbstractNode); + return; + } + n.children?.forEach(walk); + } + + walk(root); + return out; + } + + + sendMetadata(): void { + + const aliases: Map = new Map(); + const newFormData = new FormData(); + const files: Map = this.ctx.files; + + + for (const [field, file] of this.pendingFiles) { + if (!newFormData.has(field)) { + newFormData.append(field, file); + } + } + + + (this.adapter as any).metadata = Array.from(this.selected); + + this.collectAliases(this.metadata, aliases); + (this.adapter as any).columnAliases = aliases; + + const firstFileName = files.keys().next().value as string; + + if (this.adapter.settings.has('directory')) { + const fileNames = [firstFileName]; + this.adapter.settings.set('directory', JSON.stringify(fileNames)); + } + + + newFormData.forEach((value, key) => { + console.log(`${key}: ${value}`); + }); + + this._crud.createAdapter(this.adapter, newFormData).subscribe({ + next: res => { + this._toast.success('Adapter deployed!'); + this.close(); + }, + error: err => { + console.error(err); + this._toast.error('Failed to deploy adapter!'); + } + }); + + } + + private collectAliases(node: AbstractNode, out: Map, path: string = '') { + const currentPath = path ? `${path}.${node.name}` : node.name; + + if (node.type === 'column') { + const alias = node.properties?.['alias']; + if (alias) { + out[currentPath] = alias; + } + } + node.children?.forEach(c => this.collectAliases(c, out, currentPath)); + } + + getSeverityClass(sev: ChangeStatus): string { + switch (sev) { + case ChangeStatus.CRITICAL: + return 'log-critical'; + case ChangeStatus.WARNING: + return 'log-warning'; + default: + return 'log-ok'; + } + } + +} + + + +export class Node implements AbstractNode { + type: string; + name: string; + children: AbstractNode[] = []; + properties: { [key: string]: any } = {}; + isSelected?: boolean; + + jsonPath?: string; + cardCandidate?: boolean; + valueType?: string; + + + constructor(type: string, name: string) { + this.type = type; + this.name = name; + } + + addChild(node: AbstractNode): void { + this.children.push(node); + } + + addProperty(key: string, value: any): void { + this.properties[key] = value; + } + + getProperty(key: string): string { + return this.properties[key]; + } +} + + +export function reviveTree(raw: any): AbstractNode { + if (raw.children) { + raw.children = raw.children.map(reviveTree); + } + return raw as AbstractNode; +} + + diff --git a/src/app/views/views-routing.module.ts b/src/app/views/views-routing.module.ts index e40afa30..4bf79b83 100644 --- a/src/app/views/views-routing.module.ts +++ b/src/app/views/views-routing.module.ts @@ -13,6 +13,7 @@ import {QueryInterfacesComponent} from './query-interfaces/query-interfaces.comp import {NotebooksComponent} from '../plugins/notebooks/components/notebooks.component'; import {UnsavedChangesGuard} from '../plugins/notebooks/services/unsaved-changes.guard'; import {DockerconfigComponent} from './dockerconfig/dockerconfig.component'; +import {PreviewSelectionComponent} from './preview-selection/preview-selection.component'; const routes: Routes = [ { @@ -168,6 +169,14 @@ const routes: Routes = [ title: 'QueryInterfaces' } }, + { + path: 'preview-selection', + component: PreviewSelectionComponent, + data : { + title: 'Preview Selection', + breadcrumb: 'Preview' + } + }, { path: 'notebooks', children: [ diff --git a/src/app/views/views.module.ts b/src/app/views/views.module.ts index 057b8f2d..c2e3094d 100644 --- a/src/app/views/views.module.ts +++ b/src/app/views/views.module.ts @@ -26,6 +26,7 @@ import {TypeaheadModule} from 'ngx-bootstrap/typeahead'; import {TooltipModule} from 'ngx-bootstrap/tooltip'; import {BsDropdownModule} from 'ngx-bootstrap/dropdown'; import {ModalModule} from 'ngx-bootstrap/modal'; +import {PreviewSelectionComponent} from './preview-selection/preview-selection.component'; import {ProgressbarModule} from 'ngx-bootstrap/progressbar'; import {PopoverModule} from 'ngx-bootstrap/popover'; import {QueryInterfacesComponent} from './query-interfaces/query-interfaces.component'; @@ -158,7 +159,8 @@ import {PolyalgComponent, ScrollToDirective} from './querying/polyalg/polyalg.co ProgressComponent, ProgressBarComponent, CollapseDirective, - ButtonToolbarComponent + ButtonToolbarComponent, + PreviewSelectionComponent ], declarations: [ EditColumnsComponent, @@ -187,7 +189,7 @@ import {PolyalgComponent, ScrollToDirective} from './querying/polyalg/polyalg.co DockerconfigComponent, EditEntityComponent, PolyalgComponent, - ScrollToDirective + ScrollToDirective, ], exports: [] }) diff --git a/src/scss/style.scss b/src/scss/style.scss index 6d29fa04..ef4c7387 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -104,4 +104,4 @@ .subitems { width: 200px !important; } -} \ No newline at end of file +}
{{ entry.messages | json }}