diff --git a/.gitignore b/.gitignore index e418491..59c24b5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,60 @@ Thumbs.db vite.config.js.timestamp-* vite.config.ts.timestamp-* -.envrc \ No newline at end of file +.envrc +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Diagnostic reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage reports +coverage/ +*.lcov + +# Build output +dist/ +out/ +tmp/ +temp/ + +# Dependency directories +bower_components/ +jspm_packages/ + +# Compiled source +*.class +*.dll +*.exe +*.o +*.so +*.pyc +*.pyo +*.pyd +__pycache__/ + +# Package files +*.tgz +*.zip + +# IDE and editor folders +.vscode/ +.idea/ +*.swp +*.swo + +# Environment files +.env.local +docs/DEVELOPER.md +static/merge-crds.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 0e0df67..1a7e173 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,71 @@ -# EDA Resource Browser +# EDA Resource Browser - Visual Architecture + +## System Overview + +Resource Browser is a compact SvelteKit application that reads CRD manifests stored in the repo (under `static/resources//...`) and exposes: + +- A release-aware resource browser and search index +- Per-resource pages showing YAML and OpenAPI-style schema details +- Lightweight version comparison and diff views + +The app is built with SvelteKit + Vite and can be deployed as a static site (for example, Cloudflare Pages). + +# 🚀 EDA Resource Browser + +A compact, fast web UI for exploring Nokia EDA Custom Resource Definitions (CRDs) and release manifests. + +Clean, focused features: + +- 🔎 Search and browse CRDs by release +- 📄 View YAML and OpenAPI-style schemas for each resource +- 🔁 Compare versions across releases and inspect diffs + +Quick start + +1. Install dependencies: + +```bash +pnpm install +``` + +2. Run the development server (hot-reload): + +```bash +pnpm run dev +``` + +3. Build for production: + +```bash +pnpm run prepare +pnpm run build +``` + +Preview the production build locally: + +```bash +pnpm run preview +``` + +Notes and tips + +- Demo data: sample CRD manifests are placed under `static/resources//...` — the app reads these for the release browser. +- CI tip: if `pnpm install` in CI errors with a frozen-lockfile mismatch, regenerate the lockfile locally with: + +```bash +pnpm install --no-frozen-lockfile +git add pnpm-lock.yaml +git commit -m "chore: update pnpm-lock.yaml" +``` + +Developer notes + +- Built with SvelteKit + Vite and styled with TailwindCSS. Routes expose a home listing and per-resource detail pages. +- Keep changes small and UI-focused. If you add a new release, drop its manifest under `static/resources//manifest.json`. + +Contributing + +- Open issues and PRs welcome. Please include a short description and screenshots where helpful. + -## Generating -Requires `yq` and `kubectl`. diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..2b05326 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,24 @@ +module.exports = { + apps: [ + { + name: 'resource-browser', + // use pnpm if pnpm is installed; fallback to npm would be explicit + script: 'pnpm', + args: 'run dev -- --host 0.0.0.0', + // Ensure cwd points to the actual workspace path for this environment + cwd: 'resource-browser', + // Do not specify an interpreter; allow PM2 to execute `pnpm` directly + // interpreter: 'bash', + env: { + NODE_ENV: 'development' + }, + watch: false, + autorestart: true, + restart_delay: 5000, + max_restarts: 10, + error_file: 'resource-browser/logs/pm2-error.log', + out_file: 'resource-browser/logs/pm2-out.log', + log_date_format: 'YYYY-MM-DD HH:mm Z' + } + ] +} diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..6db056b --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,23 @@ +module.exports = { + apps: [ + { + name: 'resource-browser', + script: 'pnpm', + args: 'run dev -- --host 0.0.0.0', + // Ensure this matches where the repo is checked out on the host + cwd: '/home/noksysadm/work/resource-browser', + // Leave interpreter unset so PM2 executes the 'pnpm' binary directly + // interpreter: 'bash', + env: { + NODE_ENV: 'development' + }, + watch: false, + autorestart: true, + restart_delay: 5000, + max_restarts: 10, + error_file: '/home/noksysadm/work/resource-browser/logs/pm2-error.log', + out_file: '/home/noksysadm/work/resource-browser/logs/pm2-out.log', + log_date_format: 'YYYY-MM-DD HH:mm Z' + } + ] +} diff --git a/package-lock.json b/package-lock.json index b372041..81b6c9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "resource-browser", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "resource-browser", - "version": "0.0.1", + "version": "1.0.0", "devDependencies": { "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", diff --git a/package.json b/package.json index 93dd09c..d203720 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "resource-browser", "private": true, - "version": "0.0.1", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite dev --host 0.0.0.0", @@ -16,6 +16,9 @@ "test": "npm run test:unit -- --run && npm run test:e2e", "test:e2e": "playwright test" }, + "dependencies": { + "ajv": "^8.17.1" + }, "devDependencies": { "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 558cc61..2ad7e9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + ajv: + specifier: ^8.17.1 + version: 8.17.1 devDependencies: '@eslint/compat': specifier: ^1.2.5 @@ -1079,6 +1083,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1343,6 +1350,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1469,6 +1479,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1819,6 +1832,10 @@ packages: resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} engines: {node: '>=8'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2968,6 +2985,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -3271,6 +3295,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -3366,6 +3392,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} keyv@4.5.4: @@ -3601,6 +3629,8 @@ snapshots: regexparam@3.0.0: {} + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} reusify@1.1.0: {} diff --git a/src/app.css b/src/app.css index 456d675..4f9c6a7 100644 --- a/src/app.css +++ b/src/app.css @@ -2,14 +2,9 @@ @custom-variant dark (&:where(.dark, .dark *)); -@font-face { - font-family: "NokiaPureText"; - src: url("/fonts/NokiaPureText_Rg.ttf"); - font-style: normal; - font-weight: normal; - font-display: swap; -} - +/* ============================================ + FONT FACES + ============================================ */ @font-face { font-family: "NokiaPureText"; src: url("/fonts/NokiaPureText_Lt.ttf"); @@ -42,14 +37,6 @@ font-display: swap; } -@font-face { - font-family: "NokiaPureHeadline"; - src: url("/fonts/NokiaPureHeadline_Rg.ttf"); - font-style: normal; - font-weight: normal; - font-display: swap; -} - @font-face { font-family: "NokiaPureHeadline"; src: url("/fonts/NokiaPureHeadline_ULt.ttf"); @@ -84,12 +71,15 @@ @font-face { font-family: "NokiaPureHeadline"; - src: url("/fonts/NokiaPureHeadline_XBd.ttf"); + src: url("/fonts/NokiaPureHeadline_XBd..ttf"); font-weight: 800; font-style: normal; font-display: swap; } +/* ============================================ + THEME VARIABLES + ============================================ */ @theme { --font-nunito: 'Nunito', sans-serif; --font-fira: 'Fira Code', monospace; @@ -98,68 +88,276 @@ --default-font-family: 'NokiaPureText', sans-serif; } +/* ============================================ + BASE STYLES + ============================================ */ @layer base { + /* Modern CSS reset */ + :global(*), + :global(*::before), + :global(*::after) { + box-sizing: border-box; + margin: 0; + padding: 0; + } + :global(html) { scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Prevent the browser (document) from showing a scrollbar; child containers handle scrolling */ + overflow: hidden; + } + + :global(body) { + @apply antialiased text-white; + font-family: var(--font-nokia), 'Nunito', sans-serif; + background-color: #0f172a; + min-height: 100vh; + /* Hide the document scrollbar so child containers manage scrolling */ + overflow: hidden; + } + + /* Headings and display text should use the Nokia headline family */ + :global(h1), + :global(h2), + :global(h3), + :global(h4) { + font-family: var(--font-nokia-headline), var(--font-nokia), system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; + letter-spacing: -0.01em; + line-height: 1.05; + } + + /* Small UI copy / pills use medium weight for clarity */ + :global(.pill), + :global(.badge) { + font-family: var(--font-nokia), system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; + font-weight: 600; + letter-spacing: 0.01em; + } + + /* Ensure images and media are responsive by default */ + :global(img), + :global(picture), + :global(video), + :global(canvas), + :global(svg) { + display: block; + max-width: 100%; + height: auto; + } + + /* Remove default button styles */ + :global(button) { + background: none; + border: none; + font: inherit; + cursor: pointer; + } + + /* Smooth transitions for interactive elements */ + :global(button), + :global(a), + :global(input), + :global(select), + :global(textarea) { + @apply transition-colors duration-200; + } + + /* Modern focus styles */ + :global(*:focus-visible) { + @apply outline-2 outline-offset-2 outline-blue-500; + } + + /* Remove default list styles */ + :global(ul), + :global(ol) { + list-style: none; } } +/* ============================================ + UTILITY CLASSES + ============================================ */ @layer utilities { + /* Prevent root scrolling (for modals/overlays) */ + .no-root-scroll { + overflow: hidden !important; + height: 100% !important; + } + + /* Background with header image */ .has-header-img { - background: url(/images/background.webp) center center; + background-image: + linear-gradient(120deg, rgba(2,10,35,0.42), rgba(4,45,120,0.48)), + linear-gradient(60deg, rgba(7,36,120,0.25), rgba(18,164,228,0.2)), + url('/images/background-crd.webp'); background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + /* Use overlay to keep the blue tones visible while providing a darkened foreground for contrast */ + background-blend-mode: overlay, screen, normal; + /* Use a light foreground text color when over the header image to better contrast with darker background */ + color: rgba(255, 255, 255, 0.95); + } + + /* Subtle grid pattern background (dark) */ + .bg-pattern { + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px); + background-size: 40px 40px; + } + + /* Grid pattern for light backgrounds */ + .bg-grid-pattern { + background-image: + linear-gradient(to right, rgba(99, 102, 241, 0.03) 1px, transparent 1px), + linear-gradient(to bottom, rgba(99, 102, 241, 0.03) 1px, transparent 1px); + background-size: 32px 32px; } + /* Dropdown hover effect */ .dropdown:hover .dropdown-content { display: block; } + + /* Custom border width for diff indicators */ + .border-l-3 { + border-left-width: 3px; + } + + /* Smooth fade-in animation */ + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .animate-fade-in { + animation: fadeIn 0.3s ease-out; + } + + /* Pulse animation for interactive elements */ + @keyframes pulse-subtle { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.8; + } + } + + .animate-pulse-subtle { + animation: pulse-subtle 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } + + /* Professional glow effect for amber elements */ + .glow-amber { + box-shadow: 0 0 20px rgba(245, 158, 11, 0.3), 0 0 40px rgba(245, 158, 11, 0.1); + } + + /* Enhanced shadow for cards */ + .shadow-pro { + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.1); + } + + /* Subtle border glow */ + .border-glow { + border: 1px solid rgba(245, 158, 11, 0.2); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + } } +/* ============================================ + COMPONENT STYLES + ============================================ */ @layer components { + /* Custom scrollbar styling */ .scroll-thin { /* Firefox support */ scrollbar-width: thin; - scrollbar-color: #bec4c4 #f1f1f1; + scrollbar-color: rgb(109, 109, 109) rgb(55 65 81); - /* Scrollbar size */ + /* Webkit scrollbar */ &::-webkit-scrollbar { - width: 2px; - height: 2px; + width: 6px; + height: 6px; } - /* Light mode track and thumb */ &::-webkit-scrollbar-track { - background: #f1f1f1; + background: rgb(55 65 81); } + &::-webkit-scrollbar-thumb { - background: #bec4c4; + background: rgb(109, 109, 109); + border-radius: 3px; } - /* Light mode hover */ - @variant hover { - &::-webkit-scrollbar-thumb:hover { - background: #555; - } + &::-webkit-scrollbar-thumb:hover { + background: rgb(156, 163, 175); } + } + + /* Sidebar scroll thumb - a small visible indicator suitable for mobile and desktop */ + .sidebar-scroll-thumb { + position: absolute; + right: 6px; + top: 0; + width: 4px; + border-radius: 4px; + background: rgba(255,255,255,0.12); + transition: transform 120ms linear, opacity 200ms ease-in-out; + will-change: transform, opacity; + opacity: 0; + pointer-events: none; /* don't block clicks */ + } + + /* When a header image is active, remove any border/backgrounds for top elements so the image shows through */ + :global(.has-header-img) .sidebar-header, + :global(.has-header-img) .content-header { + background: transparent !important; + border-color: transparent !important; + box-shadow: none !important; + --tw-bg-opacity: 0 !important; + } - /* Dark mode overrides */ - @variant dark { - /* Firefox support */ - scrollbar-color: rgb(109, 109, 109) rgb(55 65 81); - - &::-webkit-scrollbar-track { - background: rgb(55 65 81); - } - &::-webkit-scrollbar-thumb { - background: rgb(109, 109, 109); - } - - /* Dark mode hover */ - @variant hover { - &::-webkit-scrollbar-thumb:hover { - background: #555; - } - } + /* slightly more visible on hover or when user is actively interacting*/ + .scroll-thin:hover + .sidebar-scroll-thumb, .sidebar-scroll-thumb:hover { + background: rgba(255,255,255,0.18); + opacity: 1; + } + + /* Ensure side-scroll thumb is more visible on mobile devices */ + @media (max-width: 1024px) { + .sidebar-scroll-thumb { + right: 10px; + width: 6px; + background: rgba(255,255,255,0.22); + box-shadow: 0 0 6px rgba(0,0,0,0.25) inset; } } + + /* Pro select styling used for release dropdowns and tool selects */ + .select-pro { + @apply px-3 py-1 rounded-lg text-sm transition-colors bg-white/5 dark:bg-black/30 text-gray-900 dark:text-gray-200 border border-white/10 dark:border-white/10; + } + + .select-pro:focus { + @apply outline-none ring-2 ring-cyan-400; + } + + /* Utility to suppress blur/backdrop blur on UI elements (use for buttons that must remain sharp) */ + .no-blur { + filter: none !important; + -webkit-filter: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + } } \ No newline at end of file diff --git a/src/app.html b/src/app.html index 0a28813..242755b 100644 --- a/src/app.html +++ b/src/app.html @@ -1,28 +1,25 @@ + + + + - - - - + + + - - - - %sveltekit.head% + %sveltekit.head% + + - - - - -
%sveltekit.body%
- - - \ No newline at end of file + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..121cc33 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,54 @@ +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = async ({ event, resolve }) => { + const response = await resolve(event); + + // Suppress 404 logging for CRD version availability checks + // These are intentional HEAD requests to check if a version exists + if (response.status === 404 && + event.url.pathname.includes('/resources/') && + event.url.pathname.endsWith('.yaml')) { + // Don't log these 404s - they're expected during version availability checks + return response; + } + + return response; +}; + +// Suppress console output for CRD check 404s +const originalConsoleWarn = console.warn; +const originalConsoleInfo = console.info; +const originalConsoleLog = console.log; + +console.warn = (...args: any[]) => { + const msg = args.join(' '); + if (typeof msg === 'string' && + msg.includes('Not found:') && + msg.includes('/resources/') && + msg.includes('.yaml')) { + return; // Skip logging + } + originalConsoleWarn.apply(console, args); +}; + +console.info = (...args: any[]) => { + const msg = args.join(' '); + if (typeof msg === 'string' && + msg.includes('Not found:') && + msg.includes('/resources/') && + msg.includes('.yaml')) { + return; // Skip logging + } + originalConsoleInfo.apply(console, args); +}; + +console.log = (...args: any[]) => { + const msg = args.join(' '); + if (typeof msg === 'string' && + msg.includes('Not found:') && + msg.includes('/resources/') && + msg.includes('.yaml')) { + return; // Skip logging + } + originalConsoleLog.apply(console, args); +}; diff --git a/src/lib/components/AnimatedBackground.svelte b/src/lib/components/AnimatedBackground.svelte new file mode 100644 index 0000000..b4fa359 --- /dev/null +++ b/src/lib/components/AnimatedBackground.svelte @@ -0,0 +1,52 @@ + +
+ +
+ + +
+ + +
+ + +
+ + +
+
+ + diff --git a/src/lib/components/DiffRender.svelte b/src/lib/components/DiffRender.svelte new file mode 100644 index 0000000..8ee79b1 --- /dev/null +++ b/src/lib/components/DiffRender.svelte @@ -0,0 +1,128 @@ + + +

{type.toUpperCase()}

+
    +
  • + {desc} +
  • + {#if 'properties' in scope} +
    + {#each Object.entries(scope.properties) as [key, folder]} + {@const requiredList = 'required' in scope ? scope.required : []} + {@const diffStatus = getDiffStatus(key)} + {@const bgClass = + diffStatus === 'added' ? 'bg-green-100 dark:bg-green-900/20 border-l-3 border-green-600 dark:border-green-500' : + diffStatus === 'removed' ? 'bg-red-100 dark:bg-red-900/20 border-l-3 border-red-600 dark:border-red-500' : + diffStatus === 'modified' ? 'bg-amber-100 dark:bg-yellow-900/20 border-l-3 border-amber-600 dark:border-yellow-500' : + '' + } + {@const fieldCompareData = compareData ? (() => { + const compareScope = getScope(compareData); + if ('properties' in compareScope && key in compareScope.properties) { + return compareScope.properties[key]; + } + return null; + })() : null} +
    + {#if diffStatus !== 'unchanged'} + + {diffStatus === 'added' ? '+' : diffStatus === 'removed' ? '−' : '~'} + + {/if} + +
    + {/each} +
    + {/if} +
diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index b1ca02a..10b1d79 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -1,89 +1,56 @@ -
-
- Created by - Siva Sivakumar - - & - Roman Dodin - +
+ diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index e5ad495..e32b757 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -1,67 +1,95 @@ - \ No newline at end of file diff --git a/src/lib/components/Render.svelte b/src/lib/components/Render.svelte index b592a31..ba528e0 100644 --- a/src/lib/components/Render.svelte +++ b/src/lib/components/Render.svelte @@ -19,13 +19,13 @@ : 'border-gray-300 dark:border-gray-600'; -

{type.toUpperCase()}

+

{type.toUpperCase()}

    -
  • +
  • {desc}
  • {#if 'properties' in scope} -
    +
    {#each Object.entries(scope.properties) as [key, folder]} {@const requiredList = 'required' in scope ? scope.required : []} + import { writable, derived } from 'svelte/store'; + import { sidebarOpen } from '$lib/store'; + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + import yaml from 'js-yaml'; + import releasesYaml from '$lib/releases.yaml?raw'; + import type { EdaRelease, ReleasesConfig, CrdResource } from '$lib/structure'; + import { onMount, onDestroy } from 'svelte'; + + const releasesConfig = yaml.load(releasesYaml) as ReleasesConfig; + const defaultRelease = releasesConfig.releases.find(r => r.default) || releasesConfig.releases[0]; + + export const selectedRelease = writable(defaultRelease); + export const crdMetaStore = writable([]); + const resourceSearch = writable(''); + + // Filter for resource type: 'all' | 'state' | 'config' + export const resourceTypeFilter = writable<'all' | 'state' | 'config'>('all'); + + // Mobile menu state + let isMobileMenuOpen = false; + // Desktop sidebar state (from store) — auto-subscribed via `$sidebarOpen` + let resourceListEl: HTMLElement | null = null; + let sidebarThumbEl: HTMLElement | null = null; + let hideThumbTimer: number | null = null; + + // Load CRDs dynamically for the selected release + async function loadCrdsForRelease(release: EdaRelease) { + try { + const manifestResponse = await fetch(`/${release.folder}/manifest.json`); + if (manifestResponse.ok) { + const manifest = await manifestResponse.json(); + crdMetaStore.set(manifest); + // ensure the scroll thumb updates once the DOM is updated with new content + setTimeout(() => updateThumb(), 0); + return manifest as CrdResource[]; + } + } catch (e) { + // No manifest file found, loading from resources.yaml (fallback) + } + + try { + const res = await import('$lib/resources.yaml?raw'); + const resources = yaml.load(res.default) as any; + const crdMeta = Object.values(resources).flat() as CrdResource[]; + crdMetaStore.set(crdMeta); + setTimeout(() => updateThumb(), 0); + return crdMeta; + } catch (e) { + console.error('Failed to load resources:', e); + crdMetaStore.set([]); + return [] as CrdResource[]; + } + } + + $: loadCrdsForRelease($selectedRelease); + + const resourceSearchFilter = derived( + [resourceSearch, crdMetaStore, resourceTypeFilter], + ([$resourceSearch, $crdMetaStore, $resourceTypeFilter]) => { + const query = $resourceSearch.toLowerCase(); + let filtered = $crdMetaStore.filter((x) => + query.split(/\s+/).every((term) => x.name.toLowerCase().includes(term)) + ); + + if ($resourceTypeFilter === 'state') { + filtered = filtered.filter((x) => x.name.toLowerCase().includes('states')); + } else if ($resourceTypeFilter === 'config') { + filtered = filtered.filter((x) => !x.name.toLowerCase().includes('states')); + } + + return filtered; + } + ); + + onMount(() => { + // Check if a specific release is requested + const urlParams = new URLSearchParams(window.location.search); + const releaseParam = urlParams.get('release'); + if (releaseParam) { + const foundRelease = releasesConfig.releases.find(r => r.name === releaseParam); + if (foundRelease) { + selectedRelease.set(foundRelease); + } + } + }); + + async function handleReleaseChange(event: Event) { + const select = event.target as HTMLSelectElement; + const newReleaseName = select.value; + const newRelease = releasesConfig.releases.find(r => r.name === newReleaseName); + if (newRelease) { + selectedRelease.set(newRelease); + + // Check if we're on a detail page + const currentPath = window.location.pathname; + const isDetailPage = currentPath !== '/' && currentPath.includes('/'); + + if (isDetailPage) { + // Extract current resource name and version from URL + const pathParts = currentPath.split('/').filter(p => p); + if (pathParts.length >= 1) { + const currentResourceName = pathParts[0]; + const currentVersion = pathParts.length >= 2 ? pathParts[1] : ''; + + // Load the manifest for the new release and search for the resource + const manifest = await loadCrdsForRelease(newRelease); + const resourceInNewRelease = (manifest || []).find(r => r.name === currentResourceName); + if (resourceInNewRelease) { + // If the same version exists in the new release, choose it; otherwise fall back to the first available version + let targetVersion = resourceInNewRelease.versions?.find(v => v.name === currentVersion)?.name; + if (!targetVersion) { + // Pick a reasonable default: prefer the last version in array if sorted or the first entry + targetVersion = resourceInNewRelease.versions && resourceInNewRelease.versions.length ? resourceInNewRelease.versions[0].name : ''; + } + if (targetVersion) { + goto(`/${currentResourceName}/${targetVersion}?release=${newRelease.name}`); + } else { + // No version found; redirect to browse mode for the new release + goto(`/?browse=true&release=${newRelease.name}`); + } + } else { + // Resource doesn't exist in new release, go to browse mode + goto(`/?browse=true&release=${newRelease.name}`); + } + return; + } + } + + // Default: redirect to homepage in browse mode + goto(`/?browse=true&release=${newRelease.name}`); + } + } + + async function handleResourceClick(resource: string, resDef: CrdResource) { + // Ensure we select a version that exists in the currently selected release + try { + const manifest = await loadCrdsForRelease($selectedRelease); + const resourceInRelease = (manifest || []).find(r => r.name === resource); + let targetVersion = ''; + // Prefer the same version that the resources.yaml object suggests if available in the release manifest + if (resourceInRelease && resourceInRelease.versions && resourceInRelease.versions.length) { + const pref = resDef?.versions?.[0]?.name; + targetVersion = resourceInRelease.versions.find(v => v.name === pref)?.name || resourceInRelease.versions[0].name; + } else { + // Fallback to the resources.yaml version if the manifest doesn't include it + targetVersion = resDef?.versions?.[0]?.name || ''; + } + if (targetVersion) { + goto(`/${resource}/${targetVersion}?release=${$selectedRelease.name}`); + } else { + goto(`/?browse=true&release=${$selectedRelease.name}`); + } + } catch (e) { + // In case of any error, fallback to the original behavior to avoid blocking navigation + goto(`/${resource}/${resDef.versions[0].name}?release=${$selectedRelease.name}`); + } + // Close mobile menu after navigation + isMobileMenuOpen = false; + } + + /** + * Determine the preferred version used for navigation for a resource in the current release. + * This uses the release manifest if available, and prefers the version hinted by `resDef.versions[0]`. + */ + function preferredVersionForResource(resDef: CrdResource) { + const manifestEntry = $crdMetaStore.find((r) => r.name === resDef.name) || resDef; + const pref = resDef?.versions?.[0]?.name; + if (manifestEntry && manifestEntry.versions && manifestEntry.versions.length) { + const found = manifestEntry.versions.find((v) => v.name === pref); + return found ? found.name : manifestEntry.versions[0].name; + } + return pref || ''; + } + + function isPreferredVersionDeprecated(resDef: CrdResource) { + const manifestEntry = $crdMetaStore.find((r) => r.name === resDef.name) || resDef; + const pv = preferredVersionForResource(resDef); + if (!pv || !manifestEntry.versions) return false; + const vobj = manifestEntry.versions.find((v) => v.name === pv); + return !!(vobj && vobj.deprecated); + } + + function toggleMobileMenu() { + isMobileMenuOpen = !isMobileMenuOpen; + } + + function closeMobileMenu() { + isMobileMenuOpen = false; + } + + function handleListScroll() { + updateThumb(); + } + + function updateThumb() { + if (!resourceListEl || !sidebarThumbEl) return; + const { scrollTop, scrollHeight, clientHeight } = resourceListEl; + if (scrollHeight <= clientHeight) { + // no scroll needed + sidebarThumbEl.style.opacity = '0'; + return; + } + const thumbHeight = Math.max((clientHeight / scrollHeight) * clientHeight, 20); + const maxTop = clientHeight - thumbHeight; + const top = (scrollTop / (scrollHeight - clientHeight)) * maxTop; + sidebarThumbEl.style.height = `${thumbHeight}px`; + sidebarThumbEl.style.transform = `translateY(${top}px)`; + sidebarThumbEl.style.opacity = '1'; + // reset hide timer so the thumb fades out after a brief idle + if (hideThumbTimer) window.clearTimeout(hideThumbTimer); + hideThumbTimer = window.setTimeout(() => { + if (sidebarThumbEl) sidebarThumbEl.style.opacity = '0'; + }, 1200); + } + + onMount(() => { + // initial calculation and listen for resizes to adjust thumb + updateThumb(); + const resizeObserver = new ResizeObserver(() => updateThumb()); + if (resourceListEl) resizeObserver.observe(resourceListEl); + window.addEventListener('resize', updateThumb); + onDestroy(() => { + if (resourceListEl) resizeObserver.unobserve(resourceListEl); + window.removeEventListener('resize', updateThumb); + }); + }); + + + + + + + + +{#if isMobileMenuOpen} +
    e.key === 'Escape' && closeMobileMenu()} + role="button" + tabindex="0" + >
    +{/if} + + + \ No newline at end of file diff --git a/src/lib/components/TopHeader.svelte b/src/lib/components/TopHeader.svelte new file mode 100644 index 0000000..a252ca0 --- /dev/null +++ b/src/lib/components/TopHeader.svelte @@ -0,0 +1,114 @@ + + + diff --git a/src/lib/components/Tree.svelte b/src/lib/components/Tree.svelte index f878989..e6bd58b 100644 --- a/src/lib/components/Tree.svelte +++ b/src/lib/components/Tree.svelte @@ -2,7 +2,7 @@ import { copy } from 'svelte-copy'; import { expandAll, expandAllScope, ulExpanded } from '$lib/store'; - import { getDescription, getScope, hashExistDeep, getDefault, getEnum } from './functions'; + import { getDescription, getScope, hashExistDeep, getDefault, getEnum, getFormat, getMinimum, getMaximum } from './functions'; import type { Schema } from '$lib/structure'; export let hash: string; @@ -13,12 +13,97 @@ export let parent: string; export let expanded: boolean; export let borderColor: string; + + // Diff mode props + export let diffMode: boolean = false; + export let diffCompareData: Schema | null = null; + export let diffSide: 'left' | 'right' = 'left'; + export let diffCurrentData: Schema | null = null; + + // reference exported prop so Svelte treats it as used (it's passed from parent/children) + $: { + // no-op read to avoid "unused export" diagnostics + void diffCurrentData; + } let currentId = `${parent}.${key}`; let timeout: ReturnType; let defaultVal: string = ''; + let formatVal: string = ''; + let minVal: number | undefined = undefined; + let maxVal: number | undefined = undefined; $: defaultVal = getDefault(folder); + $: formatVal = getFormat(folder); + $: minVal = getMinimum(folder); + $: maxVal = getMaximum(folder); + + // Diff helper functions + function normalizeForComparison(schema: Schema): any { + if (!schema) return null; + + // Only compare the essential properties that matter for diffs + const normalized: any = {}; + + if ('type' in schema) normalized.type = schema.type; + if ('description' in schema) normalized.description = schema.description; + if ('default' in schema) normalized.default = schema.default; + if ('enum' in schema) normalized.enum = schema.enum; + if ('format' in schema) normalized.format = schema.format; + if ('minimum' in schema) normalized.minimum = schema.minimum; + if ('maximum' in schema) normalized.maximum = schema.maximum; + if ('required' in schema) normalized.required = schema.required; + + // For objects and arrays, compare structure + const scope = getScope(schema); + if ('properties' in scope) { + normalized.properties = Object.keys(scope.properties).sort(); + } + if ('items' in scope) { + normalized.items = normalizeForComparison(scope.items); + } + + return normalized; + } + + function getNestedDiffStatus(): 'added' | 'removed' | 'modified' | 'unchanged' { + if (!diffMode) return 'unchanged'; + + // Check if this specific field exists in both versions + const existsInCompare = diffCompareData !== null && diffCompareData !== undefined; + const existsHere = folder !== null && folder !== undefined; + + // Field added (exists here but not in compare) + if (existsHere && !existsInCompare) { + return diffSide === 'left' ? 'removed' : 'added'; + } + + // Field removed (doesn't exist here but exists in compare) + if (!existsHere && existsInCompare) { + return 'unchanged'; // Don't highlight on this side + } + + // Both exist - check if they're different by comparing normalized versions + if (existsHere && existsInCompare && diffCompareData) { + // prefer an explicitly-provided current-side value when available (passed from parent), + // otherwise fall back to the local folder. This also ensures the exported + // `diffCurrentData` prop is referenced so it's not treated as unused. + const currentNormalized = normalizeForComparison(diffCurrentData ?? folder); + const compareNormalized = normalizeForComparison(diffCompareData); + + const currentStr = JSON.stringify(currentNormalized); + const compareStr = JSON.stringify(compareNormalized); + + if (currentStr !== compareStr) { + return 'modified'; + } + } + + return 'unchanged'; + } + + $: nestedDiffStatus = getNestedDiffStatus(); + $: showDiffIndicator = diffMode && nestedDiffStatus !== 'unchanged'; function handleLocalExpand() { expanded = !expanded; @@ -54,78 +139,170 @@ } -
  • -
    +
  • +
    + {#if showDiffIndicator} + + {nestedDiffStatus === 'added' ? '+' : nestedDiffStatus === 'removed' ? '−' : '~'} + + {/if} {#if source !== 'uploaded'} - { - target.innerHTML = '#'; - }, 500); + '#'} + class="cursor-pointer text-gray-400 dark:text-gray-500 {expanded + ? 'block' + : 'hidden'} hover:text-gray-700 md:hidden md:group-hover:block md:group-active:block dark:hover:text-gray-300" + on:click={(e) => { + // Prefer using the current page path/search when available (resource view) + const pathParts = window.location.pathname.split('/').filter(Boolean); + let resName = ''; + let resVersion = ''; + if (pathParts.length >= 2) { + resName = pathParts[0]; + resVersion = pathParts[1]; + } else { + const lastDot = (hash || '').lastIndexOf('.'); + resName = lastDot !== -1 ? (hash || '').substring(0, lastDot) : (hash || ''); + resVersion = lastDot !== -1 ? (hash || '').substring(lastDot + 1) : ''; } - } - }}># + const urlSearch = new URLSearchParams(window.location.search); + const selectedReleaseParam = urlSearch.get('release'); + // Use explicit URL release param if present; otherwise use `source` only when + // it represents a real release name (avoid fallback `release` string). + const releaseParam = selectedReleaseParam || (source && source !== 'release' ? source : ''); + const url = `${window.location.origin}/${resName}/${resVersion}${releaseParam ? `?release=${encodeURIComponent(releaseParam)}` : ''}#${currentId}`; + // open in a new tab/window so search results are not lost + window.open(url, '_blank'); + e.preventDefault(); + }} + use:copy={{ + text: (() => { + const pathParts = window.location.pathname.split('/').filter(Boolean); + let resName = ''; + let resVersion = ''; + if (pathParts.length >= 2) { + resName = pathParts[0]; + resVersion = pathParts[1]; + } else { + const lastDot = (hash || '').lastIndexOf('.'); + resName = lastDot !== -1 ? (hash || '').substring(0, lastDot) : (hash || ''); + resVersion = lastDot !== -1 ? (hash || '').substring(lastDot + 1) : ''; + } + const urlSearch = new URLSearchParams(window.location.search); + const selectedReleaseParam = urlSearch.get('release'); + const releaseParam = selectedReleaseParam || (source && source !== 'release' ? source : ''); + return window.location.origin + `/${resName}/${resVersion}${releaseParam ? `?release=${encodeURIComponent(releaseParam)}` : ''}#${currentId}`; + })(), + onCopy({ event }: any) { + const target = event?.target as HTMLElement | null; + if (target) { + target.innerHTML = '✓'; + timeout = setTimeout(() => { + target.innerHTML = '#'; + }, 500); + } + } + }}># {/if}
    {#if expanded} -
      -
    • +
        +
      • {getDescription(folder)}
      • - {#if defaultVal} -
      • - default: {defaultVal} -
      • - {/if} - {#if getEnum(folder)} -
      • - enum: {getEnum(folder)} -
      • - {/if} + + +
        + {#if defaultVal} +
      • + default: + {defaultVal} +
      • + {/if} + {#if getEnum(folder)} +
      • + enum: + {getEnum(folder)} +
      • + {/if} + {#if formatVal} +
      • + format: + {formatVal} +
      • + {/if} + {#if minVal !== undefined || maxVal !== undefined} +
      • + constraints: + + {#if minVal !== undefined}min: {minVal}{/if} + {#if minVal !== undefined && maxVal !== undefined}, {/if} + {#if maxVal !== undefined}max: {maxVal}{/if} + +
      • + {/if} +
        + {#if folder.type === 'object' || folder.type === 'array'} {@const props = propExist()} {#if typeof props === 'object'} {#each Object.entries(props) as [subkey, subfolder]} {@const scope = getScope(folder)} {@const requiredList = 'required' in scope ? scope.required : []} + {@const subCompareData = diffMode && diffCompareData ? (() => { + const compareScope = getScope(diffCompareData); + if ('properties' in compareScope && subkey in compareScope.properties) { + return compareScope.properties[subkey]; + } + return null; + })() : null} + {@const subCurrentData = diffMode && folder ? subfolder : null} {/each} {/if} diff --git a/src/lib/components/YangView.svelte b/src/lib/components/YangView.svelte new file mode 100644 index 0000000..557c676 --- /dev/null +++ b/src/lib/components/YangView.svelte @@ -0,0 +1,298 @@ + + +
          + {#if paths.length === 0} +
        • No fields found for this entry.
        • + {/if} + {#each paths as p} +
        • +
          +
          + + {#if p.t} + {@const typeColor = typeColors[(p.t as unknown) as keyof typeof typeColors] || 'bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-900/30 dark:text-gray-300 dark:border-gray-800'} + {p.t} + {/if} + +
          + {#if p.desc} +
          {p.desc}
          + {/if} +
          +
          + {#if p.def} + {@const defList = parseListValue(String(p.def))} + {@const defListStripped = defList ? defList.map(stripResourcePrefixItem) : null} + {#if defList} + {@const fieldId = `${p.path}:def`} +
          + {#if expandedFields[fieldId]} +
          + {#each defListStripped! as item} + {stripParens(item)} + {/each} +
          + + {:else} +
          + {#each defListStripped!.slice(0, 4) as item} + {stripParens(item)} + {/each} + {#if defListStripped!.length > 4} + , +{defListStripped!.length-4} more + + {/if} +
          + {/if} +
          + {:else} +
          + {stripParens(stripResourcePrefixItem(String(p.def)))} +
          + {/if} + {/if} + {#if p.enum} + {@const enumItems = parseListValue(String(p.enum))} + {@const enumItemsStripped = enumItems ? enumItems.map(stripResourcePrefixItem) : null} + {@const fieldIdEnum = `${p.path}:enum`} +
          + {#if enumItems} + {#if expandedFields[fieldIdEnum]} +
          + {#each enumItemsStripped! as item} + {stripParens(item)} + {/each} +
          + + {:else} +
          + {#each enumItemsStripped!.slice(0, 4) as item} + {stripParens(item)} + {/each} + {#if enumItemsStripped!.length > 4} + , +{enumItemsStripped!.length-4} more + + {/if} +
          + {/if} + {:else} + {stripParens(stripResourcePrefixItem(String(p.enum)))} + {/if} +
          + {/if} +
          +
        • + {/each} +
        + + diff --git a/src/lib/components/functions.ts b/src/lib/components/functions.ts index b4c38c3..ef23c43 100644 --- a/src/lib/components/functions.ts +++ b/src/lib/components/functions.ts @@ -45,9 +45,53 @@ export function getEnum(resource: Schema) { } } +// Get the format of a resource field (e.g., "int32", "date-time") +export function getFormat(resource: Schema): string { + if ('format' in resource && resource.format) { + return resource.format; + } + if (resource.type === 'array' && resource.items && 'format' in resource.items && resource.items.format) { + return resource.items.format; + } + return ''; +} + +// Get minimum constraint value +export function getMinimum(resource: Schema): number | undefined { + if ('minimum' in resource && resource.minimum !== undefined) { + return resource.minimum; + } + if (resource.type === 'array' && resource.items && 'minimum' in resource.items && resource.items.minimum !== undefined) { + return resource.items.minimum; + } + return undefined; +} + +// Get maximum constraint value +export function getMaximum(resource: Schema): number | undefined { + if ('maximum' in resource && resource.maximum !== undefined) { + return resource.maximum; + } + if (resource.type === 'array' && resource.items && 'maximum' in resource.items && resource.items.maximum !== undefined) { + return resource.items.maximum; + } + return undefined; +} + export function hashExistDeep(hash: string, currentId: string) { if (hash.indexOf(currentId) !== -1) { return true } return false +} + +// Remove the first label of an eda.nokia.com FQDN: 'ntpclients.timing.eda.nokia.com' -> 'timing.eda.nokia.com' +export function stripResourcePrefixFQDN(fqdn: string) { + if (!fqdn || typeof fqdn !== 'string') return fqdn; + const suffix = '.eda.nokia.com'; + const trimmed = fqdn.trim(); + if (!trimmed.endsWith(suffix)) return trimmed; + const parts = trimmed.split('.'); + if (parts.length < 3) return trimmed; // not enough parts to strip + return parts.slice(1).join('.'); } \ No newline at end of file diff --git a/src/lib/releases.yaml b/src/lib/releases.yaml new file mode 100644 index 0000000..b882950 --- /dev/null +++ b/src/lib/releases.yaml @@ -0,0 +1,16 @@ +# EDA Release Configuration +# This file defines available EDA releases +releases: + - name: "25.8.3" + label: "EDA 25.8.3" + folder: "resources/25.8.3" + default: true + - name: "25.8.2" + label: "EDA 25.8.2" + folder: "resources/25.8.2" + - name: "25.8.1" + label: "EDA 25.8.1" + folder: "resources/25.8.1" + - name: "25.4.2" + label: "EDA 25.4.2" + folder: "resources/25.4.2" diff --git a/src/lib/resources.yaml b/src/lib/resources.yaml index d976dad..de8ad4a 100644 --- a/src/lib/resources.yaml +++ b/src/lib/resources.yaml @@ -5,21 +5,21 @@ aaa.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: nodegroups.aaa.eda.nokia.com group: aaa.eda.nokia.com kind: NodeGroup versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: userdeployments.aaa.eda.nokia.com group: aaa.eda.nokia.com kind: UserDeployment versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 aifabrics.eda.nokia.com: - name: backends.aifabrics.eda.nokia.com group: aifabrics.eda.nokia.com @@ -27,7 +27,7 @@ aifabrics.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 appstore.eda.nokia.com: - name: appinstallers.appstore.eda.nokia.com group: appstore.eda.nokia.com @@ -61,14 +61,14 @@ bootstrap.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: managementrouters.bootstrap.eda.nokia.com group: bootstrap.eda.nokia.com kind: ManagementRouter versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 certcheck.eda.nokia.com: - name: certificatechecks.certcheck.eda.nokia.com group: certcheck.eda.nokia.com @@ -83,21 +83,21 @@ components.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: clusterdiscoveries.components.eda.nokia.com group: components.eda.nokia.com kind: ClusterDiscovery versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: components.components.eda.nokia.com group: components.eda.nokia.com kind: Component versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: false - name: controlmodules.components.eda.nokia.com @@ -106,21 +106,21 @@ components.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: cpuoverlays.components.eda.nokia.com group: components.eda.nokia.com kind: CPUOverlay versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: discoveries.components.eda.nokia.com group: components.eda.nokia.com kind: Discovery versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: false - name: fabricmodules.components.eda.nokia.com @@ -129,49 +129,49 @@ components.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: fans.components.eda.nokia.com group: components.eda.nokia.com kind: Fan versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: interfacemodules.components.eda.nokia.com group: components.eda.nokia.com kind: InterfaceModule versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: memoryoverlays.components.eda.nokia.com group: components.eda.nokia.com kind: MemoryOverlay versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: monitors.components.eda.nokia.com group: components.eda.nokia.com kind: Monitor versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: powersupplies.components.eda.nokia.com group: components.eda.nokia.com kind: PowerSupply versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: volumeoverlays.components.eda.nokia.com group: components.eda.nokia.com kind: VolumeOverlay versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 config.eda.nokia.com: - name: configlets.config.eda.nokia.com group: config.eda.nokia.com @@ -179,7 +179,7 @@ config.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 connect.eda.nokia.com: - name: connectaudits.connect.eda.nokia.com group: connect.eda.nokia.com @@ -429,14 +429,14 @@ environment.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: setupenvs.environment.eda.nokia.com group: environment.eda.nokia.com kind: SetupEnv versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 fabrics.eda.nokia.com: - name: fabrics.fabrics.eda.nokia.com group: fabrics.eda.nokia.com @@ -444,21 +444,21 @@ fabrics.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: islpings.fabrics.eda.nokia.com group: fabrics.eda.nokia.com kind: IslPing versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: isls.fabrics.eda.nokia.com group: fabrics.eda.nokia.com kind: ISL versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 filters.eda.nokia.com: - name: controlplanefilters.filters.eda.nokia.com group: filters.eda.nokia.com @@ -466,35 +466,35 @@ filters.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: filterdeployments.filters.eda.nokia.com group: filters.eda.nokia.com kind: FilterDeployment versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: filters.filters.eda.nokia.com group: filters.eda.nokia.com kind: Filter versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: mirrorfilterdeployments.filters.eda.nokia.com group: filters.eda.nokia.com kind: MirrorFilterDeployment versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: mirrorfilters.filters.eda.nokia.com group: filters.eda.nokia.com kind: MirrorFilter versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 influxdb.eda.nokia.com: - name: clusterexports.influxdb.eda.nokia.com group: influxdb.eda.nokia.com @@ -531,21 +531,21 @@ interfaces.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: checkinterfacess.interfaces.eda.nokia.com group: interfaces.eda.nokia.com kind: CheckInterfaces versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: interfaces.interfaces.eda.nokia.com group: interfaces.eda.nokia.com kind: Interface versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 kafka.eda.nokia.com: - name: clusterproducers.kafka.eda.nokia.com group: kafka.eda.nokia.com @@ -641,28 +641,28 @@ oam.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: pings.oam.eda.nokia.com group: oam.eda.nokia.com kind: Ping versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: techsupports.oam.eda.nokia.com group: oam.eda.nokia.com kind: TechSupport versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: thresholds.oam.eda.nokia.com group: oam.eda.nokia.com kind: Threshold versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 os.eda.nokia.com: - name: deployimages.os.eda.nokia.com group: os.eda.nokia.com @@ -670,7 +670,7 @@ os.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 otlp.eda.nokia.com: - name: clustermetricexports.otlp.eda.nokia.com group: otlp.eda.nokia.com @@ -737,7 +737,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -747,7 +747,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -757,7 +757,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -767,7 +767,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -777,7 +777,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true - name: defaultaggregateroutes.protocols.eda.nokia.com @@ -786,7 +786,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -796,7 +796,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -806,7 +806,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -816,7 +816,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -826,7 +826,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -836,7 +836,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -846,7 +846,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -856,7 +856,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -866,7 +866,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -876,7 +876,7 @@ protocols.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -887,7 +887,7 @@ qos.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -897,7 +897,7 @@ qos.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -907,7 +907,7 @@ qos.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -917,7 +917,7 @@ qos.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -927,7 +927,7 @@ qos.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -937,7 +937,7 @@ qos.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -977,42 +977,42 @@ routing.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: defaultinterfaces.routing.eda.nokia.com group: routing.eda.nokia.com kind: DefaultInterface versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: defaultrouters.routing.eda.nokia.com group: routing.eda.nokia.com kind: DefaultRouter versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: drains.routing.eda.nokia.com group: routing.eda.nokia.com kind: Drain versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: routelookups.routing.eda.nokia.com group: routing.eda.nokia.com kind: RouteLookup versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: systeminterfaces.routing.eda.nokia.com group: routing.eda.nokia.com kind: SystemInterface versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 routingpolicies.eda.nokia.com: - name: aspathsetdeployments.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com @@ -1020,56 +1020,56 @@ routingpolicies.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: aspathsets.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: ASPathSet versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: communitysetdeployments.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: CommunitySetDeployment versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: communitysets.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: CommunitySet versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: policydeployments.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: PolicyDeployment versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: policys.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: Policy versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: prefixsetdeployments.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: PrefixSetDeployment versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: prefixsets.routingpolicies.eda.nokia.com group: routingpolicies.eda.nokia.com kind: PrefixSet versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 security.eda.nokia.com: - name: keychaindeployments.security.eda.nokia.com group: security.eda.nokia.com @@ -1077,14 +1077,14 @@ security.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: keychains.security.eda.nokia.com group: security.eda.nokia.com kind: Keychain versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 services.eda.nokia.com: - name: bridgedomaindeployments.services.eda.nokia.com group: services.eda.nokia.com @@ -1092,7 +1092,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1102,7 +1102,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1112,7 +1112,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1122,7 +1122,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1132,7 +1132,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true - name: irbinterfaces.services.eda.nokia.com @@ -1141,7 +1141,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1151,7 +1151,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1161,7 +1161,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1171,7 +1171,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1181,7 +1181,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1191,7 +1191,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1201,7 +1201,7 @@ services.eda.nokia.com: versions: - name: v1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: v1alpha1 deprecated: true appVersion: v3.0.2+25.4.3 @@ -1212,7 +1212,7 @@ siteinfo.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 snow.eda.nokia.com: - name: clusterincidents.snow.eda.nokia.com group: snow.eda.nokia.com @@ -1249,7 +1249,7 @@ system.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 test.eda.nokia.com: - name: simlinks.test.eda.nokia.com group: test.eda.nokia.com @@ -1270,7 +1270,7 @@ timing.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 topologies.eda.nokia.com: - name: cpuutiloverlays.topologies.eda.nokia.com group: topologies.eda.nokia.com @@ -1285,7 +1285,7 @@ topologies.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: diskoverlays.topologies.eda.nokia.com group: topologies.eda.nokia.com kind: DiskOverlay @@ -1299,7 +1299,7 @@ topologies.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: memoryoverlays.topologies.eda.nokia.com group: topologies.eda.nokia.com kind: MemoryOverlay @@ -1313,25 +1313,25 @@ topologies.eda.nokia.com: versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: topologies.topologies.eda.nokia.com group: topologies.eda.nokia.com kind: Topology versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: topologygroupings.topologies.eda.nokia.com group: topologies.eda.nokia.com kind: TopologyGrouping versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 - name: trafficrateoverlays.topologies.eda.nokia.com group: topologies.eda.nokia.com kind: TrafficRateOverlay versions: - name: v1alpha1 deprecated: false - appVersion: v4.0.0 + appVersion: v4.0.1 diff --git a/src/lib/store.ts b/src/lib/store.ts index 8637d4b..b12e443 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -3,3 +3,31 @@ import { writable } from 'svelte/store' export const expandAll = writable(false) export const expandAllScope = writable("local") // supported value (local/global) export const ulExpanded = writable([]) + +// Sidebar open state for desktop (persist across sessions) +function createSidebarStore() { + const key = 'sidebarOpen'; + const initial = typeof localStorage !== 'undefined' && localStorage.getItem(key) !== null + ? localStorage.getItem(key) === 'true' + : true; + + const { subscribe, set, update } = writable(initial); + + subscribe((v) => { + try { + if (typeof localStorage !== 'undefined') localStorage.setItem(key, v ? 'true' : 'false'); + } catch (e) { + // ignore + } + }); + + return { + subscribe, + open: () => set(true), + close: () => set(false), + toggle: () => update((v) => !v), + set + }; +} + +export const sidebarOpen = createSidebarStore(); diff --git a/src/lib/structure.ts b/src/lib/structure.ts index 20e8e8f..94ba060 100644 --- a/src/lib/structure.ts +++ b/src/lib/structure.ts @@ -2,6 +2,7 @@ export interface CrdVersions { name: string; deprecated: boolean; appVersion: string; + edaRelease?: string; // EDA release version (e.g., "24.10", "24.11") } export interface CrdResource { @@ -9,6 +10,7 @@ export interface CrdResource { group: string; kind: string; versions: CrdVersions[]; + edaRelease?: string; // EDA release this resource belongs to } export interface CrdVersionsMap { @@ -65,4 +67,15 @@ export interface VersionSchema { status: Schema; deprecated: boolean; } +} + +export interface EdaRelease { + name: string; // "latest", "24.11", "24.10" + label: string; // "Latest", "EDA 24.11", "EDA 24.10" + folder: string; // "resources", "resources-24.11", "resources-24.10" + default?: boolean; // Is this the default release? +} + +export interface ReleasesConfig { + releases: EdaRelease[]; } \ No newline at end of file diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index 08af5cd..bf652d6 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -8,7 +8,7 @@
        Logo
        -

        Resource Browser

        +

        Resource Browser

        Error {$page.status}

        diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b93e9ba..d08089b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,36 @@ -{@render children()} + + +{#if $isDetailPage} +
        + +
        + {@render children()} +
        +
        +{:else} +
        + {@render children()} +
        +{/if} + +
      +
      +
      + + + +
      +
      +

      + Spec Search +

      +

      + Quickly search and find specific CRD schema properties and elements +

      +
      +
      + +
  • +
    + + +
+ - + {:else if loading} +
+
+ + + + +

Loading resource...

+
+
+ {:else if resourceData} +
+
+
+
+
+
+

{formatResourceName(resourceInfo?.name || '')}

+

{formatGroupName(resourceInfo?.name || '')}

+
+
+ + +
+
+
+ +
+ {#if viewMode === 'schema'} + + {:else} +
+
+ + +
+
+ +
+ {#if validationResult === 'valid'} +
+
+ + + +
+

Valid Configuration

+

Your YAML matches the schema requirements.

+
+
+
+ {/if} + {#if validationResult === 'invalid' && validationErrors.length > 0} +
+
+ + + +
+

Validation Errors ({validationErrors.length})

+
    + {#each validationErrors as error} +
  • + {error.instancePath || '/'} + {error.message} +
  • + {/each} +
+
+
+
+ {/if} +
+ {/if} +
+
+
+
+ {/if} -
-
-
+ diff --git a/src/routes/[name]/+page.ts b/src/routes/[name]/+page.ts index 5e4b974..23618a1 100644 --- a/src/routes/[name]/+page.ts +++ b/src/routes/[name]/+page.ts @@ -1,22 +1,60 @@ import { error, redirect } from '@sveltejs/kit' -import type { CrdVersionsMap } from '$lib/structure' +import type { PageLoad } from './$types' +import type { CrdVersionsMap, ReleasesConfig } from '$lib/structure' import yaml from 'js-yaml' import res from '$lib/resources.yaml?raw' +import releases from '$lib/releases.yaml?raw' const crdResources = yaml.load(res) const resources = crdResources as CrdVersionsMap +const releaseConfig = yaml.load(releases) as ReleasesConfig -export async function load({ params }) { +export const load: PageLoad = async ({ params, url, fetch }) => { const name = params.name const rest = name.substring(name.indexOf(".") + 1) - const crdMeta = resources[rest].filter(x => x.name === name) + // Check if a specific release is requested via query parameter + const requestedRelease = url.searchParams.get('release') + let selectedRelease = releaseConfig.releases.find(r => r.default) ?? releaseConfig.releases[0] + + if (requestedRelease) { + const foundRelease = releaseConfig.releases.find(r => r.name === requestedRelease) + if (foundRelease) { + selectedRelease = foundRelease + } + } + + const releaseFolder = selectedRelease?.folder ?? 'resources/25.8.2' + + // Load release-specific manifest + let releaseManifest: any[] = [] + try { + const manifestResp = await fetch(`/${releaseFolder}/manifest.json`) + if (manifestResp.ok) { + releaseManifest = await manifestResp.json() + } + } catch (e) { + console.warn(`Could not load manifest for ${releaseFolder}`) + } + + // First try to get metadata from resources.yaml + let crdMeta = resources[rest]?.filter(x => x.name === name) || [] + + // If not found in resources.yaml, try the release manifest (for "states" resources) + if (crdMeta.length === 0 && releaseManifest.length > 0) { + const manifestEntry = releaseManifest.find(x => x.name === name) + if (manifestEntry) { + crdMeta = [manifestEntry] + } + } + if(crdMeta.length !== 1) { throw error(404, "Invalid resource name") } const version = crdMeta[0].versions[0].name - throw redirect(307, `/${name}/${version}`); + const releaseParam = requestedRelease ? `?release=${requestedRelease}` : '' + throw redirect(307, `/${name}/${version}${releaseParam}`); } \ No newline at end of file diff --git a/src/routes/[name]/[version]/+page.svelte b/src/routes/[name]/[version]/+page.svelte index 6325380..9e4fa02 100644 --- a/src/routes/[name]/[version]/+page.svelte +++ b/src/routes/[name]/[version]/+page.svelte @@ -1,21 +1,122 @@ EDA Resource Browser | {name} {versionOnFocus} -
- -
-
- -
- -
- -
- -
+ +{#key `${name}-${versionOnFocus}`} + + +
+
+
+
+ +
+ +
+
+ +
+ + + +
+ + +
+ + + + +
+
+
+
+ + + {#if viewMode === 'schema'} +
+ +
+
+
+
+ + + +
+
+

+ Specification +

+

+ Required configuration fields +

+
+
+
+
+ +
+
+ + +
+
+
+
+ + + +
+
+

+ Status +

+

+ Runtime status fields +

+
+
+
+
+ +
+
+
+ {/if} + + + {#if viewMode === 'compare'} +
+
+
+
+
+ + + +
+
+

+ Comparison +

+

+ Compare schemas across versions +

+
+
+ +
+
+
Release
+
+ +
+ + + +
+
+
+
+
Version
+
+ +
+ + + +
+
+
+
+
+
+
+ {#if !compareVersion} +
+ + + +

+ No Version Selected +

+

+ Select a version to compare with {versionOnFocus}. +

+
+ {:else if isComparing} +
+
+
+ {:else if comparisonResult} +
+ +
+
+
+ {comparisonResult.baseRelease} + {versionOnFocus} +
+ + + +
+ {comparisonResult.compareRelease} + {compareVersion} +
+
+
+ + +
+

+ + + + Specification +

+
+
+

+ {versionOnFocus} +

+
+ +
+
+
+

+ {compareVersion} +

+
+ +
+
+
+
+ + +
+

+ + + + Status +

+
+
+

+ {versionOnFocus} +

+
+ +
+
+
+

+ {compareVersion} +

+
+ +
+
+
+
+
+ {/if} +
+
+ {/if} + + + {#if viewMode === 'validate'} +
+
+
+
+ + + +
+
+

+ YAML Validation +

+

+ Validate YAML against CRD schema +

+
+
+
+
+
+ +
+
+ + + +
+

How to use:

+
    +
  • Paste your complete YAML manifest
  • +
  • Or paste just the spec section
  • +
  • Supports multiple documents separated by ---
  • +
+
+
+
+ + +
+ + +
+ + +
+ + + {#if validationResult} +
+ {#if validationResult === 'valid'} +
+ + + + Valid +
+ {:else} +
+ + + + Invalid +
+ {/if} +
+ {/if} +
+ + + {#if validationErrors.length > 0} + {#if validationResult === 'valid'} +
+
+ + + +

+ {validationErrors[0].message} +

+
+
+ {:else} +
+
+ + + +
+

+ Errors ({validationErrors.length}) +

+
    + {#each validationErrors as error} +
  • +
    + {#if (error as any).instancePath || (error as any).dataPath} + {(error as any).instancePath || (error as any).dataPath}: + {/if} + {error.message} +
    +
  • + {/each} +
+
+
+
+ {/if} + {/if} +
+
+
+ {/if} +
+
+ +
+{/key} \ No newline at end of file diff --git a/src/routes/[name]/[version]/+page.ts b/src/routes/[name]/[version]/+page.ts index dc6b490..29cc68f 100644 --- a/src/routes/[name]/[version]/+page.ts +++ b/src/routes/[name]/[version]/+page.ts @@ -1,19 +1,60 @@ import { error } from '@sveltejs/kit' -import type { CrdVersionsMap, OpenAPISchema } from '$lib/structure' +import type { PageLoad } from './$types' +import type { CrdVersionsMap, OpenAPISchema, ReleasesConfig } from '$lib/structure' import yaml from 'js-yaml' import res from '$lib/resources.yaml?raw' +import releases from '$lib/releases.yaml?raw' const crdResources = yaml.load(res) const resources = crdResources as CrdVersionsMap +const releaseConfig = yaml.load(releases) as ReleasesConfig +const allReleases = releaseConfig.releases -export async function load({ fetch, params }) { +export const load: PageLoad = async ({ fetch, params, url }) => { + // Check if a specific release is requested via query parameter + const requestedRelease = url.searchParams.get('release') + let selectedRelease = releaseConfig.releases.find(r => r.default) ?? releaseConfig.releases[0] + + if (requestedRelease) { + const foundRelease = releaseConfig.releases.find(r => r.name === requestedRelease) + if (foundRelease) { + selectedRelease = foundRelease + } + } + + const releaseFolder = selectedRelease?.folder ?? 'resources/25.8.2' + const releaseLabel = selectedRelease?.label ?? selectedRelease?.name ?? '' const name = params.name const versionOnFocus = params.version const rest = name.substring(name.indexOf(".") + 1) - const crdMeta = resources[rest].filter(x => x.name === name) + // Load release-specific manifest + let releaseManifest: any[] = [] + try { + const manifestResp = await fetch(`/${releaseFolder}/manifest.json`) + if (manifestResp.ok) { + releaseManifest = await manifestResp.json() + } + } catch (e) { + console.warn(`Could not load manifest for ${releaseFolder}, using fallback`) + } + + // Prefer release manifest entries when available; fallback to static resources.yaml otherwise + let crdMeta: any[] = [] + if (releaseManifest.length > 0) { + const manifestEntry = releaseManifest.find(x => x.name === name) + if (manifestEntry) { + crdMeta = [manifestEntry] + } + } + + if (crdMeta.length === 0) { + // Try to find resource in static resources.yaml as a fallback + crdMeta = resources[rest]?.filter(x => x.name === name) || [] + } + if(crdMeta.length !== 1) { throw error(404, "Invalid resource name") } @@ -24,7 +65,16 @@ export async function load({ fetch, params }) { } try { - const resp = await fetch(`/resources/${name}/${versionOnFocus}.yaml`) + let resp = await fetch(`/${releaseFolder}/${name}/${versionOnFocus}.yaml`) + + if (!resp.ok) { + resp = await fetch(`/resources/${name}/${versionOnFocus}.yaml`) + } + + if (!resp.ok) { + throw error(404, 'Error fetching resource') + } + const crdText = await resp.text() const crd = yaml.load(crdText) as OpenAPISchema @@ -32,13 +82,53 @@ export async function load({ fetch, params }) { const kind = crdMeta[0].kind const deprecated = crdMetaVersion[0].deprecated const appVersion = ('appVersion' in crdMetaVersion[0] ? crdMetaVersion[0].appVersion : "") - const validVersions = crdMeta[0].versions.map(x => x.name) + + // Get valid versions for this resource from the manifest (release-specific) + // Fall back to crdMeta if manifest doesn't have it + const manifestResource = releaseManifest.find((r: any) => r.name === name) + const validVersions = manifestResource?.versions?.map((v: any) => v.name) || crdMeta[0].versions.map(x => x.name) const spec = crd.schema.openAPIV3Schema.properties.spec const status = crd.schema.openAPIV3Schema.properties.status - - return { name, versionOnFocus, kind, group, deprecated, appVersion, validVersions, spec, status } + // Determine the earliest release in which this specific version is marked deprecated + let deprecatedSince: string | null = null + // iterate from oldest to newest + const checkReleases = [...allReleases].reverse(); + for (const r of checkReleases) { + try { + let manifest: any[] = [] + if (r.name === selectedRelease.name && releaseManifest.length > 0) { + manifest = releaseManifest + } else { + const resp = await fetch(`/${r.folder}/manifest.json`) + if (!resp.ok) continue + manifest = await resp.json() + } + const entry = manifest.find((x: any) => x.name === name) + if (!entry || !entry.versions) continue + const v = entry.versions.find((vv: any) => vv.name === versionOnFocus) + if (v && v.deprecated) { deprecatedSince = r.label || r.name; break } + } catch (e) { + // ignore and continue + } + } + return { + name, + versionOnFocus, + kind, + group, + deprecated, + appVersion, + validVersions, + spec, + status, + releaseLabel, + releaseFolder, + allReleases, + releaseManifest, + deprecatedSince + } } catch(e) { - throw error(404, "Error fetching resource" + e) + throw error(404, 'Error fetching resource' + e) } } \ No newline at end of file diff --git a/src/routes/comparison/+page.svelte b/src/routes/comparison/+page.svelte new file mode 100644 index 0000000..ecc72cf --- /dev/null +++ b/src/routes/comparison/+page.svelte @@ -0,0 +1,905 @@ + + + + EDA Resource Browser | Release Comparison + + + + + + +
+
+ +
+
+
+
+
+ +
+ + + + +
+
+

Compare CRDs across two release versions to understand additions, removals, and modifications. Use version selectors and filters below to narrow down results and generate the comparison report.

+ +
+
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ {#if bulkDiffGenerating} +
+
+
+
+
+ {/if} +
+
+ +
+
+ +
+
+
+ + {#if bulkDiffReport} + +
+
+ {#each [ + {status: 'added', color: 'green', icon: 'M12 6v6m0 0v6m0-6h6m-6 0H6', label: 'Added'}, + {status: 'removed', color: 'red', icon: 'M20 12H4', label: 'Removed'}, + {status: 'modified', color: 'yellow', icon: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z', label: 'Modified'}, + {status: 'unchanged', color: 'gray', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', label: 'Unchanged'} + ] as item} + + {/each} +
+ + +
+
+
+
+ + + +
+
+

Comparison Report

+
+ {bulkDiffReport.sourceRelease} {bulkDiffReport.sourceVersion} + + {bulkDiffReport.targetRelease} {bulkDiffReport.targetVersion} +
+
+
+
+ + + +
+
+
+ + +
+
+
+
+ +
+ + {#if bulkDiffSearch} + + {/if} +
+
+
{filteredBulkDiffCrds.length} matches
+
+
+
+ +
+
+

Results

+
+ +
+
+ + {#if filteredBulkDiffCrds.length > 0} + +
+ {#each filteredBulkDiffCrds as crd} +
+
+
+
{@html highlightMatches(stripResourcePrefixFQDN(crd.name), debouncedBulkDiffSearch, bulkDiffSearchRegex)}
+
{crd.kind}
+
+
+ + {#if crd.status === 'added'} + + {:else if crd.status === 'removed'} + + {:else if crd.status === 'modified'} +
+ + +
+ {:else} + + {/if} + {crd.status} +
+ +
+
+ {#if expandedCrdNames.includes(crd.name)} +
+ {#if crd.details && crd.details.length > 0} + {#key debouncedBulkDiffSearch} + {#each crd.details as d} + {#if d.startsWith('+')} +
+ + {@html highlightMatches(d.replace(/^\+\s*[^:]+:\s*/, ''), debouncedBulkDiffSearch, bulkDiffSearchRegex)} +
+ {:else if d.startsWith('-')} +
+ + {@html highlightMatches(d.replace(/^\-\s*[^:]+:\s*/, ''), debouncedBulkDiffSearch, bulkDiffSearchRegex)} +
+ {:else if d.startsWith('~')} +
+ + {@html highlightMatches(d.replace(/^~\s*[^:]+:\s*/, ''), debouncedBulkDiffSearch, bulkDiffSearchRegex)} +
+ {:else} +
+ + {d} +
+ {/if} + {/each} + {/key} + {:else} +
No details.
+ {/if} +
+ {/if} +
+ {/each} +
+ + + + {:else} +
+
+
+ + + +
+
+

No Results Found

+

No CRDs match the selected filters. Try adjusting your filter criteria.

+
+
+
+ {/if} + + +
+ + +
+
+
+ {/if} +
+
+
+
+
+ +
diff --git a/src/routes/spec-search/+page.svelte b/src/routes/spec-search/+page.svelte new file mode 100644 index 0000000..f5e5fd4 --- /dev/null +++ b/src/routes/spec-search/+page.svelte @@ -0,0 +1,558 @@ + + + + EDA Resource Browser | Search CRD Specs + + + + + + + +
+
+ +
+
+
+
+
+ +
+ +
+
+

Select a release and version (leave the version blank to search all versions), then search inside CRD spec and status schemas. Descriptions are ignored to focus matches on parameters and values.

+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+
+ +
+
+
+
+ +
+ { /* no-op */ }} on:keydown={(e) => e.key === 'Enter' && performSearch()} placeholder="Search specs & status (regex)" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 pl-9 pr-10 py-3 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm" /> + {#if query} + + {/if} +
+
+ + + +
+
{displayedResults.length} matches
+
+ View: + + +
+
+
+ + +
+ {#if loading} +
+
+
+
+
+ {/if} +
+ + + {#if displayedResults.length > 0} + +
+ {#each displayedResults as r} +
+
+
+
{r.kind}
+
{stripResourcePrefixFQDN(String(r.name))}
+
+
+ {#if r.version} +
{r.version}
+ {/if} +
+
+
+
+ {#if resultsViewMode === 'tree'} + + {:else} + + {/if} +
+
+
+ {/each} +
+ + + + {:else} +
+
+
+ +
+
+

No Results Found

+

No CRD spec/status matches the selected release/version (or all versions) and query. Try adjusting your query.

+
+
+
+ {/if} +
+
+
+
+
diff --git a/src/routes/uploads/+page.svelte b/src/routes/uploads/+page.svelte index 52fb9ee..1ab4a3f 100644 --- a/src/routes/uploads/+page.svelte +++ b/src/routes/uploads/+page.svelte @@ -2,7 +2,6 @@ import yaml from 'js-yaml'; import Footer from '$lib/components/Footer.svelte'; - import Theme from '$lib/components/Theme.svelte'; import Render from '$lib/components/Render.svelte'; import type { OpenAPISchema, Schema, VersionSchema } from '$lib/structure'; @@ -26,7 +25,7 @@ function processYaml() { try { - const crd = yaml.load(plaintextCrd); + const crd = yaml.load(plaintextCrd) as any; group = crd.spec.group; kind = crd.spec.names.kind; @@ -91,11 +90,10 @@
Logo
-

Resource Browser

+

Resource Browser

Uploads

-
@@ -126,7 +124,7 @@ />