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..8abd83b 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,21 @@ 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 +- 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 ## 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 fd84443..fd1d2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +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" }, @@ -435,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", @@ -2801,6 +2814,22 @@ "@popperjs/core": "^2.11.8" } }, + "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": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -2981,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", @@ -3890,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", @@ -4003,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", @@ -4288,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", @@ -4366,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", @@ -4373,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", @@ -4418,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", @@ -4870,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", @@ -5862,6 +6007,15 @@ "node": ">=0.10.0" } }, + "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", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ofetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", @@ -6027,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", @@ -6215,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", @@ -6270,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", @@ -6282,6 +6497,21 @@ "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-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", @@ -6291,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", @@ -6304,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", @@ -7859,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 8a5082d..a08e5fc 100644 --- a/package.json +++ b/package.json @@ -19,7 +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/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..297e261 --- /dev/null +++ b/src/components/EducatesResourceSelectorIsland.jsx @@ -0,0 +1,62 @@ +import React, { useState, useEffect } from "react"; +import EducatesResourceGrid from "./EducatesResourceGrid.jsx"; + +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(() => { + // Listen for popstate (back/forward navigation) + const onPopState = () => { + setType(getTypeFromUrl(defaultType, allResources)); + }; + window.addEventListener('popstate', onPopState); + return () => window.removeEventListener('popstate', onPopState); + }, [allResources]); + + return ( + <> +
+ Educates Resource Type: + {Object.keys(allResources).map((key) => ( +
+ setType(key)} + /> + +
+ ))} +
+ + + ); +} \ 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..5dfbb60 --- /dev/null +++ b/src/components/ExtensionPackageDetail.jsx @@ -0,0 +1,257 @@ +import { useState, useEffect } from 'react'; +import InstallIsland from './InstallIsland.jsx'; +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 { 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 ( +
+
+
+
+ +
+ {/* Left: Image, Title, Labels, Main Info */} +
+
+ {extensionPackage.title} +
+

{extensionPackage.title}

+
+ {extensionPackage.labels && extensionPackage.labels.map((label) => ( + + {label} + + ))} +
+
+
+
+
DESCRIPTION:
+
{extensionPackage.description}
+
+ {extensionPackage.release_notes && extensionPackage.release_notes.trim() !== '' && ( +
+
RELEASE NOTES:
+
{extensionPackage.release_notes}
+
+ )} + {extensionPackage.oci_image && ( // Only show if the extension package has an OCI image +
+
USING THE EXTENSION PACKAGE:
+
Add to your workshop:
+
+ +
+
+ { + 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}
} +
+ )} +
+
+
+
+
+
+ ); +} \ 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..054e786 --- /dev/null +++ b/src/components/InstallIsland.jsx @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import InstallModal from './InstallModal.jsx'; + +export default function InstallIsland({ downloadUrl }) { + const [show, setShow] = useState(false); + return ( + <> + + setShow(false)} + 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..7af675e --- /dev/null +++ b/src/components/InstallModal.jsx @@ -0,0 +1,79 @@ +import { useRef, useState, useEffect } from 'react'; + +export default function InstallModal({ show, onClose, 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/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 new file mode 100644 index 0000000..fe20894 --- /dev/null +++ b/src/components/ViewSourceIsland.jsx @@ -0,0 +1,14 @@ +import { useState } from 'react'; +import ViewSourceModal from './ViewSourceModal.jsx'; + +export default function ViewSourceIsland({ downloadUrl, sourceContent }) { + const [show, setShow] = useState(false); + return ( + <> + + setShow(false)} downloadUrl={downloadUrl} sourceContent={sourceContent} /> + + ); +} \ No newline at end of file diff --git a/src/components/ViewSourceModal.jsx b/src/components/ViewSourceModal.jsx new file mode 100644 index 0000000..1954525 --- /dev/null +++ b/src/components/ViewSourceModal.jsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from "react"; +import { IoOpen, IoCopy, IoDownload } from "react-icons/io5"; + +export default function ViewSourceModal({ show, onClose, downloadUrl, sourceContent }) { + 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]); + + // 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()); + }; + const logReleaseError = (error) => { + console.error(error); + setReleaseError(error.message || error.toString()); + }; + + const handleCopy = () => { + if (displayContent && navigator.clipboard) { + navigator.clipboard.writeText(displayContent); + } 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 (displayContent) { + const blob = new Blob([displayContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + window.open(url, '_blank'); + } else { + window.open(src, '_blank'); + } + }; + + return ( +
+
+
+
+
+ Workshop Source +
+
+ + + + +
+
+
+ {/* If sourceContent is provided, show it directly, else fallback to old logic */} + {sourceContent !== undefined ? ( +
{sourceContent}
+ ) : 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..328cfa9 --- /dev/null +++ b/src/components/WorkshopDetail.jsx @@ -0,0 +1,136 @@ +import { useState, useEffect } from 'react'; +import InstallIsland from './InstallIsland.jsx'; +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 [sourceContent, setSourceContent] = useState(""); + + useEffect(() => { + async function fetchSource() { + const source = await fetchSourceFile("workshops", workshop.slug); + setSourceContent(source); + } + fetchSource(); + }, [workshop]); + + const repoUrl = getRepoUrl(workshop.repository); + + return ( +
+
+
+
+ +
+ {/* Left: Image, Title, Labels, Main Info */} +
+
+ {workshop.title} +
+

{workshop.title}

+
+ {workshop.labels && workshop.labels.map((label) => ( + + {label} + + ))} +
+
+
+
+
WORKSHOP DESCRIPTION:
+
{workshop.description}
+
+ {workshop.prerequisites && workshop.prerequisites.trim() !== '' && ( +
+
WORKSHOP PREREQUISITES:
+
{workshop.prerequisites}
+
+ )} + {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()}
} +
+ )} +
+
+
+
+
+
+ ); +} \ 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/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/content/extension-packages/spring-academy-vscode-tools.yaml b/src/content/extension-packages/spring-academy-vscode-tools.yaml new file mode 100644 index 0000000..1923eb2 --- /dev/null +++ b/src/content/extension-packages/spring-academy-vscode-tools.yaml @@ -0,0 +1,8 @@ +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 +repo_url: https://github.com/spring-academy/spring-academy-tools +version: v1.0.1 \ No newline at end of file diff --git a/src/content/kyverno-policies/educates-default.yaml b/src/content/kyverno-policies/educates-default.yaml new file mode 100644 index 0000000..94d48a9 --- /dev/null +++ b/src/content/kyverno-policies/educates-default.yaml @@ -0,0 +1,11 @@ +title: Educates Default Kyverno Policies +description: |- + Default Kyverno policies for Educates + - require-ingress-session-name + - require-ingress-session-name +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/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 50% rename from src/data/examples.yaml rename to src/content/workshops/WIP/examples.yaml__ index 79ff65f..33bfabb 100644 --- a/src/data/examples.yaml +++ b/src/content/workshops/WIP/examples.yaml__ @@ -1,14 +1,3 @@ -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. --- title: Grafana Visualization description: Visualize metrics and logs with Grafana dashboards. @@ -17,7 +6,9 @@ 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. --- @@ -29,7 +20,9 @@ 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. --- @@ -41,7 +34,9 @@ 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. --- @@ -53,7 +48,9 @@ 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. --- @@ -65,7 +62,9 @@ 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. --- @@ -77,7 +76,9 @@ 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. --- @@ -89,7 +90,9 @@ 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. --- @@ -101,6 +104,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/content/workshops/WIP/labs-authoring-guides.yaml b/src/content/workshops/WIP/labs-authoring-guides.yaml new file mode 100644 index 0000000..bbde2b2 --- /dev/null +++ b/src/content/workshops/WIP/labs-authoring-guides.yaml @@ -0,0 +1,13 @@ +title: Workshop Authoring +slug: workshop-authoring +description: Getting started with workshop authoring +author: Educates Team +image: educates +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/content/workshops/WIP/labs-educates-showcase.yaml__ b/src/content/workshops/WIP/labs-educates-showcase.yaml__ new file mode 100644 index 0000000..937b96b --- /dev/null +++ b/src/content/workshops/WIP/labs-educates-showcase.yaml__ @@ -0,0 +1,202 @@ +--- +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 +--- +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 +--- +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 +--- +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 +--- +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 +--- +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 +--- +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 +--- +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 +--- +title: Examiner Scripts +slug: examiner-scripts +description: Overview of using self check examinations. +author: Educates Team +image: educates +labels: + - educates-showcase +--- +title: Java Environment +slug: java-environment +description: Overview of working on Java applications. +author: Educates Team +image: educates +labels: + - educates-showcase + - java +--- +title: Conda Environment +slug: conda-environment +description: Overview of working on Python applications. +author: Educates Team +image: educates +labels: + - educates-showcase + - python +--- +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 +--- +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 +--- +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 +--- +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 +--- +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 +--- +title: Git Repositories +slug: git-repositories +description: Overview of using local hosted Git repositories. +author: Educates Team +image: educates +labels: + - educates-showcase +--- +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 +--- +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/content/workshops/WIP/labs-installation-guides.yaml__ b/src/content/workshops/WIP/labs-installation-guides.yaml__ new file mode 100644 index 0000000..fc47e07 --- /dev/null +++ b/src/content/workshops/WIP/labs-installation-guides.yaml__ @@ -0,0 +1,76 @@ +--- +title: Installing Educates +slug: installing-educates-cli +description: Installing Educates using the CLI +author: Educates Team +image: educates +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 +--- +title: Installing Educates +slug: installing-educates-carvel +description: installing Educates using kapp-controller +author: Educates Team +image: educates +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 +--- +title: Installing Educates Lookup Service +slug: installing-educates-lookup-service +description: Installing Educates Lookup Service +author: Educates Team +image: educates +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 +--- +title: Installing Educates standalone Lookup Service +slug: installing-educates-standalone-lookup-service +description: Installing Educates standalone Lookup Service +author: Educates Team +image: educates +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 +--- +title: Lookup Service Configuration +slug: lookup-service-configuration +description: Configuring the Educates lookup service +author: Educates Team +image: educates +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 +--- +title: Lookup Service Usage +slug: lookup-service-usage +description: Using the Educates lookup service +author: Educates Team +image: educates +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 +author: Educates Team \ No newline at end of file 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/lab-kubernetes-fundamentals.yaml b/src/data/lab-kubernetes-fundamentals.yaml deleted file mode 100644 index e8755c6..0000000 --- a/src/data/lab-kubernetes-fundamentals.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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 -version: 7.4 -release_notes: Updating for new repository location diff --git a/src/data/labs-authoring-guides.yaml b/src/data/labs-authoring-guides.yaml deleted file mode 100644 index 904298c..0000000 --- a/src/data/labs-authoring-guides.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -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 -labels: - - educates-showcase diff --git a/src/data/labs-educates-showcase.yaml b/src/data/labs-educates-showcase.yaml deleted file mode 100644 index 561765d..0000000 --- a/src/data/labs-educates-showcase.yaml +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: Workshop Session -slug: workshop-session -description: Overview of the containerized workshop environment. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Session Namespace -slug: session-namespace -description: Overview of Kubernetes application deployment. -author: Educates Team -image: kubernetes -labels: - - educates-showcase ---- -title: Virtual Cluster -slug: virtual-cluster -description: Overview of optional virtual Kubernetes cluster. -author: Educates Team -image: kubernetes -labels: - - educates-showcase - - vcluster ---- -title: Multiple Clusters -slug: multiple-clusters -description: Overview of working with multiple clusters. -author: Educates Team -image: kubernetes -labels: - - educates-showcase ---- -title: Virtual Machines -slug: virtual-machines -description: Overview of provisioning virtual machines. -author: Educates Team -image: educates -labels: - - educates-showcase - - virtual-machine ---- -title: Integrated Editor -slug: integrated-editor -description: Overview of integrated VS Code editor. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Slide Presentations -slug: slide-presentations -description: Overview of integrating slide presentations. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Docker Runtime -slug: docker-runtime -description: Overview of deploying applications using docker. -author: Educates Team -image: educates -labels: - - educates-showcase - - docker ---- -title: Examiner Scripts -slug: examiner-scripts -description: Overview of using self check examinations. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Java Environment -slug: java-environment -description: Overview of working on Java applications. -author: Educates Team -image: educates -labels: - - educates-showcase - - java ---- -title: Conda Environment -slug: conda-environment -description: Overview of working on Python applications. -author: Educates Team -image: educates -labels: - - educates-showcase - - python ---- -title: Shared Resources -slug: shared-resources -description: Overview of pre-creating shared resources. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Session Resources -slug: session-resources -description: Overview of creating per session resources. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Extension Packages -slug: extension-packages -description: Overview of adding additional extension packages. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Managed Services -slug: managed-services -description: Overview of running additional managed processes. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Ingress Proxy -slug: ingress-proxy -description: Overview of adding ingresses for local processes. -author: Educates Team -image: nginx -labels: - - educates-showcase ---- -title: Git Repositories -slug: git-repositories -description: Overview of using local hosted Git repositories. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Installing Educates -slug: installing-educates -description: Overview of installing Educates using the CLI. -author: Educates Team -image: educates -labels: - - educates-showcase ---- -title: Workshop Authoring -slug: workshop-authoring -description: Overview of authoring workshops for Educates. -author: Educates Team -image: educates -labels: - - educates-showcase diff --git a/src/data/labs-installation-guides.yaml b/src/data/labs-installation-guides.yaml deleted file mode 100644 index 60313ba..0000000 --- a/src/data/labs-installation-guides.yaml +++ /dev/null @@ -1,64 +0,0 @@ ---- -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 -labels: - - educates-showcase - - installation-guides ---- -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 -labels: - - educates-showcase - - installation-guides ---- -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 -labels: - - educates-showcase - - installation-guides - - lookup-service ---- -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 -labels: - - educates-showcase - - installation-guides - - lookup-service ---- -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 -labels: - - educates-showcase - - installation-guides - - lookup-service ---- -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 -labels: - - educates-showcase - - lookup-service -author: Educates Team \ No newline at end of file diff --git a/src/data/labs-spring-workshops.yaml b/src/data/labs-spring-workshops.yaml deleted file mode 100644 index 51c0da3..0000000 --- a/src/data/labs-spring-workshops.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -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 -labels: - - educates-showcase - - spring - - java ---- -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 -labels: - - educates-showcase - - spring - - java - - docker ---- -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 -labels: - - educates-showcase - - spring - - java - - kubernetes \ 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..225e2a5 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -1,152 +1,71 @@ --- -import { loadWorkshops } 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 workshops = await loadWorkshops(); - return workshops.map((w) => ({ - params: { slug: w.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 workshops = await loadWorkshops(); -const workshop = workshops.find((w) => w.slug === slug); -if (!workshop) { - throw new Error("Workshop not found"); + +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"); } --- -
-
-
-
-
-
- {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 === 'extension-package' ? ( + + ) : resource.type === 'kyverno-policy' ? ( + + ) : resource.type === 'theme' ? ( + + ) : null}
diff --git a/src/pages/about.astro b/src/pages/about.astro index 659ad33..b78d7a2 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, Themes 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, themes, 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..8eae789 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,15 +1,38 @@ --- -import { loadWorkshops } from "../utils/loadWorkshops"; +import { getCollection } from "astro:content"; +import { imageMap, educatesDefaultImage } from "../utils/imageMap"; import MainLayout from "../layouts/MainLayout.astro"; -import WorkshopGrid from "../components/WorkshopGrid.jsx"; -const workshops = await loadWorkshops(); +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" })); +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 64af89a..0000000 --- a/src/utils/loadWorkshops.ts +++ /dev/null @@ -1,65 +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 async function loadWorkshops() { - // 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) => { - 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 entriesNested.flat(); -} - -// 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