feature updates

This commit is contained in:
NinjaPug
2025-07-25 09:19:26 -04:00
parent 1446c2a553
commit d698dddfe7
12 changed files with 1376 additions and 163 deletions

119
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "docker-registry-browser",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "docker-registry-browser",
"version": "1.1.0",
"version": "1.2.0",
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/cdk": "^17.0.0",
@@ -18,6 +18,8 @@
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
@@ -4600,7 +4602,7 @@
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@@ -4621,7 +4623,7 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -4661,7 +4663,7 @@
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@@ -4687,7 +4689,7 @@
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -4700,14 +4702,13 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/http-proxy": {
"version": "1.17.16",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -4724,14 +4725,13 @@
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.115",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.115.tgz",
"integrity": "sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -4751,14 +4751,14 @@
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/retry": {
@@ -4772,7 +4772,7 @@
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@@ -4793,7 +4793,7 @@
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@@ -5030,7 +5030,6 @@
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
@@ -5044,7 +5043,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -5280,7 +5278,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true,
"license": "MIT"
},
"node_modules/autoprefixer": {
@@ -5494,7 +5491,6 @@
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@@ -5519,7 +5515,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
@@ -5529,7 +5524,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/bonjour-service": {
@@ -5565,7 +5559,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -5643,7 +5636,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -5750,7 +5742,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -5764,7 +5755,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -6141,7 +6131,6 @@
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
@@ -6154,7 +6143,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -6183,7 +6171,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"dev": true,
"license": "MIT"
},
"node_modules/copy-anything": {
@@ -6544,7 +6531,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -6554,7 +6540,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8",
@@ -6681,7 +6666,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -6703,7 +6687,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true,
"license": "MIT"
},
"node_modules/electron-to-chromium": {
@@ -6734,7 +6717,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -6910,7 +6892,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6920,7 +6901,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6937,7 +6917,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -7013,7 +6992,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
@@ -7101,7 +7079,6 @@
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7111,7 +7088,6 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true,
"license": "MIT"
},
"node_modules/events": {
@@ -7159,7 +7135,6 @@
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@@ -7206,7 +7181,6 @@
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7216,7 +7190,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
@@ -7226,7 +7199,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -7236,7 +7208,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
@@ -7255,14 +7226,12 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/express/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -7366,7 +7335,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -7486,7 +7454,6 @@
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"dev": true,
"funding": [
{
"type": "individual",
@@ -7537,7 +7504,6 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7561,7 +7527,6 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7630,7 +7595,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -7660,7 +7624,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -7695,7 +7658,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -7784,7 +7746,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7834,7 +7795,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7865,7 +7825,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -7995,7 +7954,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
@@ -8012,7 +7970,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -8029,7 +7986,6 @@
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.0",
@@ -8058,7 +8014,6 @@
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.8.tgz",
"integrity": "sha512-/iazaeFPmL8KLA6QB7DFAU4O5j+9y/TA0D019MbLtPuFI56VK4BXFzM6j6QS9oGpScy8IIDH4S2LHv3zg/63Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.8",
@@ -8107,7 +8062,6 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
@@ -8283,7 +8237,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
@@ -8423,7 +8376,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -8443,7 +8395,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -8473,7 +8424,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -8483,7 +8433,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -9246,7 +9195,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9256,7 +9204,6 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -9279,7 +9226,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -9306,7 +9252,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -9316,7 +9261,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -9330,7 +9274,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -9358,7 +9301,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -9368,7 +9310,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -9674,7 +9615,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/multicast-dns": {
@@ -10135,7 +10075,6 @@
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10155,7 +10094,6 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
@@ -10478,7 +10416,6 @@
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -10549,7 +10486,6 @@
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": {
@@ -10888,7 +10824,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
@@ -10902,7 +10837,6 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
@@ -10941,7 +10875,6 @@
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
@@ -10988,7 +10921,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -10998,7 +10930,6 @@
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@@ -11245,7 +11176,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true,
"license": "MIT"
},
"node_modules/resolve": {
@@ -11466,7 +11396,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -11507,7 +11436,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/safevalues": {
@@ -11664,7 +11592,6 @@
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
@@ -11689,7 +11616,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
@@ -11699,14 +11625,12 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/send/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"license": "MIT",
"bin": {
"mime": "cli.js"
@@ -11719,7 +11643,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -11815,7 +11738,6 @@
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
@@ -11831,7 +11753,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -11859,7 +11780,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true,
"license": "ISC"
},
"node_modules/shallow-clone": {
@@ -11915,7 +11835,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -11935,7 +11854,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -11952,7 +11870,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -11971,7 +11888,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -12688,7 +12604,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -12701,7 +12616,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
@@ -12755,7 +12669,6 @@
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
@@ -12819,7 +12732,6 @@
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -12908,7 +12820,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -12976,7 +12887,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
@@ -13017,7 +12927,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"

View File

@@ -1,6 +1,6 @@
{
"name": "docker-registry-browser",
"version": "1.1.0",
"version": "1.2.0",
"scripts": {
"ng": "ng",
"start": "node generate-proxy-config.js && ng serve --proxy-config proxy.conf.json",

View File

@@ -42,7 +42,14 @@ const proxyOptions = {
console.log(`[PROXY] ${req.method} ${req.url} -> ${registryUrl}${req.url.replace('/api', '')}`);
// Ensure proper headers
proxyReq.setHeader('User-Agent', 'Docker-Registry-Browser/1.0');
proxyReq.setHeader('User-Agent', 'Docker-Registry-Browser/1.2');
// Handle DELETE operations with special logging
if (req.method === 'DELETE') {
console.log(`[DELETE OPERATION] Deleting: ${req.url}`);
// Add Docker-specific headers for delete operations
proxyReq.setHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
}
},
onProxyRes: (proxyRes, req, res) => {
@@ -86,6 +93,29 @@ app.options('/api/*', (req, res) => {
res.sendStatus(204);
});
// Delete operation validation middleware
app.delete('/api/v2/:repository/manifests/:reference', (req, res, next) => {
const { repository, reference } = req.params;
console.log(`[DELETE VALIDATION] Repository: ${repository}, Reference: ${reference}`);
// Log the delete operation for audit purposes
console.log(`[AUDIT] DELETE request for ${repository}:${reference} at ${new Date().toISOString()}`);
// Continue to proxy
next();
});
app.delete('/api/v2/:repository/tags/:tag', (req, res, next) => {
const { repository, tag } = req.params;
console.log(`[DELETE VALIDATION] Repository: ${repository}, Tag: ${tag}`);
// Log the delete operation for audit purposes
console.log(`[AUDIT] DELETE request for tag ${repository}:${tag} at ${new Date().toISOString()}`);
// Continue to proxy
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({

View File

@@ -3,6 +3,7 @@
<mat-toolbar color="primary" class="app-toolbar">
<mat-icon>storage</mat-icon>
<span class="toolbar-title">Docker Registry Browser</span>
<span class="version-badge">v1.2.0</span>
<span class="spacer"></span>
<!-- Menu Button -->
@@ -14,6 +15,11 @@
<mat-icon>cloud_upload</mat-icon>
<span>Push Image Commands</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="searchFilterService.clearSearchHistory()">
<mat-icon>history</mat-icon>
<span>Clear Search History</span>
</button>
</mat-menu>
<button mat-raised-button color="accent" (click)="loadRepositories()" [disabled]="loading">
@@ -33,10 +39,28 @@
</mat-card-header>
<mat-card-content>
<p>Connected to: <code>{{ registryHost }}</code></p>
<p><small>Using development proxy to bypass CORS restrictions</small></p>
<p><small>Using Node.js proxy to bypass CORS restrictions</small></p>
<div class="stats" *ngIf="repositories.length > 0">
<span class="stat-item">
<mat-icon>folder</mat-icon>
{{ repositories.length }} repositories
</span>
<span class="stat-item" *ngIf="favorites.length > 0">
<mat-icon>star</mat-icon>
{{ favorites.length }} favorites
</span>
</div>
</mat-card-content>
</mat-card>
<!-- Advanced Search Component -->
<app-advanced-search
[filter]="searchFilter"
[sortOption]="sortOption"
(filterChange)="onFilterChange($event)"
(sortChange)="onSortChange($event)">
</app-advanced-search>
<!-- Copy Success Message -->
<div *ngIf="copyMessage" class="copy-message">
<mat-icon>check_circle</mat-icon>
@@ -49,6 +73,10 @@
<div class="error-content">
<mat-icon color="warn">error</mat-icon>
<span>{{ error }}</span>
<button mat-button color="primary" (click)="retryConnection()" *ngIf="!loading">
<mat-icon>refresh</mat-icon>
Retry
</button>
</div>
</mat-card-content>
</mat-card>
@@ -60,17 +88,14 @@
<mat-card-header>
<mat-card-title>
<mat-icon>folder</mat-icon>
Repositories ({{ filteredRepositories.length }})
Repositories
<mat-chip-listbox *ngIf="filteredRepositories.length !== repositories.length">
<mat-chip-option>{{ filteredRepositories.length }} of {{ repositories.length }}</mat-chip-option>
</mat-chip-listbox>
<span *ngIf="filteredRepositories.length === repositories.length">({{ repositories.length }})</span>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<!-- Search -->
<mat-form-field appearance="outline" class="search-field">
<mat-label>Search repositories</mat-label>
<input matInput [(ngModel)]="searchTerm" placeholder="Filter repositories...">
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
<!-- Loading -->
<div *ngIf="loading" class="loading-container">
<mat-spinner diameter="40"></mat-spinner>
@@ -81,13 +106,40 @@
<mat-nav-list *ngIf="!loading">
<mat-list-item *ngFor="let repo of filteredRepositories"
(click)="loadTags(repo)"
[class.selected]="selectedRepo?.name === repo.name">
<mat-icon matListItemIcon>folder</mat-icon>
<div matListItemTitle>{{ repo.name }}</div>
<mat-icon matListItemMeta>chevron_right</mat-icon>
[class.selected]="selectedRepo?.name === repo.name"
class="repository-item">
<mat-icon matListItemIcon [class.favorite-icon]="repo.isFavorite">
{{ repo.isFavorite ? 'star' : 'folder' }}
</mat-icon>
<div matListItemTitle class="repo-title">
{{ repo.name }}
<mat-chip-listbox *ngIf="repo.tagCount">
<mat-chip-option>{{ repo.tagCount }} tags</mat-chip-option>
</mat-chip-listbox>
</div>
<div matListItemMeta class="repo-actions">
<button mat-icon-button
(click)="toggleFavorite(repo); $event.stopPropagation()"
[matTooltip]="repo.isFavorite ? 'Remove from favorites' : 'Add to favorites'">
<mat-icon [color]="repo.isFavorite ? 'warn' : 'default'">
{{ repo.isFavorite ? 'star' : 'star_border' }}
</mat-icon>
</button>
<button mat-icon-button
color="warn"
(click)="deleteRepository(repo); $event.stopPropagation()"
matTooltip="Delete repository">
<mat-icon>delete</mat-icon>
</button>
<mat-icon>chevron_right</mat-icon>
</div>
</mat-list-item>
<mat-list-item *ngIf="filteredRepositories.length === 0 && !loading">
<div matListItemTitle class="no-data">No repositories found</div>
<div matListItemTitle class="no-data">
<mat-icon>info</mat-icon>
<span *ngIf="repositories.length === 0">No repositories found</span>
<span *ngIf="repositories.length > 0">No repositories match the current filters</span>
</div>
</mat-list-item>
</mat-nav-list>
</mat-card-content>
@@ -99,7 +151,13 @@
<mat-card-title>
<mat-icon>label</mat-icon>
Tags
<span *ngIf="selectedRepo" class="tag-count">({{ selectedRepo.name }}) - {{ tags.length }} tag{{ tags.length !== 1 ? 's' : '' }}</span>
<span *ngIf="selectedRepo" class="tag-count">
({{ selectedRepo.name }}) -
<mat-chip-listbox *ngIf="filteredTags.length !== tags.length">
<mat-chip-option>{{ filteredTags.length }} of {{ tags.length }}</mat-chip-option>
</mat-chip-listbox>
<span *ngIf="filteredTags.length === tags.length">{{ tags.length }} tag{{ tags.length !== 1 ? 's' : '' }}</span>
</span>
</mat-card-title>
</mat-card-header>
<mat-card-content>
@@ -122,9 +180,14 @@
<div *ngIf="selectedRepo && !loading" class="tags-container">
<div class="tags-list-wrapper">
<mat-nav-list>
<mat-list-item *ngFor="let tag of tags" class="tag-item">
<mat-list-item *ngFor="let tag of filteredTags" class="tag-item">
<mat-icon matListItemIcon>label</mat-icon>
<div matListItemTitle class="tag-name">{{ tag.name }}</div>
<div matListItemTitle class="tag-name">
{{ tag.name }}
<mat-chip-listbox *ngIf="tag.size">
<mat-chip-option>{{ formatBytes(tag.size) }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div matListItemMeta class="tag-actions">
<button mat-icon-button
color="accent"
@@ -138,10 +201,20 @@
(click)="copyToClipboard(getDockerPullCommand(selectedRepo!.name, tag.name))">
<mat-icon>content_copy</mat-icon>
</button>
<button mat-icon-button
color="warn"
matTooltip="Delete tag"
(click)="deleteTag(selectedRepo!, tag)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-list-item>
<mat-list-item *ngIf="tags.length === 0">
<div matListItemTitle class="no-data">No tags found</div>
<mat-list-item *ngIf="filteredTags.length === 0">
<div matListItemTitle class="no-data">
<mat-icon>info</mat-icon>
<span *ngIf="tags.length === 0">No tags found</span>
<span *ngIf="tags.length > 0">No tags match the current filters</span>
</div>
</mat-list-item>
</mat-nav-list>
</div>
@@ -258,7 +331,7 @@
</div>
<!-- Docker Pull Commands -->
<mat-expansion-panel *ngIf="tags.length > 0" class="pull-commands">
<mat-expansion-panel *ngIf="filteredTags.length > 0" class="pull-commands">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon>code</mat-icon>
@@ -266,7 +339,7 @@
</mat-panel-title>
</mat-expansion-panel-header>
<div class="commands-content">
<div *ngFor="let tag of tags" class="command-item">
<div *ngFor="let tag of filteredTags" class="command-item">
<div class="command-text">
<code>{{ getDockerPullCommand(selectedRepo!.name, tag.name) }}</code>
</div>

View File

@@ -23,6 +23,20 @@
}
}
.version-badge {
background-color: rgba(255, 255, 255, 0.15);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
font-weight: 500;
margin-left: 8px;
@media (max-width: 480px) {
display: none;
}
}
.spacer {
flex: 1 1 auto;
}
@@ -174,6 +188,35 @@
}
}
}
.stats {
display: flex;
gap: 16px;
margin-top: 8px;
@media (max-width: 480px) {
gap: 12px;
flex-wrap: wrap;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
color: #666;
font-size: 0.9em;
@media (max-width: 480px) {
font-size: 0.85em;
}
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
}
}
.copy-message {
@@ -221,6 +264,17 @@
@media (max-width: 480px) {
font-size: 0.9rem;
flex-wrap: wrap;
}
button {
margin-left: auto;
@media (max-width: 480px) {
margin-left: 0;
margin-top: 8px;
width: 100%;
}
}
}
}
@@ -310,30 +364,80 @@
padding: 0;
mat-list-item {
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
height: 64px;
flex-shrink: 0;
@media (max-width: 768px) {
height: 56px;
}
@media (max-width: 480px) {
height: 48px;
font-size: 0.9rem;
}
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f5f5f5;
}
&.selected {
background-color: #e3f2fd;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
height: 64px;
flex-shrink: 0;
@media (max-width: 768px) {
height: 56px;
}
@media (max-width: 480px) {
height: 48px;
font-size: 0.9rem;
}
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f5f5f5;
}
&.selected {
background-color: #e3f2fd;
}
&.repository-item {
.favorite-icon {
color: #ff9800;
}
.repo-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
mat-chip-listbox {
mat-chip-option {
font-size: 0.75em;
height: 20px;
min-height: 20px;
background-color: #e3f2fd;
color: #1976d2;
}
}
}
.repo-actions {
display: flex;
align-items: center;
gap: 4px;
button {
margin: 0;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
@media (max-width: 480px) {
min-width: 32px;
width: 32px;
height: 32px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
}
}
}
&:not(.tag-item) {
@@ -379,9 +483,24 @@
.tag-name {
font-weight: 500;
flex: 1;
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 480px) {
font-size: 0.9rem;
gap: 6px;
}
mat-chip-listbox {
mat-chip-option {
font-size: 0.7em;
height: 18px;
min-height: 18px;
background-color: #f5f5f5;
color: #666;
border: 1px solid #e0e0e0;
}
}
}
@@ -899,9 +1018,25 @@
.no-data {
color: #666;
font-style: italic;
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 480px) {
font-size: 0.9rem;
gap: 6px;
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
@media (max-width: 480px) {
font-size: 16px;
width: 16px;
height: 16px;
}
}
}

View File

@@ -1,9 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { DockerRegistryService } from './services/docker-registry.service';
import { EnvironmentService } from './services/environment.service';
import { Repository, Tag, ImageDetails } from './models/registry.model';
import { SearchFilterService } from './services/search-filter.service';
import { Repository, Tag, ImageDetails, SearchFilter, SortOption, DeleteResult } from './models/registry.model';
import { PushCommandsDialogComponent } from './components/push-commands-dialog.component';
import { ConfirmationDialogComponent } from './components/confirmation-dialog.component';
@Component({
selector: 'app-root',
@@ -12,8 +15,10 @@ import { PushCommandsDialogComponent } from './components/push-commands-dialog.c
})
export class AppComponent implements OnInit {
repositories: Repository[] = [];
filteredRepositories: Repository[] = [];
selectedRepo: Repository | null = null;
tags: Tag[] = [];
filteredTags: Tag[] = [];
loading = false;
loadingDetails = false;
error = '';
@@ -22,15 +27,36 @@ export class AppComponent implements OnInit {
selectedTag: Tag | null = null;
showingDetails = false;
connectionStatus: { success: boolean; message: string } | null = null;
// New v1.2.0 properties
searchFilter: SearchFilter = {};
sortOption: SortOption = { field: 'name', direction: 'asc' };
globalSearchMode = false;
globalSearchResults: any = null;
favorites: string[] = [];
searchHistory: any[] = [];
constructor(
private registryService: DockerRegistryService,
private environmentService: EnvironmentService,
private dialog: MatDialog
private searchFilterService: SearchFilterService,
private dialog: MatDialog,
private snackBar: MatSnackBar
) {}
ngOnInit() {
this.testConnectionAndLoad();
// Subscribe to favorites changes
this.searchFilterService.favorites$.subscribe(favorites => {
this.favorites = favorites;
this.updateRepositoryFavorites();
});
// Subscribe to search history changes
this.searchFilterService.searchHistory$.subscribe(history => {
this.searchHistory = history;
});
}
get registryInfo() {
@@ -74,6 +100,12 @@ export class AppComponent implements OnInit {
this.repositories = await this.registryService.getRepositories();
console.log(`Loaded ${this.repositories.length} repositories`);
// Apply favorites to repositories
this.updateRepositoryFavorites();
// Apply current filters
this.applyFilters();
if (this.repositories.length === 0) {
this.error = 'No repositories found in the registry. Make sure your registry contains some images.';
}
@@ -98,6 +130,9 @@ export class AppComponent implements OnInit {
this.tags = await this.registryService.getTags(repo.name);
console.log(`Loaded ${this.tags.length} tags`);
// Apply current filters and sorting to tags
this.applyTagFilters();
if (this.tags.length === 0) {
this.error = `No tags found for repository ${repo.name}`;
}
@@ -191,12 +226,6 @@ export class AppComponent implements OnInit {
return labels?.[key] || 'N/A';
}
get filteredRepositories(): Repository[] {
return this.repositories.filter(repo =>
repo.name.toLowerCase().includes(this.searchTerm.toLowerCase())
);
}
// Retry connection
async retryConnection() {
await this.testConnectionAndLoad();
@@ -206,4 +235,152 @@ export class AppComponent implements OnInit {
async refresh() {
await this.loadRepositories();
}
// New v1.2.0 methods
// Filter and search methods
onFilterChange(filter: SearchFilter): void {
this.searchFilter = filter;
this.applyFilters();
// Add to search history if there's a name search
if (filter.name && filter.name.trim()) {
this.searchFilterService.addSearchToHistory(
filter.name.trim(),
this.filteredRepositories.length
);
}
}
onSortChange(sortOption: SortOption): void {
this.sortOption = sortOption;
this.applyFilters();
this.applyTagFilters();
}
applyFilters(): void {
let filtered = this.searchFilterService.filterRepositories(this.repositories, this.searchFilter);
filtered = this.searchFilterService.sortRepositories(filtered, this.sortOption);
this.filteredRepositories = filtered;
}
applyTagFilters(): void {
let filtered = [...this.tags];
// Apply name filter if searching
if (this.searchFilter.name) {
const searchTerm = this.searchFilter.name.toLowerCase();
filtered = filtered.filter(tag =>
tag.name.toLowerCase().includes(searchTerm)
);
}
// Apply sorting
filtered = this.searchFilterService.sortTags(filtered, this.sortOption);
this.filteredTags = filtered;
}
updateRepositoryFavorites(): void {
this.repositories.forEach(repo => {
repo.isFavorite = this.searchFilterService.isFavorite(repo.name);
});
}
toggleFavorite(repository: Repository): void {
this.searchFilterService.toggleFavorite(repository.name);
}
// Delete operations
async deleteTag(repository: Repository, tag: Tag): Promise<void> {
const isLastTag = await this.registryService.isLastTag(repository.name);
const dialogData = {
title: isLastTag ? 'Delete Repository' : 'Delete Tag',
message: isLastTag
? `This is the last tag in repository "${repository.name}". Deleting it will remove the entire repository.`
: `Are you sure you want to delete tag "${tag.name}" from repository "${repository.name}"?`,
confirmText: 'Delete',
cancelText: 'Cancel',
dangerous: true,
itemName: `${repository.name}:${tag.name}`
};
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
width: '500px',
data: dialogData
});
const confirmed = await dialogRef.afterClosed().toPromise();
if (!confirmed) return;
this.loading = true;
try {
const result = await this.registryService.deleteTag(repository.name, tag.name);
if (result.success) {
this.snackBar.open(result.message, 'Close', { duration: 3000 });
if (isLastTag) {
// Repository was deleted, refresh repository list
await this.loadRepositories();
this.selectedRepo = null;
this.tags = [];
this.filteredTags = [];
} else {
// Just reload tags for current repository
await this.loadTags(repository);
}
} else {
this.error = result.message;
}
} catch (error: any) {
this.error = `Delete failed: ${error.message}`;
}
this.loading = false;
}
async deleteRepository(repository: Repository): Promise<void> {
const dialogData = {
title: 'Delete Repository',
message: `Are you sure you want to delete the entire repository "${repository.name}"? This will delete ALL tags in this repository.`,
confirmText: 'Delete Repository',
cancelText: 'Cancel',
dangerous: true,
itemName: repository.name
};
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
width: '500px',
data: dialogData
});
const confirmed = await dialogRef.afterClosed().toPromise();
if (!confirmed) return;
this.loading = true;
try {
const result = await this.registryService.deleteRepository(repository.name);
if (result.success) {
this.snackBar.open(result.message, 'Close', { duration: 5000 });
// Refresh repository list
await this.loadRepositories();
// Clear selection if deleted repo was selected
if (this.selectedRepo?.name === repository.name) {
this.selectedRepo = null;
this.tags = [];
this.filteredTags = [];
}
} else {
this.error = result.message;
}
} catch (error: any) {
this.error = `Delete failed: ${error.message}`;
}
this.loading = false;
}
}

View File

@@ -18,14 +18,27 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { MatDialogModule } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { MatSelectModule } from '@angular/material/select';
import { MatChipsModule } from '@angular/material/chips';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatBadgeModule } from '@angular/material/badge';
import { AppComponent } from './app.component';
import { PushCommandsDialogComponent } from './components/push-commands-dialog.component';
import { ConfirmationDialogComponent } from './components/confirmation-dialog.component';
import { AdvancedSearchComponent } from './components/advanced-search.component';
@NgModule({
declarations: [
AppComponent,
PushCommandsDialogComponent
PushCommandsDialogComponent,
ConfirmationDialogComponent,
AdvancedSearchComponent
],
imports: [
BrowserModule,
@@ -44,7 +57,16 @@ import { PushCommandsDialogComponent } from './components/push-commands-dialog.c
MatTooltipModule,
MatDialogModule,
MatMenuModule,
MatDividerModule
MatDividerModule,
MatSelectModule,
MatChipsModule,
MatDatepickerModule,
MatNativeDateModule,
MatCheckboxModule,
MatSliderModule,
MatSnackBarModule,
MatTabsModule,
MatBadgeModule
],
providers: [],
bootstrap: [AppComponent]

View File

@@ -0,0 +1,311 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { SearchFilter, SortOption } from '../models/registry.model';
import { SearchFilterService } from '../services/search-filter.service';
@Component({
selector: 'app-advanced-search',
template: `
<mat-card class="search-card">
<mat-card-header>
<mat-card-title>
<mat-icon>search</mat-icon>
Advanced Search & Filters
</mat-card-title>
<div class="search-actions">
<button mat-icon-button (click)="toggleExpanded()" matTooltip="Toggle advanced filters">
<mat-icon>{{ expanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
<button mat-icon-button (click)="clearFilters()" matTooltip="Clear all filters">
<mat-icon>clear</mat-icon>
</button>
</div>
</mat-card-header>
<mat-card-content>
<!-- Basic Search -->
<div class="basic-search">
<mat-form-field appearance="outline" class="search-field">
<mat-label>Search repositories and tags</mat-label>
<input matInput
[(ngModel)]="filter.name"
(ngModelChange)="onFilterChange()"
placeholder="Type to search...">
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
<!-- Quick Filters -->
<div class="quick-filters">
<mat-chip-listbox>
<mat-chip-option
*ngFor="let quickFilter of quickFilters"
(click)="applyQuickFilter(quickFilter.filter)"
[selected]="isQuickFilterActive(quickFilter.filter)">
{{ quickFilter.name }}
</mat-chip-option>
</mat-chip-listbox>
</div>
</div>
<!-- Advanced Filters (Expandable) -->
<div *ngIf="expanded" class="advanced-filters">
<mat-divider></mat-divider>
<div class="filter-row">
<!-- Size Filters -->
<mat-form-field appearance="outline">
<mat-label>Min Size (MB)</mat-label>
<input matInput
type="number"
[(ngModel)]="minSizeMB"
(ngModelChange)="onSizeChange()"
placeholder="0">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Size (MB)</mat-label>
<input matInput
type="number"
[(ngModel)]="maxSizeMB"
(ngModelChange)="onSizeChange()"
placeholder="∞">
</mat-form-field>
</div>
<div class="filter-row">
<!-- Date Filters -->
<mat-form-field appearance="outline">
<mat-label>From Date</mat-label>
<input matInput
[matDatepicker]="fromPicker"
[(ngModel)]="filter.dateFrom"
(ngModelChange)="onFilterChange()">
<mat-datepicker-toggle matIconSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>To Date</mat-label>
<input matInput
[matDatepicker]="toPicker"
[(ngModel)]="filter.dateTo"
(ngModelChange)="onFilterChange()">
<mat-datepicker-toggle matIconSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
</div>
<div class="filter-row">
<!-- Architecture & OS -->
<mat-form-field appearance="outline">
<mat-label>Architecture</mat-label>
<mat-select [(ngModel)]="filter.architecture" (ngModelChange)="onFilterChange()">
<mat-option value="">Any</mat-option>
<mat-option value="amd64">AMD64</mat-option>
<mat-option value="arm64">ARM64</mat-option>
<mat-option value="arm">ARM</mat-option>
<mat-option value="386">386</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Operating System</mat-label>
<mat-select [(ngModel)]="filter.os" (ngModelChange)="onFilterChange()">
<mat-option value="">Any</mat-option>
<mat-option value="linux">Linux</mat-option>
<mat-option value="windows">Windows</mat-option>
<mat-option value="darwin">macOS</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- Sort Options -->
<div class="sort-section">
<mat-divider></mat-divider>
<div class="sort-controls">
<mat-form-field appearance="outline">
<mat-label>Sort By</mat-label>
<mat-select [(ngModel)]="sortOption.field" (ngModelChange)="onSortChange()">
<mat-option value="name">Name</mat-option>
<mat-option value="size">Size</mat-option>
<mat-option value="modified">Last Modified</mat-option>
</mat-select>
</mat-form-field>
<button mat-icon-button
(click)="toggleSortDirection()"
matTooltip="Toggle sort direction">
<mat-icon>{{ sortOption.direction === 'asc' ? 'arrow_upward' : 'arrow_downward' }}</mat-icon>
</button>
</div>
</div>
<!-- Search History -->
<div *ngIf="searchHistory.length > 0" class="search-history">
<mat-divider></mat-divider>
<h4>Recent Searches</h4>
<div class="history-chips">
<mat-chip-listbox>
<mat-chip-option
*ngFor="let search of searchHistory.slice(0, 5)"
(click)="applyHistorySearch(search)">
{{ search.query }} ({{ search.results }})
</mat-chip-option>
</mat-chip-listbox>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: [`
.search-card {
margin-bottom: 16px;
}
.search-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.basic-search {
margin-bottom: 16px;
}
.search-field {
width: 100%;
margin-bottom: 16px;
}
.quick-filters {
margin-bottom: 16px;
}
.advanced-filters {
padding-top: 16px;
}
.filter-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.filter-row mat-form-field {
flex: 1;
}
.sort-section {
padding-top: 16px;
}
.sort-controls {
display: flex;
align-items: center;
gap: 16px;
}
.sort-controls mat-form-field {
flex: 1;
}
.search-history {
padding-top: 16px;
}
.search-history h4 {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
}
.history-chips mat-chip-option {
margin-right: 8px;
margin-bottom: 8px;
}
mat-card-header {
display: flex;
align-items: center;
}
mat-card-title {
display: flex;
align-items: center;
gap: 8px;
}
`]
})
export class AdvancedSearchComponent {
@Input() filter: SearchFilter = {};
@Input() sortOption: SortOption = { field: 'name', direction: 'asc' };
@Output() filterChange = new EventEmitter<SearchFilter>();
@Output() sortChange = new EventEmitter<SortOption>();
expanded = false;
minSizeMB: number | null = null;
maxSizeMB: number | null = null;
quickFilters: Array<{ name: string; filter: SearchFilter }> = [];
searchHistory: any[] = [];
constructor(private searchFilterService: SearchFilterService) {
this.quickFilters = this.searchFilterService.getQuickFilters();
this.searchFilterService.searchHistory$.subscribe(history => {
this.searchHistory = history;
});
}
toggleExpanded(): void {
this.expanded = !this.expanded;
}
onFilterChange(): void {
this.filterChange.emit(this.filter);
}
onSortChange(): void {
this.sortChange.emit(this.sortOption);
}
onSizeChange(): void {
this.filter.minSize = this.minSizeMB ? this.minSizeMB * 1024 * 1024 : undefined;
this.filter.maxSize = this.maxSizeMB ? this.maxSizeMB * 1024 * 1024 : undefined;
this.onFilterChange();
}
toggleSortDirection(): void {
this.sortOption.direction = this.sortOption.direction === 'asc' ? 'desc' : 'asc';
this.onSortChange();
}
applyQuickFilter(quickFilter: SearchFilter): void {
this.filter = { ...this.filter, ...quickFilter };
this.updateSizeFields();
this.onFilterChange();
}
isQuickFilterActive(quickFilter: SearchFilter): boolean {
return Object.keys(quickFilter).every(key =>
this.filter[key as keyof SearchFilter] === quickFilter[key as keyof SearchFilter]
);
}
applyHistorySearch(search: any): void {
this.filter = { name: search.query };
this.onFilterChange();
}
clearFilters(): void {
this.filter = {};
this.minSizeMB = null;
this.maxSizeMB = null;
this.sortOption = { field: 'name', direction: 'asc' };
this.onFilterChange();
this.onSortChange();
}
private updateSizeFields(): void {
this.minSizeMB = this.filter.minSize ? this.filter.minSize / (1024 * 1024) : null;
this.maxSizeMB = this.filter.maxSize ? this.filter.maxSize / (1024 * 1024) : null;
}
}

View File

@@ -0,0 +1,102 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ConfirmationDialogData } from '../models/registry.model';
@Component({
selector: 'app-confirmation-dialog',
template: `
<div class="confirmation-dialog">
<h2 mat-dialog-title>
<mat-icon [color]="data.dangerous ? 'warn' : 'primary'">
{{ data.dangerous ? 'warning' : 'help_outline' }}
</mat-icon>
{{ data.title }}
</h2>
<mat-dialog-content>
<div class="dialog-content">
<p>{{ data.message }}</p>
<div *ngIf="data.itemName" class="item-highlight">
<strong>{{ data.itemName }}</strong>
</div>
<div *ngIf="data.dangerous" class="warning-section">
<mat-icon color="warn">error</mat-icon>
<span>This action cannot be undone!</span>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">
{{ data.cancelText }}
</button>
<button
mat-raised-button
[color]="data.dangerous ? 'warn' : 'primary'"
(click)="onConfirm()"
[class.dangerous-button]="data.dangerous">
{{ data.confirmText }}
</button>
</mat-dialog-actions>
</div>
`,
styles: [`
.confirmation-dialog {
min-width: 400px;
max-width: 600px;
}
.dialog-content {
padding: 16px 0;
line-height: 1.5;
}
.item-highlight {
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
margin: 16px 0;
font-family: 'Courier New', monospace;
border-left: 4px solid #2196f3;
}
.warning-section {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
padding: 12px;
background-color: #fff3e0;
border-radius: 4px;
border-left: 4px solid #ff9800;
}
.dangerous-button {
background-color: #f44336 !important;
color: white !important;
}
h2[mat-dialog-title] {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 0;
}
`]
})
export class ConfirmationDialogComponent {
constructor(
public dialogRef: MatDialogRef<ConfirmationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData
) {}
onCancel(): void {
this.dialogRef.close(false);
}
onConfirm(): void {
this.dialogRef.close(true);
}
}

View File

@@ -1,10 +1,16 @@
export interface Repository {
name: string;
tagCount?: number;
totalSize?: number;
lastModified?: string;
isFavorite?: boolean;
}
export interface Tag {
name: string;
details?: ImageDetails;
lastModified?: string;
size?: number;
}
export interface RegistryResponse {
@@ -69,4 +75,41 @@ export interface ConfigResponse {
created_by: string;
empty_layer?: boolean;
}[];
}
// New interfaces for v1.2.0 features
export interface DeleteResult {
success: boolean;
message: string;
deletedItem?: string;
}
export interface SearchFilter {
name?: string;
minSize?: number;
maxSize?: number;
dateFrom?: Date;
dateTo?: Date;
architecture?: string;
os?: string;
}
export interface SortOption {
field: 'name' | 'size' | 'created' | 'modified';
direction: 'asc' | 'desc';
}
export interface SearchHistory {
query: string;
timestamp: Date;
results: number;
}
export interface ConfirmationDialogData {
title: string;
message: string;
confirmText: string;
cancelText: string;
dangerous?: boolean;
itemName?: string;
}

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { firstValueFrom, timeout } from 'rxjs';
import { Repository, Tag, RegistryResponse, TagsResponse, ImageDetails, ManifestResponse, ConfigResponse } from '../models/registry.model';
import { Repository, Tag, RegistryResponse, TagsResponse, ImageDetails, ManifestResponse, ConfigResponse, DeleteResult } from '../models/registry.model';
import { EnvironmentService } from './environment.service';
@Injectable({
@@ -247,6 +247,142 @@ export class DockerRegistryService {
}
}
// Delete operations
async deleteTag(repositoryName: string, tag: string): Promise<DeleteResult> {
try {
// First, get the manifest to obtain the digest
const manifestUrl = `${this.baseUrl}/v2/${repositoryName}/manifests/${tag}`;
console.log(`Getting manifest for deletion: ${manifestUrl}`);
const manifestHeaders = new HttpHeaders({
'Accept': [
'application/vnd.docker.distribution.manifest.v2+json',
'application/vnd.docker.distribution.manifest.list.v2+json',
'application/vnd.oci.image.manifest.v1+json',
'application/vnd.oci.image.index.v1+json'
].join(', ')
});
const manifestResponse = await firstValueFrom(
this.http.get(manifestUrl, {
headers: manifestHeaders,
observe: 'response' // Get full response to access headers
}).pipe(timeout(this.requestTimeout))
);
// Get the digest from the Docker-Content-Digest header
const digest = manifestResponse.headers.get('Docker-Content-Digest');
if (!digest) {
throw new Error('Could not obtain manifest digest for deletion');
}
// Now delete using the digest
const deleteUrl = `${this.baseUrl}/v2/${repositoryName}/manifests/${digest}`;
console.log(`Deleting manifest: ${deleteUrl}`);
await firstValueFrom(
this.http.delete(deleteUrl, this.getRequestOptions())
.pipe(timeout(this.requestTimeout))
);
console.log(`Successfully deleted tag: ${repositoryName}:${tag}`);
return {
success: true,
message: `Tag ${tag} deleted successfully`,
deletedItem: `${repositoryName}:${tag}`
};
} catch (error) {
console.error('Delete tag failed:', error);
let errorMessage = `Failed to delete tag ${repositoryName}:${tag}`;
if (error instanceof HttpErrorResponse) {
switch (error.status) {
case 404:
errorMessage = `Tag ${tag} not found in repository ${repositoryName}`;
break;
case 405:
errorMessage = 'Delete operation not supported by this registry';
break;
case 401:
case 403:
errorMessage = 'Insufficient permissions to delete tags';
break;
default:
errorMessage = `Delete failed (${error.status}): ${error.message || 'Unknown error'}`;
}
}
return {
success: false,
message: errorMessage
};
}
}
async deleteRepository(repositoryName: string): Promise<DeleteResult> {
try {
console.log(`Attempting to delete repository: ${repositoryName}`);
// Get all tags first
const tags = await this.getTags(repositoryName);
if (tags.length === 0) {
return {
success: false,
message: 'Repository appears to be empty or already deleted'
};
}
// Delete all tags
let deletedCount = 0;
let errors: string[] = [];
for (const tag of tags) {
try {
const result = await this.deleteTag(repositoryName, tag.name);
if (result.success) {
deletedCount++;
} else {
errors.push(`${tag.name}: ${result.message}`);
}
} catch (error) {
errors.push(`${tag.name}: ${error}`);
}
}
if (deletedCount === tags.length) {
console.log(`Successfully deleted repository: ${repositoryName}`);
return {
success: true,
message: `Repository ${repositoryName} deleted successfully (${deletedCount} tags removed)`,
deletedItem: repositoryName
};
} else {
return {
success: false,
message: `Partially deleted repository ${repositoryName}: ${deletedCount}/${tags.length} tags deleted. Errors: ${errors.join(', ')}`
};
}
} catch (error) {
console.error('Delete repository failed:', error);
return {
success: false,
message: `Failed to delete repository ${repositoryName}: ${error}`
};
}
}
// Check if tag is the last one in repository
async isLastTag(repositoryName: string): Promise<boolean> {
try {
const tags = await this.getTags(repositoryName);
return tags.length <= 1;
} catch (error) {
console.error('Failed to check tag count:', error);
return false;
}
}
// Test registry connectivity via proxy
async testConnection(): Promise<{ success: boolean; message: string }> {
try {

View File

@@ -0,0 +1,275 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Repository, Tag, SearchFilter, SortOption, SearchHistory } from '../models/registry.model';
@Injectable({
providedIn: 'root'
})
export class SearchFilterService {
private searchHistoryKey = 'registry-browser-search-history';
private favoritesKey = 'registry-browser-favorites';
private maxHistoryItems = 10;
private searchHistorySubject = new BehaviorSubject<SearchHistory[]>(this.loadSearchHistory());
public searchHistory$ = this.searchHistorySubject.asObservable();
private favoritesSubject = new BehaviorSubject<string[]>(this.loadFavorites());
public favorites$ = this.favoritesSubject.asObservable();
constructor() {}
// Search History Management
addSearchToHistory(query: string, results: number): void {
const history = this.loadSearchHistory();
const newSearch: SearchHistory = {
query: query.trim(),
timestamp: new Date(),
results
};
// Remove duplicate queries
const filteredHistory = history.filter(h => h.query !== newSearch.query);
// Add new search at the beginning
const updatedHistory = [newSearch, ...filteredHistory].slice(0, this.maxHistoryItems);
this.saveSearchHistory(updatedHistory);
this.searchHistorySubject.next(updatedHistory);
}
clearSearchHistory(): void {
localStorage.removeItem(this.searchHistoryKey);
this.searchHistorySubject.next([]);
}
private loadSearchHistory(): SearchHistory[] {
try {
const stored = localStorage.getItem(this.searchHistoryKey);
if (stored) {
const parsed = JSON.parse(stored);
return parsed.map((item: any) => ({
...item,
timestamp: new Date(item.timestamp)
}));
}
} catch (error) {
console.error('Error loading search history:', error);
}
return [];
}
private saveSearchHistory(history: SearchHistory[]): void {
try {
localStorage.setItem(this.searchHistoryKey, JSON.stringify(history));
} catch (error) {
console.error('Error saving search history:', error);
}
}
// Favorites Management
toggleFavorite(repositoryName: string): void {
const favorites = this.loadFavorites();
const index = favorites.indexOf(repositoryName);
if (index === -1) {
favorites.push(repositoryName);
} else {
favorites.splice(index, 1);
}
this.saveFavorites(favorites);
this.favoritesSubject.next(favorites);
}
isFavorite(repositoryName: string): boolean {
return this.loadFavorites().includes(repositoryName);
}
private loadFavorites(): string[] {
try {
const stored = localStorage.getItem(this.favoritesKey);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Error loading favorites:', error);
return [];
}
}
private saveFavorites(favorites: string[]): void {
try {
localStorage.setItem(this.favoritesKey, JSON.stringify(favorites));
} catch (error) {
console.error('Error saving favorites:', error);
}
}
// Repository Filtering
filterRepositories(repositories: Repository[], filter: SearchFilter): Repository[] {
return repositories.filter(repo => {
// Name filter
if (filter.name && !repo.name.toLowerCase().includes(filter.name.toLowerCase())) {
return false;
}
// Size filters
if (filter.minSize && repo.totalSize && repo.totalSize < filter.minSize) {
return false;
}
if (filter.maxSize && repo.totalSize && repo.totalSize > filter.maxSize) {
return false;
}
// Date filters
if (filter.dateFrom && repo.lastModified) {
const repoDate = new Date(repo.lastModified);
if (repoDate < filter.dateFrom) {
return false;
}
}
if (filter.dateTo && repo.lastModified) {
const repoDate = new Date(repo.lastModified);
if (repoDate > filter.dateTo) {
return false;
}
}
return true;
});
}
// Repository Sorting
sortRepositories(repositories: Repository[], sortOption: SortOption): Repository[] {
return [...repositories].sort((a, b) => {
let comparison = 0;
switch (sortOption.field) {
case 'name':
comparison = this.compareStrings(a.name, b.name);
break;
case 'size':
comparison = (a.totalSize || 0) - (b.totalSize || 0);
break;
case 'created':
case 'modified':
const dateA = new Date(a.lastModified || 0);
const dateB = new Date(b.lastModified || 0);
comparison = dateA.getTime() - dateB.getTime();
break;
}
return sortOption.direction === 'desc' ? -comparison : comparison;
});
}
// Tag Sorting with Semantic Versioning Support
sortTags(tags: Tag[], sortOption: SortOption): Tag[] {
return [...tags].sort((a, b) => {
let comparison = 0;
switch (sortOption.field) {
case 'name':
// Try semantic version comparison first
comparison = this.compareVersions(a.name, b.name);
if (comparison === 0) {
// Fall back to string comparison
comparison = this.compareStrings(a.name, b.name);
}
break;
case 'size':
comparison = (a.size || 0) - (b.size || 0);
break;
case 'created':
case 'modified':
const dateA = new Date(a.lastModified || a.details?.created || 0);
const dateB = new Date(b.lastModified || b.details?.created || 0);
comparison = dateA.getTime() - dateB.getTime();
break;
}
return sortOption.direction === 'desc' ? -comparison : comparison;
});
}
// Global Search across repositories and tags
globalSearch(repositories: Repository[], allTags: Map<string, Tag[]>, query: string): {
repositories: Repository[];
tags: Array<{ repository: string; tag: Tag; }>;
} {
const lowercaseQuery = query.toLowerCase();
const matchingRepos: Repository[] = [];
const matchingTags: Array<{ repository: string; tag: Tag; }> = [];
// Search repositories
repositories.forEach(repo => {
if (repo.name.toLowerCase().includes(lowercaseQuery)) {
matchingRepos.push(repo);
}
});
// Search tags
allTags.forEach((tags, repoName) => {
tags.forEach(tag => {
if (tag.name.toLowerCase().includes(lowercaseQuery)) {
matchingTags.push({ repository: repoName, tag });
}
});
});
return { repositories: matchingRepos, tags: matchingTags };
}
// Quick filter presets
getQuickFilters(): Array<{ name: string; filter: SearchFilter; }> {
return [
{
name: 'Large Images (>500MB)',
filter: { minSize: 500 * 1024 * 1024 }
},
{
name: 'Recent (Last 30 days)',
filter: {
dateFrom: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
}
},
{
name: 'Linux AMD64',
filter: { architecture: 'amd64', os: 'linux' }
}
];
}
// Helper methods
private compareStrings(a: string, b: string): number {
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
}
private compareVersions(a: string, b: string): number {
// Simple semantic versioning comparison
const versionRegex = /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/;
const matchA = a.match(versionRegex);
const matchB = b.match(versionRegex);
if (!matchA && !matchB) return 0;
if (!matchA) return 1;
if (!matchB) return -1;
// Compare major.minor.patch
for (let i = 1; i <= 3; i++) {
const numA = parseInt(matchA[i], 10);
const numB = parseInt(matchB[i], 10);
if (numA !== numB) {
return numA - numB;
}
}
// Compare pre-release identifiers
const preA = matchA[4];
const preB = matchB[4];
if (!preA && !preB) return 0;
if (!preA) return 1; // Release versions come after pre-release
if (!preB) return -1;
return preA.localeCompare(preB);
}
}