From d698dddfe7e9eb101f65b57ea373e7318c9b9c90 Mon Sep 17 00:00:00 2001 From: NinjaPug <36635276+programmingPug@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:19:26 -0400 Subject: [PATCH] feature updates --- package-lock.json | 119 +------ package.json | 2 +- server.js | 32 +- src/app/app.component.html | 115 +++++-- src/app/app.component.scss | 183 +++++++++-- src/app/app.component.ts | 193 ++++++++++- src/app/app.module.ts | 26 +- .../components/advanced-search.component.ts | 311 ++++++++++++++++++ .../confirmation-dialog.component.ts | 102 ++++++ src/app/models/registry.model.ts | 43 +++ src/app/services/docker-registry.service.ts | 138 +++++++- src/app/services/search-filter.service.ts | 275 ++++++++++++++++ 12 files changed, 1376 insertions(+), 163 deletions(-) create mode 100644 src/app/components/advanced-search.component.ts create mode 100644 src/app/components/confirmation-dialog.component.ts create mode 100644 src/app/services/search-filter.service.ts diff --git a/package-lock.json b/package-lock.json index 01f6c33..1af4543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 2b4a761..224e2b6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.js b/server.js index b517e5f..3bccfb3 100644 --- a/server.js +++ b/server.js @@ -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({ diff --git a/src/app/app.component.html b/src/app/app.component.html index b314f76..33899f3 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -3,6 +3,7 @@ storage Docker Registry Browser + v1.2.0 @@ -14,6 +15,11 @@ cloud_upload Push Image Commands + + @@ -60,17 +88,14 @@ folder - Repositories ({{ filteredRepositories.length }}) + Repositories + + {{ filteredRepositories.length }} of {{ repositories.length }} + + ({{ repositories.length }}) - - - Search repositories - - search - -
@@ -81,13 +106,40 @@ - folder -
{{ repo.name }}
- chevron_right + [class.selected]="selectedRepo?.name === repo.name" + class="repository-item"> + + {{ repo.isFavorite ? 'star' : 'folder' }} + +
+ {{ repo.name }} + + {{ repo.tagCount }} tags + +
+
+ + + chevron_right +
-
No repositories found
+
+ info + No repositories found + No repositories match the current filters +
@@ -99,7 +151,13 @@ label Tags - ({{ selectedRepo.name }}) - {{ tags.length }} tag{{ tags.length !== 1 ? 's' : '' }} + + ({{ selectedRepo.name }}) - + + {{ filteredTags.length }} of {{ tags.length }} + + {{ tags.length }} tag{{ tags.length !== 1 ? 's' : '' }} + @@ -122,9 +180,14 @@
- + label -
{{ tag.name }}
+
+ {{ tag.name }} + + {{ formatBytes(tag.size) }} + +
+
- -
No tags found
+ +
+ info + No tags found + No tags match the current filters +
@@ -258,7 +331,7 @@
- + code @@ -266,7 +339,7 @@
-
+
{{ getDockerPullCommand(selectedRepo!.name, tag.name) }}
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 82d0f7e..92f94ee 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -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; + } } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f3aacd5..cd6a63e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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 { + 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 { + 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; + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4adcc78..adbc49c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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] diff --git a/src/app/components/advanced-search.component.ts b/src/app/components/advanced-search.component.ts new file mode 100644 index 0000000..d7b55d4 --- /dev/null +++ b/src/app/components/advanced-search.component.ts @@ -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: ` + + + + search + Advanced Search & Filters + +
+ + +
+
+ + + + + + +
+ + +
+ + + Min Size (MB) + + + + + Max Size (MB) + + +
+ +
+ + + From Date + + + + + + + To Date + + + + +
+ +
+ + + Architecture + + Any + AMD64 + ARM64 + ARM + 386 + + + + + Operating System + + Any + Linux + Windows + macOS + + +
+
+ + +
+ +
+ + Sort By + + Name + Size + Last Modified + + + + +
+
+ + +
+ +

Recent Searches

+
+ + + {{ search.query }} ({{ search.results }}) + + +
+
+
+
+ `, + 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(); + @Output() sortChange = new EventEmitter(); + + 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; + } +} diff --git a/src/app/components/confirmation-dialog.component.ts b/src/app/components/confirmation-dialog.component.ts new file mode 100644 index 0000000..318f1b5 --- /dev/null +++ b/src/app/components/confirmation-dialog.component.ts @@ -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: ` +
+

+ + {{ data.dangerous ? 'warning' : 'help_outline' }} + + {{ data.title }} +

+ + +
+

{{ data.message }}

+ +
+ {{ data.itemName }} +
+ +
+ error + This action cannot be undone! +
+
+
+ + + + + +
+ `, + 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, + @Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData + ) {} + + onCancel(): void { + this.dialogRef.close(false); + } + + onConfirm(): void { + this.dialogRef.close(true); + } +} diff --git a/src/app/models/registry.model.ts b/src/app/models/registry.model.ts index af44981..2c0a96e 100644 --- a/src/app/models/registry.model.ts +++ b/src/app/models/registry.model.ts @@ -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; } \ No newline at end of file diff --git a/src/app/services/docker-registry.service.ts b/src/app/services/docker-registry.service.ts index 5e9b3ea..c41df46 100644 --- a/src/app/services/docker-registry.service.ts +++ b/src/app/services/docker-registry.service.ts @@ -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 { + 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 { + 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 { + 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 { diff --git a/src/app/services/search-filter.service.ts b/src/app/services/search-filter.service.ts new file mode 100644 index 0000000..afb4e02 --- /dev/null +++ b/src/app/services/search-filter.service.ts @@ -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(this.loadSearchHistory()); + public searchHistory$ = this.searchHistorySubject.asObservable(); + + private favoritesSubject = new BehaviorSubject(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, 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); + } +}