From 96d4896d65ed04e65935043aa3706265f331bc86 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Thu, 29 May 2025 13:33:30 +0200 Subject: [PATCH 1/3] Added workshops, extensions, editor-extensions and kyverno policies --- package-lock.json | 428 ++++++++++++++++++ package.json | 1 + ...kshopGrid.jsx => EducatesResourceGrid.jsx} | 55 +-- .../EducatesResourceSelectorIsland.jsx | 98 ++++ src/components/EducatesResourceTile.jsx | 31 ++ src/components/ExtensionPackageDetail.jsx | 100 ++++ src/components/FilterSidebar.astro | 35 -- src/components/InstallIsland.jsx | 27 ++ src/components/InstallModal.jsx | 79 ++++ src/components/ViewSourceIsland.jsx | 14 + src/components/ViewSourceModal.jsx | 217 +++++++++ src/components/WorkshopDetail.jsx | 100 ++++ src/components/WorkshopTile.astro | 38 -- .../educates-default-kyverno-policies.yaml | 9 + src/data/examples.yaml | 51 ++- src/data/extension-package.yaml | 23 + src/data/lab-kubernetes-fundamentals.yaml | 10 +- src/data/labs-authoring-guides.yaml | 5 +- src/data/labs-educates-showcase.yaml | 64 +++ src/data/labs-installation-guides.yaml | 30 +- src/data/labs-spring-workshops.yaml | 36 +- src/data/spring-academy-vscode-tools.yaml | 11 + src/{components => layouts}/Footer.astro | 0 src/layouts/MainLayout.astro | 4 +- src/{components => layouts}/Topbar.astro | 0 src/pages/[slug].astro | 156 +------ src/pages/about.astro | 8 +- src/pages/index.astro | 13 +- src/utils/loadWorkshops.ts | 146 +++++- 29 files changed, 1472 insertions(+), 317 deletions(-) rename src/components/{WorkshopGrid.jsx => EducatesResourceGrid.jsx} (71%) create mode 100644 src/components/EducatesResourceSelectorIsland.jsx create mode 100644 src/components/EducatesResourceTile.jsx create mode 100644 src/components/ExtensionPackageDetail.jsx delete mode 100644 src/components/FilterSidebar.astro create mode 100644 src/components/InstallIsland.jsx create mode 100644 src/components/InstallModal.jsx create mode 100644 src/components/ViewSourceIsland.jsx create mode 100644 src/components/ViewSourceModal.jsx create mode 100644 src/components/WorkshopDetail.jsx delete mode 100644 src/components/WorkshopTile.astro create mode 100644 src/data/educates-default-kyverno-policies.yaml create mode 100644 src/data/extension-package.yaml create mode 100644 src/data/spring-academy-vscode-tools.yaml rename src/{components => layouts}/Footer.astro (100%) rename src/{components => layouts}/Topbar.astro (100%) diff --git a/package-lock.json b/package-lock.json index fd84443..4f9560c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "bootstrap": "5.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", "typescript": "^5.8.3", "yaml": "^2.8.0" }, @@ -1644,6 +1645,347 @@ "node": ">= 8" } }, + "node_modules/@octokit/app": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.0.1.tgz", + "integrity": "sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^8.0.1", + "@octokit/auth-unauthenticated": "^7.0.1", + "@octokit/core": "^7.0.2", + "@octokit/oauth-app": "^8.0.1", + "@octokit/plugin-paginate-rest": "^13.0.0", + "@octokit/types": "^14.0.0", + "@octokit/webhooks": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-app": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.1.tgz", + "integrity": "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", + "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", + "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", + "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/oauth-methods": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz", + "integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", + "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", + "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/auth-unauthenticated": "^7.0.1", + "@octokit/core": "^7.0.2", + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/oauth-methods": "^6.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", + "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", + "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-11.0.0.tgz", + "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz", + "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.0.0.tgz", + "integrity": "sha512-nPXM3wgil9ONnAINcm8cN+nwso4QhNB13PtnlRFkYFHCUIogcH9DHak/StQYcwkkjuc7pUluLG1AWZNscgvH7Q==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-15.0.0.tgz", + "integrity": "sha512-db6UdWvpX7O6tNsdkPk1BttVwTeVdA4n8RDFeXOyjBCPjE2YPufIAlzWh8CyeH8hl/3dSuQXDa+qLmsBlkTY+Q==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.1.tgz", + "integrity": "sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.1.tgz", + "integrity": "sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": "^7.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz", + "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", + "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.0.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.0.0.tgz", + "integrity": "sha512-IZV4vg/s1pqIpCs86a0tp5FQ/O94DUaqksMdNrXFSaE037TXsB+fIhr8OVig09oEx3WazVgE6B2U+u7/Fvdlsw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-webhooks-types": "11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/webhooks-methods": "^6.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", + "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", @@ -2038,6 +2380,12 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/aws-lambda": { + "version": "8.10.149", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz", + "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2762,6 +3110,12 @@ ], "license": "MIT" }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, "node_modules/blob-to-buffer": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz", @@ -2801,6 +3155,12 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -3829,6 +4189,22 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5862,6 +6238,28 @@ "node": ">=0.10.0" } }, + "node_modules/octokit": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.2.tgz", + "integrity": "sha512-WCO9Oip2F+qsrIcNMfLwm1+dL2g70oO++pkmiluisJDMXXwdO4susVaVg1iQZgZNiDtA1qcLXs5662Mdj/vqdw==", + "license": "MIT", + "dependencies": { + "@octokit/app": "^16.0.1", + "@octokit/core": "^7.0.2", + "@octokit/oauth-app": "^8.0.1", + "@octokit/plugin-paginate-graphql": "^6.0.0", + "@octokit/plugin-paginate-rest": "^13.0.0", + "@octokit/plugin-rest-endpoint-methods": "^15.0.0", + "@octokit/plugin-retry": "^8.0.1", + "@octokit/plugin-throttling": "^11.0.1", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "@octokit/webhooks": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/ofetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", @@ -6282,6 +6680,15 @@ "react": "^19.1.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -6942,6 +7349,15 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7256,6 +7672,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, "node_modules/unstorage": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.16.0.tgz", diff --git a/package.json b/package.json index 8a5082d..a0086b2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "bootstrap": "5.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", "typescript": "^5.8.3", "yaml": "^2.8.0" }, diff --git a/src/components/WorkshopGrid.jsx b/src/components/EducatesResourceGrid.jsx similarity index 71% rename from src/components/WorkshopGrid.jsx rename to src/components/EducatesResourceGrid.jsx index 9ad6904..7d84ee0 100644 --- a/src/components/WorkshopGrid.jsx +++ b/src/components/EducatesResourceGrid.jsx @@ -1,8 +1,9 @@ import { useState, useMemo } from "react"; +import EducatesResourceTile from './EducatesResourceTile.jsx'; const PAGE_SIZE_OPTIONS = [12, 24, 48]; -export default function WorkshopGrid({ workshops }) { +export default function EducatesResourceGrid({ resources = [] }) { const [search, setSearch] = useState(""); const [selectedLabels, setSelectedLabels] = useState([]); const [page, setPage] = useState(1); @@ -10,13 +11,13 @@ export default function WorkshopGrid({ workshops }) { // Get all unique labels const allLabels = useMemo( - () => Array.from(new Set(workshops.flatMap((w) => w.labels || []))).sort(), - [workshops], + () => Array.from(new Set(resources.flatMap((w) => w.labels || []))).sort(), + [resources], ); // Filtering logic const filtered = useMemo(() => { - return workshops.filter((w) => { + return resources.filter((w) => { const matchesSearch = w.title.toLowerCase().includes(search.toLowerCase()) || w.description.toLowerCase().includes(search.toLowerCase()); @@ -25,7 +26,7 @@ export default function WorkshopGrid({ workshops }) { selectedLabels.every((l) => (w.labels || []).includes(l)); return matchesSearch && matchesLabels; }); - }, [workshops, search, selectedLabels]); + }, [resources, search, selectedLabels]); // Pagination logic const totalPages = Math.ceil(filtered.length / pageSize); @@ -96,7 +97,7 @@ export default function WorkshopGrid({ workshops }) {
- {paginated.map((workshop) => ( -
- -
- {workshop.title} -
-

- {workshop.title} -

-

- {workshop.description} -

-
- {(workshop.labels || []).map((label) => ( - - {label} - - ))} -
-
- By {workshop.author} -
-
-
-
+ {paginated.map((resource) => ( +
+
))}
diff --git a/src/components/EducatesResourceSelectorIsland.jsx b/src/components/EducatesResourceSelectorIsland.jsx new file mode 100644 index 0000000..f8533d2 --- /dev/null +++ b/src/components/EducatesResourceSelectorIsland.jsx @@ -0,0 +1,98 @@ +import React, { useState, useEffect } from "react"; +import EducatesResourceGrid from "./EducatesResourceGrid.jsx"; +import { loadWorkshops, loadExtensionPackages, loadEditorExtensions, loadKyvernoPolicies } from "../utils/loadWorkshops.js"; + +export default function EducatesResourceSelectorIsland() { + const [type, setType] = useState("Workshop"); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + const load = async () => { + if (type === "Workshop") { + setItems(await loadWorkshops()); + } else if (type === "ExtensionPackage") { + setItems(await loadExtensionPackages()); + } else if (type === "EditorExtension") { + setItems(await loadEditorExtensions()); + } else if (type === "KyvernoPolicy") { + setItems(await loadKyvernoPolicies()); + } + else { + setItems([]); + } + setLoading(false); + }; + load(); + }, [type]); + + return ( + <> +
+ Educates Resource Type: +
+ setType("Workshop")} + /> + +
+
+ setType("ExtensionPackage")} + /> + +
+
+ setType("EditorExtension")} + /> + +
+
+ setType("KyvernoPolicy")} + /> + +
+
+ {loading ? ( +
Loading...
+ ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/src/components/EducatesResourceTile.jsx b/src/components/EducatesResourceTile.jsx new file mode 100644 index 0000000..463b679 --- /dev/null +++ b/src/components/EducatesResourceTile.jsx @@ -0,0 +1,31 @@ +import React from "react"; + +export default function EducatesResourceTile({ educatesResource, class: className = "" }) { + return ( + +
+ {educatesResource.title} +
+

{educatesResource.title}

+

{educatesResource.description}

+
+ {(educatesResource.labels || []).map((label) => ( + + {label} + + ))} +
+
By {educatesResource.author}
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/ExtensionPackageDetail.jsx b/src/components/ExtensionPackageDetail.jsx new file mode 100644 index 0000000..d1c7b9f --- /dev/null +++ b/src/components/ExtensionPackageDetail.jsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import ViewSourceIsland from './ViewSourceIsland.jsx'; +import InstallIsland from './InstallIsland.jsx'; +import { Octokit } from 'octokit'; + +export default function ExtensionPackageDetail({ extensionPackage }) { + const [showInstall, setShowInstall] = useState(false); + const [downloadUrl, setDownloadUrl] = useState(""); + const [assetId, setAssetId] = useState(""); + + // useEffect(() => { + // async function fetchDownloadUrl() { + // if (extensionPackage.repo && extensionPackage.repo.org && extensionPackage.repo.name && extensionPackage.repo.asset_name) { + // try { + // const octokit = new Octokit(); + // const res = await octokit.rest.repos.getReleaseByTag({ + // owner: extensionPackage.repo.org, + // repo: extensionPackage.repo.name, + // tag: extensionPackage.repo.tag || 'latest', + // }); + // const asset = res.data.assets.find(asset => asset.name === extensionPackage.repo.asset_name); + // if (asset) { + // setDownloadUrl(asset.browser_download_url); + // setAssetId(asset.id); + // } else { + // setDownloadUrl(""); + // setAssetId(""); + // } + // } catch (e) { + // setDownloadUrl(""); + // setAssetId(""); + // } + // } else { + // setDownloadUrl(""); + // setAssetId(""); + // } + // console.log("downloadUrl", downloadUrl); + // console.log("assetId", assetId); + // } + // fetchDownloadUrl(); + // }, [extensionPackage]); + + return ( +
+
+
+
+
+
+ {extensionPackage.title} +
+
+
+

+ {extensionPackage.title} +

+
+ {extensionPackage.labels && + extensionPackage.labels.map((label) => ( + + {label} + + ))} +
+

+ {extensionPackage.description} +

+
+ By {extensionPackage.author} • Created {extensionPackage.date_created} +
+
+ Version: {extensionPackage.version} +
+ {/* Release Notes section, only if release_notes is not empty */} + {extensionPackage.release_notes && extensionPackage.release_notes.trim() !== '' && ( +
+

Release Notes

+
+ {extensionPackage.release_notes} +
+
+ )} + {/*
+ + +
*/} +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/FilterSidebar.astro b/src/components/FilterSidebar.astro deleted file mode 100644 index 1e7c029..0000000 --- a/src/components/FilterSidebar.astro +++ /dev/null @@ -1,35 +0,0 @@ ---- -const { allLabels } = Astro.props; ---- - - diff --git a/src/components/InstallIsland.jsx b/src/components/InstallIsland.jsx new file mode 100644 index 0000000..23cec19 --- /dev/null +++ b/src/components/InstallIsland.jsx @@ -0,0 +1,27 @@ +import { useState } from 'react'; +import InstallModal from './InstallModal.jsx'; + +export default function InstallIsland({ installUrl, workshopSlug, downloadUrl }) { + const [show, setShow] = useState(false); + return ( + <> + + setShow(false)} + installUrl={installUrl} + workshopSlug={workshopSlug} + downloadUrl={downloadUrl} + /> + {show && ( +
setShow(false)} + >
+ )} + + ); +} \ No newline at end of file diff --git a/src/components/InstallModal.jsx b/src/components/InstallModal.jsx new file mode 100644 index 0000000..9b780f9 --- /dev/null +++ b/src/components/InstallModal.jsx @@ -0,0 +1,79 @@ +import { useRef, useState, useEffect } from 'react'; + +export default function InstallModal({ show, onClose, installUrl, workshopSlug, downloadUrl }) { + const [copyStatus, setCopyStatus] = useState('Copy'); + const installCmdRef = useRef(null); + const modalRef = useRef(null); + + useEffect(() => { + if (window.bootstrap && modalRef.current) { + const modal = new window.bootstrap.Modal(modalRef.current, { + backdrop: 'static', + }); + if (show) { + modal.show(); + } else { + modal.hide(); + } + return () => { + modal.hide(); + }; + } + }, [show]); + + const handleCopy = () => { + if (installCmdRef.current) { + const text = installCmdRef.current.innerText; + navigator.clipboard.writeText(text).then(() => { + setCopyStatus('Copied!'); + setTimeout(() => setCopyStatus('Copy'), 1500); + }); + } + }; + + return ( +
+
+
+
+
+ Install Instructions +
+ +
+
+
+ Install Command + +
+
+{`educates deploy-workshop -f ${downloadUrl}`}
+            
+ {/*
Replace my-{workshopSlug} with your desired release name.
*/} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/ViewSourceIsland.jsx b/src/components/ViewSourceIsland.jsx new file mode 100644 index 0000000..cf1a488 --- /dev/null +++ b/src/components/ViewSourceIsland.jsx @@ -0,0 +1,14 @@ +import { useState } from 'react'; +import ViewSourceModal from './ViewSourceModal.jsx'; + +export default function ViewSourceIsland({ src, repo, assetId, downloadUrl }) { + const [show, setShow] = useState(false); + return ( + <> + + setShow(false)} src={src} repo={repo} assetId={assetId} downloadUrl={downloadUrl} /> + + ); +} \ No newline at end of file diff --git a/src/components/ViewSourceModal.jsx b/src/components/ViewSourceModal.jsx new file mode 100644 index 0000000..e0b57e5 --- /dev/null +++ b/src/components/ViewSourceModal.jsx @@ -0,0 +1,217 @@ +import { useEffect, useRef, useState } from "react"; +import { IoOpen, IoCopy, IoDownload } from "react-icons/io5"; +import { Octokit } from "octokit"; + +export default function ViewSourceModal({ show, onClose, src, repo, assetId, downloadUrl }) { + const modalRef = useRef(null); + const [copyStatus, setCopyStatus] = useState("copy"); + const [fileContent, setFileContent] = useState(""); + const [releaseContent, setReleaseContent] = useState(""); + const [loading, setLoading] = useState(false); + const [releaseLoading, setReleaseLoading] = useState(false); + const [error, setError] = useState(""); + const [releaseError, setReleaseError] = useState(""); + + useEffect(() => { + if (window.bootstrap && modalRef.current) { + const modal = new window.bootstrap.Modal(modalRef.current, { + backdrop: 'static', + }); + if (show) { + modal.show(); + } else { + modal.hide(); + } + return () => { + modal.hide(); + }; + } + }, [show]); + + const logError = (error) => { + console.error(error); + setError(error.message || error.toString()); + }; + const logReleaseError = (error) => { + console.error(error); + setReleaseError(error.message || error.toString()); + }; + + // Fetch file content from GitHub using Octokit when modal is shown + useEffect(() => { + if (show && repo && repo.org && repo.name && repo.path) { + setLoading(true); + logError(""); + setFileContent(""); + const octokit = new Octokit(); + octokit.rest.repos.getContent({ + owner: repo.org, + repo: repo.name, + path: repo.path, + ref: repo.branch || 'main', + }) + .then((res) => { + // If file, decode base64 + if (res.data && res.data.type === 'file' && res.data.content) { + const decoded = atob(res.data.content.replace(/\n/g, '')); + setFileContent(decoded); + } else { + logError('File not found or not a regular file.'); + } + }) + .catch((e) => { + logError('Error loading file: ' + (e.message || e.toString())); + }) + .finally(() => setLoading(false)); + } else { + logError("No repo or path provided"); + } + }, [show, repo]); + + // Fetch release asset content if asset_id is provided + // TODO: We can not do this without a proxy because of CORS + // useEffect(() => { + // if (show && repo && repo.org && repo.name && repo.asset_name) { + // setReleaseLoading(true); + // setReleaseError(""); + // setReleaseContent(""); + // const octokit = new Octokit(); + // octokit.rest.repos.getReleaseAsset({ + // owner: repo.org, + // repo: repo.name, + // asset_id: assetId, + // // Accept header for raw content + // headers: { Accept: 'application/octet-stream' }, + // }) + // .then((res) => { + // // The content is in res.data (as a Blob or ArrayBuffer) + // // Try to convert to string if possible + // let content = res.data; + // if (content instanceof ArrayBuffer) { + // content = new TextDecoder().decode(content); + // } else if (typeof content === 'object' && content instanceof Blob) { + // content.text().then(setReleaseContent); + // return; + // } + // setReleaseContent(content); + // }) + // .catch((e) => { + // logReleaseError('Error loading release asset: ' + (e.message || e.toString())); + // }) + // .finally(() => setReleaseLoading(false)); + // } else { + // setReleaseContent(""); + // } + // }, [show, repo]); + + const handleCopy = () => { + if (fileContent && navigator.clipboard) { + navigator.clipboard.writeText(fileContent); + } else if (navigator.clipboard) { + navigator.clipboard.writeText(src); + } + setCopyStatus("copied"); + setTimeout(() => setCopyStatus("copy"), 1500); + }; + + // We link to the download url from GitHub + const handleDownload = () => { + console.log("downloadUrl: ", downloadUrl); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = downloadUrl.split('/').pop() || 'workshop.yaml'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const handleOpenInNewTab = () => { + if (fileContent) { + const blob = new Blob([fileContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + window.open(url, '_blank'); + } else { + window.open(src, '_blank'); + } + }; + + return ( +
+
+
+
+
+ Workshop Source +
+
+ + + + +
+
+
+ {loading ? ( +
+ Loading... +
+ ) : error ? ( +
{error}
+ ) : ( +
{fileContent}
+ )} + {releaseLoading && ( +
+ Loading release asset... +
+ )} + {releaseError && ( +
{releaseError}
+ )} +
+
+ NOTE: Keep in mind that this is the source code for the workshop and not the distribution workshop file that is available at{' '} + {downloadUrl} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/WorkshopDetail.jsx b/src/components/WorkshopDetail.jsx new file mode 100644 index 0000000..b427cdc --- /dev/null +++ b/src/components/WorkshopDetail.jsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import ViewSourceIsland from './ViewSourceIsland.jsx'; +import InstallIsland from './InstallIsland.jsx'; +import { Octokit } from 'octokit'; + +export default function WorkshopDetail({ workshop }) { + const [showInstall, setShowInstall] = useState(false); + const [downloadUrl, setDownloadUrl] = useState(""); + const [assetId, setAssetId] = useState(""); + + useEffect(() => { + async function fetchDownloadUrl() { + if (workshop.repo && workshop.repo.org && workshop.repo.name && workshop.repo.asset_name) { + try { + const octokit = new Octokit(); + const res = await octokit.rest.repos.getReleaseByTag({ + owner: workshop.repo.org, + repo: workshop.repo.name, + tag: workshop.repo.tag || 'latest', + }); + const asset = res.data.assets.find(asset => asset.name === workshop.repo.asset_name); + if (asset) { + setDownloadUrl(asset.browser_download_url); + setAssetId(asset.id); + } else { + setDownloadUrl(""); + setAssetId(""); + } + } catch (e) { + setDownloadUrl(""); + setAssetId(""); + } + } else { + setDownloadUrl(""); + setAssetId(""); + } + console.log("downloadUrl", downloadUrl); + console.log("assetId", assetId); + } + fetchDownloadUrl(); + }, [workshop]); + + return ( +
+
+
+
+
+
+ {workshop.title} +
+
+
+

+ {workshop.title} +

+
+ {workshop.labels && + workshop.labels.map((label) => ( + + {label} + + ))} +
+

+ {workshop.description} +

+
+ By {workshop.author} • Created {workshop.date_created} +
+
+ Version: {workshop.version} +
+ {/* Release Notes section, only if release_notes is not empty */} + {workshop.release_notes && workshop.release_notes.trim() !== '' && ( +
+

Release Notes

+
+ {workshop.release_notes} +
+
+ )} +
+ + +
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/WorkshopTile.astro b/src/components/WorkshopTile.astro deleted file mode 100644 index f2507ba..0000000 --- a/src/components/WorkshopTile.astro +++ /dev/null @@ -1,38 +0,0 @@ ---- -const { workshop, class: className = "" } = Astro.props; ---- - - -
- {workshop.title} -
-

{workshop.title}

-

{workshop.description}

-
- { - workshop.labels && - workshop.labels.map((label: string) => ( - - {label} - - )) - } -
-
By {workshop.author}
-
-
-
- diff --git a/src/data/educates-default-kyverno-policies.yaml b/src/data/educates-default-kyverno-policies.yaml new file mode 100644 index 0000000..33626df --- /dev/null +++ b/src/data/educates-default-kyverno-policies.yaml @@ -0,0 +1,9 @@ +type: KyvernoPolicy +title: Educates Default Kyverno Policies +description: Default Kyverno policies for Educates +author: Educates +image: educates +labels: + - educates + - kyverno-policy +repo_url: https://github.com/educates/educates-training-platform/blob/develop/carvel-packages/installer/bundle/config/ytt/_ytt_lib/packages/educates/_ytt_lib/kyverno-policies/require-ingress-session-name.yaml \ No newline at end of file diff --git a/src/data/examples.yaml b/src/data/examples.yaml index 79ff65f..e74dfb6 100644 --- a/src/data/examples.yaml +++ b/src/data/examples.yaml @@ -1,15 +1,5 @@ -title: GitOps with Argo CD -description: Implement GitOps workflows using Argo CD in Kubernetes. -image: argo -author: Carlos Rivera -date_created: 2024-06-12 -labels: - - gitops - - kubernetes -url: https://github.com/educates/workshops/gitops-argo -version: 1.0.2 -release_notes: Added new GitOps deployment strategies. --- +type: Workshop title: Grafana Visualization description: Visualize metrics and logs with Grafana dashboards. image: grafana @@ -17,10 +7,13 @@ author: Sofia Garcia date_created: 2024-06-22 labels: - monitoring -url: https://github.com/educates/workshops/grafana-visualization +install_url: https://github.com/educates/workshops/grafana-visualization +repo_url: https://github.com/educates/workshops/grafana-visualization +workshop_definition_url: https://github.com/educates/workshops/grafana-visualization/releases/download/main/workshop.yaml version: 1.0.1 release_notes: Added new dashboard templates. --- +type: Workshop title: Helm Basics description: Introduction to Helm for Kubernetes package management. image: helm @@ -29,10 +22,13 @@ date_created: 2024-06-15 labels: - kubernetes - beginner -url: https://github.com/educates/workshops/helm-basics +install_url: https://github.com/educates/workshops/helm-basics +repo_url: https://github.com/educates/workshops/helm-basics +workshop_definition_url: https://github.com/educates/workshops/helm-basics/releases/download/main/workshop.yaml version: 1.0.0 release_notes: First release. --- +type: Workshop title: Istio Service Mesh description: Explore service mesh concepts and Istio features in Kubernetes. image: istio @@ -41,10 +37,13 @@ date_created: 2024-06-18 labels: - service-mesh - kubernetes -url: https://github.com/educates/workshops/istio-service-mesh +install_url: https://github.com/educates/workshops/istio-service-mesh +repo_url: https://github.com/educates/workshops/istio-service-mesh +workshop_definition_url: https://github.com/educates/workshops/istio-service-mesh/releases/download/main/workshop.yaml version: 1.2.0 release_notes: Added traffic management labs. --- +type: Workshop title: Kubernetes 101 description: Introductory workshop for Kubernetes basics. image: kubernetes @@ -53,10 +52,13 @@ date_created: 2024-06-01 labels: - kubernetes - beginner -url: https://github.com/educates/workshops/k8s101 +install_url: https://github.com/educates/workshops/k8s101 +repo_url: https://github.com/educates/workshops/k8s101 +workshop_definition_url: https://github.com/educates/workshops/k8s101/releases/download/main/workshop.yaml version: 1.0.0 release_notes: Initial release. --- +type: Workshop title: NGINX Ingress Controller description: Configure and use NGINX as an ingress controller in Kubernetes. image: nginx @@ -65,10 +67,13 @@ date_created: 2024-06-28 labels: - ingress - kubernetes -url: https://github.com/educates/workshops/nginx-ingress +install_url: https://github.com/educates/workshops/nginx-ingress +repo_url: https://github.com/educates/workshops/nginx-ingress +workshop_definition_url: https://github.com/educates/workshops/nginx-ingress/releases/download/main/workshop.yaml version: 1.0.0 release_notes: Initial NGINX Ingress Controller workshop. --- +type: Workshop title: PostgreSQL on Kubernetes description: Deploy and manage PostgreSQL databases in Kubernetes. image: postgresql @@ -77,10 +82,13 @@ date_created: 2024-06-25 labels: - database - kubernetes -url: https://github.com/educates/workshops/postgresql-k8s +install_url: https://github.com/educates/workshops/postgresql-k8s +repo_url: https://github.com/educates/workshops/postgresql-k8s +workshop_definition_url: https://github.com/educates/workshops/postgresql-k8s/releases/download/main/workshop.yaml version: 1.0.0 release_notes: Initial PostgreSQL on Kubernetes workshop. --- +type: Workshop title: Prometheus Monitoring description: Learn how to monitor Kubernetes clusters with Prometheus. image: prometheus @@ -89,10 +97,13 @@ date_created: 2024-06-10 labels: - monitoring - kubernetes -url: https://github.com/educates/workshops/prometheus-monitoring +install_url: https://github.com/educates/workshops/prometheus-monitoring +repo_url: https://github.com/educates/workshops/prometheus-monitoring +workshop_definition_url: https://github.com/educates/workshops/prometheus-monitoring/releases/download/main/workshop.yaml version: 1.1.0 release_notes: Updated for Prometheus Operator. --- +type: Workshop title: Tekton CI/CD description: Build CI/CD pipelines for Kubernetes using Tekton. image: tekton @@ -101,6 +112,8 @@ date_created: 2024-06-20 labels: - ci/cd - kubernetes -url: https://github.com/educates/workshops/tekton-ci +install_url: https://github.com/educates/workshops/tekton-ci +repo_url: https://github.com/educates/workshops/tekton-ci +workshop_definition_url: https://github.com/educates/workshops/tekton-ci/releases/download/main/workshop.yaml version: 1.0.0 release_notes: Initial Tekton CI/CD workshop. diff --git a/src/data/extension-package.yaml b/src/data/extension-package.yaml new file mode 100644 index 0000000..e559796 --- /dev/null +++ b/src/data/extension-package.yaml @@ -0,0 +1,23 @@ +type: ExtensionPackage +title: Educates CLI Extension Package +description: Provides Educates CLI to any workshop +image: educates +author: Jorge Morales +date_created: 2025-05-26 +labels: + - educates + - cli +repo_url: https://github.com/educates/educates-extension-packages +version: v3.3.2 +release_notes: Initial release. +--- +type: ExtensionPackage +title: Spring Academy Extension Package +description: Extension package for the Spring Academy +author: Spring Academy +image: spring +labels: + - spring-academy + - editor-extension +repo_url: https://github.com/spring-academy/spring-academy-extension-package +version: v1.0.1 \ No newline at end of file diff --git a/src/data/lab-kubernetes-fundamentals.yaml b/src/data/lab-kubernetes-fundamentals.yaml index e8755c6..7ecd019 100644 --- a/src/data/lab-kubernetes-fundamentals.yaml +++ b/src/data/lab-kubernetes-fundamentals.yaml @@ -1,9 +1,15 @@ +--- +type: Workshop title: Kubernetes Fundamentals description: An interactive workshop on Kubernetes fundamentals image: kubernetes author: GrahamDumpleton date_created: 2024-07-12 -labels: [kubernetes, fundamentals] -url: https://github.com/educates/lab-k8s-fundamentals/releases/download/7.4/workshop.yaml +labels: + - kubernetes + - fundamentals +install_url: https://github.com/educates/lab-k8s-fundamentals/releases/download/7.4/workshop.yaml version: 7.4 release_notes: Updating for new repository location +repo_url: https://github.com/educates/lab-k8s-fundamentals +workshop_definition_url: https://github.com/educates/lab-k8s-fundamentals/releases/download/7.4/workshop.yaml diff --git a/src/data/labs-authoring-guides.yaml b/src/data/labs-authoring-guides.yaml index 904298c..8cda00c 100644 --- a/src/data/labs-authoring-guides.yaml +++ b/src/data/labs-authoring-guides.yaml @@ -1,9 +1,12 @@ --- +type: Workshop title: Workshop Authoring slug: workshop-authoring description: Getting started with workshop authoring author: Educates Team image: educates -url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-authoring-guides +workshop_definition_url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml labels: - educates-showcase diff --git a/src/data/labs-educates-showcase.yaml b/src/data/labs-educates-showcase.yaml index 561765d..2b036b3 100644 --- a/src/data/labs-educates-showcase.yaml +++ b/src/data/labs-educates-showcase.yaml @@ -1,71 +1,104 @@ --- +type: Workshop title: Workshop Session slug: workshop-session description: Overview of the containerized workshop environment. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Session Namespace slug: session-namespace description: Overview of Kubernetes application deployment. author: Educates Team image: kubernetes +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Virtual Cluster slug: virtual-cluster description: Overview of optional virtual Kubernetes cluster. author: Educates Team image: kubernetes +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase - vcluster --- +type: Workshop title: Multiple Clusters slug: multiple-clusters description: Overview of working with multiple clusters. author: Educates Team image: kubernetes +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Virtual Machines slug: virtual-machines description: Overview of provisioning virtual machines. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase - virtual-machine --- +type: Workshop title: Integrated Editor slug: integrated-editor description: Overview of integrated VS Code editor. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Slide Presentations slug: slide-presentations description: Overview of integrating slide presentations. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Docker Runtime slug: docker-runtime description: Overview of deploying applications using docker. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase - docker --- +type: Workshop title: Examiner Scripts slug: examiner-scripts description: Overview of using self check examinations. @@ -74,6 +107,7 @@ image: educates labels: - educates-showcase --- +type: Workshop title: Java Environment slug: java-environment description: Overview of working on Java applications. @@ -83,6 +117,7 @@ labels: - educates-showcase - java --- +type: Workshop title: Conda Environment slug: conda-environment description: Overview of working on Python applications. @@ -92,46 +127,67 @@ labels: - educates-showcase - python --- +type: Workshop title: Shared Resources slug: shared-resources description: Overview of pre-creating shared resources. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Session Resources slug: session-resources description: Overview of creating per session resources. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Extension Packages slug: extension-packages description: Overview of adding additional extension packages. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Managed Services slug: managed-services description: Overview of running additional managed processes. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Ingress Proxy slug: ingress-proxy description: Overview of adding ingresses for local processes. author: Educates Team image: nginx +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Git Repositories slug: git-repositories description: Overview of using local hosted Git repositories. @@ -140,18 +196,26 @@ image: educates labels: - educates-showcase --- +type: Workshop title: Installing Educates slug: installing-educates description: Overview of installing Educates using the CLI. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase --- +type: Workshop title: Workshop Authoring slug: workshop-authoring description: Overview of authoring workshops for Educates. author: Educates Team image: educates +install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-educates-showcase +workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml labels: - educates-showcase diff --git a/src/data/labs-installation-guides.yaml b/src/data/labs-installation-guides.yaml index 60313ba..a913759 100644 --- a/src/data/labs-installation-guides.yaml +++ b/src/data/labs-installation-guides.yaml @@ -1,63 +1,81 @@ --- +type: Workshop title: Installing Educates slug: installing-educates-cli description: Installing Educates using the CLI author: Educates Team image: educates -url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-installation-guides +workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml labels: - educates-showcase - installation-guides --- +type: Workshop title: Installing Educates slug: installing-educates-carvel description: installing Educates using kapp-controller author: Educates Team image: educates -url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-installation-guides +workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml labels: - educates-showcase - installation-guides --- +type: Workshop title: Installing Educates Lookup Service slug: installing-educates-lookup-service description: Installing Educates Lookup Service author: Educates Team image: educates -url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-installation-guides +workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml labels: - educates-showcase - installation-guides - lookup-service --- +type: Workshop title: Installing Educates standalone Lookup Service slug: installing-educates-standalone-lookup-service description: Installing Educates standalone Lookup Service author: Educates Team image: educates -url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-installation-guides +workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml labels: - educates-showcase - installation-guides - lookup-service --- +type: Workshop title: Lookup Service Configuration slug: lookup-service-configuration description: Configuring the Educates lookup service author: Educates Team image: educates -url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-installation-guides +workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml labels: - educates-showcase - installation-guides - lookup-service --- +type: Workshop title: Lookup Service Usage slug: lookup-service-usage description: Using the Educates lookup service author: Educates Team image: educates -url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml +repo_url: https://github.com/educates/labs-installation-guides +workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml labels: - educates-showcase - lookup-service diff --git a/src/data/labs-spring-workshops.yaml b/src/data/labs-spring-workshops.yaml index 51c0da3..ffe048f 100644 --- a/src/data/labs-spring-workshops.yaml +++ b/src/data/labs-spring-workshops.yaml @@ -1,33 +1,63 @@ --- +type: Workshop title: Creating a Spring Application slug: creating-a-spring-application description: Introduces how to use the start.spring.io web site to create a Spring Boot application author: Educates Team image: spring -url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml +repo: + org: educates + name: labs-spring-workshops + #branch: main + tag: 4.2 + path: workshops/lab-creating-a-spring-app/resources/workshop.yaml + asset_name: workshops.yaml +# install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml +# repo_url: https://github.com/educates/labs-spring-workshops +# workshop_definition_url: https://rawcdn.githack.com/educates/labs-spring-workshops/refs/heads/main/workshops/lab-containerizing-spring/resources/workshop.yaml?min=1 labels: - educates-showcase - spring - java --- +type: Workshop title: Containerizing a Spring Application slug: containerizing-a-spring-application description: How to containerize a Spring Boot application and run it in docker author: Educates Team image: spring -url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml +repo: + org: educates + name: labs-spring-workshops + # branch: refs/heads/main + tag: 4.2 + path: workshops/lab-containerizing-spring/resources/workshop.yaml + asset_name: workshops.yaml +# install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml +# repo_url: https://github.com/educates/labs-spring-workshops +# workshop_definition_url: https://raw.githubusercontent.com/educates/labs-spring-workshops/refs/heads/main/workshops/lab-creating-a-spring-app/resources/workshop.yaml labels: - educates-showcase - spring - java - docker --- +type: Workshop title: SpringBoot on Kubernetes slug: springboot-on-kubernetes description: Steps through creating a Spring Boot application, containerizing it, and deploying it to Kubernetes. author: Educates Team image: spring -url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml +repo: + org: educates + name: labs-spring-workshops + # branch: refs/heads/main + tag: 4.2 + path: workshops/lab-spring-boot-on-k8s/resources/workshop.yaml + asset_name: workshops.yaml +# install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml +# repo_url: https://github.com/educates/labs-spring-workshops +# workshop_definition_url: https://raw.githubusercontent.com/educates/labs-spring-workshops/refs/heads/main/workshops/lab-spring-boot-on-k8s/resources/workshop.yaml labels: - educates-showcase - spring diff --git a/src/data/spring-academy-vscode-tools.yaml b/src/data/spring-academy-vscode-tools.yaml new file mode 100644 index 0000000..6028800 --- /dev/null +++ b/src/data/spring-academy-vscode-tools.yaml @@ -0,0 +1,11 @@ +type: EditorExtension +title: Spring Academy VSCode Extension Package +description: VSCode extension package for the Spring Academy to add support for Spring Boot and Spring Cloud. +author: Spring Academy +image: spring +labels: + - spring-academy + - editor-extension +repo_url: https://github.com/spring-academy/spring-academy-tools +version: v1.0.1 +--- \ No newline at end of file diff --git a/src/components/Footer.astro b/src/layouts/Footer.astro similarity index 100% rename from src/components/Footer.astro rename to src/layouts/Footer.astro diff --git a/src/layouts/MainLayout.astro b/src/layouts/MainLayout.astro index 8151154..1adf6ca 100644 --- a/src/layouts/MainLayout.astro +++ b/src/layouts/MainLayout.astro @@ -1,6 +1,6 @@ --- -import Topbar from "../components/Topbar.astro"; -import Footer from "../components/Footer.astro"; +import Topbar from "../layouts/Topbar.astro"; +import Footer from "../layouts/Footer.astro"; const { title = "EducatesHub - Workshop Catalog", description = "Browse and discover Educates workshops.", diff --git a/src/components/Topbar.astro b/src/layouts/Topbar.astro similarity index 100% rename from src/components/Topbar.astro rename to src/layouts/Topbar.astro diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro index a7f2a80..b94164a 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -1,152 +1,32 @@ --- -import { loadWorkshops } from "../utils/loadWorkshops"; +import { loadEducatesResources } from "../utils/loadWorkshops"; +import type { EducatesResourceBase } from "../utils/loadWorkshops"; import MainLayout from "../layouts/MainLayout.astro"; +import WorkshopDetail from '../components/WorkshopDetail.jsx'; +import ExtensionPackageDetail from '../components/ExtensionPackageDetail.jsx'; export async function getStaticPaths() { - const workshops = await loadWorkshops(); - return workshops.map((w) => ({ - params: { slug: w.slug }, + const resources = await loadEducatesResources(); + return resources.map((r) => ({ + params: { slug: r.slug }, })); } const { slug } = Astro.params; -const workshops = await loadWorkshops(); -const workshop = workshops.find((w) => w.slug === slug); -if (!workshop) { - throw new Error("Workshop not found"); +const resources = await loadEducatesResources(); +const resource: EducatesResourceBase | undefined = resources.find((r) => r.slug === slug); +if (!resource) { + throw new Error("Resource not found"); } --- -
-
-
-
-
-
- {workshop.title} -
-
-
-

- {workshop.title} -

-
- { - workshop.labels && - workshop.labels.map((label: string) => ( - - {label} - - )) - } -
-

- {workshop.description} -

-
- By {workshop.author} • Created {workshop.date_created} -
-
- Version: {workshop.version} -
-
-

Release Notes

-
- {workshop.release_notes} -
-
-
- View Source - -
-
-
-
-
-
-
- - - -
+ {resource.type === 'Workshop' ? ( + + ) : resource.type === 'ExtensionPackage' ? ( + + ) : null}
diff --git a/src/pages/about.astro b/src/pages/about.astro index 659ad33..2c59104 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -9,9 +9,9 @@ import MainLayout from "../layouts/MainLayout.astro";

About EducatesHub

EducatesHub is a community-driven catalog of Educates WorkshopsEducates Workshops, Extension Packages, Editor Extensions, and Kyverno Policies, making it easy to discover, search, and install hands-on Kubernetes - training content. + training content and other Educates resources.


What is Educates?

@@ -29,9 +29,9 @@ import MainLayout from "../layouts/MainLayout.astro";

What can you do with EducatesHub?

Here, you will be able to:

    -
  • Browse curated workshops and training scenarios
  • +
  • Browse curated workshops, extension packages, editor extensions, and kyverno policies
  • Filter by topic, labels, and search keywords
  • -
  • View install instructions and workshop details
  • +
  • View install instructions and resource details

Learn More

    diff --git a/src/pages/index.astro b/src/pages/index.astro index 010c915..b109663 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,15 +1,14 @@ --- -import { loadWorkshops } from "../utils/loadWorkshops"; +import { loadWorkshops, loadExtensionPackages } from "../utils/loadWorkshops"; import MainLayout from "../layouts/MainLayout.astro"; -import WorkshopGrid from "../components/WorkshopGrid.jsx"; -const workshops = await loadWorkshops(); +import EducatesResourceSelectorIsland from "../components/EducatesResourceSelectorIsland.jsx"; ---
    - +
    -
    + \ No newline at end of file diff --git a/src/utils/loadWorkshops.ts b/src/utils/loadWorkshops.ts index 64af89a..32d79bf 100644 --- a/src/utils/loadWorkshops.ts +++ b/src/utils/loadWorkshops.ts @@ -19,7 +19,81 @@ const imageMap: Record = { const educatesDefaultImage = "/images/educates.svg"; -export async function loadWorkshops() { +export interface Workshop { + type: 'Workshop'; + slug: string; + title: string; + description: string; + author: string; + image: string; + date_created?: string; + version?: string; + release_notes?: string; + labels?: string[]; + install_url?: string; + repo_url?: string; + workshop_definition_url?: string; + repo?: { + org: string; + name: string; + tag?: string; + path?: string; + asset_name?: string; + branch?: string; + }; + [key: string]: any; +} + +export interface ExtensionPackage { + type: 'ExtensionPackage'; + slug: string; + title: string; + description: string; + author: string; + image: string; + date_created?: string; + version?: string; + release_notes?: string; + labels?: string[]; + repo_url?: string; + [key: string]: any; +} + +export interface EditorExtension { + type: 'EditorExtension'; + slug: string; + title: string; + description: string; + author: string; + image: string; + date_created?: string; + version?: string; + release_notes?: string; + labels?: string[]; + repo_url?: string; + [key: string]: any; +} + +export interface KyvernoPolicy { + type: 'KyvernoPolicy'; + slug: string; + title: string; + description: string; + author: string; + image: string; + date_created?: string; + version?: string; + release_notes?: string; + labels?: string[]; + repo_url?: string; + [key: string]: any; +} + +export type EducatesResourceBase = Workshop | ExtensionPackage | EditorExtension | KyvernoPolicy; + +async function loadYamlDocuments( + typeFilter: string +): Promise { // Get all available images in public/images const imageFiles = import.meta.glob("/public/images/*", { query: "?url", @@ -42,24 +116,62 @@ export async function loadWorkshops() { } const docs = yaml.parseAllDocuments(raw).map((doc) => doc.toJSON()); // Derive slug from filename, but allow override in doc - return docs.map((data, i) => { - let slug = - data.slug || - (docs.length === 1 - ? path.split("/").pop()?.replace(".yaml", "") || "" - : `${path.split("/").pop()?.replace(".yaml", "")}-${i + 1}`); - let image = data.image; - if (typeof image === "string" && imageMap[image]) { - image = imageMap[image]; - } - if (typeof image === "string" && !availableImages.has(image)) { - image = educatesDefaultImage; - } - return { ...data, image, slug }; - }); + return docs + .map((data, i) => { + // Check if document is empty, and if so, skip it + if (data === null || data === undefined) { + return null; + } + let slug = + data.slug || + (docs.length === 1 + ? path.split("/").pop()?.replace(".yaml", "") || "" + : `${path.split("/").pop()?.replace(".yaml", "")}-${i + 1}`); + let image = data.image; + if (typeof image === "string" && imageMap[image]) { + image = imageMap[image]; + } + if (typeof image === "string" && !availableImages.has(image)) { + image = educatesDefaultImage; + } + return { ...data, image, slug }; + }) + .filter((data) => { + // Check if document is empty, and if so, skip it + if (data === null || data === undefined) { + return false; + } + return data.type === typeFilter; + }); }), ); - return entriesNested.flat(); + return entriesNested.flat() as T[]; +} + +export async function loadWorkshops(): Promise { + return loadYamlDocuments('Workshop'); +} + +export async function loadExtensionPackages(): Promise { + return loadYamlDocuments('ExtensionPackage'); +} + +export async function loadEditorExtensions(): Promise { + return loadYamlDocuments('EditorExtension'); +} + +export async function loadKyvernoPolicies(): Promise { + return loadYamlDocuments('KyvernoPolicy'); +} + +export async function loadEducatesResources(): Promise { + const [workshops, extensionPackages, editorExtensions, kyvernoPolicies] = await Promise.all([ + loadWorkshops(), + loadExtensionPackages(), + loadEditorExtensions(), + loadKyvernoPolicies(), + ]); + return [...workshops, ...extensionPackages, ...editorExtensions, ...kyvernoPolicies]; } // Later, this can be replaced with a server-side loader or API endpoint that reads YAML files from /public/workshops From 27dd090acb693e3b2b4b1b17234d522b2bd6f75e Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Tue, 17 Jun 2025 20:43:53 +0200 Subject: [PATCH 2/3] Initial version with few workshops and one working extension --- .github/workflows/deploy.yaml | 59 +- README.md | 11 +- astro.config.mjs | 1 + fetch_assets.sh | 29 + package-lock.json | 751 ++++++++---------- package.json | 2 + public/CNAME | 1 + .../kyverno-policies/educates-default.yaml | 33 + .../containerizing-a-spring-application.yaml | 36 + .../creating-a-spring-application.yaml | 84 ++ .../lab-kubernetes-fundamentals.yaml | 33 + .../workshops/lab-spring-boot-on-k8s.yaml | 89 +++ .../EducatesResourceSelectorIsland.jsx | 130 ++- src/components/ExtensionPackageDetail.jsx | 305 +++++-- src/components/InstallIsland.jsx | 6 +- src/components/InstallModal.jsx | 2 +- src/components/KyvernoPolicyDetail.jsx | 119 +++ src/components/ThemeDetail.jsx | 119 +++ src/components/ViewSourceIsland.jsx | 4 +- src/components/ViewSourceModal.jsx | 86 +- src/components/WorkshopDetail.jsx | 176 ++-- src/content/config.ts | 86 ++ .../extension-packages/educates-cli.yaml | 12 + .../spring-academy-vscode-tools.yaml | 5 +- .../kyverno-policies/educates-default.yaml} | 6 +- src/content/themes/.gitkeep | 0 .../workshops/WIP/examples.yaml__} | 8 - .../workshops/WIP}/labs-authoring-guides.yaml | 11 +- .../WIP/labs-educates-showcase.yaml__} | 19 - .../WIP/labs-installation-guides.yaml__} | 6 - .../containerizing-a-spring-application.yaml | 32 + .../creating-a-spring-application.yaml | 16 + .../lab-kubernetes-fundamentals.yaml | 18 + .../workshops/lab-spring-boot-on-k8s.yaml | 16 + src/data/extension-package.yaml | 23 - src/data/lab-kubernetes-fundamentals.yaml | 15 - src/data/labs-spring-workshops.yaml | 65 -- src/pages/[slug].astro | 59 +- src/pages/about.astro | 4 +- src/pages/index.astro | 29 +- src/utils/imageMap.ts | 18 + src/utils/loadWorkshops.ts | 177 ----- src/utils/sources.ts | 62 ++ 43 files changed, 1665 insertions(+), 1098 deletions(-) create mode 100755 fetch_assets.sh create mode 100644 public/CNAME create mode 100644 public/assets/kyverno-policies/educates-default.yaml create mode 100644 public/assets/workshops/containerizing-a-spring-application.yaml create mode 100644 public/assets/workshops/creating-a-spring-application.yaml create mode 100644 public/assets/workshops/lab-kubernetes-fundamentals.yaml create mode 100644 public/assets/workshops/lab-spring-boot-on-k8s.yaml create mode 100644 src/components/KyvernoPolicyDetail.jsx create mode 100644 src/components/ThemeDetail.jsx create mode 100644 src/content/config.ts create mode 100644 src/content/extension-packages/educates-cli.yaml rename src/{data => content/extension-packages}/spring-academy-vscode-tools.yaml (82%) rename src/{data/educates-default-kyverno-policies.yaml => content/kyverno-policies/educates-default.yaml} (72%) create mode 100644 src/content/themes/.gitkeep rename src/{data/examples.yaml => content/workshops/WIP/examples.yaml__} (97%) rename src/{data => content/workshops/WIP}/labs-authoring-guides.yaml (58%) rename src/{data/labs-educates-showcase.yaml => content/workshops/WIP/labs-educates-showcase.yaml__} (96%) rename src/{data/labs-installation-guides.yaml => content/workshops/WIP/labs-installation-guides.yaml__} (97%) create mode 100644 src/content/workshops/containerizing-a-spring-application.yaml create mode 100644 src/content/workshops/creating-a-spring-application.yaml create mode 100644 src/content/workshops/lab-kubernetes-fundamentals.yaml create mode 100644 src/content/workshops/lab-spring-boot-on-k8s.yaml delete mode 100644 src/data/extension-package.yaml delete mode 100644 src/data/lab-kubernetes-fundamentals.yaml delete mode 100644 src/data/labs-spring-workshops.yaml create mode 100644 src/utils/imageMap.ts delete mode 100644 src/utils/loadWorkshops.ts create mode 100644 src/utils/sources.ts diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 57078ff..979b6df 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,56 +1,39 @@ name: Deploy to GitHub Pages on: - workflow_dispatch: + # Trigger the workflow every time you push to the `main` branch + # Using a different branch name? Replace `main` with your branch’s name push: - branches: - - main - # Review gh actions docs if you want to further define triggers, paths, etc - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + branches: [ main ] + # Allows you to run this workflow manually from the Actions tab on GitHub. + workflow_dispatch: + +# Allow this job to clone the repo and create a page deployment +permissions: + contents: read + pages: write + id-token: write jobs: build: - name: Build Site runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm install - - name: Run astro check - run: npm run astro check - - name: Run lint - run: npm run lint - - name: Test build website - run: npm run build - - - name: Upload Build Artifact - uses: actions/upload-pages-artifact@v3 - with: - path: dist + - name: Checkout your repository using git + uses: actions/checkout@v4 + - name: Install, build, and upload your site + uses: withastro/action@v3 + # with: + # path: . # The root location of your Astro project inside the repository. (optional) + # node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 20. (optional) + # package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional) deploy: - name: Deploy to GitHub Pages needs: build - - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment - permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source - - # Deploy to the github-pages environment + runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - - runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/README.md b/README.md index 3f87e3f..102b43b 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,17 @@ EducatesHub is a static site built with [Astro](https://astro.build/) and TypeSc 3. **Open your browser:** Visit [http://localhost:4321](http://localhost:4321) -## Customizing Workshops +## Adding Educates Resources -- Add or edit YAML files in `src/data/` to update the workshop catalog. +- Add or edit YAML files in `src/content/` to update the workshop, extensions, kyverno policies or themes catalog. - Images referenced in YAML should be placed in `public/images/` or mapped in the loader utility. +- Fetch referenced assets, run `./fetch_assets.sh` to fetch referenced workshops, kyverno policies, themes and extensions. + +## Release a new version of the site + +- Always work in develop branch +- When things are ready to be released, merge code into main via PR (this will publish the site) +- Rebase develop on main ## Contributing diff --git a/astro.config.mjs b/astro.config.mjs index f17737c..d154610 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,4 +6,5 @@ import react from "@astrojs/react"; // https://astro.build/config export default defineConfig({ integrations: [react()], + site: 'https://hub.educates.dev', }); diff --git a/fetch_assets.sh b/fetch_assets.sh new file mode 100755 index 0000000..1654155 --- /dev/null +++ b/fetch_assets.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +WORKSHOP_DIR="src/content/workshops" +ASSET_DIR="public/assets/workshops" +mkdir -p "$ASSET_DIR" + +for yaml in "$WORKSHOP_DIR"/*.yaml*; do + # Extract slug (first occurrence) + slug=$(grep '^slug:' "$yaml" | head -n1 | awk '{print $2}') + # Extract install_url (first occurrence) + url=$(grep '^install_url:' "$yaml" | head -n1 | awk '{print $2}') + if [ -z "$slug" ]; then + echo "No slug found in $yaml, skipping." + continue + fi + if [ -n "$url" ]; then + outname="$slug.yaml" + echo "Downloading $url to $ASSET_DIR/$outname" + if curl -sSL --fail "$url" -o "$ASSET_DIR/$outname"; then + echo "Downloaded successfully." + else + echo "Failed to download $url (not found or error)." + rm -f "$ASSET_DIR/$outname" + fi + else + echo "No install_url found in $yaml, skipping." + fi +done \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4f9560c..fd1d2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,12 @@ "@types/react-dom": "^19.1.5", "astro": "^5.7.13", "bootstrap": "5.3", + "bootstrap-icons": "^1.13.1", "react": "^19.1.0", + "react-bootstrap-icons": "^1.11.6", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-syntax-highlighter": "^15.6.1", "typescript": "^5.8.3", "yaml": "^2.8.0" }, @@ -436,6 +439,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz", + "integrity": "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1645,347 +1657,6 @@ "node": ">= 8" } }, - "node_modules/@octokit/app": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.0.1.tgz", - "integrity": "sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==", - "license": "MIT", - "dependencies": { - "@octokit/auth-app": "^8.0.1", - "@octokit/auth-unauthenticated": "^7.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-app": "^8.0.1", - "@octokit/plugin-paginate-rest": "^13.0.0", - "@octokit/types": "^14.0.0", - "@octokit/webhooks": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.1.tgz", - "integrity": "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "toad-cache": "^3.7.0", - "universal-github-app-jwt": "^2.2.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", - "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/auth-unauthenticated": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz", - "integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", - "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", - "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/auth-unauthenticated": "^7.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.0", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", - "license": "MIT", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", - "license": "MIT" - }, - "node_modules/@octokit/openapi-webhooks-types": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-11.0.0.tgz", - "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-graphql": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz", - "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.0.0.tgz", - "integrity": "sha512-nPXM3wgil9ONnAINcm8cN+nwso4QhNB13PtnlRFkYFHCUIogcH9DHak/StQYcwkkjuc7pUluLG1AWZNscgvH7Q==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-15.0.0.tgz", - "integrity": "sha512-db6UdWvpX7O6tNsdkPk1BttVwTeVdA4n8RDFeXOyjBCPjE2YPufIAlzWh8CyeH8hl/3dSuQXDa+qLmsBlkTY+Q==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-retry": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.1.tgz", - "integrity": "sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==", - "license": "MIT", - "dependencies": { - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=7" - } - }, - "node_modules/@octokit/plugin-throttling": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.1.tgz", - "integrity": "sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": "^7.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz", - "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.0.0" - } - }, - "node_modules/@octokit/webhooks": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.0.0.tgz", - "integrity": "sha512-IZV4vg/s1pqIpCs86a0tp5FQ/O94DUaqksMdNrXFSaE037TXsB+fIhr8OVig09oEx3WazVgE6B2U+u7/Fvdlsw==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-webhooks-types": "11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/webhooks-methods": "^6.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/webhooks-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", - "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", @@ -2380,12 +2051,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@types/aws-lambda": { - "version": "8.10.149", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz", - "integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==", - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3110,12 +2775,6 @@ ], "license": "MIT" }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "license": "Apache-2.0" - }, "node_modules/blob-to-buffer": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz", @@ -3155,10 +2814,20 @@ "@popperjs/core": "^2.11.8" } }, - "node_modules/bottleneck": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], "license": "MIT" }, "node_modules/boxen": { @@ -3341,6 +3010,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -4189,22 +3868,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4266,6 +3929,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -4379,6 +4055,14 @@ "unicode-trie": "^2.0.0" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4664,6 +4348,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -4742,6 +4441,30 @@ "url": "https://github.com/sponsors/brc-dd" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -4749,6 +4472,16 @@ "license": "MIT", "optional": true }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -4794,6 +4527,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -5246,6 +4989,32 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -6238,26 +6007,13 @@ "node": ">=0.10.0" } }, - "node_modules/octokit": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.2.tgz", - "integrity": "sha512-WCO9Oip2F+qsrIcNMfLwm1+dL2g70oO++pkmiluisJDMXXwdO4susVaVg1iQZgZNiDtA1qcLXs5662Mdj/vqdw==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", - "dependencies": { - "@octokit/app": "^16.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-app": "^8.0.1", - "@octokit/plugin-paginate-graphql": "^6.0.0", - "@octokit/plugin-paginate-rest": "^13.0.0", - "@octokit/plugin-rest-endpoint-methods": "^15.0.0", - "@octokit/plugin-retry": "^8.0.1", - "@octokit/plugin-throttling": "^11.0.1", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "@octokit/webhooks": "^14.0.0" - }, "engines": { - "node": ">= 20" + "node": ">=0.10.0" } }, "node_modules/ofetch": { @@ -6425,6 +6181,44 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-latin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", @@ -6613,6 +6407,17 @@ "node": ">=6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -6668,6 +6473,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap-icons": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.6.tgz", + "integrity": "sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -6689,6 +6506,12 @@ "react": "*" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -6698,6 +6521,23 @@ "node": ">=0.10.0" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6711,6 +6551,105 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/refractor/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/refractor/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/refractor/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", @@ -7349,15 +7288,6 @@ "node": ">=8.0" } }, - "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -7672,18 +7602,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", - "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", - "license": "MIT" - }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "license": "ISC" - }, "node_modules/unstorage": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.16.0.tgz", @@ -8287,6 +8205,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/package.json b/package.json index a0086b2..a08e5fc 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "astro": "^5.7.13", "bootstrap": "5.3", "react": "^19.1.0", + "react-bootstrap-icons": "^1.11.6", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-syntax-highlighter": "^15.6.1", "typescript": "^5.8.3", "yaml": "^2.8.0" }, diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..cad3f81 --- /dev/null +++ b/public/CNAME @@ -0,0 +1 @@ +hub.educates.dev \ No newline at end of file diff --git a/public/assets/kyverno-policies/educates-default.yaml b/public/assets/kyverno-policies/educates-default.yaml new file mode 100644 index 0000000..f4ea9e9 --- /dev/null +++ b/public/assets/kyverno-policies/educates-default.yaml @@ -0,0 +1,33 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-ingress-session-name +spec: + validationFailureAction: enforce + background: true + rules: + - name: require-ingress-session-name + match: + resources: + kinds: + - Ingress + context: + - name: session_namespace + apiCall: + urlPath: "/api/v1/namespaces/{{request.namespace}}" + jmesPath: 'metadata.labels."training.educates.dev/session.name" || ''@''' + preconditions: + all: + - key: "{{ request.operation }}" + operator: AnyIn + value: ["CREATE", "UPDATE"] + validate: + message: "Ingress host name must embed the workshop session name." + foreach: + - list: "request.object.spec.rules" + deny: + conditions: + any: + - key: "{{ contains(element.host, session_namespace) }}" + operator: NotEquals + value: true \ No newline at end of file diff --git a/public/assets/workshops/containerizing-a-spring-application.yaml b/public/assets/workshops/containerizing-a-spring-application.yaml new file mode 100644 index 0000000..646ed3b --- /dev/null +++ b/public/assets/workshops/containerizing-a-spring-application.yaml @@ -0,0 +1,36 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-containerizing-spring +spec: + description: A workshop describing how to run a Spring Boot application in docker. + session: + applications: + docker: + enabled: true + editor: + enabled: true + terminal: + enabled: true + layout: split + ingresses: + - name: demo + port: 8080 + namespaces: + security: + token: + enabled: false + resources: + memory: 1Gi + title: Containerizing a Spring Boot application + version: "4.3" + workshop: + files: + - image: + url: ghcr.io/educates/lab-containerizing-spring-files:4.3 + includePaths: + - /workshop/** + - /exercises/** + - /README.md + newRootPath: workshops/lab-containerizing-spring + image: jdk17-environment:* diff --git a/public/assets/workshops/creating-a-spring-application.yaml b/public/assets/workshops/creating-a-spring-application.yaml new file mode 100644 index 0000000..0b5af50 --- /dev/null +++ b/public/assets/workshops/creating-a-spring-application.yaml @@ -0,0 +1,84 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-creating-a-spring-app +spec: + description: A workshop introducing the start.spring.io web site. + environment: + objects: + - apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app: initializr + name: initializr + spec: + replicas: 1 + selector: + matchLabels: + app: initializr + template: + metadata: + labels: + app: initializr + spec: + containers: + - image: ghcr.io/vmware-tanzu-labs/educates-spring-initializr:2.0 + imagePullPolicy: Always + name: dashboard + ports: + - containerPort: 8080 + name: 8080-tcp + protocol: TCP + - apiVersion: v1 + kind: Service + metadata: + labels: + app: initializr + name: initializr + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app: initializr + type: ClusterIP + session: + applications: + editor: + enabled: true + terminal: + enabled: true + layout: split + dashboards: + - name: Initializr + url: $(ingress_protocol)://initializr-$(session_namespace).$(ingress_domain) + ingresses: + - host: initializr.$(workshop_namespace).svc.cluster.local + name: initializr + port: 8080 + protocol: http + - name: demo + port: 8080 + namespaces: + security: + token: + enabled: false + resources: + memory: 1Gi + title: Creating a Spring Boot application + version: "4.3" + workshop: + files: + - image: + url: ghcr.io/educates/labs-creating-a-spring-app-files:4.3 + includePaths: + - /workshop/** + - /exercises/** + - /README.md + - image: + url: ghcr.io/educates/lab-spring-boot-on-k8s-maven:latest + path: .m2 + image: jdk17-environment:* diff --git a/public/assets/workshops/lab-kubernetes-fundamentals.yaml b/public/assets/workshops/lab-kubernetes-fundamentals.yaml new file mode 100644 index 0000000..d717a97 --- /dev/null +++ b/public/assets/workshops/lab-kubernetes-fundamentals.yaml @@ -0,0 +1,33 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-k8s-fundamentals +spec: + description: An interactive workshop on Kubernetes fundamentals. + session: + applications: + console: + enabled: true + editor: + enabled: true + slides: + enabled: true + reveal.js: + version: 3.x + terminal: + enabled: true + layout: split + namespaces: + budget: medium + security: + policy: restricted + title: Kubernetes Fundamentals + version: "8.3" + workshop: + files: + - image: + url: ghcr.io/educates/lab-k8s-fundamentals-files:8.3 + includePaths: + - /workshop/** + - /templates/** + - /README.md diff --git a/public/assets/workshops/lab-spring-boot-on-k8s.yaml b/public/assets/workshops/lab-spring-boot-on-k8s.yaml new file mode 100644 index 0000000..f71132e --- /dev/null +++ b/public/assets/workshops/lab-spring-boot-on-k8s.yaml @@ -0,0 +1,89 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-spring-boot-on-k8s +spec: + description: Introduction to Spring Boot on Kubernetes + environment: + objects: + - apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app: initializr + name: initializr + spec: + replicas: 1 + selector: + matchLabels: + app: initializr + template: + metadata: + labels: + app: initializr + spec: + containers: + - image: ghcr.io/vmware-tanzu-labs/educates-spring-initializr:2.0 + imagePullPolicy: Always + name: dashboard + ports: + - containerPort: 8080 + name: 8080-tcp + protocol: TCP + - apiVersion: v1 + kind: Service + metadata: + labels: + app: initializr + name: initializr + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app: initializr + type: ClusterIP + session: + applications: + console: + enabled: true + vendor: octant + docker: + enabled: true + editor: + enabled: true + registry: + enabled: true + terminal: + enabled: true + layout: split + dashboards: + - name: Initializr + url: $(ingress_protocol)://initializr-$(session_namespace).$(ingress_domain) + ingresses: + - host: initializr.$(workshop_namespace).svc.cluster.local + name: initializr + port: 8080 + protocol: http + namespaces: + budget: large + security: + policy: baseline + resources: + memory: 2Gi + title: Spring Boot on Kubernetes + version: "4.3" + workshop: + files: + - image: + url: ghcr.io/educates/lab-spring-boot-on-k8s-files:4.3 + includePaths: + - /workshop/** + - /exercises/** + - /README.md + - image: + url: ghcr.io/educates/lab-spring-boot-on-k8s-maven:latest + path: .m2 + image: jdk17-environment:* diff --git a/src/components/EducatesResourceSelectorIsland.jsx b/src/components/EducatesResourceSelectorIsland.jsx index f8533d2..297e261 100644 --- a/src/components/EducatesResourceSelectorIsland.jsx +++ b/src/components/EducatesResourceSelectorIsland.jsx @@ -1,98 +1,62 @@ import React, { useState, useEffect } from "react"; import EducatesResourceGrid from "./EducatesResourceGrid.jsx"; -import { loadWorkshops, loadExtensionPackages, loadEditorExtensions, loadKyvernoPolicies } from "../utils/loadWorkshops.js"; -export default function EducatesResourceSelectorIsland() { - const [type, setType] = useState("Workshop"); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); +function getTypeFromUrl(defaultType, allResources) { + if (typeof window === 'undefined') return defaultType; + const params = new URLSearchParams(window.location.search); + const type = params.get('type'); + if (type && Object.keys(allResources).includes(type)) { + return type; + } + return defaultType; +} + +export default function EducatesResourceSelectorIsland({ allResources }) { + const defaultType = "Workshop"; + const [type, setType] = useState(() => getTypeFromUrl(defaultType, allResources)); + const [items, setItems] = useState(allResources[type] || []); + + useEffect(() => { + setItems(allResources[type] || []); + // Update the URL query parameter when type changes + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + params.set('type', type); + window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`); + } + }, [type, allResources]); useEffect(() => { - setLoading(true); - const load = async () => { - if (type === "Workshop") { - setItems(await loadWorkshops()); - } else if (type === "ExtensionPackage") { - setItems(await loadExtensionPackages()); - } else if (type === "EditorExtension") { - setItems(await loadEditorExtensions()); - } else if (type === "KyvernoPolicy") { - setItems(await loadKyvernoPolicies()); - } - else { - setItems([]); - } - setLoading(false); + // Listen for popstate (back/forward navigation) + const onPopState = () => { + setType(getTypeFromUrl(defaultType, allResources)); }; - load(); - }, [type]); + window.addEventListener('popstate', onPopState); + return () => window.removeEventListener('popstate', onPopState); + }, [allResources]); return ( <>
    Educates Resource Type: -
    - setType("Workshop")} - /> - -
    -
    - setType("ExtensionPackage")} - /> - -
    -
    - setType("EditorExtension")} - /> - -
    -
    - setType("KyvernoPolicy")} - /> - -
    + {Object.keys(allResources).map((key) => ( +
    + setType(key)} + /> + +
    + ))}
    - {loading ? ( -
    Loading...
    - ) : ( - - )} + ); } \ No newline at end of file diff --git a/src/components/ExtensionPackageDetail.jsx b/src/components/ExtensionPackageDetail.jsx index d1c7b9f..5dfbb60 100644 --- a/src/components/ExtensionPackageDetail.jsx +++ b/src/components/ExtensionPackageDetail.jsx @@ -1,95 +1,252 @@ import { useState, useEffect } from 'react'; -import ViewSourceIsland from './ViewSourceIsland.jsx'; import InstallIsland from './InstallIsland.jsx'; -import { Octokit } from 'octokit'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Clipboard, X } from 'react-bootstrap-icons'; +import { fetchSourceFile, handleDownloadSourceFile, parseOciUrl } from '../utils/sources.ts'; export default function ExtensionPackageDetail({ extensionPackage }) { - const [showInstall, setShowInstall] = useState(false); - const [downloadUrl, setDownloadUrl] = useState(""); - const [assetId, setAssetId] = useState(""); - // useEffect(() => { - // async function fetchDownloadUrl() { - // if (extensionPackage.repo && extensionPackage.repo.org && extensionPackage.repo.name && extensionPackage.repo.asset_name) { - // try { - // const octokit = new Octokit(); - // const res = await octokit.rest.repos.getReleaseByTag({ - // owner: extensionPackage.repo.org, - // repo: extensionPackage.repo.name, - // tag: extensionPackage.repo.tag || 'latest', - // }); - // const asset = res.data.assets.find(asset => asset.name === extensionPackage.repo.asset_name); - // if (asset) { - // setDownloadUrl(asset.browser_download_url); - // setAssetId(asset.id); - // } else { - // setDownloadUrl(""); - // setAssetId(""); - // } - // } catch (e) { - // setDownloadUrl(""); - // setAssetId(""); - // } - // } else { - // setDownloadUrl(""); - // setAssetId(""); - // } - // console.log("downloadUrl", downloadUrl); - // console.log("assetId", assetId); - // } - // fetchDownloadUrl(); - // }, [extensionPackage]); + + const { repo, prefix, version, destination } = parseOciUrl(extensionPackage.oci_image); + + const sampleSourceCopyContent = + ` packages: + - name: ${extensionPackage.slug} + files: + - path: . + image: + url: ${extensionPackage.oci_image} + ...` + + const sampleSource = + ` apiVersion: training.educates.dev/v1beta1 + kind: Workshop + metadata: + name: "your-workshop" + spec: + ... + workshop: + files: + - image: + url: $(image_repository)/your-workshop-files:$(workshop_version) + path: . + ${sampleSourceCopyContent}` + + + const sampleSourceWithCacheCopyContent = + ` packages: + - name: ${extensionPackage.slug} + files: + - path: . + image: + url: $(oci_image_cache)/${destination}:${version}` + + const sampleSourceWithCache = + ` apiVersion: training.educates.dev/v1beta1 + kind: Workshop + metadata: + name: "your-workshop" + spec: + ... + workshop: + files: + - image: + url: $(image_repository)/your-workshop-files:$(workshop_version) + path: . + ${sampleSourceWithCacheCopyContent} + ...` + + const ociImageCacheCopyContent = `environment: + images: + registries: + - content: + - destination: ${destination} + prefix: ${prefix} + stripPrefix: true + onDemand: true + urls: + - https://${repo}` + + const ociImageCache = `... + ${ociImageCacheCopyContent} + ...` + + useEffect(() => { + async function fetchSource() { + const source = await fetchSourceFile("extension-packages", extensionPackage.slug); + setSourceContent(source); + } + fetchSource(); + }, [extensionPackage]); return ( -
    +
    -
    -
    -
    - {extensionPackage.title} -
    +
    + +
    + {/* Left: Image, Title, Labels, Main Info */}
    -
    -

    - {extensionPackage.title} -

    -
    - {extensionPackage.labels && - extensionPackage.labels.map((label) => ( +
    + {extensionPackage.title} +
    +

    {extensionPackage.title}

    +
    + {extensionPackage.labels && extensionPackage.labels.map((label) => ( {label} ))} +
    -

    - {extensionPackage.description} -

    -
    - By {extensionPackage.author} • Created {extensionPackage.date_created} +
    +
    +
    DESCRIPTION:
    +
    {extensionPackage.description}
    +
    + {extensionPackage.release_notes && extensionPackage.release_notes.trim() !== '' && ( +
    +
    RELEASE NOTES:
    +
    {extensionPackage.release_notes}
    -
    - Version: {extensionPackage.version} + )} + {extensionPackage.oci_image && ( // Only show if the extension package has an OCI image +
    +
    USING THE EXTENSION PACKAGE:
    +
    Add to your workshop:
    +
    +
    - {/* Release Notes section, only if release_notes is not empty */} - {extensionPackage.release_notes && extensionPackage.release_notes.trim() !== '' && ( -
    -

    Release Notes

    -
    - {extensionPackage.release_notes} -
    +
    + { + let style = { display: 'block' }; + if (lineNumber >= 12 && lineNumber <= 17) { + style.backgroundColor = '#dbffdb'; + } + return { style }; + }} + > + {sampleSource} + +
    +
    + Note that typically you would cache the extension images locally to speed up the process. For that you can use + Educates built in OCI image cache  + functionality, in that case, replace the extension package image reference + with the local image reference. +
    +
    + +
    +
    + { + let style = { display: 'block' }; + if (lineNumber >= 12 && lineNumber <= 17) { + style.backgroundColor = '#dbffdb'; + } + return { style }; + }}> + {sampleSourceWithCache} + +
    +
    + This feature, makes the url of the extension on the previous snippet different than if you + were to fetch the upstream extension {extensionPackage.oci_image}  + but the one from the cache $(oci_image_cache)/{destination}:{version} +
    +
    + +
    +
    + { + let style = { display: 'block' }; + if (lineNumber >= 2 && lineNumber <= 11) { + style.backgroundColor = '#dbffdb'; + } + return { style }; + }} + > + {ociImageCache} +
    - )} - {/*
    - - -
    */} -
    +
    + )} +
    + {/* Right: Actions and Repo Info */} +
    + {extensionPackage.repo_url && ( +
    +
    REPOSITORY:
    + + Go to Repo + +
    + )} + {extensionPackage.oci_image && ( +
    +
    OCI IMAGE:
    +
    {extensionPackage.oci_image}
    +
    + )} + {(extensionPackage.author || extensionPackage.date_created || extensionPackage.version) && ( +
    + {extensionPackage.author &&
    Author: {extensionPackage.author}
    } + {extensionPackage.date_created &&
    Date: {new Date(extensionPackage.date_created).toLocaleDateString()}
    } + {extensionPackage.version &&
    Version: {extensionPackage.version}
    } +
    + )}
    diff --git a/src/components/InstallIsland.jsx b/src/components/InstallIsland.jsx index 23cec19..054e786 100644 --- a/src/components/InstallIsland.jsx +++ b/src/components/InstallIsland.jsx @@ -1,18 +1,16 @@ import { useState } from 'react'; import InstallModal from './InstallModal.jsx'; -export default function InstallIsland({ installUrl, workshopSlug, downloadUrl }) { +export default function InstallIsland({ downloadUrl }) { const [show, setShow] = useState(false); return ( <> - setShow(false)} - installUrl={installUrl} - workshopSlug={workshopSlug} downloadUrl={downloadUrl} /> {show && ( diff --git a/src/components/InstallModal.jsx b/src/components/InstallModal.jsx index 9b780f9..7af675e 100644 --- a/src/components/InstallModal.jsx +++ b/src/components/InstallModal.jsx @@ -1,6 +1,6 @@ import { useRef, useState, useEffect } from 'react'; -export default function InstallModal({ show, onClose, installUrl, workshopSlug, downloadUrl }) { +export default function InstallModal({ show, onClose, downloadUrl }) { const [copyStatus, setCopyStatus] = useState('Copy'); const installCmdRef = useRef(null); const modalRef = useRef(null); diff --git a/src/components/KyvernoPolicyDetail.jsx b/src/components/KyvernoPolicyDetail.jsx new file mode 100644 index 0000000..8ab889e --- /dev/null +++ b/src/components/KyvernoPolicyDetail.jsx @@ -0,0 +1,119 @@ +import { useState, useEffect } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Clipboard, X } from 'react-bootstrap-icons'; +import { fetchSourceFile, handleDownloadSourceFile } from '../utils/sources.ts'; + +export default function KyvernoPolicyDetail({ kyvernoPolicy }) { + const [sourceContent, setSourceContent] = useState(""); + + useEffect(() => { + async function fetchSource() { + const source = await fetchSourceFile("kyverno-policies", kyvernoPolicy.slug); + setSourceContent(source); + } + fetchSource(); + }, [kyvernoPolicy]); + + return ( +
    +
    +
    +
    + +
    + {/* Left: Image, Title, Labels, Main Info */} +
    +
    + {kyvernoPolicy.title} +
    +

    {kyvernoPolicy.title}

    +
    + {kyvernoPolicy.labels && kyvernoPolicy.labels.map((label) => ( + + {label} + + ))} +
    +
    +
    +
    +
    DESCRIPTION:
    +
    {kyvernoPolicy.description}
    +
    + {kyvernoPolicy.release_notes && kyvernoPolicy.release_notes.trim() !== '' && ( +
    +
    RELEASE NOTES:
    +
    {kyvernoPolicy.release_notes}
    +
    + )} +
    +
    SOURCE FILE
    + {/* Copy button overlay */} + +
    + + {sourceContent} + +
    +
    +
    + {/* Right: Actions and Repo Info */} +
    +
    + +
    + {kyvernoPolicy.repo_url && ( +
    +
    REPOSITORY:
    + + Go to Repo + +
    + )} + {(kyvernoPolicy.author || kyvernoPolicy.date_created || kyvernoPolicy.version) && ( +
    + {kyvernoPolicy.author &&
    Author: {kyvernoPolicy.author}
    } + {kyvernoPolicy.date_created &&
    Date: {new Date(kyvernoPolicy.date_created).toLocaleDateString()}
    } + {kyvernoPolicy.version &&
    Version: {kyvernoPolicy.version}
    } +
    + )} +
    +
    +
    +
    +
    +
    + ); +} \ No newline at end of file diff --git a/src/components/ThemeDetail.jsx b/src/components/ThemeDetail.jsx new file mode 100644 index 0000000..6d32f6b --- /dev/null +++ b/src/components/ThemeDetail.jsx @@ -0,0 +1,119 @@ +import { useState, useEffect } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Clipboard, X } from 'react-bootstrap-icons'; +import { fetchSourceFile, handleDownloadSourceFile } from '../utils/sources.ts'; + +export default function ThemeDetail({ theme }) { + const [sourceContent, setSourceContent] = useState(""); + + useEffect(() => { + async function fetchSource() { + const source = await fetchSourceFile("themes", theme.slug); + setSourceContent(source); + } + fetchSource(); + }, [theme]); + + return ( +
    +
    +
    +
    + +
    + {/* Left: Image, Title, Labels, Main Info */} +
    +
    + {theme.title} +
    +

    {theme.title}

    +
    + {theme.labels && theme.labels.map((label) => ( + + {label} + + ))} +
    +
    +
    +
    +
    DESCRIPTION:
    +
    {theme.description}
    +
    + {theme.release_notes && theme.release_notes.trim() !== '' && ( +
    +
    RELEASE NOTES:
    +
    {theme.release_notes}
    +
    + )} +
    +
    SOURCE FILE
    + {/* Copy button overlay */} + +
    + + {sourceContent} + +
    +
    +
    + {/* Right: Actions and Repo Info */} +
    +
    + +
    + {theme.repo_url && ( +
    +
    REPOSITORY:
    + + Go to Repo + +
    + )} + {(theme.author || theme.date_created || theme.version) && ( +
    + {theme.author &&
    Author: {theme.author}
    } + {theme.date_created &&
    Date: {new Date(theme.date_created).toLocaleDateString()}
    } + {theme.version &&
    Version: {theme.version}
    } +
    + )} +
    +
    +
    +
    +
    +
    + ); +} \ No newline at end of file diff --git a/src/components/ViewSourceIsland.jsx b/src/components/ViewSourceIsland.jsx index cf1a488..fe20894 100644 --- a/src/components/ViewSourceIsland.jsx +++ b/src/components/ViewSourceIsland.jsx @@ -1,14 +1,14 @@ import { useState } from 'react'; import ViewSourceModal from './ViewSourceModal.jsx'; -export default function ViewSourceIsland({ src, repo, assetId, downloadUrl }) { +export default function ViewSourceIsland({ downloadUrl, sourceContent }) { const [show, setShow] = useState(false); return ( <> - setShow(false)} src={src} repo={repo} assetId={assetId} downloadUrl={downloadUrl} /> + setShow(false)} downloadUrl={downloadUrl} sourceContent={sourceContent} /> ); } \ No newline at end of file diff --git a/src/components/ViewSourceModal.jsx b/src/components/ViewSourceModal.jsx index e0b57e5..1954525 100644 --- a/src/components/ViewSourceModal.jsx +++ b/src/components/ViewSourceModal.jsx @@ -1,8 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { IoOpen, IoCopy, IoDownload } from "react-icons/io5"; -import { Octokit } from "octokit"; -export default function ViewSourceModal({ show, onClose, src, repo, assetId, downloadUrl }) { +export default function ViewSourceModal({ show, onClose, downloadUrl, sourceContent }) { const modalRef = useRef(null); const [copyStatus, setCopyStatus] = useState("copy"); const [fileContent, setFileContent] = useState(""); @@ -28,6 +27,9 @@ export default function ViewSourceModal({ show, onClose, src, repo, assetId, dow } }, [show]); + // If sourceContent is provided, use it as the file content + const displayContent = sourceContent !== undefined ? sourceContent : fileContent; + const logError = (error) => { console.error(error); setError(error.message || error.toString()); @@ -37,76 +39,9 @@ export default function ViewSourceModal({ show, onClose, src, repo, assetId, dow setReleaseError(error.message || error.toString()); }; - // Fetch file content from GitHub using Octokit when modal is shown - useEffect(() => { - if (show && repo && repo.org && repo.name && repo.path) { - setLoading(true); - logError(""); - setFileContent(""); - const octokit = new Octokit(); - octokit.rest.repos.getContent({ - owner: repo.org, - repo: repo.name, - path: repo.path, - ref: repo.branch || 'main', - }) - .then((res) => { - // If file, decode base64 - if (res.data && res.data.type === 'file' && res.data.content) { - const decoded = atob(res.data.content.replace(/\n/g, '')); - setFileContent(decoded); - } else { - logError('File not found or not a regular file.'); - } - }) - .catch((e) => { - logError('Error loading file: ' + (e.message || e.toString())); - }) - .finally(() => setLoading(false)); - } else { - logError("No repo or path provided"); - } - }, [show, repo]); - - // Fetch release asset content if asset_id is provided - // TODO: We can not do this without a proxy because of CORS - // useEffect(() => { - // if (show && repo && repo.org && repo.name && repo.asset_name) { - // setReleaseLoading(true); - // setReleaseError(""); - // setReleaseContent(""); - // const octokit = new Octokit(); - // octokit.rest.repos.getReleaseAsset({ - // owner: repo.org, - // repo: repo.name, - // asset_id: assetId, - // // Accept header for raw content - // headers: { Accept: 'application/octet-stream' }, - // }) - // .then((res) => { - // // The content is in res.data (as a Blob or ArrayBuffer) - // // Try to convert to string if possible - // let content = res.data; - // if (content instanceof ArrayBuffer) { - // content = new TextDecoder().decode(content); - // } else if (typeof content === 'object' && content instanceof Blob) { - // content.text().then(setReleaseContent); - // return; - // } - // setReleaseContent(content); - // }) - // .catch((e) => { - // logReleaseError('Error loading release asset: ' + (e.message || e.toString())); - // }) - // .finally(() => setReleaseLoading(false)); - // } else { - // setReleaseContent(""); - // } - // }, [show, repo]); - const handleCopy = () => { - if (fileContent && navigator.clipboard) { - navigator.clipboard.writeText(fileContent); + if (displayContent && navigator.clipboard) { + navigator.clipboard.writeText(displayContent); } else if (navigator.clipboard) { navigator.clipboard.writeText(src); } @@ -126,8 +61,8 @@ export default function ViewSourceModal({ show, onClose, src, repo, assetId, dow }; const handleOpenInNewTab = () => { - if (fileContent) { - const blob = new Blob([fileContent], { type: 'text/plain' }); + if (displayContent) { + const blob = new Blob([displayContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); } else { @@ -188,7 +123,10 @@ export default function ViewSourceModal({ show, onClose, src, repo, assetId, dow
    - {loading ? ( + {/* If sourceContent is provided, show it directly, else fallback to old logic */} + {sourceContent !== undefined ? ( +
    {sourceContent}
    + ) : loading ? (
    Loading...
    diff --git a/src/components/WorkshopDetail.jsx b/src/components/WorkshopDetail.jsx index b427cdc..328cfa9 100644 --- a/src/components/WorkshopDetail.jsx +++ b/src/components/WorkshopDetail.jsx @@ -1,96 +1,132 @@ import { useState, useEffect } from 'react'; -import ViewSourceIsland from './ViewSourceIsland.jsx'; import InstallIsland from './InstallIsland.jsx'; -import { Octokit } from 'octokit'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { Clipboard, X } from 'react-bootstrap-icons'; +import { fetchSourceFile, handleDownloadSourceFile, getRepoUrl } from '../utils/sources.ts'; export default function WorkshopDetail({ workshop }) { - const [showInstall, setShowInstall] = useState(false); - const [downloadUrl, setDownloadUrl] = useState(""); - const [assetId, setAssetId] = useState(""); + const [sourceContent, setSourceContent] = useState(""); useEffect(() => { - async function fetchDownloadUrl() { - if (workshop.repo && workshop.repo.org && workshop.repo.name && workshop.repo.asset_name) { - try { - const octokit = new Octokit(); - const res = await octokit.rest.repos.getReleaseByTag({ - owner: workshop.repo.org, - repo: workshop.repo.name, - tag: workshop.repo.tag || 'latest', - }); - const asset = res.data.assets.find(asset => asset.name === workshop.repo.asset_name); - if (asset) { - setDownloadUrl(asset.browser_download_url); - setAssetId(asset.id); - } else { - setDownloadUrl(""); - setAssetId(""); - } - } catch (e) { - setDownloadUrl(""); - setAssetId(""); - } - } else { - setDownloadUrl(""); - setAssetId(""); - } - console.log("downloadUrl", downloadUrl); - console.log("assetId", assetId); + async function fetchSource() { + const source = await fetchSourceFile("workshops", workshop.slug); + setSourceContent(source); } - fetchDownloadUrl(); + fetchSource(); }, [workshop]); + const repoUrl = getRepoUrl(workshop.repository); + return ( -
    +
    -
    -
    -
    - {workshop.title} -
    +
    + +
    + {/* Left: Image, Title, Labels, Main Info */}
    -
    -

    - {workshop.title} -

    -
    - {workshop.labels && - workshop.labels.map((label) => ( +
    + {workshop.title} +
    +

    {workshop.title}

    +
    + {workshop.labels && workshop.labels.map((label) => ( {label} ))} +
    -

    - {workshop.description} -

    -
    - By {workshop.author} • Created {workshop.date_created} +
    +
    +
    WORKSHOP DESCRIPTION:
    +
    {workshop.description}
    +
    + {workshop.prerequisites && workshop.prerequisites.trim() !== '' && ( +
    +
    WORKSHOP PREREQUISITES:
    +
    {workshop.prerequisites}
    -
    - Version: {workshop.version} + )} + {workshop.release_notes && workshop.release_notes.trim() !== '' && ( +
    +
    RELEASE NOTES:
    +
    {workshop.release_notes}
    - {/* Release Notes section, only if release_notes is not empty */} - {workshop.release_notes && workshop.release_notes.trim() !== '' && ( -
    -

    Release Notes

    -
    - {workshop.release_notes} -
    -
    - )} -
    - - + )} +
    +
    SOURCE FILE
    + {/* Copy button overlay */} + +
    + + {sourceContent} +
    + {/* Right: Actions and Repo Info */} +
    +
    + + +
    + {workshop.repository && (workshop.repository.org || workshop.repository.name || workshop.repository.ref || workshop.repository.path) && ( +
    +
    REPOSITORY:
    +
      + {workshop.repository.org &&
    • org: {workshop.repository.org}
    • } + {workshop.repository.name &&
    • name: {workshop.repository.name}
    • } + {workshop.repository.ref &&
    • ref: {workshop.repository.ref}
    • } + {workshop.repository.path &&
    • path: {workshop.repository.path}
    • } +
    + {repoUrl && ( + + Go to Repo + + )} +
    + )} + {(workshop.author || workshop.date_created) && ( +
    + {workshop.author &&
    Author: {workshop.author}
    } + {workshop.date_created &&
    Date: {new Date(workshop.date_created).toLocaleDateString()}
    } +
    + )} +
    diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..1201aec --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,86 @@ +import { defineCollection, z } from "astro:content"; + +// 2. Import loader(s) +import { glob, file } from 'astro/loaders'; + +export const workshops = defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/workshops" }), + schema: z.object({ + title: z.string(), + slug: z.string().optional(), + description: z.string(), + prerequisites: z.string().optional(), + runs_on_docker: z.boolean().optional(), + author: z.string(), + image: z.string(), + date_created: z.coerce.string().optional(), + version: z.coerce.string().optional(), + release_notes: z.string().optional(), + labels: z.array(z.string()).optional(), + install_url: z.string().optional(), + repository: z + .object({ + org: z.string(), + name: z.string(), + ref: z.string().optional(), + path: z.string().optional(), + }) + .optional(), + }), +}); + +export const extensionPackages = defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/extension-packages" }), + schema: z.object({ + title: z.string(), + slug: z.string().optional(), + description: z.string(), + author: z.string(), + image: z.string(), + date_created: z.coerce.string().optional(), + version: z.coerce.string().optional(), + release_notes: z.string().optional(), + labels: z.array(z.string()).optional(), + repo_url: z.string().optional(), + oci_image: z.string().optional(), + }), +}); + +export const kyvernoPolicies = defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/kyverno-policies" }), + schema: z.object({ + title: z.string(), + slug: z.string().optional(), + description: z.string(), + author: z.string(), + image: z.string(), + date_created: z.coerce.string().optional(), + version: z.coerce.string().optional(), + release_notes: z.string().optional(), + labels: z.array(z.string()).optional(), + repo_url: z.string().optional(), + }), +}); + +export const themes = defineCollection({ + loader: glob({ pattern: "*.yaml", base: "./src/content/themes" }), + schema: z.object({ + title: z.string(), + slug: z.string().optional(), + description: z.string(), + author: z.string(), + image: z.string(), + date_created: z.coerce.string().optional(), + version: z.coerce.string().optional(), + release_notes: z.string().optional(), + labels: z.array(z.string()).optional(), + repo_url: z.string().optional(), + }), +}); + +export const collections = { + "workshops": workshops, + "extension-packages": extensionPackages, + "kyverno-policies": kyvernoPolicies, + "themes": themes, +}; \ No newline at end of file diff --git a/src/content/extension-packages/educates-cli.yaml b/src/content/extension-packages/educates-cli.yaml new file mode 100644 index 0000000..c8ba97a --- /dev/null +++ b/src/content/extension-packages/educates-cli.yaml @@ -0,0 +1,12 @@ +title: Educates CLI Extension Package +description: Provides Educates CLI to any workshop +image: educates +author: Jorge Morales +date_created: 2025-05-26 +labels: + - educates + - cli +version: 3.3.2 +repo_url: https://github.com/educates/educates-extension-packages +oci_image: ghcr.io/educates/educates-extension-packages/educates:v3.3.2 +release_notes: Initial release with latest versions of the Educates CLI. \ No newline at end of file diff --git a/src/data/spring-academy-vscode-tools.yaml b/src/content/extension-packages/spring-academy-vscode-tools.yaml similarity index 82% rename from src/data/spring-academy-vscode-tools.yaml rename to src/content/extension-packages/spring-academy-vscode-tools.yaml index 6028800..1923eb2 100644 --- a/src/data/spring-academy-vscode-tools.yaml +++ b/src/content/extension-packages/spring-academy-vscode-tools.yaml @@ -1,11 +1,8 @@ -type: EditorExtension title: Spring Academy VSCode Extension Package description: VSCode extension package for the Spring Academy to add support for Spring Boot and Spring Cloud. author: Spring Academy image: spring labels: - spring-academy - - editor-extension repo_url: https://github.com/spring-academy/spring-academy-tools -version: v1.0.1 ---- \ No newline at end of file +version: v1.0.1 \ No newline at end of file diff --git a/src/data/educates-default-kyverno-policies.yaml b/src/content/kyverno-policies/educates-default.yaml similarity index 72% rename from src/data/educates-default-kyverno-policies.yaml rename to src/content/kyverno-policies/educates-default.yaml index 33626df..94d48a9 100644 --- a/src/data/educates-default-kyverno-policies.yaml +++ b/src/content/kyverno-policies/educates-default.yaml @@ -1,6 +1,8 @@ -type: KyvernoPolicy title: Educates Default Kyverno Policies -description: Default Kyverno policies for Educates +description: |- + Default Kyverno policies for Educates + - require-ingress-session-name + - require-ingress-session-name author: Educates image: educates labels: diff --git a/src/content/themes/.gitkeep b/src/content/themes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/data/examples.yaml b/src/content/workshops/WIP/examples.yaml__ similarity index 97% rename from src/data/examples.yaml rename to src/content/workshops/WIP/examples.yaml__ index e74dfb6..33bfabb 100644 --- a/src/data/examples.yaml +++ b/src/content/workshops/WIP/examples.yaml__ @@ -1,5 +1,4 @@ --- -type: Workshop title: Grafana Visualization description: Visualize metrics and logs with Grafana dashboards. image: grafana @@ -13,7 +12,6 @@ workshop_definition_url: https://github.com/educates/workshops/grafana-visualiza version: 1.0.1 release_notes: Added new dashboard templates. --- -type: Workshop title: Helm Basics description: Introduction to Helm for Kubernetes package management. image: helm @@ -28,7 +26,6 @@ workshop_definition_url: https://github.com/educates/workshops/helm-basics/relea version: 1.0.0 release_notes: First release. --- -type: Workshop title: Istio Service Mesh description: Explore service mesh concepts and Istio features in Kubernetes. image: istio @@ -43,7 +40,6 @@ workshop_definition_url: https://github.com/educates/workshops/istio-service-mes version: 1.2.0 release_notes: Added traffic management labs. --- -type: Workshop title: Kubernetes 101 description: Introductory workshop for Kubernetes basics. image: kubernetes @@ -58,7 +54,6 @@ workshop_definition_url: https://github.com/educates/workshops/k8s101/releases/d version: 1.0.0 release_notes: Initial release. --- -type: Workshop title: NGINX Ingress Controller description: Configure and use NGINX as an ingress controller in Kubernetes. image: nginx @@ -73,7 +68,6 @@ workshop_definition_url: https://github.com/educates/workshops/nginx-ingress/rel version: 1.0.0 release_notes: Initial NGINX Ingress Controller workshop. --- -type: Workshop title: PostgreSQL on Kubernetes description: Deploy and manage PostgreSQL databases in Kubernetes. image: postgresql @@ -88,7 +82,6 @@ workshop_definition_url: https://github.com/educates/workshops/postgresql-k8s/re version: 1.0.0 release_notes: Initial PostgreSQL on Kubernetes workshop. --- -type: Workshop title: Prometheus Monitoring description: Learn how to monitor Kubernetes clusters with Prometheus. image: prometheus @@ -103,7 +96,6 @@ workshop_definition_url: https://github.com/educates/workshops/prometheus-monito version: 1.1.0 release_notes: Updated for Prometheus Operator. --- -type: Workshop title: Tekton CI/CD description: Build CI/CD pipelines for Kubernetes using Tekton. image: tekton diff --git a/src/data/labs-authoring-guides.yaml b/src/content/workshops/WIP/labs-authoring-guides.yaml similarity index 58% rename from src/data/labs-authoring-guides.yaml rename to src/content/workshops/WIP/labs-authoring-guides.yaml index 8cda00c..bbde2b2 100644 --- a/src/data/labs-authoring-guides.yaml +++ b/src/content/workshops/WIP/labs-authoring-guides.yaml @@ -1,12 +1,13 @@ ---- -type: Workshop title: Workshop Authoring slug: workshop-authoring description: Getting started with workshop authoring author: Educates Team image: educates -install_url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml -repo_url: https://github.com/educates/labs-authoring-guides -workshop_definition_url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml labels: - educates-showcase +repository: + org: educates + name: labs-authoring-guides + ref: "main" + path: workshops.yaml +install_url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml diff --git a/src/data/labs-educates-showcase.yaml b/src/content/workshops/WIP/labs-educates-showcase.yaml__ similarity index 96% rename from src/data/labs-educates-showcase.yaml rename to src/content/workshops/WIP/labs-educates-showcase.yaml__ index 2b036b3..937b96b 100644 --- a/src/data/labs-educates-showcase.yaml +++ b/src/content/workshops/WIP/labs-educates-showcase.yaml__ @@ -1,5 +1,4 @@ --- -type: Workshop title: Workshop Session slug: workshop-session description: Overview of the containerized workshop environment. @@ -11,7 +10,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Session Namespace slug: session-namespace description: Overview of Kubernetes application deployment. @@ -23,7 +21,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Virtual Cluster slug: virtual-cluster description: Overview of optional virtual Kubernetes cluster. @@ -36,7 +33,6 @@ labels: - educates-showcase - vcluster --- -type: Workshop title: Multiple Clusters slug: multiple-clusters description: Overview of working with multiple clusters. @@ -48,7 +44,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Virtual Machines slug: virtual-machines description: Overview of provisioning virtual machines. @@ -61,7 +56,6 @@ labels: - educates-showcase - virtual-machine --- -type: Workshop title: Integrated Editor slug: integrated-editor description: Overview of integrated VS Code editor. @@ -73,7 +67,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Slide Presentations slug: slide-presentations description: Overview of integrating slide presentations. @@ -85,7 +78,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Docker Runtime slug: docker-runtime description: Overview of deploying applications using docker. @@ -98,7 +90,6 @@ labels: - educates-showcase - docker --- -type: Workshop title: Examiner Scripts slug: examiner-scripts description: Overview of using self check examinations. @@ -107,7 +98,6 @@ image: educates labels: - educates-showcase --- -type: Workshop title: Java Environment slug: java-environment description: Overview of working on Java applications. @@ -117,7 +107,6 @@ labels: - educates-showcase - java --- -type: Workshop title: Conda Environment slug: conda-environment description: Overview of working on Python applications. @@ -127,7 +116,6 @@ labels: - educates-showcase - python --- -type: Workshop title: Shared Resources slug: shared-resources description: Overview of pre-creating shared resources. @@ -139,7 +127,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Session Resources slug: session-resources description: Overview of creating per session resources. @@ -151,7 +138,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Extension Packages slug: extension-packages description: Overview of adding additional extension packages. @@ -163,7 +149,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Managed Services slug: managed-services description: Overview of running additional managed processes. @@ -175,7 +160,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Ingress Proxy slug: ingress-proxy description: Overview of adding ingresses for local processes. @@ -187,7 +171,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Git Repositories slug: git-repositories description: Overview of using local hosted Git repositories. @@ -196,7 +179,6 @@ image: educates labels: - educates-showcase --- -type: Workshop title: Installing Educates slug: installing-educates description: Overview of installing Educates using the CLI. @@ -208,7 +190,6 @@ workshop_definition_url: https://github.com/educates/labs-educates-showcase/rele labels: - educates-showcase --- -type: Workshop title: Workshop Authoring slug: workshop-authoring description: Overview of authoring workshops for Educates. diff --git a/src/data/labs-installation-guides.yaml b/src/content/workshops/WIP/labs-installation-guides.yaml__ similarity index 97% rename from src/data/labs-installation-guides.yaml rename to src/content/workshops/WIP/labs-installation-guides.yaml__ index a913759..fc47e07 100644 --- a/src/data/labs-installation-guides.yaml +++ b/src/content/workshops/WIP/labs-installation-guides.yaml__ @@ -1,5 +1,4 @@ --- -type: Workshop title: Installing Educates slug: installing-educates-cli description: Installing Educates using the CLI @@ -12,7 +11,6 @@ labels: - educates-showcase - installation-guides --- -type: Workshop title: Installing Educates slug: installing-educates-carvel description: installing Educates using kapp-controller @@ -25,7 +23,6 @@ labels: - educates-showcase - installation-guides --- -type: Workshop title: Installing Educates Lookup Service slug: installing-educates-lookup-service description: Installing Educates Lookup Service @@ -39,7 +36,6 @@ labels: - installation-guides - lookup-service --- -type: Workshop title: Installing Educates standalone Lookup Service slug: installing-educates-standalone-lookup-service description: Installing Educates standalone Lookup Service @@ -53,7 +49,6 @@ labels: - installation-guides - lookup-service --- -type: Workshop title: Lookup Service Configuration slug: lookup-service-configuration description: Configuring the Educates lookup service @@ -67,7 +62,6 @@ labels: - installation-guides - lookup-service --- -type: Workshop title: Lookup Service Usage slug: lookup-service-usage description: Using the Educates lookup service diff --git a/src/content/workshops/containerizing-a-spring-application.yaml b/src/content/workshops/containerizing-a-spring-application.yaml new file mode 100644 index 0000000..e4109c2 --- /dev/null +++ b/src/content/workshops/containerizing-a-spring-application.yaml @@ -0,0 +1,32 @@ +title: Containerizing a Spring Application +slug: containerizing-a-spring-application +description: How to containerize a Spring Boot application and run it in docker +# prerequisites: |- +# In order to install this workshop, you need to have the following prerequisites: +# - Docker installed +# - Java installed +# - Spring Boot installed +# - Kubernetes installed +# - Kubernetes CLI installed +# - Kubernetes Dashboard installed +#runs_on_docker: true +author: Educates Team +date_created: 2025-05-30 +version: "4.2" +release_notes: |- + - Initial release + - Added prerequisites + - Added release notes + - Added source file +image: spring +repository: + org: educates + name: labs-spring-workshops + ref: "4.3" + path: workshops/lab-containerizing-spring/resources/workshop.yaml +install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.3/lab-containerizing-spring.yaml +labels: + - educates-showcase + - spring + - java + - docker diff --git a/src/content/workshops/creating-a-spring-application.yaml b/src/content/workshops/creating-a-spring-application.yaml new file mode 100644 index 0000000..6fc01e1 --- /dev/null +++ b/src/content/workshops/creating-a-spring-application.yaml @@ -0,0 +1,16 @@ +title: Creating a Spring Application +slug: creating-a-spring-application +description: Introduces how to use the start.spring.io web site to create a Spring Boot application +author: Educates Team +image: spring +# runs_on_docker: true +repository: + org: educates + name: labs-spring-workshops + ref: "4.3" + path: workshops/lab-creating-a-spring-app/resources/workshop.yaml +install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.3/lab-creating-a-spring-app.yaml +labels: + - educates-showcase + - spring + - java \ No newline at end of file diff --git a/src/content/workshops/lab-kubernetes-fundamentals.yaml b/src/content/workshops/lab-kubernetes-fundamentals.yaml new file mode 100644 index 0000000..defef2d --- /dev/null +++ b/src/content/workshops/lab-kubernetes-fundamentals.yaml @@ -0,0 +1,18 @@ +slug: lab-kubernetes-fundamentals +title: Kubernetes Fundamentals +description: An interactive workshop on Kubernetes fundamentals +image: kubernetes +author: GrahamDumpleton +#date_created: 2024-07-12 +labels: + - kubernetes + - fundamentals +release_notes: |- + - Convert it to Hugo + - Upgrade sample app to make it work on arm64 +repository: + org: educates + name: lab-k8s-fundamentals + ref: "8.3" + path: workshop.yaml +install_url: https://github.com/educates/lab-k8s-fundamentals/releases/download/8.3/workshop.yaml diff --git a/src/content/workshops/lab-spring-boot-on-k8s.yaml b/src/content/workshops/lab-spring-boot-on-k8s.yaml new file mode 100644 index 0000000..df52d9b --- /dev/null +++ b/src/content/workshops/lab-spring-boot-on-k8s.yaml @@ -0,0 +1,16 @@ +title: SpringBoot on Kubernetes +slug: lab-spring-boot-on-k8s +description: Steps through creating a Spring Boot application, containerizing it, and deploying it to Kubernetes. +author: Educates Team +image: spring +repository: + org: educates + name: labs-spring-workshops + ref: "4.3" + path: workshops/lab-spring-boot-on-k8s/resources/workshop.yaml +install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.3/lab-spring-boot-on-k8s.yaml +labels: + - educates-showcase + - spring + - java + - kubernetes \ No newline at end of file diff --git a/src/data/extension-package.yaml b/src/data/extension-package.yaml deleted file mode 100644 index e559796..0000000 --- a/src/data/extension-package.yaml +++ /dev/null @@ -1,23 +0,0 @@ -type: ExtensionPackage -title: Educates CLI Extension Package -description: Provides Educates CLI to any workshop -image: educates -author: Jorge Morales -date_created: 2025-05-26 -labels: - - educates - - cli -repo_url: https://github.com/educates/educates-extension-packages -version: v3.3.2 -release_notes: Initial release. ---- -type: ExtensionPackage -title: Spring Academy Extension Package -description: Extension package for the Spring Academy -author: Spring Academy -image: spring -labels: - - spring-academy - - editor-extension -repo_url: https://github.com/spring-academy/spring-academy-extension-package -version: v1.0.1 \ No newline at end of file diff --git a/src/data/lab-kubernetes-fundamentals.yaml b/src/data/lab-kubernetes-fundamentals.yaml deleted file mode 100644 index 7ecd019..0000000 --- a/src/data/lab-kubernetes-fundamentals.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -type: Workshop -title: Kubernetes Fundamentals -description: An interactive workshop on Kubernetes fundamentals -image: kubernetes -author: GrahamDumpleton -date_created: 2024-07-12 -labels: - - kubernetes - - fundamentals -install_url: https://github.com/educates/lab-k8s-fundamentals/releases/download/7.4/workshop.yaml -version: 7.4 -release_notes: Updating for new repository location -repo_url: https://github.com/educates/lab-k8s-fundamentals -workshop_definition_url: https://github.com/educates/lab-k8s-fundamentals/releases/download/7.4/workshop.yaml diff --git a/src/data/labs-spring-workshops.yaml b/src/data/labs-spring-workshops.yaml deleted file mode 100644 index ffe048f..0000000 --- a/src/data/labs-spring-workshops.yaml +++ /dev/null @@ -1,65 +0,0 @@ ---- -type: Workshop -title: Creating a Spring Application -slug: creating-a-spring-application -description: Introduces how to use the start.spring.io web site to create a Spring Boot application -author: Educates Team -image: spring -repo: - org: educates - name: labs-spring-workshops - #branch: main - tag: 4.2 - path: workshops/lab-creating-a-spring-app/resources/workshop.yaml - asset_name: workshops.yaml -# install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml -# repo_url: https://github.com/educates/labs-spring-workshops -# workshop_definition_url: https://rawcdn.githack.com/educates/labs-spring-workshops/refs/heads/main/workshops/lab-containerizing-spring/resources/workshop.yaml?min=1 -labels: - - educates-showcase - - spring - - java ---- -type: Workshop -title: Containerizing a Spring Application -slug: containerizing-a-spring-application -description: How to containerize a Spring Boot application and run it in docker -author: Educates Team -image: spring -repo: - org: educates - name: labs-spring-workshops - # branch: refs/heads/main - tag: 4.2 - path: workshops/lab-containerizing-spring/resources/workshop.yaml - asset_name: workshops.yaml -# install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml -# repo_url: https://github.com/educates/labs-spring-workshops -# workshop_definition_url: https://raw.githubusercontent.com/educates/labs-spring-workshops/refs/heads/main/workshops/lab-creating-a-spring-app/resources/workshop.yaml -labels: - - educates-showcase - - spring - - java - - docker ---- -type: Workshop -title: SpringBoot on Kubernetes -slug: springboot-on-kubernetes -description: Steps through creating a Spring Boot application, containerizing it, and deploying it to Kubernetes. -author: Educates Team -image: spring -repo: - org: educates - name: labs-spring-workshops - # branch: refs/heads/main - tag: 4.2 - path: workshops/lab-spring-boot-on-k8s/resources/workshop.yaml - asset_name: workshops.yaml -# install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml -# repo_url: https://github.com/educates/labs-spring-workshops -# workshop_definition_url: https://raw.githubusercontent.com/educates/labs-spring-workshops/refs/heads/main/workshops/lab-spring-boot-on-k8s/resources/workshop.yaml -labels: - - educates-showcase - - spring - - java - - kubernetes \ No newline at end of file diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro index b94164a..225e2a5 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -1,20 +1,55 @@ --- -import { loadEducatesResources } from "../utils/loadWorkshops"; -import type { EducatesResourceBase } from "../utils/loadWorkshops"; +import { getCollection } from "astro:content"; +import { imageMap, educatesDefaultImage } from "../utils/imageMap"; import MainLayout from "../layouts/MainLayout.astro"; import WorkshopDetail from '../components/WorkshopDetail.jsx'; import ExtensionPackageDetail from '../components/ExtensionPackageDetail.jsx'; +import KyvernoPolicyDetail from '../components/KyvernoPolicyDetail.jsx'; +import ThemeDetail from '../components/ThemeDetail.jsx'; export async function getStaticPaths() { - const resources = await loadEducatesResources(); - return resources.map((r) => ({ - params: { slug: r.slug }, - })); + const workshops = await getCollection('workshops'); + const extensionPackages = await getCollection('extension-packages'); + const kyvernoPolicies = await getCollection('kyverno-policies'); + const themes = await getCollection('themes'); + return [ + ...workshops.map(w => ({ params: { slug: w.id } })), + ...extensionPackages.map(e => ({ params: { slug: e.id } })), + ...kyvernoPolicies.map(e => ({ params: { slug: e.id } })), + ...themes.map(t => ({ params: { slug: t.id } })), + ]; } const { slug } = Astro.params; -const resources = await loadEducatesResources(); -const resource: EducatesResourceBase | undefined = resources.find((r) => r.slug === slug); + +function mapImage(resource: any) { + let image = resource.image; + if (typeof image === "string" && imageMap[image]) { + image = imageMap[image]; + } + if (typeof image === "string" && !Object.values(imageMap).includes(image)) { + image = educatesDefaultImage; + } + return { ...resource, image }; +} + +const collections: { name: any, type: string }[] = [ + { name: "workshops", type: "workshop" }, + { name: "extension-packages", type: "extension-package" }, + { name: "kyverno-policies", type: "kyverno-policy" }, + { name: "themes", type: "theme" }, +]; + +let resource = null; +for (const { name, type } of collections) { + const entries = (await getCollection(name)) as any[]; + const found = entries.find((e: any) => e.id === slug); + if (found) { + resource = mapImage({ ...found.data, type, slug: found.id }); + break; + } +} + if (!resource) { throw new Error("Resource not found"); } @@ -24,9 +59,13 @@ if (!resource) { title={`${resource.title} - EducatesHub - ${resource.type}`} description={resource.description} > - {resource.type === 'Workshop' ? ( + {resource.type === 'workshop' ? ( - ) : resource.type === 'ExtensionPackage' ? ( + ) : resource.type === 'extension-package' ? ( + ) : resource.type === 'kyverno-policy' ? ( + + ) : resource.type === 'theme' ? ( + ) : null} diff --git a/src/pages/about.astro b/src/pages/about.astro index 2c59104..b78d7a2 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -9,7 +9,7 @@ import MainLayout from "../layouts/MainLayout.astro";

    About EducatesHub

    EducatesHub is a community-driven catalog of Educates Workshops, Extension Packages, Editor Extensions, and Kyverno PoliciesEducates Workshops, Extension Packages, Themes and Kyverno Policies, making it easy to discover, search, and install hands-on Kubernetes training content and other Educates resources.

    @@ -29,7 +29,7 @@ import MainLayout from "../layouts/MainLayout.astro";

    What can you do with EducatesHub?

    Here, you will be able to:

      -
    • Browse curated workshops, extension packages, editor extensions, and kyverno policies
    • +
    • Browse curated workshops, extension packages, themes, and kyverno policies
    • Filter by topic, labels, and search keywords
    • View install instructions and resource details
    diff --git a/src/pages/index.astro b/src/pages/index.astro index b109663..8c9cd79 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,7 +1,32 @@ --- -import { loadWorkshops, loadExtensionPackages } from "../utils/loadWorkshops"; +import { getCollection } from "astro:content"; +import { imageMap, educatesDefaultImage } from "../utils/imageMap"; import MainLayout from "../layouts/MainLayout.astro"; import EducatesResourceSelectorIsland from "../components/EducatesResourceSelectorIsland.jsx"; + +function mapImage(resource: any) { + let image = resource.image; + if (typeof image === "string" && imageMap[image]) { + image = imageMap[image]; + } + if (typeof image === "string" && !Object.values(imageMap).includes(image)) { + image = educatesDefaultImage; + } + return { ...resource, image }; +} + +const workshops = (await getCollection('workshops')).map(w => mapImage({ ...w.data, slug: w.id, type: "workshop" })); +const extensionPackages = (await getCollection('extension-packages')).map(e => mapImage({ ...e.data, slug: e.id, type: "extension-package" })); +const kyvernoPolicies = (await getCollection('kyverno-policies')).map(e => mapImage({ ...e.data, slug: e.id, type: "kyverno-policy" })); +// @ts-expect-error: 'themes' is a valid collection defined in config.ts +const themes = (await getCollection('themes')).map(t => mapImage({ ...t.data, slug: t.id, type: "theme" })); + +const allResources = { + Workshop: workshops, + ExtensionPackage: extensionPackages, + // KyvernoPolicy: kyvernoPolicies, + // Theme: themes, +}; ---
    - +
    \ No newline at end of file diff --git a/src/utils/imageMap.ts b/src/utils/imageMap.ts new file mode 100644 index 0000000..788f49d --- /dev/null +++ b/src/utils/imageMap.ts @@ -0,0 +1,18 @@ +export const imageMap: Record = { + kubernetes: "/images/kubernetes.svg", + prometheus: "/images/prometheus.png", + grafana: "/images/grafana.svg", + argo: "/images/argo-cd.png", + helm: "/images/helm.png", + istio: "/images/istio.png", + tekton: "/images/tekton.png", + postgresql: "/images/postgresql.png", + nginx: "/images/nginx.png", + educates: "/images/educates.svg", + spring: "/images/spring.png", + java: "/images/java.png", + docker: "/images/docker.png", + // Add more mappings as needed +}; + +export const educatesDefaultImage = "/images/educates.svg"; \ No newline at end of file diff --git a/src/utils/loadWorkshops.ts b/src/utils/loadWorkshops.ts deleted file mode 100644 index 32d79bf..0000000 --- a/src/utils/loadWorkshops.ts +++ /dev/null @@ -1,177 +0,0 @@ -import yaml from "yaml"; - -const imageMap: Record = { - kubernetes: "/images/k8s101.png", - prometheus: "/images/prometheus.png", - grafana: "/images/grafana.svg", - argo: "/images/argo-cd.png", - helm: "/images/helm.png", - istio: "/images/istio.png", - tekton: "/images/tekton.png", - postgresql: "/images/postgresql.png", - nginx: "/images/nginx.png", - educates: "/images/educates.svg", - spring: "/images/spring.png", - java: "/images/java.png", - docker: "/images/docker.png", - // Add more mappings as needed -}; - -const educatesDefaultImage = "/images/educates.svg"; - -export interface Workshop { - type: 'Workshop'; - slug: string; - title: string; - description: string; - author: string; - image: string; - date_created?: string; - version?: string; - release_notes?: string; - labels?: string[]; - install_url?: string; - repo_url?: string; - workshop_definition_url?: string; - repo?: { - org: string; - name: string; - tag?: string; - path?: string; - asset_name?: string; - branch?: string; - }; - [key: string]: any; -} - -export interface ExtensionPackage { - type: 'ExtensionPackage'; - slug: string; - title: string; - description: string; - author: string; - image: string; - date_created?: string; - version?: string; - release_notes?: string; - labels?: string[]; - repo_url?: string; - [key: string]: any; -} - -export interface EditorExtension { - type: 'EditorExtension'; - slug: string; - title: string; - description: string; - author: string; - image: string; - date_created?: string; - version?: string; - release_notes?: string; - labels?: string[]; - repo_url?: string; - [key: string]: any; -} - -export interface KyvernoPolicy { - type: 'KyvernoPolicy'; - slug: string; - title: string; - description: string; - author: string; - image: string; - date_created?: string; - version?: string; - release_notes?: string; - labels?: string[]; - repo_url?: string; - [key: string]: any; -} - -export type EducatesResourceBase = Workshop | ExtensionPackage | EditorExtension | KyvernoPolicy; - -async function loadYamlDocuments( - typeFilter: string -): Promise { - // Get all available images in public/images - const imageFiles = import.meta.glob("/public/images/*", { - query: "?url", - import: "default", - eager: true, - }); - const availableImages = new Set( - Object.keys(imageFiles).map((path) => path.replace("/public", "")), - ); - - const files = import.meta.glob("../data/*.yaml", { - query: "?raw", - import: "default", - }); - const entriesNested = await Promise.all( - Object.entries(files).map(async ([path, loader]) => { - const raw = await loader(); - if (typeof raw !== "string") { - throw new Error("Invalid YAML file"); - } - const docs = yaml.parseAllDocuments(raw).map((doc) => doc.toJSON()); - // Derive slug from filename, but allow override in doc - return docs - .map((data, i) => { - // Check if document is empty, and if so, skip it - if (data === null || data === undefined) { - return null; - } - let slug = - data.slug || - (docs.length === 1 - ? path.split("/").pop()?.replace(".yaml", "") || "" - : `${path.split("/").pop()?.replace(".yaml", "")}-${i + 1}`); - let image = data.image; - if (typeof image === "string" && imageMap[image]) { - image = imageMap[image]; - } - if (typeof image === "string" && !availableImages.has(image)) { - image = educatesDefaultImage; - } - return { ...data, image, slug }; - }) - .filter((data) => { - // Check if document is empty, and if so, skip it - if (data === null || data === undefined) { - return false; - } - return data.type === typeFilter; - }); - }), - ); - return entriesNested.flat() as T[]; -} - -export async function loadWorkshops(): Promise { - return loadYamlDocuments('Workshop'); -} - -export async function loadExtensionPackages(): Promise { - return loadYamlDocuments('ExtensionPackage'); -} - -export async function loadEditorExtensions(): Promise { - return loadYamlDocuments('EditorExtension'); -} - -export async function loadKyvernoPolicies(): Promise { - return loadYamlDocuments('KyvernoPolicy'); -} - -export async function loadEducatesResources(): Promise { - const [workshops, extensionPackages, editorExtensions, kyvernoPolicies] = await Promise.all([ - loadWorkshops(), - loadExtensionPackages(), - loadEditorExtensions(), - loadKyvernoPolicies(), - ]); - return [...workshops, ...extensionPackages, ...editorExtensions, ...kyvernoPolicies]; -} - -// Later, this can be replaced with a server-side loader or API endpoint that reads YAML files from /public/workshops diff --git a/src/utils/sources.ts b/src/utils/sources.ts new file mode 100644 index 0000000..584b6a7 --- /dev/null +++ b/src/utils/sources.ts @@ -0,0 +1,62 @@ +export async function fetchSourceFile(type: string, slug: string): Promise { + if (type && slug) { + try { + const res = await fetch(`/assets/${type}/${slug}.yaml`); + if (res.ok) { + const text = await res.text(); + return text; + } else { + return "Source file not found."; + } + } catch (e) { + return "Error loading source file."; + } + } else { + return ""; + } +} + +export function handleDownloadSourceFile(type: string, slug: string) { + const assetPath = `/assets/${type}/${slug}.yaml`; + const link = document.createElement('a'); + link.href = assetPath; + link.download = `${slug}.yaml`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +export function getRepoUrl(repository: { org?: string, name?: string }) { + if (repository && repository.org && repository.name) { + return `https://github.com/${repository.org}/${repository.name}`; + } + return null; +} + +export function parseOciUrl(url: string | undefined | null): { repo: string, prefix: string, version: string, destination: string } { + if (!url) { + return { repo: '', prefix: '', version: '', destination: '' }; + } + // Split off the version (after the last colon) + const [main, version] = url.split(/:(?=[^:]*$)/); + // The repo is the first segment before the first '/' + const firstSlash = main.indexOf('/'); + const repo = main.substring(0, firstSlash); + // The prefix is everything after the first slash + const prefix = main.substring(firstSlash); + // The destination is the last segment before the colon + const segments = main.split('/'); + const destination = segments[segments.length - 1]; + return { repo, prefix, version, destination }; +} + +/* +Example usage: + +const url = "ghcr.io/educates/educates-extension-packages/educates:v3.3.2"; +const { repo, prefix, version, destination } = parseOciUrl(url); +console.log(repo); // "ghcr.io" +console.log(prefix); // "/educates/educates-extension-packages/educates" +console.log(version); // "v3.3.2" +console.log(destination); // "educates" +*/ \ No newline at end of file From 1e29fde586028c101f47ece8526d144c274c126a Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Tue, 17 Jun 2025 20:48:17 +0200 Subject: [PATCH 3/3] Fixing a lint issue --- README.md | 4 ++++ src/pages/index.astro | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 102b43b..8abd83b 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ EducatesHub is a static site built with [Astro](https://astro.build/) and TypeSc ## Release a new version of the site - Always work in develop branch +- Make sure that everything works locally and make astro check: + ``` + npm run astro check + ``` - When things are ready to be released, merge code into main via PR (this will publish the site) - Rebase develop on main diff --git a/src/pages/index.astro b/src/pages/index.astro index 8c9cd79..8eae789 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -18,7 +18,6 @@ function mapImage(resource: any) { const workshops = (await getCollection('workshops')).map(w => mapImage({ ...w.data, slug: w.id, type: "workshop" })); const extensionPackages = (await getCollection('extension-packages')).map(e => mapImage({ ...e.data, slug: e.id, type: "extension-package" })); const kyvernoPolicies = (await getCollection('kyverno-policies')).map(e => mapImage({ ...e.data, slug: e.id, type: "kyverno-policy" })); -// @ts-expect-error: 'themes' is a valid collection defined in config.ts const themes = (await getCollection('themes')).map(t => mapImage({ ...t.data, slug: t.id, type: "theme" })); const allResources = {