From 664b1e66425cda19ab405b674b467a0699f32321 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Sun, 2 Feb 2025 13:17:24 +0100 Subject: [PATCH] First template of Pf6 --- README.md | 6 + server/ui/package-lock.json | 408 +++++++++++++++++- server/ui/package.json | 10 +- server/ui/src/App.css | 42 -- server/ui/src/App.tsx | 35 -- server/ui/src/app/App.css | 22 + server/ui/src/app/App.tsx | 24 ++ server/ui/src/app/Constants.ts | 0 server/ui/src/app/api/models.ts | 42 ++ server/ui/src/app/assets/avatar.svg | 18 + .../ui/src/app/assets/patternfly_avatar.jpg | Bin 0 -> 34826 bytes server/ui/src/app/assets/pfbg-icon.svg | 1 + server/ui/src/app/common/types.ts | 9 + .../ui/src/app/components/Notifications.tsx | 36 ++ .../app/components/NotificationsContext.tsx | 66 +++ .../src/app/components/PageDrawerContext.tsx | 199 +++++++++ server/ui/src/app/layout/about.tsx | 58 +++ server/ui/src/app/layout/default-layout.tsx | 34 ++ server/ui/src/app/layout/header.tsx | 250 +++++++++++ server/ui/src/app/layout/index.ts | 1 + server/ui/src/app/layout/sidebar.tsx | 109 +++++ server/ui/src/app/pages/home/home.tsx | 9 + server/ui/src/app/pages/home/index.ts | 1 + server/ui/src/assets/react.svg | 1 - server/ui/src/index.css | 68 --- server/ui/src/main.tsx | 22 +- server/ui/tsconfig.app.json | 6 +- server/ui/vite.config.ts | 5 + 28 files changed, 1324 insertions(+), 158 deletions(-) delete mode 100644 server/ui/src/App.css delete mode 100644 server/ui/src/App.tsx create mode 100644 server/ui/src/app/App.css create mode 100644 server/ui/src/app/App.tsx create mode 100644 server/ui/src/app/Constants.ts create mode 100644 server/ui/src/app/api/models.ts create mode 100644 server/ui/src/app/assets/avatar.svg create mode 100644 server/ui/src/app/assets/patternfly_avatar.jpg create mode 100644 server/ui/src/app/assets/pfbg-icon.svg create mode 100644 server/ui/src/app/common/types.ts create mode 100644 server/ui/src/app/components/Notifications.tsx create mode 100644 server/ui/src/app/components/NotificationsContext.tsx create mode 100644 server/ui/src/app/components/PageDrawerContext.tsx create mode 100644 server/ui/src/app/layout/about.tsx create mode 100644 server/ui/src/app/layout/default-layout.tsx create mode 100644 server/ui/src/app/layout/header.tsx create mode 100644 server/ui/src/app/layout/index.ts create mode 100644 server/ui/src/app/layout/sidebar.tsx create mode 100644 server/ui/src/app/pages/home/home.tsx create mode 100644 server/ui/src/app/pages/home/index.ts delete mode 100644 server/ui/src/assets/react.svg delete mode 100644 server/ui/src/index.css diff --git a/README.md b/README.md index b8715e7d..3a99868d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ XMLs basados en UBL y SUNAT cargo run --bin server ``` +## Server UI + +```shell +npm run dev --prefix server/ui +``` + ## License - [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/server/ui/package-lock.json b/server/ui/package-lock.json index 47bfe9dd..f23a5cb5 100644 --- a/server/ui/package-lock.json +++ b/server/ui/package-lock.json @@ -8,6 +8,14 @@ "name": "openubl-ui", "version": "0.0.0", "dependencies": { + "@patternfly/patternfly": "^6.1.0", + "@patternfly/react-core": "^6.1.0", + "@patternfly/react-table": "^6.1.0", + "@patternfly/react-tokens": "^6.1.0", + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", + "@tanstack/react-router": "^1.99.0", + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -696,6 +704,70 @@ "node": ">= 8" } }, + "node_modules/@patternfly/patternfly": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.1.0.tgz", + "integrity": "sha512-w+QazL8NHKkg5j01eotblsswKxQQSYB0CN3yBXQL9ScpHdp/fK8M6TqWbKZNRpf+NqhMxcH/om8eR0N/fDCJqw==", + "license": "MIT" + }, + "node_modules/@patternfly/react-core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.1.0.tgz", + "integrity": "sha512-zj0lJPZxQanXKD8ae2kYnweT0kpp1CzpHYAkaBjTrw2k6ZMfr/UPlp0/ugCjWEokBqh79RUADLkKJJPce/yoSQ==", + "license": "MIT", + "dependencies": { + "@patternfly/react-icons": "^6.1.0", + "@patternfly/react-styles": "^6.1.0", + "@patternfly/react-tokens": "^6.1.0", + "focus-trap": "7.6.2", + "react-dropzone": "^14.3.5", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, + "node_modules/@patternfly/react-icons": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.1.0.tgz", + "integrity": "sha512-V1w/j19YmOgvh72IRRf1p07k+u4M5+9P+o/IxunlF0fWzLDX4Hf+utBI11A8cRfUzpQN7eLw/vZIS3BLM8Ge3Q==", + "license": "MIT", + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, + "node_modules/@patternfly/react-styles": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.1.0.tgz", + "integrity": "sha512-JQ3zIl5SFiSB0YWVYibcUwgZdsp6Wn8hkfZ7KhtCjHFccSDdJexPOXVV1O9f2h4PfxTlY3YntZ81ZsguBx/Q7A==", + "license": "MIT" + }, + "node_modules/@patternfly/react-table": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.1.0.tgz", + "integrity": "sha512-eC8mKkvFR0btfv6yEOvE+J4gBXU8ZGe9i2RSezBM+MJaXEQt/CKRjV+SAB5EeE3PyBYKG8yYDdsOoNmaPxxvSA==", + "license": "MIT", + "dependencies": { + "@patternfly/react-core": "^6.1.0", + "@patternfly/react-icons": "^6.1.0", + "@patternfly/react-styles": "^6.1.0", + "@patternfly/react-tokens": "^6.1.0", + "lodash": "^4.17.21", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, + "node_modules/@patternfly/react-tokens": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.1.0.tgz", + "integrity": "sha512-t1UcHbOa4txczTR5UlnG4XcAAdnDSfSlCaOddw/HTqRF59pn2ks2JUu9sfnFRZ8SiAAxKRiYdX5bT7Mf4R24+w==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.0.tgz", @@ -1188,6 +1260,142 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/history": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.99.0.tgz", + "integrity": "sha512-MQS1Lg8D+1vpasEJKf4zs1sxhxbXcoejmVCZDbo0bq2wq+xVK+kRixj5Pae2kb2APzdXYga4u236GBbgCKTcnQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.0.tgz", + "integrity": "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.65.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz", + "integrity": "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.0.tgz", + "integrity": "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.66.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.0.tgz", + "integrity": "sha512-uB57wA2YZaQ2fPcFW0E9O1zAGDGSbRKRx84uMk/86VyU9jWVxvJ3Uzp+zNm+nZJYsuekCIo2opTdgNuvM3cKgA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.65.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.66.0", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.99.0.tgz", + "integrity": "sha512-MwKQxckJMdxpYwpZsItM1SCodcAKu1bqWha2s2d5/Iy1F/9XzqT8gZXpZW5fvC7+21YOuwMxvJmwbSPkpVeWRw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.99.0", + "@tanstack/react-store": "^0.7.0", + "@tanstack/router-core": "^1.99.0", + "jsesc": "^3.1.0", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.0.tgz", + "integrity": "sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.0", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.99.0.tgz", + "integrity": "sha512-/9HMagUilxdjHNfgZaRSjyMvuJ+BL8/IXLTN9NtbXOJZvB2q4Vu0GIWZJfwMiz9czXqdQ/NxtPxeG/b+YTnUOg==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.99.0", + "@tanstack/store": "^0.7.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.0.tgz", + "integrity": "sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1658,6 +1866,21 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1674,6 +1897,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1802,6 +2036,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1946,6 +2192,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2493,6 +2748,18 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2544,6 +2811,35 @@ "dev": true, "license": "ISC" }, + "node_modules/focus-trap": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.2.tgz", + "integrity": "sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", @@ -2560,6 +2856,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3311,6 +3621,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3388,6 +3710,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3441,6 +3769,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3491,7 +3840,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3775,7 +4123,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -3783,6 +4130,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3839,11 +4192,27 @@ "react": "^18.3.1" } }, + "node_modules/react-dropzone": { + "version": "14.3.5", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz", + "integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/reflect.getprototypeof": { @@ -4364,6 +4733,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4390,6 +4777,12 @@ "typescript": ">=4.8.4" } }, + "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4547,6 +4940,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", diff --git a/server/ui/package.json b/server/ui/package.json index 3adc9562..3fcbfaf5 100644 --- a/server/ui/package.json +++ b/server/ui/package.json @@ -4,12 +4,20 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { + "@patternfly/patternfly": "^6.1.0", + "@patternfly/react-core": "^6.1.0", + "@patternfly/react-table": "^6.1.0", + "@patternfly/react-tokens": "^6.1.0", + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", + "@tanstack/react-router": "^1.99.0", + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/server/ui/src/App.css b/server/ui/src/App.css deleted file mode 100644 index b9d355df..00000000 --- a/server/ui/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/server/ui/src/App.tsx b/server/ui/src/App.tsx deleted file mode 100644 index 3ed854c7..00000000 --- a/server/ui/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App diff --git a/server/ui/src/app/App.css b/server/ui/src/app/App.css new file mode 100644 index 00000000..aa6a34de --- /dev/null +++ b/server/ui/src/app/App.css @@ -0,0 +1,22 @@ +.pf-c-select__toggle:before { + border-top: var(--pf-c-select__toggle--before--BorderTopWidth) solid + var(--pf-c-select__toggle--before--BorderTopColor) !important; + border-right: var(--pf-c-select__toggle--before--BorderRightWidth) solid + var(--pf-c-select__toggle--before--BorderRightColor) !important; + border-bottom: var(--pf-c-select__toggle--before--BorderBottomWidth) solid + var(--pf-c-select__toggle--before--BorderBottomColor) !important; + border-left: var(--pf-c-select__toggle--before--BorderLeftWidth) solid + var(--pf-c-select__toggle--before--BorderLeftColor) !important; +} + +table.vertical-middle-aligned-table tr td { + vertical-align: middle; +} + +.multiple-file-upload-status-item-force-blue .pf-v6-c-progress__indicator { + background-color: #0066cc !important; +} + +.multiple-file-upload-status-item-force-blue .pf-v6-c-progress__status-icon { + color: #0066cc !important; +} diff --git a/server/ui/src/app/App.tsx b/server/ui/src/app/App.tsx new file mode 100644 index 00000000..876295b9 --- /dev/null +++ b/server/ui/src/app/App.tsx @@ -0,0 +1,24 @@ +import "./App.css"; +import React from "react"; + +import { DefaultLayout } from "./layout"; +// import { AppRoutes } from "./Routes"; +import { NotificationsProvider } from "./components/NotificationsContext"; + +import "@patternfly/patternfly/patternfly.css"; +import "@patternfly/patternfly/patternfly-addons.css"; + +const App: React.FC = () => { + return ( + // + + + {/**/} + routes + + + // + ); +}; + +export default App; diff --git a/server/ui/src/app/Constants.ts b/server/ui/src/app/Constants.ts new file mode 100644 index 00000000..e69de29b diff --git a/server/ui/src/app/api/models.ts b/server/ui/src/app/api/models.ts new file mode 100644 index 00000000..92851fb3 --- /dev/null +++ b/server/ui/src/app/api/models.ts @@ -0,0 +1,42 @@ +export type WithUiId = T & { _ui_unique_id: string }; + +/** Mark an object as "New" therefore does not have an `id` field. */ +export type New = Omit; + +export interface HubFilter { + field: string; + operator?: "=" | "!=" | "~" | ">" | ">=" | "<" | "<="; + value: + | string + | number + | { + list: (string | number)[]; + operator?: "AND" | "OR"; + }; +} + +export interface HubRequestParams { + filters?: HubFilter[]; + sort?: { + field: string; + direction: "asc" | "desc"; + }; + page?: { + pageNumber: number; // 1-indexed + itemsPerPage: number; + }; +} + +export interface HubPaginatedResult { + data: T[]; + total: number; + params: HubRequestParams; +} + +// Common + +export type VulnerabilityStatus = + | "fixed" + | "not_affected" + | "known_not_affected" + | "affected"; diff --git a/server/ui/src/app/assets/avatar.svg b/server/ui/src/app/assets/avatar.svg new file mode 100644 index 00000000..4e160bec --- /dev/null +++ b/server/ui/src/app/assets/avatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/server/ui/src/app/assets/patternfly_avatar.jpg b/server/ui/src/app/assets/patternfly_avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2af4f86eafbd9f4b64a3b70ddf9d2ff180192e27 GIT binary patch literal 34826 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 \ No newline at end of file diff --git a/server/ui/src/app/common/types.ts b/server/ui/src/app/common/types.ts new file mode 100644 index 00000000..4bbf6791 --- /dev/null +++ b/server/ui/src/app/common/types.ts @@ -0,0 +1,9 @@ +export interface Page { + page: number; + perPage: number; +} + +export interface SortBy { + index: number; + direction: "asc" | "desc"; +} diff --git a/server/ui/src/app/components/Notifications.tsx b/server/ui/src/app/components/Notifications.tsx new file mode 100644 index 00000000..2d51d05f --- /dev/null +++ b/server/ui/src/app/components/Notifications.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { + Alert, + AlertActionCloseButton, + AlertGroup, +} from "@patternfly/react-core"; +import { NotificationsContext } from "./NotificationsContext"; + +export const Notifications: React.FunctionComponent = () => { + const appContext = React.useContext(NotificationsContext); + return ( + + {appContext.notifications.map((notification) => { + return ( + { + appContext.dismissNotification(notification.title); + }} + /> + ), + })} + timeout={notification.timeout ? notification.timeout : 4000} + > + {notification.message} + + ); + })} + + ); +}; diff --git a/server/ui/src/app/components/NotificationsContext.tsx b/server/ui/src/app/components/NotificationsContext.tsx new file mode 100644 index 00000000..e21a8d35 --- /dev/null +++ b/server/ui/src/app/components/NotificationsContext.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import { AlertProps } from "@patternfly/react-core"; + +export type INotification = { + title: string; + variant: AlertProps["variant"]; + message?: React.ReactNode; + hideCloseButton?: boolean; + timeout?: number | boolean; +}; + +interface INotificationsProvider { + children: React.ReactNode; +} + +interface INotificationsContext { + pushNotification: (notification: INotification) => void; + dismissNotification: (key: string) => void; + notifications: INotification[]; +} + +const appContextDefaultValue = {} as INotificationsContext; + +const notificationDefault: Pick = { + hideCloseButton: false, +}; + +export const NotificationsContext = React.createContext( + appContextDefaultValue +); + +export const NotificationsProvider: React.FunctionComponent< + INotificationsProvider +> = ({ children }: INotificationsProvider) => { + const [notifications, setNotifications] = React.useState([]); + + const pushNotification = ( + notification: INotification, + clearNotificationDelay?: number + ) => { + setNotifications([ + ...notifications, + { ...notificationDefault, ...notification }, + ]); + setTimeout(() => setNotifications([]), clearNotificationDelay || 10000); + }; + + const dismissNotification = (title: string) => { + const remainingNotifications = notifications.filter( + (n) => n.title !== title + ); + setNotifications(remainingNotifications); + }; + + return ( + + {children} + + ); +}; diff --git a/server/ui/src/app/components/PageDrawerContext.tsx b/server/ui/src/app/components/PageDrawerContext.tsx new file mode 100644 index 00000000..90eb886a --- /dev/null +++ b/server/ui/src/app/components/PageDrawerContext.tsx @@ -0,0 +1,199 @@ +import * as React from "react"; +import { + Drawer, + DrawerActions, + DrawerCloseButton, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerPanelBody, + DrawerPanelContent, + DrawerPanelContentProps, +} from "@patternfly/react-core"; +import pageStyles from "@patternfly/react-styles/css/components/Page/page"; + +const usePageDrawerState = () => { + const [isDrawerExpanded, setIsDrawerExpanded] = React.useState(false); + const [drawerPanelContent, setDrawerPanelContent] = + React.useState(null); + const [drawerPanelContentProps, setDrawerPanelContentProps] = React.useState< + Partial + >({}); + const [drawerPageKey, setDrawerPageKey] = React.useState(""); + const drawerFocusRef = React.useRef(document.createElement("span")); + return { + isDrawerExpanded, + setIsDrawerExpanded, + drawerPanelContent, + setDrawerPanelContent, + drawerPanelContentProps, + setDrawerPanelContentProps, + drawerPageKey, + setDrawerPageKey, + drawerFocusRef: drawerFocusRef as typeof drawerFocusRef | null, + }; +}; + +type PageDrawerState = ReturnType; + +const PageDrawerContext = React.createContext({ + isDrawerExpanded: false, + setIsDrawerExpanded: () => {}, + drawerPanelContent: null, + setDrawerPanelContent: () => {}, + drawerPanelContentProps: {}, + setDrawerPanelContentProps: () => {}, + drawerPageKey: "", + setDrawerPageKey: () => {}, + drawerFocusRef: null, +}); + +// PageContentWithDrawerProvider should only be rendered as the direct child of a PatternFly Page component. +interface IPageContentWithDrawerProviderProps { + children: React.ReactNode; // The entire content of the page. See usage in client/src/app/layout/DefaultLayout. +} +export const PageContentWithDrawerProvider: React.FC< + IPageContentWithDrawerProviderProps +> = ({ children }) => { + const pageDrawerState = usePageDrawerState(); + const { + isDrawerExpanded, + drawerFocusRef, + drawerPanelContent, + drawerPanelContentProps, + drawerPageKey, + } = pageDrawerState; + return ( + +
+ drawerFocusRef?.current?.focus()} + position="right" + > + + {drawerPanelContent} + + } + > + {children} + + +
+
+ ); +}; + +let numPageDrawerContentInstances = 0; + +// PageDrawerContent can be rendered anywhere, but must have only one instance rendered at a time. +export interface IPageDrawerContentProps { + isExpanded: boolean; + onCloseClick: () => void; // Should be used to update local state such that `isExpanded` becomes false. + header?: React.ReactNode; + children: React.ReactNode; // The content to show in the drawer when `isExpanded` is true. + drawerPanelContentProps?: Partial; // Additional props for the DrawerPanelContent component. + focusKey?: string | number; // A unique key representing the object being described in the drawer. When this changes, the drawer will regain focus. + pageKey: string; // A unique key representing the page where the drawer is used. Causes the drawer to remount when changing pages. +} + +export const PageDrawerContent: React.FC = ({ + isExpanded, + onCloseClick, + header = null, + children, + drawerPanelContentProps, + pageKey: localPageKeyProp, +}) => { + const { + setIsDrawerExpanded, + drawerFocusRef, + setDrawerPanelContent, + setDrawerPanelContentProps, + setDrawerPageKey, + } = React.useContext(PageDrawerContext); + + // Warn if we are trying to render more than one PageDrawerContent (they'll fight over the same state). + React.useEffect(() => { + numPageDrawerContentInstances++; + return () => { + numPageDrawerContentInstances--; + }; + }, []); + if (numPageDrawerContentInstances > 1) { + console.warn( + `${numPageDrawerContentInstances} instances of PageDrawerContent are currently rendered! Only one instance of this component should be rendered at a time.` + ); + } + + // Lift the value of isExpanded out to the context, but derive it from local state such as a selected table row. + // This is the ONLY place where `setIsDrawerExpanded` should be called. + // To expand/collapse the drawer, use the `isExpanded` prop when rendering PageDrawerContent. + React.useEffect(() => { + setIsDrawerExpanded(isExpanded); + return () => { + setIsDrawerExpanded(false); + setDrawerPanelContent(null); + }; + }, [isExpanded, setDrawerPanelContent, setIsDrawerExpanded]); + + // Same with pageKey and drawerPanelContentProps, keep them in sync with the local prop on PageDrawerContent. + React.useEffect(() => { + setDrawerPageKey(localPageKeyProp); + return () => { + setDrawerPageKey(""); + }; + }, [localPageKeyProp, setDrawerPageKey]); + + React.useEffect(() => { + setDrawerPanelContentProps(drawerPanelContentProps || {}); + }, [drawerPanelContentProps, setDrawerPanelContentProps]); + + // If the drawer is already expanded describing app A, then the user clicks app B, we want to send focus back to the drawer. + + // TODO: This introduces a layout issue bug when clicking in between the columns of a table. + // React.useEffect(() => { + // drawerFocusRef?.current?.focus(); + // }, [drawerFocusRef, focusKey]); + + React.useEffect(() => { + const drawerHead = header === null ? children : header; + const drawerPanelBody = header === null ? null : children; + + setDrawerPanelContent( + <> + + + {drawerHead} + + + + + + {drawerPanelBody} + + ); + }, [ + children, + drawerFocusRef, + header, + isExpanded, + onCloseClick, + setDrawerPanelContent, + ]); + + return null; +}; diff --git a/server/ui/src/app/layout/about.tsx b/server/ui/src/app/layout/about.tsx new file mode 100644 index 00000000..a7426452 --- /dev/null +++ b/server/ui/src/app/layout/about.tsx @@ -0,0 +1,58 @@ +import React from "react"; + +import {AboutModal, Content, ContentVariants} from "@patternfly/react-core"; + +import backgroundImage from "@app/assets/pfbg-icon.svg"; + +interface IButtonAboutAppProps { + isOpen: boolean; + onClose: () => void; +} + +const TRANSPARENT_1x1_GIF = + "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw== "; + +export const AboutApp: React.FC = ({ + isOpen, + onClose, + }) => { + return ( + + + + OpenUBL is vendor-neutral, thought-leadering, mostly + informational collection of resources devoted to making Software + Supply Chains easier to create, manage, consume and ultimately… to + trust! + + + + For more information refer to{" "} + + OpenUBL documentation + + + + + + + Version + 99.0.0 + + + + + ); +}; diff --git a/server/ui/src/app/layout/default-layout.tsx b/server/ui/src/app/layout/default-layout.tsx new file mode 100644 index 00000000..97dd74e2 --- /dev/null +++ b/server/ui/src/app/layout/default-layout.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +import { Page, SkipToContent } from "@patternfly/react-core"; + +import { HeaderApp } from "./header"; +import { SidebarApp } from "./sidebar"; +import { Notifications } from "@app/components/Notifications"; +import { PageContentWithDrawerProvider } from "@app/components/PageDrawerContext"; + +interface DefaultLayoutProps { + children?: React.ReactNode; +} + +export const DefaultLayout: React.FC = ({ children }) => { + const pageId = "main-content-page-layout-horizontal-nav"; + const PageSkipToContent = ( + Skip to content + ); + + return ( + } + sidebar={} + isManagedSidebar + skipToContent={PageSkipToContent} + mainContainerId={pageId} + > + + {children} + + + + ); +}; diff --git a/server/ui/src/app/layout/header.tsx b/server/ui/src/app/layout/header.tsx new file mode 100644 index 00000000..77811981 --- /dev/null +++ b/server/ui/src/app/layout/header.tsx @@ -0,0 +1,250 @@ +import React, { useReducer, useState } from "react"; +// import { useNavigate } from "react-router-dom"; + +import { + Avatar, + Button, + ButtonVariant, + Divider, + Dropdown, + DropdownItem, + DropdownList, + Masthead, + MastheadBrand, + MastheadContent, + MastheadLogo, + MastheadMain, + MastheadToggle, + MenuToggle, + MenuToggleElement, + PageToggleButton, + Split, + SplitItem, + Title, + ToggleGroup, + ToggleGroupItem, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from "@patternfly/react-core"; + +import { MoonIcon, SunIcon } from "@patternfly/react-icons/"; +import EllipsisVIcon from "@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon"; +import HelpIcon from "@patternfly/react-icons/dist/esm/icons/help-icon"; +import BarsIcon from "@patternfly/react-icons/dist/js/icons/bars-icon"; + +import imgAvatar from "../assets/avatar.svg"; +import { AboutApp } from "./about"; + +export const HeaderApp: React.FC = () => { + const [isDarkTheme, setIsDarkTheme] = useState( + localStorage.getItem("isDarkTheme") === "true" ? true : false + ); + + React.useEffect(() => { + if (isDarkTheme) { + document.documentElement.classList.add("pf-v6-theme-dark"); + localStorage.setItem("isDarkTheme", "true"); + } else { + document.documentElement.classList.remove("pf-v6-theme-dark"); + localStorage.setItem("isDarkTheme", "false"); + } + }, [isDarkTheme]); + + // const navigate = useNavigate(); + + const [isAboutOpen, toggleIsAboutOpen] = useReducer((state) => !state, false); + const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false); + const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false); + + const onKebabDropdownToggle = () => { + setIsKebabDropdownOpen(!isKebabDropdownOpen); + }; + + const onKebabDropdownSelect = () => { + setIsKebabDropdownOpen(!isKebabDropdownOpen); + }; + + return ( + <> + + + + + + + + + + + + + + {/**/} + + + + OpenUBL + + + + + + + + + + {/* toolbar items to always show */} + + + + } + isSelected={!isDarkTheme} + onChange={() => setIsDarkTheme(false)} + /> + } + isSelected={isDarkTheme} + onChange={() => setIsDarkTheme(true)} + /> + + + + + {/* toolbar items to show at desktop sizes */} + + +