From 7ea54fc29e6d148cd67e30ea239203c5502ac839 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Sat, 6 Dec 2025 21:04:18 +0100 Subject: [PATCH 1/2] Adding an about section, with diagrams and popup for carrousel --- about-educates/_category_.json | 5 + about-educates/concepts.md | 241 ++++++++++++++ about-educates/history.md | 132 ++++++++ about-educates/index.md | 126 ++++++++ about-educates/workflows.md | 295 ++++++++++++++++++ .../index.md | 272 ++++++++++++++++ docusaurus.config.ts | 27 +- sidebars.ts | 6 + .../AutoClickableDiagrams/index.tsx | 198 ++++++++++++ src/components/Carrousel/Carrousel.tsx | 282 ++++++++++++----- src/components/ClickableDiagram/index.tsx | 47 +++ src/components/ClickableDiagram/styles.css | 60 ++++ src/components/DiagramModal/client.tsx | 222 +++++++++++++ src/components/DiagramModal/index.tsx | 261 ++++++++++++++++ src/components/DiagramModal/styles.css | 280 +++++++++++++++++ src/components/sections/FeaturedContent.tsx | 102 +++--- src/css/custom.css | 237 ++++++++++++++ src/theme/MDXComponents.js | 3 + src/theme/Root.tsx | 13 + 19 files changed, 2685 insertions(+), 124 deletions(-) create mode 100644 about-educates/_category_.json create mode 100644 about-educates/concepts.md create mode 100644 about-educates/history.md create mode 100644 about-educates/index.md create mode 100644 about-educates/workflows.md create mode 100644 blog/_2025-03-28-update-a-local-clusters-capabilities/index.md create mode 100644 src/components/AutoClickableDiagrams/index.tsx create mode 100644 src/components/ClickableDiagram/index.tsx create mode 100644 src/components/ClickableDiagram/styles.css create mode 100644 src/components/DiagramModal/client.tsx create mode 100644 src/components/DiagramModal/index.tsx create mode 100644 src/components/DiagramModal/styles.css create mode 100644 src/theme/Root.tsx diff --git a/about-educates/_category_.json b/about-educates/_category_.json new file mode 100644 index 0000000..f36dc58 --- /dev/null +++ b/about-educates/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "About Educates", + "position": 2 +} + diff --git a/about-educates/concepts.md b/about-educates/concepts.md new file mode 100644 index 0000000..e9db4fd --- /dev/null +++ b/about-educates/concepts.md @@ -0,0 +1,241 @@ +--- +sidebar_position: 2 +--- + +# Core Concepts + +This page explains the fundamental concepts that make up the Educates platform. + +## Workshop + +A **Workshop** is the fundamental unit of training content in Educates. It represents a complete, self-contained learning experience that includes: + +- **Content**: Markdown or AsciiDoc files containing instructions, exercises, and explanations +- **Configuration**: Workshop-specific settings, resource requirements, and capabilities +- **Resources**: Kubernetes manifests for deploying applications and services needed for the workshop +- **Container Image**: The workshop content is packaged as an OCI image, typically stored in a container registry + +Workshops are defined using the `Workshop` Custom Resource Definition (CRD), which specifies: +- The location of the workshop content (Git repository or container image) +- Resource quotas and limits for workshop sessions +- RBAC permissions required +- Additional capabilities needed (web terminal, editor, etc.) +- Shared resources that should be deployed for all sessions +- Per-session resources that should be created for each user + +### Workshop Lifecycle + +1. **Definition**: A `Workshop` resource is created in the cluster +2. **Content Distribution**: Workshop content is packaged and published to a container registry +3. **Environment Setup**: When a Training Portal references the workshop, a `WorkshopEnvironment` is created +4. **Session Creation**: Individual `WorkshopSession` resources are created as users access the workshop + +## Training Portal + +A **Training Portal** is the web-based interface that provides access to one or more workshops. It serves as the entry point for users to: + +- Browse available workshops +- Register for workshops +- Access their workshop sessions +- View their progress and history + +The Training Portal consists of: + +- **Web UI**: A user-friendly interface built with React +- **REST API**: A programmatic interface for integration with external systems +- **Authentication**: User registration and login mechanisms +- **Session Management**: Automatic allocation and management of workshop sessions + +Training Portals are defined using the `TrainingPortal` CRD, which specifies: +- Which workshops should be available +- Authentication configuration +- Portal branding and customization +- Access control policies + +### Portal Types + +Educates supports different portal deployment scenarios: + +- **Supervised Workshops**: Time-limited workshops for conferences or customer sites +- **Temporary Learning Portal**: Short-duration demos at conferences or vendor booths +- **Permanent Learning Portal**: Long-running public websites for continuous learning +- **Personal Training**: Individual use for learning or product demos + +## Controllers + +Educates uses Kubernetes controllers to manage the platform's resources. The main controllers are: + +### Session Manager Controller + +The **Session Manager Controller** is responsible for managing workshop sessions. It: + +- Watches for `WorkshopSession` resources +- Creates and manages namespaces for each session +- Allocates resources according to workshop requirements +- Applies RBAC policies and resource quotas +- Manages the lifecycle of session resources +- Handles session cleanup when sessions are deleted + +The Session Manager ensures that each user gets an isolated environment with the appropriate permissions and resource limits. + +### Secrets Manager Controller + +The **Secrets Manager Controller** handles secret management across the platform. It: + +- Manages secrets for workshop sessions +- Injects secrets into workshop environments +- Copies secrets between namespaces when needed +- Ensures secure secret distribution +- Manages secret lifecycle + +This controller uses several CRDs: +- `SecretInjector`: Injects secrets into specific resources +- `SecretCopier`: Copies secrets between namespaces +- `SecretExporter`: Exports secrets for external use +- `SecretImporter`: Imports secrets from external sources + +## Additional Capabilities + +Educates provides a rich set of capabilities that can be enabled for workshops: + +### Web Terminal + +The **Web Terminal** provides a browser-based terminal interface that allows users to: +- Execute commands directly in the workshop environment +- Run scripts and tools +- Interact with Kubernetes clusters +- Execute commands from workshop instructions with a single click + +The terminal runs in a container within the workshop session namespace and provides full shell access to the workshop environment. + +### VS Code Editor + +The **VS Code Editor** (or compatible editor) provides a full-featured code editing experience: +- Edit files directly in the browser +- Syntax highlighting for multiple languages +- Support for VS Code extensions +- Integrated terminal access +- Git integration +- Debugging capabilities + +This allows users to write and modify code as part of the workshop without leaving the browser. + +### Kubernetes Web Console + +The **Kubernetes Web Console** provides a visual interface for: +- Viewing Kubernetes resources +- Managing deployments, services, and pods +- Inspecting logs and events +- Executing commands in containers +- Monitoring resource usage + +This makes it easier for users to understand and interact with Kubernetes resources during workshops. + +### vCluster (Virtual Cluster) + +**vCluster** provides isolated virtual Kubernetes clusters for each user session. This is useful for: +- Workshops requiring cluster-admin access +- Testing cluster-level operations +- Isolating users completely from each other +- Providing a full Kubernetes cluster experience + +vCluster runs as a lightweight virtual cluster within the host cluster, providing complete isolation while sharing the underlying infrastructure. + +### File Server + +The **File Server** provides HTTP access to files in the workshop environment: +- Serving static files and assets +- Downloading workshop resources +- Accessing generated files and outputs +- Sharing files between components + +### Git Server + +The **Git Server** provides Git repository access within the workshop: +- Cloning repositories +- Pushing and pulling changes +- Managing branches and tags +- Integrating with Git workflows + +This enables workshops that involve Git operations and version control. + +### Image Registry + +The **Image Registry** provides container image storage and distribution: +- Storing workshop-specific images +- Building and pushing images during workshops +- Pulling images for deployments +- Managing image versions + +For local development, Educates provides a local registry that simplifies the workflow. + +### Docker Runtime + +The **Docker Runtime** enables: +- Building container images +- Running containers +- Pushing images to registries +- Testing containerized applications + +This is essential for workshops that involve containerization and Docker operations. + +## Infrastructure Requirements + +Educates relies on several infrastructure components that should be installed in the Kubernetes cluster: + +### Kyverno + +**Kyverno** is used for advanced policy management: +- Enforcing security policies +- Validating resource configurations +- Mutating resources to meet standards +- Managing network policies + +Kyverno policies can be used to ensure workshop sessions comply with organizational security requirements. + +### kapp-controller + +**kapp-controller** is used for deploying additional workloads: +- Managing application deployments +- Handling package installations +- Coordinating multi-resource deployments +- Managing application lifecycle + +kapp-controller enables Educates to deploy complex applications and dependencies as part of workshop environments. + +### external-dns + +**external-dns** provides DNS name integration (primarily for cloud providers): +- Automatically creating DNS records +- Managing subdomain routing +- Integrating with cloud DNS services +- Providing user-friendly URLs for workshops + +This component is optional and mainly used in cloud deployments. + +### cert-manager + +**cert-manager** handles certificate management: +- Integration with Let's Encrypt for automatic HTTPS +- Creating and managing TLS certificates +- Certificate renewal +- Custom certificate authorities + +cert-manager ensures that workshop portals and services can be accessed securely over HTTPS. + +## Local Development + +For local development, Educates provides a streamlined experience using **kind** (Kubernetes in Docker): + +- **Fully Configured Cluster**: The Educates CLI creates a complete Kubernetes cluster with all necessary components +- **Image Registry**: A local container registry is automatically set up for publishing and pulling workshop images +- **Local DNS Resolver**: A local DNS resolver improves the development workflow by providing proper DNS resolution for local services + +This setup allows developers to: +- Test workshops locally before deploying to production +- Iterate quickly on workshop content +- Develop and test Educates features +- Experiment with different configurations + +The local development environment mirrors the production setup, making it easy to transition from development to production. + diff --git a/about-educates/history.md b/about-educates/history.md new file mode 100644 index 0000000..2b997e1 --- /dev/null +++ b/about-educates/history.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 4 +--- + +# Educates History + +This page documents the evolution of the Educates project from its inception as an internal tool to becoming an independent open-source project. + +## Project Timeline + +The following timeline shows the major milestones in Educates' history: + +```mermaid +gitGraph + commit id: "Dec 2019 - Educates 1.x Born" + commit id: "Sept 2020 - Spring One Conference" + commit id: "Apr 2021 - Tanzu Learning Center" + commit id: "Feb 2022 - Educates 2.x Launch" + commit id: "Nov 2023 - Broadcom Acquisition" + commit id: "Aug 2024 - Educates 3.x Release" + commit id: "Oct 2024 - Independent OSS Project" + commit id: "Jun 2025 - Educates Hub Launch" +``` + +## Key Milestones + +### December 2019 - Educates 1.x is Born + +Educates was originally created as an internal tool for the VMware Tanzu Developer Advocates team. The team needed a platform to train users in Kubernetes and showcase developer tools and applications running on Kubernetes. This initial version laid the foundation for what would become a comprehensive training platform. + +**Key Features:** +- Basic workshop hosting capabilities +- Kubernetes-based architecture +- Support for interactive training sessions + +### September 2020 - Spring One Conference + +Educates proved its scalability and reliability when it was used at the Spring One conference to run **over 5,000 workshop executions**. This large-scale deployment demonstrated that Educates could handle enterprise-level training scenarios and validated the platform's architecture and design decisions. + +**Achievements:** +- Successfully scaled to support thousands of concurrent users +- Validated the platform's reliability and performance +- Demonstrated real-world applicability for large conferences + +### April 2021 - Tanzu Learning Center Launch + +VMware launched **Tanzu Learning Center** using Educates 1.x as its foundation. This marked Educates' transition from an internal tool to a production platform powering VMware's official learning initiatives. The Learning Center provided structured training paths for VMware Tanzu products and technologies. + +**Impact:** +- Educates became a core component of VMware's training infrastructure +- Enabled structured learning paths for Tanzu products +- Expanded the platform's user base significantly + +### February 2022 - Educates 2.x Launch + +The Developer Advocates team resumed active development on Educates to create version 2.x, which was designed to power multiple learning platforms: +- **Tanzu.academy** - VMware Tanzu training platform +- **Kube.academy** - Kubernetes training platform +- **Spring.academy** - Spring framework training platform + +**Version 2.x Improvements:** +- Enhanced workshop authoring capabilities +- Improved user experience +- Better integration with learning management systems +- Expanded support for different training scenarios + +### November 2023 - Broadcom Acquisition + +Broadcom acquired VMware and gained ownership of the Educates project. This transition period involved organizational changes and strategic decisions about the future of the platform. During this time, the project continued to be maintained and used across the various learning platforms. + +**Transition Period:** +- Project ownership transferred to Broadcom +- Continued maintenance and support +- Strategic evaluation of project direction + +### August 2024 - Educates 3.x Release + +Educates 3.x was released with significant improvements focused on usability and deployment: + +**Major Enhancements:** +- **Cloud Installers**: Simplified installation process for cloud providers +- **Improved Usability**: Enhanced user experience and workflow +- **Better Documentation**: Comprehensive guides and references +- **Enhanced Features**: New capabilities and integrations + +This release marked a significant step forward in making Educates more accessible and easier to deploy. + +### October 2024 - Independent Open Source Project + +In a significant milestone, Educates was **donated by Broadcom** and became an **independent open-source project**. This transition ensured the project's long-term sustainability and community-driven development. + +**Benefits of Independence:** +- Community-driven development and governance +- Open contribution model +- Independent project roadmap +- Broader adoption and ecosystem growth + +The project is now maintained by the Educates community, with contributions from developers, users, and organizations worldwide. + +### June 2025 - Educates Hub Launch + +The **Educates Hub** was launched, providing a centralized platform for: +- Discovering workshops +- Sharing workshop content +- Community collaboration +- Workshop marketplace + +**Hub Features:** +- Workshop catalog and discovery +- Community contributions +- Easy workshop sharing +- Integration with Educates deployments + +## Evolution Summary + +Educates has evolved from a small internal tool to a comprehensive, independent open-source platform: + +1. **Internal Tool (2019-2020)**: Created to solve specific needs of the Developer Advocates team +2. **Production Platform (2020-2023)**: Scaled to support enterprise training and multiple learning platforms +3. **Corporate Ownership (2023-2024)**: Maintained under VMware/Broadcom ownership +4. **Independent OSS (2024-present)**: Community-driven open-source project with broad adoption + +## Looking Forward + +As an independent open-source project, Educates continues to evolve with: +- Active community contributions +- Regular feature releases +- Expanding use cases and adoption +- Growing ecosystem of workshops and integrations + +The project's journey from an internal tool to an independent open-source platform demonstrates its value and the commitment of its community to making interactive Kubernetes training accessible to everyone. + diff --git a/about-educates/index.md b/about-educates/index.md new file mode 100644 index 0000000..29e3be9 --- /dev/null +++ b/about-educates/index.md @@ -0,0 +1,126 @@ +--- +sidebar_position: 1 +--- + +# Architecture + +Educates is a Kubernetes-based platform designed to provide interactive workshop environments. This section provides a comprehensive overview of the Educates architecture, its core concepts, and how the system works. + +## High-Level Architecture + +The Educates platform is built on Kubernetes and uses a controller-based architecture to manage workshop environments and sessions. The following diagram illustrates the high-level architecture: + +```mermaid +graph TB + subgraph "Kubernetes Cluster" + subgraph "Educates Operator" + OP[Educates Operator] + SM[Session Manager Controller] + SC[Secrets Manager Controller] + end + + subgraph "Training Portal" + TP[Training Portal Service] + UI[Web UI] + API[REST API] + end + + subgraph "Workshop Environment" + WE[Workshop Environment Namespace] + WR[Workshop Resources] + end + + subgraph "Workshop Session" + WS[Workshop Session Namespace] + WT[Web Terminal] + VE[VS Code Editor] + KC[Kubernetes Console] + FS[File Server] + GS[Git Server] + IR[Image Registry] + DR[Docker Runtime] + VC[vCluster - Optional] + end + + subgraph "Infrastructure Components" + KY[Kyverno] + KAPP[kapp-controller] + DNS[external-dns] + CERT[cert-manager] + end + end + + User[User] -->|Access| UI + User -->|API Calls| API + UI --> TP + API --> TP + TP -->|Creates| WE + TP -->|Creates| WS + OP -->|Manages| WE + OP -->|Manages| WS + SM -->|Manages| WS + SC -->|Manages| WS + WS --> WT + WS --> VE + WS --> KC + WS --> FS + WS --> GS + WS --> IR + WS --> DR + WS -.->|Optional| VC + OP -->|Uses| KY + OP -->|Uses| KAPP + OP -->|Uses| DNS + OP -->|Uses| CERT +``` + +## Core Components + +### Educates Operator + +The Educates Operator is the central component that manages the lifecycle of workshops. It watches for Custom Resources (CRs) and ensures the desired state is achieved: + +- **Workshop Controller**: Manages `Workshop` resources that define workshop content and configuration +- **Training Portal Controller**: Manages `TrainingPortal` resources that provide the web interface +- **Workshop Environment Controller**: Manages `WorkshopEnvironment` resources for setting up workshop environments +- **Workshop Session Controller**: Manages `WorkshopSession` resources for individual user sessions + +### Session Manager Controller + +The Session Manager Controller is responsible for: +- Creating and managing workshop session namespaces +- Allocating resources to sessions +- Managing session lifecycle (creation, updates, deletion) +- Ensuring proper RBAC and resource quotas are applied + +### Secrets Manager Controller + +The Secrets Manager Controller handles: +- Secret management across workshop sessions +- Secret injection into workshop environments +- Secret copying between namespaces +- Secure secret distribution + +### Training Portal + +The Training Portal provides: +- **Web UI**: A user-friendly interface for browsing and accessing workshops +- **REST API**: Programmatic access to workshop catalog and session management +- **Authentication**: User registration and authentication mechanisms +- **Session Management**: Allocation and management of workshop sessions + +## Resource Flow + +1. **Workshop Definition**: A `Workshop` resource is created, defining the workshop content and requirements +2. **Training Portal**: A `TrainingPortal` resource is created, which sets up the web interface +3. **Workshop Environment**: The portal creates `WorkshopEnvironment` resources for each workshop +4. **Workshop Session**: When a user requests access, a `WorkshopSession` resource is created +5. **Session Resources**: The operator creates all necessary resources for the session (namespaces, services, etc.) + +## Next Steps + +- Learn about [Core Concepts](./concepts.md) to understand the building blocks of Educates +- Explore [Workflows](./workflows.md) to see how Educates operates in practice +- Discover the [History](./history.md) of Educates and its evolution +- Check out the [Getting Started Guides](/getting-started-guides) for hands-on tutorials + diff --git a/about-educates/workflows.md b/about-educates/workflows.md new file mode 100644 index 0000000..117eedc --- /dev/null +++ b/about-educates/workflows.md @@ -0,0 +1,295 @@ +--- +sidebar_position: 3 +--- + +# Workflows + +This page explains how Educates works from a workflow perspective, covering the lifecycle of workshops and sessions. + +## Workshop Deployment Workflow + +The following diagram illustrates how workshops are deployed and made available: + +```mermaid +sequenceDiagram + participant Admin + participant Portal as Training Portal + participant Operator as Educates Operator + participant Cluster as Kubernetes Cluster + + Admin->>Cluster: Create Workshop Resource + Operator->>Operator: Watch Workshop CR + Operator->>Operator: Validate Workshop Definition + + Admin->>Cluster: Create TrainingPortal Resource + Operator->>Operator: Watch TrainingPortal CR + Operator->>Cluster: Create WorkshopEnvironment + Operator->>Cluster: Deploy Shared Resources + Operator->>Portal: Register Workshop + + Portal->>Portal: Display Workshop in Catalog +``` + +### Step-by-Step Process + +1. **Workshop Definition**: An administrator creates a `Workshop` Custom Resource that defines: + - Workshop content location (Git repo or container image) + - Resource requirements + - Required capabilities + - RBAC permissions + +2. **Training Portal Creation**: An administrator creates a `TrainingPortal` resource that: + - References one or more workshops + - Configures authentication + - Sets up the web interface + +3. **Workshop Environment Setup**: The operator creates a `WorkshopEnvironment` for each workshop: + - Sets up a namespace for the workshop + - Deploys shared resources (common to all sessions) + - Configures the environment according to workshop requirements + +4. **Portal Registration**: The workshop becomes available in the Training Portal catalog + +## User Session Workflow + +When a user accesses a workshop, the following workflow is triggered: + +```mermaid +sequenceDiagram + participant User + participant Portal as Training Portal + participant Operator as Educates Operator + participant SM as Session Manager + participant SC as Secrets Manager + participant Session as Workshop Session + + User->>Portal: Request Workshop Access + Portal->>Portal: Check Available Sessions + alt Session Available + Portal->>Session: Allocate Existing Session + else No Session Available + Portal->>Operator: Create WorkshopSession CR + Operator->>SM: Create Session Namespace + SM->>Session: Create Session Resources + SM->>Session: Apply RBAC & Quotas + SC->>Session: Inject Required Secrets + Operator->>Session: Deploy Per-Session Resources + Operator->>Session: Start Capabilities (Terminal, Editor, etc.) + Operator->>Portal: Session Ready + Portal->>Session: Allocate Session to User + end + Portal->>User: Redirect to Workshop Session + User->>Session: Access Workshop Dashboard +``` + +### Session Creation Process + +1. **Session Request**: User selects a workshop from the Training Portal +2. **Session Allocation**: Portal checks for available pre-created sessions or creates a new one +3. **Namespace Creation**: Session Manager creates a dedicated namespace for the session +4. **Resource Setup**: + - RBAC policies are applied + - Resource quotas are set + - Required secrets are injected +5. **Capability Deployment**: Required capabilities are started: + - Web terminal + - VS Code editor + - Kubernetes console + - File server, Git server, etc. +6. **Per-Session Resources**: Workshop-specific resources are deployed to the session namespace +7. **Session Access**: User is redirected to the workshop session dashboard + +## Workshop Content Workflow + +The workflow for creating and publishing workshop content: + +```mermaid +graph LR + A[Write Markdown Content] --> B[Package as OCI Image] + B --> C[Push to Registry] + C --> D[Create Workshop CR] + D --> E[Workshop Available] + + style A fill:#e1f5ff + style B fill:#e1f5ff + style C fill:#fff4e1 + style D fill:#fff4e1 + style E fill:#e8f5e9 +``` + +### Content Development Process + +1. **Content Creation**: Workshop authors write content in Markdown or AsciiDoc +2. **Local Testing**: Content is tested using the local Educates environment +3. **Image Building**: Content is packaged into an OCI container image +4. **Image Publishing**: Image is pushed to a container registry +5. **Workshop Deployment**: Workshop CR is created referencing the image +6. **Content Updates**: For content-only changes, sessions can be updated without recreating + +## Session Lifecycle + +The lifecycle of a workshop session: + +```mermaid +stateDiagram-v2 + [*] --> Requested: User Requests Workshop + Requested --> Allocating: Portal Processes Request + Allocating --> Creating: No Session Available + Creating --> Starting: Resources Created + Starting --> Ready: Capabilities Started + Allocating --> Ready: Session Available + Ready --> Active: User Accesses Session + Active --> Idle: User Inactive + Idle --> Active: User Returns + Active --> Expired: Time Limit Reached + Expired --> Terminating: Cleanup Initiated + Ready --> Terminating: Session Not Used + Terminating --> [*]: Resources Deleted +``` + +### Session States + +- **Requested**: User has requested access to a workshop +- **Allocating**: Portal is finding or creating a session +- **Creating**: Session resources are being created +- **Starting**: Capabilities are being started +- **Ready**: Session is ready but not yet accessed +- **Active**: User is actively using the session +- **Idle**: Session is active but user is inactive +- **Expired**: Session has reached its time limit +- **Terminating**: Session is being cleaned up + +## Resource Management Workflow + +How resources are managed across the platform: + +```mermaid +graph TB + subgraph "Workshop Level" + WR[Workshop Resources] + WE[Workshop Environment] + end + + subgraph "Session Level" + SR[Session Resources] + SN[Session Namespace] + end + + subgraph "Shared Resources" + SH[Shared Applications] + SHN[Workshop Environment Namespace] + end + + WE --> SHN + WE --> SH + SN --> SR + SN -.->|Access| SHN + + style WR fill:#e1f5ff + style WE fill:#e1f5ff + style SR fill:#fff4e1 + style SN fill:#fff4e1 + style SH fill:#e8f5e9 + style SHN fill:#e8f5e9 +``` + +### Resource Hierarchy + +1. **Workshop Environment Resources**: Deployed once per workshop, shared by all sessions +2. **Session Resources**: Deployed per session, isolated to each user +3. **Resource Quotas**: Applied at the session level to limit resource usage +4. **RBAC Policies**: Applied at the session level to control access + +## Authentication and Authorization Workflow + +How users are authenticated and authorized: + +```mermaid +sequenceDiagram + participant User + participant Portal as Training Portal + participant Auth as Auth Provider + participant Operator as Educates Operator + + User->>Portal: Access Portal + Portal->>Auth: Authenticate User + Auth-->>Portal: Authentication Result + alt Authenticated + Portal->>Portal: Check User Permissions + Portal->>User: Show Available Workshops + User->>Portal: Request Workshop + Portal->>Operator: Create/Allocate Session + Operator->>Operator: Apply RBAC to Session + Operator-->>Portal: Session Ready + Portal->>User: Grant Access + else Not Authenticated + Portal->>User: Show Login/Register + end +``` + +## Content Update Workflow + +How workshop content updates are handled: + +```mermaid +graph LR + A[Update Content] --> B{Workshop Config Changed?} + B -->|Yes| C[Rebuild Image] + B -->|No| D[Update Content Only] + C --> E[Update Workshop CR] + E --> F[Recreate Environments] + D --> G[Update Running Sessions] + + style A fill:#e1f5ff + style C fill:#fff4e1 + style D fill:#e8f5e9 + style E fill:#fff4e1 + style F fill:#ffebee + style G fill:#e8f5e9 +``` + +### Update Scenarios + +1. **Content-Only Updates**: When only Markdown content changes: + - Content can be updated in running sessions + - No need to recreate workshop environments + - Faster iteration during development + +2. **Configuration Changes**: When workshop configuration changes: + - New container image must be built + - Workshop CR must be updated + - Workshop environments may need to be recreated + - Existing sessions continue until they expire + +## Local Development Workflow + +The workflow for developing workshops locally: + +```mermaid +graph LR + A[Write Content] --> B[Test Locally] + B --> C{Changes OK?} + C -->|No| A + C -->|Yes| D[Publish to Local Registry] + D --> E[Deploy to Local Cluster] + E --> F[Test Workshop] + F --> G{Workshop Works?} + G -->|No| A + G -->|Yes| H[Publish to Production Registry] + H --> I[Deploy to Production] + + style A fill:#e1f5ff + style B fill:#e8f5e9 + style D fill:#fff4e1 + style E fill:#fff4e1 + style H fill:#ffebee + style I fill:#ffebee +``` + +### Local Development Benefits + +- **Fast Iteration**: Content updates can be tested immediately +- **Isolated Environment**: No impact on production +- **Full Feature Set**: Local environment includes all capabilities +- **Easy Debugging**: Direct access to cluster and resources + diff --git a/blog/_2025-03-28-update-a-local-clusters-capabilities/index.md b/blog/_2025-03-28-update-a-local-clusters-capabilities/index.md new file mode 100644 index 0000000..e9836dc --- /dev/null +++ b/blog/_2025-03-28-update-a-local-clusters-capabilities/index.md @@ -0,0 +1,272 @@ +--- +slug: update-a-local-clusters-capabilities +title: Update a local cluster's capabilities +authors: [jorge] +tags: [getting-started, educates, kind, local] +--- + +When you create a local cluster as explained in [Getting Started on Kind](/blog/getting-started-on-kind), the cluster comes configured with some additional capabilities based on the provided configuration (or the default if none is provided), namely [Contour](https://projectcontour.io) as `Ingress controller` and [Kyverno](https://kyverno.io/) as `policy management`, and then, obviously, Educates Training Platform. + + + +These capabilities are from the specific version that Educates provides and configured in an opinionated way. In order to see what are the values used by the installer to configure these capabilities, run: + +``` +$ educates admin platform values --local-config +--- +clusterPackages: + contour: + enabled: true + settings: + infraProvider: kind + contour: + replicas: 1 + configFileContents: + defaultHttpVersions: + - HTTP/1.1 + service: + type: ClusterIP + useHostPorts: true + cert-manager: + enabled: false + settings: {} + external-dns: + enabled: false + settings: {} + certs: + enabled: false + settings: {} + kyverno: + enabled: true + settings: {} + kapp-controller: + enabled: false + settings: {} + educates: + enabled: true + settings: + imageVersions: + - name: session-manager + image: ghcr.io/educates/educates-session-manager:3.2.0 + - name: training-portal + image: ghcr.io/educates/educates-training-portal:3.2.0 + - name: docker-registry + image: ghcr.io/educates/educates-docker-registry:3.2.0 + - name: pause-container + image: ghcr.io/educates/educates-pause-container:3.2.0 + - name: base-environment + image: ghcr.io/educates/educates-base-environment:3.2.0 + - name: jdk8-environment + image: ghcr.io/educates/educates-jdk8-environment:3.2.0 + - name: jdk11-environment + image: ghcr.io/educates/educates-jdk11-environment:3.2.0 + - name: jdk17-environment + image: ghcr.io/educates/educates-jdk17-environment:3.2.0 + - name: jdk21-environment + image: ghcr.io/educates/educates-jdk21-environment:3.2.0 + - name: conda-environment + image: ghcr.io/educates/educates-conda-environment:3.2.0 + - name: secrets-manager + image: ghcr.io/educates/educates-secrets-manager:3.2.0 + - name: tunnel-manager + image: ghcr.io/educates/educates-tunnel-manager:3.2.0 + - name: image-cache + image: ghcr.io/educates/educates-image-cache:3.2.0 + - name: assets-server + image: ghcr.io/educates/educates-assets-server:3.2.0 + - name: lookup-service + image: ghcr.io/educates/educates-lookup-service:3.2.0 + - name: debian-base-image + image: debian:sid-20230502-slim + - name: docker-in-docker + image: docker:27.5.1-dind + - name: rancher-k3s-v1.27 + image: rancher/k3s:v1.27.14-k3s1 + - name: rancher-k3s-v1.28 + image: rancher/k3s:v1.28.10-k3s1 + - name: rancher-k3s-v1.29 + image: rancher/k3s:v1.29.5-k3s1 + - name: rancher-k3s-v1.30 + image: rancher/k3s:v1.30.1-k3s1 + - name: loftsh-vcluster + image: loftsh/vcluster:0.18.1 + clusterIngress: + domain: educates.test + tlsCertificateRef: + namespace: educates-secrets + name: educates.test-tls + caCertificateRef: + namespace: educates-secrets + name: educates.test-ca + clusterSecurity: + policyEngine: kyverno + workshopSecurity: + rulesEngine: kyverno +``` + +__NOTE__: This configuration presents values that were introduced in our [previous blog](/blog/how-to-best-work-locally/) + +The `clusterPackages` section is the one that contains the configuration Educates installer will use when creating the cluster, but also when deploying the platform to remote clusters, but that's for another blog. +As you will notice, only the `enabled` packages are installed, and the configuration in `settings` is the +one that will be provided to the installer mechanism. + +Let's say that for some reason, you want to test a different version of contour, or contour configured in a +different way, but you have already created the cluster. What do you do? + +Let's create a cluster so that you can test this yourself. + +``` +$ educates local cluster create +``` + +Once the cluster is created, we will go ahead and modify the configuration of our contour installation so that it uses `2 replicas`. For that we will copy and paste the whole output of our previous `educates admin platform values --local-config` command and paste it in our local configuration file, via: + +``` +$ educates local config edit +clusterPackages: + contour: + enabled: true + settings: + configFileContents: + defaultHttpVersions: + - HTTP/1.1 + contour: + replicas: 2 + infraProvider: kind + service: + type: ClusterIP + useHostPorts: true + cert-manager: + enabled: false + settings: {} + external-dns: + enabled: false + settings: {} + certs: + enabled: false + settings: {} + kyverno: + enabled: true + settings: {} + kapp-controller: + enabled: false + settings: {} + educates: + enabled: true + settings: + imageVersions: + - name: session-manager + image: ghcr.io/educates/educates-session-manager:develop + - name: training-portal + image: ghcr.io/educates/educates-training-portal:develop + - name: docker-registry + image: ghcr.io/educates/educates-docker-registry:develop + - name: pause-container + image: ghcr.io/educates/educates-pause-container:develop + - name: base-environment + image: ghcr.io/educates/educates-base-environment:develop + - name: jdk8-environment + image: ghcr.io/educates/educates-jdk8-environment:develop + - name: jdk11-environment + image: ghcr.io/educates/educates-jdk11-environment:develop + - name: jdk17-environment + image: ghcr.io/educates/educates-jdk17-environment:develop + - name: jdk21-environment + image: ghcr.io/educates/educates-jdk21-environment:develop + - name: conda-environment + image: ghcr.io/educates/educates-conda-environment:develop + - name: secrets-manager + image: ghcr.io/educates/educates-secrets-manager:develop + - name: tunnel-manager + image: ghcr.io/educates/educates-tunnel-manager:develop + - name: image-cache + image: ghcr.io/educates/educates-image-cache:develop + - name: assets-server + image: ghcr.io/educates/educates-assets-server:develop + - name: lookup-service + image: ghcr.io/educates/educates-lookup-service:develop + - name: debian-base-image + image: debian:sid-20230502-slim + - name: docker-in-docker + image: docker:27.5.1-dind + - name: rancher-k3s-v1.27 + image: rancher/k3s:v1.27.14-k3s1 + - name: rancher-k3s-v1.28 + image: rancher/k3s:v1.28.10-k3s1 + - name: rancher-k3s-v1.29 + image: rancher/k3s:v1.29.5-k3s1 + - name: rancher-k3s-v1.30 + image: rancher/k3s:v1.30.1-k3s1 + - name: loftsh-vcluster + image: loftsh/vcluster:0.18.1 + - image: ghcr.io/educates/educates-session-manager:develop + name: session-manager + - image: ghcr.io/educates/educates-training-portal:develop + name: training-portal + - image: ghcr.io/educates/educates-docker-registry:develop + name: docker-registry + - image: ghcr.io/educates/educates-pause-container:develop + name: pause-container + - image: ghcr.io/educates/educates-base-environment:develop + name: base-environment + - image: ghcr.io/educates/educates-jdk8-environment:develop + name: jdk8-environment + - image: ghcr.io/educates/educates-jdk11-environment:develop + name: jdk11-environment + - image: ghcr.io/educates/educates-jdk17-environment:develop + name: jdk17-environment + - image: ghcr.io/educates/educates-jdk21-environment:develop + name: jdk21-environment + - image: ghcr.io/educates/educates-conda-environment:develop + name: conda-environment + - image: ghcr.io/educates/educates-secrets-manager:develop + name: secrets-manager + - image: ghcr.io/educates/educates-tunnel-manager:develop + name: tunnel-manager + - image: ghcr.io/educates/educates-image-cache:develop + name: image-cache + - image: ghcr.io/educates/educates-assets-server:develop + name: assets-server + - image: ghcr.io/educates/educates-lookup-service:develop + name: lookup-service + - image: debian:sid-20230502-slim + name: debian-base-image + - image: docker:27.5.1-dind + name: docker-in-docker + - image: rancher/k3s:v1.27.14-k3s1 + name: rancher-k3s-v1.27 + - image: rancher/k3s:v1.28.10-k3s1 + name: rancher-k3s-v1.28 + - image: rancher/k3s:v1.29.5-k3s1 + name: rancher-k3s-v1.29 + - image: rancher/k3s:v1.30.1-k3s1 + name: rancher-k3s-v1.30 + - image: loftsh/vcluster:0.18.1 + name: loftsh-vcluster + clusterIngress: + caCertificateRef: + name: educates.test-ca + namespace: educates-secrets + domain: educates.test + tlsCertificateRef: + name: educates.test-tls + namespace: educates-secrets + clusterSecurity: + policyEngine: kyverno + workshopSecurity: + rulesEngine: kyverno +``` + +We will then add the following snippet, so that the installer understands we're providing customized configuration to the opinionated installers. + +``` +clusterInfrastructure: + provider: custom +``` + +Now that the configuration in our local file has been modified, we can run the installer to apply the changes: + +``` +$ educates admin platform deploy --local-config +``` + diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 8d5e956..7df8eeb 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -73,6 +73,18 @@ const config: Config = { editUrl: "https://github.com/educates/educates.github.io/tree/develop/", }, ], + [ + "@docusaurus/plugin-content-docs", + { + id: "about-educates", + sidebarPath: "./sidebars.ts", + path: "./about-educates", + routeBasePath: "about-educates", + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + editUrl: "https://github.com/educates/educates.github.io/tree/develop/", + }, + ], [ "@docusaurus/plugin-ideal-image", { @@ -98,8 +110,8 @@ const config: Config = { blog: { blogTitle: "Educates Training Platform blog!", blogDescription: "A blog about all things Educates!", - // blogSidebarTitle: "All posts", - // blogSidebarCount: "ALL", + blogSidebarTitle: "All posts", + blogSidebarCount: "ALL", // postsPerPage: "ALL", showReadingTime: true, feedOptions: { @@ -163,6 +175,17 @@ const config: Config = { ], }, { to: "/downloads", label: "Downloads", position: "left" }, + { + type: "dropdown", + label: "About", + position: "left", + items: [ + { to: "/about-educates", label: "Architecture" }, + { to: "/about-educates/concepts", label: "Concepts" }, + { to: "/about-educates/workflows", label: "Workflows" }, + { to: "/about-educates/history", label: "History" }, + ], + }, { type: "dropdown", label: "Guides", diff --git a/sidebars.ts b/sidebars.ts index 6bb17b7..3e07a58 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -21,6 +21,12 @@ const sidebars: SidebarsConfig = { dirName: ".", }, ], + aboutEducatesSidebar: [ + { + type: "autogenerated", + dirName: ".", + }, + ], // But you can create a sidebar manually /* tutorialSidebar: [ diff --git a/src/components/AutoClickableDiagrams/index.tsx b/src/components/AutoClickableDiagrams/index.tsx new file mode 100644 index 0000000..31fbd45 --- /dev/null +++ b/src/components/AutoClickableDiagrams/index.tsx @@ -0,0 +1,198 @@ +import React, { useEffect, useState, useRef } from 'react'; +import DiagramModal from '../DiagramModal'; +import '../ClickableDiagram/styles.css'; + +interface DiagramData { + id: string; + html: string; + title: string; +} + +export default function AutoClickableDiagrams() { + const [diagrams, setDiagrams] = useState>(new Map()); + const [openDiagramId, setOpenDiagramId] = useState(null); + const processedRef = useRef>(new Set()); + + // Debug: log component mount + useEffect(() => { + console.log('[AutoClickableDiagrams] Component mounted'); + }, []); + + useEffect(() => { + const wrapDiagrams = () => { + // Find all Mermaid containers - Docusaurus renders them in different ways + // Try multiple selectors to catch all cases + const selectors = [ + '.mermaid', + 'pre code.language-mermaid', + 'code.language-mermaid', + '[class*="mermaid"]', + ]; + + let mermaidContainers: NodeListOf = document.querySelectorAll('.mermaid'); + + // If no .mermaid found, try other selectors + if (mermaidContainers.length === 0) { + for (const selector of selectors) { + mermaidContainers = document.querySelectorAll(selector); + if (mermaidContainers.length > 0) break; + } + } + + // Debug: log found diagrams + if (mermaidContainers.length > 0) { + console.log(`[AutoClickableDiagrams] Found ${mermaidContainers.length} Mermaid diagram(s)`); + } + + const newDiagrams = new Map(); + + mermaidContainers.forEach((element, index) => { + const htmlElement = element as HTMLElement; + + // Skip if already processed + if (processedRef.current.has(htmlElement) || htmlElement.closest('.clickable-diagram-wrapper')) { + return; + } + + // Find the actual container - could be the element itself or its parent + let diagramContainer = htmlElement; + + // If it's a code element, get the pre parent + if (htmlElement.tagName === 'CODE') { + const preParent = htmlElement.closest('pre'); + if (preParent) { + diagramContainer = preParent; + } + } else { + // For .mermaid divs, check if there's a parent container we should wrap + const parent = htmlElement.parentElement; + if (parent && (parent.classList.contains('mermaid') || parent.tagName === 'P')) { + diagramContainer = parent; + } + } + + // Skip if already wrapped + if (diagramContainer.closest('.clickable-diagram-wrapper')) { + return; + } + + const id = `diagram-${index}-${Date.now()}`; + + // Get title from previous heading + let title = 'Diagram'; + let prevElement = diagramContainer.previousElementSibling; + let searchCount = 0; + while (prevElement && searchCount < 10) { + if (prevElement.tagName?.match(/^H[1-6]$/)) { + title = prevElement.textContent?.trim() || 'Diagram'; + break; + } + prevElement = prevElement.previousElementSibling; + searchCount++; + } + + // Create wrapper + const wrapper = document.createElement('div'); + wrapper.className = 'clickable-diagram-wrapper'; + wrapper.setAttribute('role', 'button'); + wrapper.setAttribute('tabindex', '0'); + wrapper.setAttribute('data-diagram-id', id); + wrapper.setAttribute('aria-label', 'Click to expand diagram'); + + // Wrap the container + diagramContainer.parentNode?.insertBefore(wrapper, diagramContainer); + wrapper.appendChild(diagramContainer); + + // Store the HTML content for the modal + const htmlContent = diagramContainer.outerHTML; + + newDiagrams.set(id, { + id, + html: htmlContent, + title, + }); + + processedRef.current.add(htmlElement); + + // Add click handler + const handleClick = () => { + setOpenDiagramId(id); + }; + + wrapper.addEventListener('click', handleClick); + + // Add keyboard support + wrapper.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } + }); + }); + + if (newDiagrams.size > 0) { + setDiagrams((prev) => { + const updated = new Map(prev); + newDiagrams.forEach((value, key) => { + updated.set(key, value); + }); + return updated; + }); + } + }; + + // Run after Mermaid renders - Mermaid might take time to initialize + const checkInterval = setInterval(() => { + wrapDiagrams(); + }, 500); + + // Initial attempts with delays to catch rendered diagrams + // Mermaid typically renders after page load + const timeouts = [ + setTimeout(wrapDiagrams, 100), + setTimeout(wrapDiagrams, 500), + setTimeout(wrapDiagrams, 1000), + setTimeout(wrapDiagrams, 2000), + setTimeout(wrapDiagrams, 3000), + setTimeout(wrapDiagrams, 5000), + ]; + + // Also listen for Mermaid initialization events + const handleMermaidInit = () => { + setTimeout(wrapDiagrams, 100); + }; + + window.addEventListener('mermaid-ready', handleMermaidInit); + document.addEventListener('DOMContentLoaded', wrapDiagrams); + + // Cleanup after 20 seconds + setTimeout(() => { + clearInterval(checkInterval); + timeouts.forEach(clearTimeout); + window.removeEventListener('mermaid-ready', handleMermaidInit); + }, 20000); + + return () => { + clearInterval(checkInterval); + timeouts.forEach(clearTimeout); + window.removeEventListener('mermaid-ready', handleMermaidInit); + }; + }, []); + + const currentDiagram = openDiagramId ? diagrams.get(openDiagramId) : null; + + return ( + <> + {currentDiagram && ( + setOpenDiagramId(null)} + title={currentDiagram.title} + > +
+ + )} + + ); +} + diff --git a/src/components/Carrousel/Carrousel.tsx b/src/components/Carrousel/Carrousel.tsx index 667d401..f7011a8 100644 --- a/src/components/Carrousel/Carrousel.tsx +++ b/src/components/Carrousel/Carrousel.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import ArrowBackIos from '@mui/icons-material/ArrowBackIos'; import ArrowForwardIos from '@mui/icons-material/ArrowForwardIos'; +import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; +import Modal from '@mui/material/Modal'; export interface CarrouselItem { image: string; @@ -21,11 +23,12 @@ interface CarrouselProps { const Carrousel: React.FC = ({ images, altPrefix = 'Carousel image', imageClassName, boxSx }) => { const [currentImage, setCurrentImage] = useState(0); const [paused, setPaused] = useState(false); + const [modalOpen, setModalOpen] = useState(false); const intervalRef = useRef(null); const interval = 3000; useEffect(() => { - if (!paused) { + if (!paused && !modalOpen) { intervalRef.current = setInterval(() => { setCurrentImage((prev) => (prev + 1) % images.length); }, interval); @@ -33,90 +36,227 @@ const Carrousel: React.FC = ({ images, altPrefix = 'Carousel ima return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; - }, [paused, images.length]); + }, [paused, modalOpen, images.length]); - const goToPrev = () => { + const goToPrev = (e?: React.MouseEvent) => { + e?.stopPropagation(); setCurrentImage((prev) => (prev - 1 + images.length) % images.length); }; - const goToNext = () => { + const goToNext = (e?: React.MouseEvent) => { + e?.stopPropagation(); setCurrentImage((prev) => (prev + 1) % images.length); }; const goToIndex = (idx: number) => setCurrentImage(idx); + const handleOpen = () => { + setModalOpen(true); + setPaused(true); + }; + + const handleClose = () => { + setModalOpen(false); + setPaused(false); + }; + + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (!modalOpen) return; + if (e.key === 'ArrowLeft') { + setCurrentImage((prev) => (prev - 1 + images.length) % images.length); + } else if (e.key === 'ArrowRight') { + setCurrentImage((prev) => (prev + 1) % images.length); + } + }, [modalOpen, images.length]); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + const { image, title, description } = images[currentImage]; return ( - setPaused(true)} - onMouseLeave={() => setPaused(false)} - > - {/* Left Arrow */} - - - - {/* Image */} - {`${altPrefix} - {/* Right Arrow */} - + setPaused(true)} + onMouseLeave={() => setPaused(false)} > - - - {/* Dots - below the image */} - - {images.map((_, idx) => ( - goToIndex(idx)} + {/* Left Arrow */} + + + + {/* Image */} + {`${altPrefix} + {/* Right Arrow */} + + + + {/* Dots - below the image */} + + {images.map((_, idx) => ( + goToIndex(idx)} + sx={{ + width: 12, + height: 12, + borderRadius: '50%', + background: idx === currentImage ? '#1976d2' : '#ccc', + cursor: 'pointer', + border: idx === currentImage ? '2px solid #1976d2' : '2px solid #ccc', + transition: 'background 0.2s, border 0.2s', + }} + /> + ))} + + {/* Title and Description below indicators */} + + + {title} + + - ))} + > + {description} + + - {/* Title and Description below indicators */} - - - {title} - - + + + {/* Close Button */} + + + + + {/* Modal Left Arrow */} + + + + + {/* Modal Right Arrow */} + + + + + {/* Main Content Container */} + - {description} - - - + position: 'relative' + }}> + {`${altPrefix} + + + {/* Modal Footer (Title/Desc) */} + + + {title} + + + {description} + + + + + ); }; -export default Carrousel; \ No newline at end of file +export default Carrousel; \ No newline at end of file diff --git a/src/components/ClickableDiagram/index.tsx b/src/components/ClickableDiagram/index.tsx new file mode 100644 index 0000000..76e672d --- /dev/null +++ b/src/components/ClickableDiagram/index.tsx @@ -0,0 +1,47 @@ +import React, { useState, useRef, useEffect } from 'react'; +import DiagramModal from '../DiagramModal'; +import './styles.css'; + +interface ClickableDiagramProps { + children: React.ReactNode; + title?: string; + className?: string; +} + +export default function ClickableDiagram({ children, title, className = '' }: ClickableDiagramProps) { + const [isModalOpen, setIsModalOpen] = useState(false); + const containerRef = useRef(null); + + const handleClick = () => { + setIsModalOpen(true); + }; + + return ( + <> +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } + }} + aria-label="Click to expand diagram" + > + {children} +
+ setIsModalOpen(false)} + title={title} + > + {children} + + + ); +} + diff --git a/src/components/ClickableDiagram/styles.css b/src/components/ClickableDiagram/styles.css new file mode 100644 index 0000000..ea840fa --- /dev/null +++ b/src/components/ClickableDiagram/styles.css @@ -0,0 +1,60 @@ +.clickable-diagram-wrapper { + position: relative; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + border-radius: 8px; + overflow: hidden; + margin: 1.5rem 0; + outline: none; +} + +.clickable-diagram-wrapper:focus { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; +} + +.clickable-diagram-wrapper:hover { + transform: scale(1.01); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +.clickable-diagram-wrapper::after { + content: '🔍 Click to expand'; + position: absolute; + top: 0.75rem; + right: 0.75rem; + background: rgba(0, 0, 0, 0.75); + color: white; + padding: 0.375rem 0.875rem; + border-radius: 6px; + font-size: 0.8125rem; + font-weight: 500; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; + z-index: 10; + backdrop-filter: blur(4px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.clickable-diagram-wrapper:hover::after { + opacity: 1; +} + +[data-theme='dark'] .clickable-diagram-wrapper::after { + background: rgba(255, 255, 255, 0.9); + color: #1a1a1a; +} + +/* Ensure Mermaid diagrams are properly contained */ +.clickable-diagram-wrapper .mermaid { + display: block; + margin: 0; +} + +.clickable-diagram-wrapper pre { + margin: 0; + border-radius: 8px; + overflow: hidden; +} + diff --git a/src/components/DiagramModal/client.tsx b/src/components/DiagramModal/client.tsx new file mode 100644 index 0000000..6923e57 --- /dev/null +++ b/src/components/DiagramModal/client.tsx @@ -0,0 +1,222 @@ +'use client'; + +import { useEffect } from 'react'; +import DiagramModal from './index'; + +// This component will automatically wrap Mermaid diagrams +export default function AutoWrapMermaidDiagrams() { + useEffect(() => { + // Wait for Mermaid to render + const wrapDiagrams = () => { + // Find all rendered Mermaid diagrams + const mermaidElements = document.querySelectorAll('.mermaid'); + + mermaidElements.forEach((element) => { + // Skip if already wrapped + if (element.closest('.clickable-diagram-wrapper')) { + return; + } + + // Create wrapper + const wrapper = document.createElement('div'); + wrapper.className = 'clickable-diagram-wrapper'; + wrapper.setAttribute('role', 'button'); + wrapper.setAttribute('tabindex', '0'); + wrapper.setAttribute('aria-label', 'Click to expand diagram'); + + // Get the parent (usually a pre or div) + const parent = element.parentElement; + if (parent && (parent.tagName === 'PRE' || parent.classList.contains('mermaid'))) { + // Wrap the parent element + parent.parentNode?.insertBefore(wrapper, parent); + wrapper.appendChild(parent); + } else { + // Wrap the element itself + element.parentNode?.insertBefore(wrapper, element); + wrapper.appendChild(element); + } + + // Add click handler + wrapper.addEventListener('click', () => { + // Create modal dynamically + const modal = document.createElement('div'); + modal.className = 'diagram-modal-overlay'; + modal.innerHTML = ` +
+
+

Diagram

+
+ + 100% + + + +
+
+
+
+ ${parent ? parent.outerHTML : element.outerHTML} +
+
+ +
+ `; + + document.body.appendChild(modal); + document.body.style.overflow = 'hidden'; + + // Initialize zoom and pan + let scale = 1; + let position = { x: 0, y: 0 }; + let isDragging = false; + let dragStart = { x: 0, y: 0 }; + const diagramEl = modal.querySelector('#diagram-diagram') as HTMLElement; + const contentEl = modal.querySelector('#diagram-content') as HTMLElement; + const zoomLevelEl = modal.querySelector('#zoom-level') as HTMLElement; + + const updateTransform = () => { + if (diagramEl) { + diagramEl.style.transform = `translate(${position.x}px, ${position.y}px) scale(${scale})`; + } + if (zoomLevelEl) { + zoomLevelEl.textContent = `${Math.round(scale * 100)}%`; + } + }; + + // Zoom functions + (window as any).diagramZoomIn = () => { + scale = Math.min(scale + 0.25, 3); + updateTransform(); + }; + + (window as any).diagramZoomOut = () => { + scale = Math.max(scale - 0.25, 0.5); + updateTransform(); + }; + + (window as any).diagramReset = () => { + scale = 1; + position = { x: 0, y: 0 }; + updateTransform(); + }; + + (window as any).closeDiagramModal = () => { + document.body.removeChild(modal); + document.body.style.overflow = ''; + delete (window as any).diagramZoomIn; + delete (window as any).diagramZoomOut; + delete (window as any).diagramReset; + delete (window as any).closeDiagramModal; + }; + + // Pan functionality + if (contentEl) { + contentEl.addEventListener('mousedown', (e) => { + if (e.button === 0) { + isDragging = true; + dragStart = { + x: e.clientX - position.x, + y: e.clientY - position.y, + }; + contentEl.style.cursor = 'grabbing'; + } + }); + + contentEl.addEventListener('mousemove', (e) => { + if (isDragging) { + position = { + x: e.clientX - dragStart.x, + y: e.clientY - dragStart.y, + }; + updateTransform(); + } + }); + + contentEl.addEventListener('mouseup', () => { + isDragging = false; + if (contentEl) contentEl.style.cursor = 'grab'; + }); + + contentEl.addEventListener('mouseleave', () => { + isDragging = false; + if (contentEl) contentEl.style.cursor = 'grab'; + }); + + contentEl.addEventListener('wheel', (e) => { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + scale = Math.max(0.5, Math.min(3, scale + delta)); + updateTransform(); + }); + } + + // Close on overlay click + modal.addEventListener('click', (e) => { + if (e.target === modal) { + (window as any).closeDiagramModal(); + } + }); + + // Close on ESC + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + (window as any).closeDiagramModal(); + window.removeEventListener('keydown', handleEsc); + } + }; + window.addEventListener('keydown', handleEsc); + }); + + // Add keyboard support + wrapper.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + wrapper.click(); + } + }); + }); + }; + + // Try multiple times to catch all rendered diagrams + const interval = setInterval(() => { + wrapDiagrams(); + }, 500); + + // Also run immediately and after a delay + setTimeout(wrapDiagrams, 100); + setTimeout(wrapDiagrams, 1000); + setTimeout(() => clearInterval(interval), 5000); + + return () => clearInterval(interval); + }, []); + + return null; +} + diff --git a/src/components/DiagramModal/index.tsx b/src/components/DiagramModal/index.tsx new file mode 100644 index 0000000..cc02fea --- /dev/null +++ b/src/components/DiagramModal/index.tsx @@ -0,0 +1,261 @@ +import React, { useState, useEffect, useRef } from 'react'; +import './styles.css'; + +interface DiagramModalProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; + title?: string; +} + +export default function DiagramModal({ isOpen, onClose, children, title }: DiagramModalProps) { + const [scale, setScale] = useState(1); + const [baseScale, setBaseScale] = useState(1); // The "fit to width" scale (100%) + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [isReady, setIsReady] = useState(false); + const containerRef = useRef(null); + const contentRef = useRef(null); + const diagramRef = useRef(null); + + // Calculate fit-to-width scale when modal opens + useEffect(() => { + if (isOpen && containerRef.current && diagramRef.current) { + // Wait a bit for the diagram to render + const calculateFitScale = () => { + const container = containerRef.current; + const diagram = diagramRef.current; + + if (!container || !diagram) return; + + // Find the actual diagram element (could be SVG, pre, or div) + let diagramElement = diagram.querySelector('svg') as unknown as HTMLElement; + if (!diagramElement) { + diagramElement = diagram.querySelector('pre, .mermaid') as HTMLElement; + } + if (!diagramElement) { + // Fallback: use the first child element + diagramElement = diagram.firstElementChild as HTMLElement; + } + if (!diagramElement) return; + + // Get natural dimensions + let naturalWidth = 0; + let naturalHeight = 0; + + if (diagramElement.tagName === 'SVG') { + const svg = diagramElement as unknown as SVGSVGElement; + // Try viewBox first (most accurate for Mermaid) + if (svg.viewBox && svg.viewBox.baseVal) { + naturalWidth = svg.viewBox.baseVal.width; + naturalHeight = svg.viewBox.baseVal.height; + } else { + // Fallback to actual dimensions + const rect = svg.getBoundingClientRect(); + naturalWidth = rect.width || svg.clientWidth || 800; + naturalHeight = rect.height || svg.clientHeight || 600; + } + } else { + // For other elements, get their actual size + const rect = diagramElement.getBoundingClientRect(); + naturalWidth = rect.width || diagramElement.scrollWidth || diagramElement.clientWidth || 800; + naturalHeight = rect.height || diagramElement.scrollHeight || diagramElement.clientHeight || 600; + } + + // Get container dimensions (accounting for padding) + const containerRect = container.getBoundingClientRect(); + const containerWidth = containerRect.width - 40; // 20px padding on each side + const containerHeight = containerRect.height - 40; + + if (naturalWidth > 0 && containerWidth > 0) { + // Calculate scale to fit width (this becomes 100%) + const fitToWidthScale = Math.min(containerWidth / naturalWidth, (containerHeight / naturalHeight) || containerWidth / naturalWidth); + setBaseScale(fitToWidthScale); + setScale(fitToWidthScale); + setPosition({ x: 0, y: 0 }); + } + }; + + // Try multiple times to catch rendered content + setTimeout(calculateFitScale, 50); + setTimeout(calculateFitScale, 200); + setTimeout(() => { + calculateFitScale(); + setIsReady(true); + }, 500); + + // Prevent body scroll when modal is open + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + + return () => { + document.body.style.overflow = ''; + setIsReady(false); + }; + }, [isOpen]); + + const handleZoomIn = () => { + setScale((prev) => Math.min(prev + baseScale * 0.25, baseScale * 3)); + }; + + const handleZoomOut = () => { + setScale((prev) => Math.max(prev - baseScale * 0.25, baseScale * 0.5)); + }; + + const handleReset = () => { + setScale(baseScale); + setPosition({ x: 0, y: 0 }); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + if (e.button !== 0) return; // Only handle left mouse button + setIsDragging(true); + setDragStart({ + x: e.clientX - position.x, + y: e.clientY - position.y, + }); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging) return; + setPosition({ + x: e.clientX - dragStart.x, + y: e.clientY - dragStart.y, + }); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleWheel = (e: React.WheelEvent) => { + e.preventDefault(); + const delta = e.deltaY > 0 ? -baseScale * 0.1 : baseScale * 0.1; + setScale((prev) => Math.max(baseScale * 0.5, Math.min(baseScale * 3, prev + delta))); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (!isOpen) return; + if (e.key === 'Escape') { + onClose(); + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen]); + + if (!isOpen) return null; + + return ( +
+ + {!isReady && ( +
+
+

Loading diagram...

+
+ )} + +
e.stopPropagation()} + style={{ visibility: isReady ? 'visible' : 'hidden' }} + > +
+ {title &&

{title}

} +
+ + {Math.round((scale / baseScale) * 100)}% + + + +
+
+
+
+
+ {children} +
+
+
+
+

+ Click and drag to pan • Scroll to zoom • Press ESC to close +

+
+
+
+ ); +} + diff --git a/src/components/DiagramModal/styles.css b/src/components/DiagramModal/styles.css new file mode 100644 index 0000000..5728741 --- /dev/null +++ b/src/components/DiagramModal/styles.css @@ -0,0 +1,280 @@ +.diagram-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(4px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.diagram-modal-container { + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + width: 100%; + max-width: 95vw; + height: 90vh; + max-height: 90vh; + display: flex; + flex-direction: column; + overflow: hidden; + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.diagram-modal-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + color: white; +} + +.diagram-modal-spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +[data-theme='dark'] .diagram-modal-container { + background: #1a1a1a; + color: #f5f6fa; +} + +.diagram-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + flex-shrink: 0; +} + +[data-theme='dark'] .diagram-modal-header { + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +.diagram-modal-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--ifm-color-content); +} + +.diagram-modal-controls { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.diagram-modal-button { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: white; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + color: var(--ifm-color-content); +} + +[data-theme='dark'] .diagram-modal-button { + background: #2a2a2a; + border-color: rgba(255, 255, 255, 0.1); + color: #f5f6fa; +} + +.diagram-modal-button:hover { + background: var(--ifm-color-primary); + color: white; + border-color: var(--ifm-color-primary); + transform: scale(1.05); +} + +.diagram-modal-button:active { + transform: scale(0.95); +} + +.diagram-modal-close:hover { + background: #dc3545; + border-color: #dc3545; + color: white; +} + +.diagram-modal-zoom-level { + font-size: 0.875rem; + font-weight: 500; + min-width: 3rem; + text-align: center; + color: var(--ifm-color-content-secondary); +} + +.diagram-modal-content { + flex: 1; + overflow: hidden; + position: relative; + background: #f8f9fa; + display: flex; + align-items: center; + justify-content: center; + user-select: none; +} + +[data-theme='dark'] .diagram-modal-content { + background: #121212; +} + +.diagram-modal-diagram { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; +} + +.diagram-modal-diagram > div { + transition: transform 0.1s ease-out; +} + +.diagram-modal-diagram svg, +.diagram-modal-diagram .mermaid, +.diagram-modal-diagram pre { + max-width: none !important; + max-height: none !important; + display: block; +} + +.diagram-modal-footer { + padding: 0.75rem 1.5rem; + border-top: 1px solid rgba(0, 0, 0, 0.1); + flex-shrink: 0; + text-align: center; +} + +[data-theme='dark'] .diagram-modal-footer { + border-top-color: rgba(255, 255, 255, 0.1); +} + +.diagram-modal-hint { + margin: 0; + font-size: 0.875rem; + color: var(--ifm-color-content-secondary); +} + +/* Clickable diagram wrapper */ +.clickable-diagram { + position: relative; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + border-radius: 8px; + overflow: hidden; + margin: 1.5rem 0; +} + +.clickable-diagram:hover { + transform: scale(1.02); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.clickable-diagram::after { + content: '🔍 Click to expand'; + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; + z-index: 10; +} + +.clickable-diagram:hover::after { + opacity: 1; +} + +[data-theme='dark'] .clickable-diagram::after { + background: rgba(255, 255, 255, 0.9); + color: #1a1a1a; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .diagram-modal-container { + max-width: 100vw; + height: 100vh; + max-height: 100vh; + border-radius: 0; + } + + .diagram-modal-overlay { + padding: 0; + } + + .diagram-modal-header { + padding: 0.75rem 1rem; + } + + .diagram-modal-title { + font-size: 1rem; + } + + .diagram-modal-button { + width: 2.25rem; + height: 2.25rem; + } + + .diagram-modal-hint { + font-size: 0.75rem; + } +} + diff --git a/src/components/sections/FeaturedContent.tsx b/src/components/sections/FeaturedContent.tsx index c284771..34876d0 100644 --- a/src/components/sections/FeaturedContent.tsx +++ b/src/components/sections/FeaturedContent.tsx @@ -54,57 +54,57 @@ const FeaturedContent: React.FC = ({ {/* {description} */} - - - - {cards.map((card, idx) => ( - // @ts-ignore - - - - - - {card.title} - - - {card.description} - - - - - - ))} - - + + + + {cards.map((card, idx) => ( + // @ts-ignore + + + + + + {card.title} + + + {card.description} + + + + + + ))} + + diff --git a/src/css/custom.css b/src/css/custom.css index 2b30d52..9b14247 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -145,6 +145,168 @@ .navbar-login-button { display: none !important; } + + /* Ensure mobile menu is properly styled */ + .navbar-sidebar { + background: rgba(203, 225, 240, 0.95); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } + + [data-theme='dark'] .navbar-sidebar { + background: #181c20; + } +} + +/* Mobile menu (navbar-sidebar) styling for all screen sizes */ +@media (max-width: 996px) { + /* Mobile menu sidebar container */ + .navbar-sidebar { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100% !important; + max-width: 100% !important; + height: 100vh !important; + z-index: 999 !important; + background: rgba(203, 225, 240, 0.98) !important; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + overflow-y: auto; + overflow-x: hidden; + padding: 0 !important; + margin: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + } + + /* Mobile menu backdrop overlay */ + .navbar-sidebar__backdrop { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + background: rgba(0, 0, 0, 0.3) !important; + z-index: 998 !important; + } + + /* Mobile menu inner container */ + .navbar-sidebar__brand { + padding: 1rem 1.5rem !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + background: transparent !important; + } + + /* Mobile menu close button */ + .navbar-sidebar__close { + position: absolute !important; + top: 1rem !important; + right: 1rem !important; + z-index: 1000 !important; + width: 2.5rem !important; + height: 2.5rem !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: rgba(255, 255, 255, 0.9) !important; + border-radius: 50% !important; + border: 1px solid rgba(0, 0, 0, 0.1) !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + } + + .navbar-sidebar__close:hover { + background: rgba(255, 255, 255, 1) !important; + transform: scale(1.1); + } + + /* Mobile menu items container */ + .navbar-sidebar__items { + padding: 1rem 0 !important; + display: flex !important; + flex-direction: column !important; + gap: 0.5rem !important; + } + + /* Mobile menu items */ + .navbar-sidebar__item { + padding: 0.75rem 1.5rem !important; + margin: 0 !important; + width: 100% !important; + display: block !important; + } + + /* Mobile menu links */ + .navbar-sidebar__item .menu__link, + .navbar-sidebar__item .dropdown__link { + color: var(--headerColor) !important; + font-size: 1rem !important; + font-weight: 500 !important; + padding: 0.75rem 1rem !important; + border-radius: 0.5rem !important; + width: 100% !important; + display: block !important; + text-align: left !important; + transition: all 0.2s ease !important; + } + + .navbar-sidebar__item .menu__link:hover, + .navbar-sidebar__item .dropdown__link:hover { + background: rgba(186, 207, 251, 0.5) !important; + color: var(--headerColor) !important; + text-decoration: none !important; + } + + /* Mobile menu dropdowns */ + .navbar-sidebar__item .dropdown { + width: 100% !important; + } + + .navbar-sidebar__item .dropdown__menu { + position: static !important; + box-shadow: none !important; + border: none !important; + background: transparent !important; + padding: 0.5rem 0 0.5rem 1.5rem !important; + margin: 0 !important; + } + + .navbar-sidebar__item .dropdown__link { + padding-left: 2rem !important; + font-size: 0.9rem !important; + } + + /* Dark theme mobile menu */ + [data-theme='dark'] .navbar-sidebar { + background: #181c20 !important; + } + + [data-theme='dark'] .navbar-sidebar__brand { + border-bottom-color: rgba(255, 255, 255, 0.1) !important; + } + + [data-theme='dark'] .navbar-sidebar__close { + background: rgba(255, 255, 255, 0.1) !important; + border-color: rgba(255, 255, 255, 0.2) !important; + } + + [data-theme='dark'] .navbar-sidebar__close:hover { + background: rgba(255, 255, 255, 0.2) !important; + } + + [data-theme='dark'] .navbar-sidebar__item .menu__link, + [data-theme='dark'] .navbar-sidebar__item .dropdown__link { + color: #f5f6fa !important; + } + + [data-theme='dark'] .navbar-sidebar__item .menu__link:hover, + [data-theme='dark'] .navbar-sidebar__item .dropdown__link:hover { + background: rgba(255, 255, 255, 0.1) !important; + color: #fff !important; + } } /* Small devices (tablets, 768px and down) */ @@ -209,6 +371,81 @@ padding-right: 2rem; } +/* Responsive navbar fixes for small screens */ +@media (max-width: 996px) { + .navbar { + width: 100%; + margin: 0; + border-radius: 0; + top: 0; + padding-left: 1rem; + padding-right: 1rem; + } + + .navbar__inner { + border-radius: 0; + } + + /* Ensure navbar items wrap properly */ + .navbar__items { + flex-wrap: wrap; + } + + /* Make title shorter on tablets */ + .navbar__title { + font-size: 0.9rem; + } +} + +@media (max-width: 768px) { + .navbar { + padding-left: 0.75rem; + padding-right: 0.75rem; + } + + /* Shorten title on mobile */ + .navbar__title { + font-size: 0.85rem; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + /* Ensure dropdown menus work properly */ + .navbar__item { + margin: 0.25rem 0; + } + + /* Make sure the mobile menu button is visible */ + .navbar__toggle { + display: block; + } +} + +@media (max-width: 576px) { + .navbar { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + .navbar__title { + font-size: 0.75rem; + max-width: 120px; + } + + /* Reduce spacing between navbar items */ + .navbar__items--right { + gap: 0.5rem; + } + + /* Ensure logo and title fit */ + .navbar__brand { + min-width: auto; + flex-shrink: 1; + } +} + .navbar__inner { border-radius: 2rem; background: transparent; diff --git a/src/theme/MDXComponents.js b/src/theme/MDXComponents.js index e96cf98..67ed5c6 100644 --- a/src/theme/MDXComponents.js +++ b/src/theme/MDXComponents.js @@ -2,6 +2,7 @@ import React from 'react'; // Import the original mapper import MDXComponents from '@theme-original/MDXComponents'; import AsciinemaPlayer from '@site/src/components/AsciinemaPlayer'; +import ClickableDiagram from '@site/src/components/ClickableDiagram'; export default { // Re-use the default mapping @@ -9,4 +10,6 @@ export default { // Map the "" tag to our AsciinemaPlayer component // `AsciinemaPlayer` will receive all props that were passed to `` in MDX AsciinemaPlayer, + // Map the "" tag to our ClickableDiagram component + ClickableDiagram, }; \ No newline at end of file diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx new file mode 100644 index 0000000..d86011c --- /dev/null +++ b/src/theme/Root.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import type { ReactNode } from 'react'; +import AutoClickableDiagrams from '@site/src/components/AutoClickableDiagrams'; + +export default function Root({ children }: { children: ReactNode }): JSX.Element { + return ( + <> + {children} + + + ); +} + From 0bf2e200f0550142f661747ed387e4f32c4a1c58 Mon Sep 17 00:00:00 2001 From: Jorge Morales Pou Date: Sun, 7 Dec 2025 20:13:05 +0100 Subject: [PATCH 2/2] Updates to additional information about educates --- about-educates/concepts.md | 241 ------------------------ about-educates/deployment.md | 39 ++++ about-educates/history.md | 37 ++-- about-educates/index.md | 128 ++++++++----- about-educates/local-dev.md | 21 +++ about-educates/workflows.md | 5 +- about-educates/workshop-capabilities.md | 87 +++++++++ docusaurus.config.ts | 6 +- src/components/sections/UseCases.tsx | 128 ++++++++++--- src/data/usecases.tsx | 26 +-- 10 files changed, 378 insertions(+), 340 deletions(-) delete mode 100644 about-educates/concepts.md create mode 100644 about-educates/deployment.md create mode 100644 about-educates/local-dev.md create mode 100644 about-educates/workshop-capabilities.md diff --git a/about-educates/concepts.md b/about-educates/concepts.md deleted file mode 100644 index e9db4fd..0000000 --- a/about-educates/concepts.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Core Concepts - -This page explains the fundamental concepts that make up the Educates platform. - -## Workshop - -A **Workshop** is the fundamental unit of training content in Educates. It represents a complete, self-contained learning experience that includes: - -- **Content**: Markdown or AsciiDoc files containing instructions, exercises, and explanations -- **Configuration**: Workshop-specific settings, resource requirements, and capabilities -- **Resources**: Kubernetes manifests for deploying applications and services needed for the workshop -- **Container Image**: The workshop content is packaged as an OCI image, typically stored in a container registry - -Workshops are defined using the `Workshop` Custom Resource Definition (CRD), which specifies: -- The location of the workshop content (Git repository or container image) -- Resource quotas and limits for workshop sessions -- RBAC permissions required -- Additional capabilities needed (web terminal, editor, etc.) -- Shared resources that should be deployed for all sessions -- Per-session resources that should be created for each user - -### Workshop Lifecycle - -1. **Definition**: A `Workshop` resource is created in the cluster -2. **Content Distribution**: Workshop content is packaged and published to a container registry -3. **Environment Setup**: When a Training Portal references the workshop, a `WorkshopEnvironment` is created -4. **Session Creation**: Individual `WorkshopSession` resources are created as users access the workshop - -## Training Portal - -A **Training Portal** is the web-based interface that provides access to one or more workshops. It serves as the entry point for users to: - -- Browse available workshops -- Register for workshops -- Access their workshop sessions -- View their progress and history - -The Training Portal consists of: - -- **Web UI**: A user-friendly interface built with React -- **REST API**: A programmatic interface for integration with external systems -- **Authentication**: User registration and login mechanisms -- **Session Management**: Automatic allocation and management of workshop sessions - -Training Portals are defined using the `TrainingPortal` CRD, which specifies: -- Which workshops should be available -- Authentication configuration -- Portal branding and customization -- Access control policies - -### Portal Types - -Educates supports different portal deployment scenarios: - -- **Supervised Workshops**: Time-limited workshops for conferences or customer sites -- **Temporary Learning Portal**: Short-duration demos at conferences or vendor booths -- **Permanent Learning Portal**: Long-running public websites for continuous learning -- **Personal Training**: Individual use for learning or product demos - -## Controllers - -Educates uses Kubernetes controllers to manage the platform's resources. The main controllers are: - -### Session Manager Controller - -The **Session Manager Controller** is responsible for managing workshop sessions. It: - -- Watches for `WorkshopSession` resources -- Creates and manages namespaces for each session -- Allocates resources according to workshop requirements -- Applies RBAC policies and resource quotas -- Manages the lifecycle of session resources -- Handles session cleanup when sessions are deleted - -The Session Manager ensures that each user gets an isolated environment with the appropriate permissions and resource limits. - -### Secrets Manager Controller - -The **Secrets Manager Controller** handles secret management across the platform. It: - -- Manages secrets for workshop sessions -- Injects secrets into workshop environments -- Copies secrets between namespaces when needed -- Ensures secure secret distribution -- Manages secret lifecycle - -This controller uses several CRDs: -- `SecretInjector`: Injects secrets into specific resources -- `SecretCopier`: Copies secrets between namespaces -- `SecretExporter`: Exports secrets for external use -- `SecretImporter`: Imports secrets from external sources - -## Additional Capabilities - -Educates provides a rich set of capabilities that can be enabled for workshops: - -### Web Terminal - -The **Web Terminal** provides a browser-based terminal interface that allows users to: -- Execute commands directly in the workshop environment -- Run scripts and tools -- Interact with Kubernetes clusters -- Execute commands from workshop instructions with a single click - -The terminal runs in a container within the workshop session namespace and provides full shell access to the workshop environment. - -### VS Code Editor - -The **VS Code Editor** (or compatible editor) provides a full-featured code editing experience: -- Edit files directly in the browser -- Syntax highlighting for multiple languages -- Support for VS Code extensions -- Integrated terminal access -- Git integration -- Debugging capabilities - -This allows users to write and modify code as part of the workshop without leaving the browser. - -### Kubernetes Web Console - -The **Kubernetes Web Console** provides a visual interface for: -- Viewing Kubernetes resources -- Managing deployments, services, and pods -- Inspecting logs and events -- Executing commands in containers -- Monitoring resource usage - -This makes it easier for users to understand and interact with Kubernetes resources during workshops. - -### vCluster (Virtual Cluster) - -**vCluster** provides isolated virtual Kubernetes clusters for each user session. This is useful for: -- Workshops requiring cluster-admin access -- Testing cluster-level operations -- Isolating users completely from each other -- Providing a full Kubernetes cluster experience - -vCluster runs as a lightweight virtual cluster within the host cluster, providing complete isolation while sharing the underlying infrastructure. - -### File Server - -The **File Server** provides HTTP access to files in the workshop environment: -- Serving static files and assets -- Downloading workshop resources -- Accessing generated files and outputs -- Sharing files between components - -### Git Server - -The **Git Server** provides Git repository access within the workshop: -- Cloning repositories -- Pushing and pulling changes -- Managing branches and tags -- Integrating with Git workflows - -This enables workshops that involve Git operations and version control. - -### Image Registry - -The **Image Registry** provides container image storage and distribution: -- Storing workshop-specific images -- Building and pushing images during workshops -- Pulling images for deployments -- Managing image versions - -For local development, Educates provides a local registry that simplifies the workflow. - -### Docker Runtime - -The **Docker Runtime** enables: -- Building container images -- Running containers -- Pushing images to registries -- Testing containerized applications - -This is essential for workshops that involve containerization and Docker operations. - -## Infrastructure Requirements - -Educates relies on several infrastructure components that should be installed in the Kubernetes cluster: - -### Kyverno - -**Kyverno** is used for advanced policy management: -- Enforcing security policies -- Validating resource configurations -- Mutating resources to meet standards -- Managing network policies - -Kyverno policies can be used to ensure workshop sessions comply with organizational security requirements. - -### kapp-controller - -**kapp-controller** is used for deploying additional workloads: -- Managing application deployments -- Handling package installations -- Coordinating multi-resource deployments -- Managing application lifecycle - -kapp-controller enables Educates to deploy complex applications and dependencies as part of workshop environments. - -### external-dns - -**external-dns** provides DNS name integration (primarily for cloud providers): -- Automatically creating DNS records -- Managing subdomain routing -- Integrating with cloud DNS services -- Providing user-friendly URLs for workshops - -This component is optional and mainly used in cloud deployments. - -### cert-manager - -**cert-manager** handles certificate management: -- Integration with Let's Encrypt for automatic HTTPS -- Creating and managing TLS certificates -- Certificate renewal -- Custom certificate authorities - -cert-manager ensures that workshop portals and services can be accessed securely over HTTPS. - -## Local Development - -For local development, Educates provides a streamlined experience using **kind** (Kubernetes in Docker): - -- **Fully Configured Cluster**: The Educates CLI creates a complete Kubernetes cluster with all necessary components -- **Image Registry**: A local container registry is automatically set up for publishing and pulling workshop images -- **Local DNS Resolver**: A local DNS resolver improves the development workflow by providing proper DNS resolution for local services - -This setup allows developers to: -- Test workshops locally before deploying to production -- Iterate quickly on workshop content -- Develop and test Educates features -- Experiment with different configurations - -The local development environment mirrors the production setup, making it easy to transition from development to production. - diff --git a/about-educates/deployment.md b/about-educates/deployment.md new file mode 100644 index 0000000..4e18f7a --- /dev/null +++ b/about-educates/deployment.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 2 +--- + +# Deployment + +Educates is a Kubernetes-based platform designed to provide interactive workshop environments. This section provides a comprehensive overview of the Educates architecture, its core concepts, and how the system works. + +## Deployment Components + +The Educates Training Platform has some requirements for it's configuration to properly work on a Kubernetes cluster on a production environment. + +These requirements are: + +- A **Kubernetes cluster**. Educates has been tested on **GKE**, **EKS** and **AKS**, **Minishift** and **Kind**. +- A **domain name** so that training portals will be accessible from the internet. In our cloud deployment we use **ExternalDNS** to create the DNS records, but you can use any other DNS provider. On local cloud deployments you can rely on external services like **nip.io** or **xip.io** or use Educates Local Resolver. +- A **wildcard certificate** for the domain name as we want access to the training portals to be secured. You can run Educates Training Platform over **http** but we don't recommend it. For local deployments, Educates CLI helps you manage your own certificates. For cloud deployments, you can use **cert-manager** to issue certificates from Let's Encrypt. +- A **policy engine**. Educates can enforce policies on the cluster and on every workshop session. It can use Kubernetes native policies or external policy engines like **Kyverno**, which is our recommended policy engine and the default one. + + +```mermaid +architecture-beta + group k8s(cloud)[Kubernetes Cluster] + + service educates(server)[Educates Training Platform] in k8s + service kyverno(internet)[Kyverno] in k8s + service kappcontroller(internet)[Kapp Controller] in k8s + service externaldns(internet)[ExternalDNS] in k8s + service certmanager(internet)[CertManager] in k8s + + educates:T -- B:kyverno + educates:T -- B:kappcontroller + educates:T -- B:externaldns + educates:T -- B:certmanager +``` + +**Educates Training Platform** does provide an installation mechanism via it's CLI that will provide oppinionated +deployments of the required components on local clusters, **GKE** and **EKS**. It does installation in an imperative way, +but you can also install Educates declaratively using **kapp-controller**. diff --git a/about-educates/history.md b/about-educates/history.md index 2b997e1..fea11d0 100644 --- a/about-educates/history.md +++ b/about-educates/history.md @@ -1,9 +1,8 @@ --- -sidebar_position: 4 +sidebar_position: 6 +title: Educates History --- -# Educates History - This page documents the evolution of the Educates project from its inception as an internal tool to becoming an independent open-source project. ## Project Timeline @@ -11,15 +10,21 @@ This page documents the evolution of the Educates project from its inception as The following timeline shows the major milestones in Educates' history: ```mermaid -gitGraph - commit id: "Dec 2019 - Educates 1.x Born" - commit id: "Sept 2020 - Spring One Conference" - commit id: "Apr 2021 - Tanzu Learning Center" - commit id: "Feb 2022 - Educates 2.x Launch" - commit id: "Nov 2023 - Broadcom Acquisition" - commit id: "Aug 2024 - Educates 3.x Release" - commit id: "Oct 2024 - Independent OSS Project" - commit id: "Jun 2025 - Educates Hub Launch" +--- +config: + timeline: + disableMulticolor: true +--- +timeline + title: "Educates History" + Dec 2019: "Educates 1.x Born" + Sept 2020: "Spring One Conference" + Apr 2021: "Tanzu Learning Center" + Feb 2022: "Educates 2.x Launch" + Nov 2023: "Broadcom Acquisition" + Aug 2024: "Educates 3.x Release" + Oct 2024: "Independent OSS Project" + Jun 2025: "Educates Hub Launch" ``` ## Key Milestones @@ -38,7 +43,7 @@ Educates was originally created as an internal tool for the VMware Tanzu Develop Educates proved its scalability and reliability when it was used at the Spring One conference to run **over 5,000 workshop executions**. This large-scale deployment demonstrated that Educates could handle enterprise-level training scenarios and validated the platform's architecture and design decisions. **Achievements:** -- Successfully scaled to support thousands of concurrent users +- Successfully scaled to support hundreds of concurrent users - Validated the platform's reliability and performance - Demonstrated real-world applicability for large conferences @@ -54,9 +59,9 @@ VMware launched **Tanzu Learning Center** using Educates 1.x as its foundation. ### February 2022 - Educates 2.x Launch The Developer Advocates team resumed active development on Educates to create version 2.x, which was designed to power multiple learning platforms: -- **Tanzu.academy** - VMware Tanzu training platform -- **Kube.academy** - Kubernetes training platform -- **Spring.academy** - Spring framework training platform +- **[Tanzu.academy](https://tanzu.academy)** - VMware Tanzu training platform +- **[Kube.academy](https://kube.academy)** - Kubernetes training platform +- **[Spring.academy](https://spring.academy)** - Spring framework training platform **Version 2.x Improvements:** - Enhanced workshop authoring capabilities diff --git a/about-educates/index.md b/about-educates/index.md index 29e3be9..ca782a0 100644 --- a/about-educates/index.md +++ b/about-educates/index.md @@ -1,22 +1,20 @@ --- sidebar_position: 1 +title: Architecture --- -# Architecture - Educates is a Kubernetes-based platform designed to provide interactive workshop environments. This section provides a comprehensive overview of the Educates architecture, its core concepts, and how the system works. -## High-Level Architecture +## Architectural Components -The Educates platform is built on Kubernetes and uses a controller-based architecture to manage workshop environments and sessions. The following diagram illustrates the high-level architecture: +The core architecture consists of the **Session Manager**, **Secrets Manager**, which are the main Kubernetes Controllers. These controllers manages a set of **Custom Resource Definitions (CRDs)**, the **Training Portal** and **Workshop**, which are the main Kubernetes Resources a user will deal with. These resources work together to create **Workshop Environments** and **Workshop Sessions**. ```mermaid graph TB subgraph "Kubernetes Cluster" subgraph "Educates Operator" - OP[Educates Operator] - SM[Session Manager Controller] - SC[Secrets Manager Controller] + SM[Session Manager] + SC[Secrets Manager] end subgraph "Training Portal" @@ -41,13 +39,6 @@ graph TB DR[Docker Runtime] VC[vCluster - Optional] end - - subgraph "Infrastructure Components" - KY[Kyverno] - KAPP[kapp-controller] - DNS[external-dns] - CERT[cert-manager] - end end User[User] -->|Access| UI @@ -56,8 +47,7 @@ graph TB API --> TP TP -->|Creates| WE TP -->|Creates| WS - OP -->|Manages| WE - OP -->|Manages| WS + SM -->|Manages| WE SM -->|Manages| WS SC -->|Manages| WS WS --> WT @@ -68,46 +58,102 @@ graph TB WS --> IR WS --> DR WS -.->|Optional| VC - OP -->|Uses| KY - OP -->|Uses| KAPP - OP -->|Uses| DNS - OP -->|Uses| CERT ``` ## Core Components -### Educates Operator - -The Educates Operator is the central component that manages the lifecycle of workshops. It watches for Custom Resources (CRs) and ensures the desired state is achieved: - -- **Workshop Controller**: Manages `Workshop` resources that define workshop content and configuration -- **Training Portal Controller**: Manages `TrainingPortal` resources that provide the web interface -- **Workshop Environment Controller**: Manages `WorkshopEnvironment` resources for setting up workshop environments -- **Workshop Session Controller**: Manages `WorkshopSession` resources for individual user sessions +```mermaid +graph TB + subgraph "Kubernetes Cluster" + subgraph "Educates Operator" + SM[Session Manager] + CM[Secrets Manager] + end + end +``` -### Session Manager Controller +### Session Manager -The Session Manager Controller is responsible for: +The Session Manager is responsible for: - Creating and managing workshop session namespaces - Allocating resources to sessions - Managing session lifecycle (creation, updates, deletion) - Ensuring proper RBAC and resource quotas are applied -### Secrets Manager Controller +### Secrets Manager -The Secrets Manager Controller handles: +The Secrets Manager handles: - Secret management across workshop sessions - Secret injection into workshop environments - Secret copying between namespaces - Secure secret distribution +### Workshop + +Provides the definition of a workshop. Preloaded by an administrator into the cluster, it defines where the workshop content is hosted, or the location of a container image which bundles the workshop content and any additional tools required for the workshop. The definition also lists additional resources that should be created which are to be shared between all workshop sessions, or for each session, along with details of resources quotas and access roles required by the workshop. + +```mermaid +--- + config: + class: + hideEmptyMembersBox: true +--- +classDiagram + direction LR + TrainingPortal *-- "many" Workshop + class TrainingPortal{ + Title + Logo + Authentication + Access Charactertistics + Capacity + Timeouts + Workshops[] + } + class Workshop{ + Title + Description + ContentLocation + Extensions + Budgets and resource quotas + Capabilities + Additional Session Resources + Additional Environment Resources + } +``` + + ### Training Portal -The Training Portal provides: -- **Web UI**: A user-friendly interface for browsing and accessing workshops -- **REST API**: Programmatic access to workshop catalog and session management -- **Authentication**: User registration and authentication mechanisms -- **Session Management**: Allocation and management of workshop sessions +Created by an administrator in the cluster to trigger the deployment of a training portal. The training portal can provide access to **one or more** distinct workshops defined by a **Workshop** resource. + +```mermaid +graph TB + TP1[Training Portal] + TP2[Training Portal] + W1[Workshop 1] + W2[Workshop 2] + W3[Workshop 3] + W4[Workshop 4] + W5[Workshop 5] + W6[Workshop 6] + + + TP1 --> W1 + TP1 --> W2 + TP1 --> W3 + TP1 --> W4 + TP1 --> W6 + + TP2 --> W2 + TP2 --> W3 + TP2 --> W5 +``` + +The training portal provides a **web based interface** for registering for workshops and accessing them. + +It also provides a **REST API** for requesting access to workshops, allowing custom front ends to be created which integrate with separate identity providers and which provide an alternate means for browsing and accessing workshops. + ## Resource Flow @@ -116,11 +162,3 @@ The Training Portal provides: 3. **Workshop Environment**: The portal creates `WorkshopEnvironment` resources for each workshop 4. **Workshop Session**: When a user requests access, a `WorkshopSession` resource is created 5. **Session Resources**: The operator creates all necessary resources for the session (namespaces, services, etc.) - -## Next Steps - -- Learn about [Core Concepts](./concepts.md) to understand the building blocks of Educates -- Explore [Workflows](./workflows.md) to see how Educates operates in practice -- Discover the [History](./history.md) of Educates and its evolution -- Check out the [Getting Started Guides](/getting-started-guides) for hands-on tutorials - diff --git a/about-educates/local-dev.md b/about-educates/local-dev.md new file mode 100644 index 0000000..702a108 --- /dev/null +++ b/about-educates/local-dev.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 5 +title: Local Development +--- + +For local development, Educates provides a streamlined experience using **kind** (Kubernetes in Docker): + +- **Fully Configured Cluster**: The Educates CLI creates a complete Kubernetes cluster with all necessary components. +- **Container Registry**: A local container registry is automatically set up for publishing and pulling workshop images. +- **Local DNS Resolver**: A local DNS resolver improves the development workflow by providing proper DNS resolution for local services. +- **Container Registry Mirrors**: Container registry mirrors can be configured to mirror upstream registries to optimize download speed times on local clusters. +- **Wildcard TLS certificate and Certificate Authority**: A local wildcard TLS certificate and Certificate Authority can be registered to allow secure communication within the cluster. + +This setup allows developers to: +- Test workshops locally before deploying to production +- Iterate quickly on workshop content +- Develop and test Educates features +- Experiment with different configurations + +The local development environment mirrors the production setup, making it easy to transition from development to production. + diff --git a/about-educates/workflows.md b/about-educates/workflows.md index 117eedc..bf8e509 100644 --- a/about-educates/workflows.md +++ b/about-educates/workflows.md @@ -1,9 +1,8 @@ --- -sidebar_position: 3 +sidebar_position: 4 +title: Workflows --- -# Workflows - This page explains how Educates works from a workflow perspective, covering the lifecycle of workshops and sessions. ## Workshop Deployment Workflow diff --git a/about-educates/workshop-capabilities.md b/about-educates/workshop-capabilities.md new file mode 100644 index 0000000..4d0241c --- /dev/null +++ b/about-educates/workshop-capabilities.md @@ -0,0 +1,87 @@ +--- +sidebar_position: 3 +title: Workshop Capabilities +--- + +Educates provides a rich set of capabilities that can be enabled for workshops: + +### Web Terminal + +The **Web Terminal** provides a browser-based terminal interface that allows users to: +- Execute commands directly in the workshop environment +- Run scripts and tools +- Interact with Kubernetes clusters +- Execute commands from workshop instructions with a single click + +The terminal runs in a container within the workshop session namespace and provides full shell access to the workshop environment. + +### VS Code Editor + +The **VS Code Editor** (or compatible editor) provides a full-featured code editing experience: +- Edit files directly in the browser +- Syntax highlighting for multiple languages +- Support for VS Code extensions +- Integrated terminal access +- Git integration +- Debugging capabilities + +This allows users to write and modify code as part of the workshop without leaving the browser. + +### Kubernetes Web Console + +The **Kubernetes Web Console** provides a visual interface for: +- Viewing Kubernetes resources +- Managing deployments, services, and pods +- Inspecting logs and events +- Executing commands in containers +- Monitoring resource usage + +This makes it easier for users to understand and interact with Kubernetes resources during workshops. + +### vCluster (Virtual Cluster) + +**vCluster** provides isolated virtual Kubernetes clusters for each user session. This is useful for: +- Workshops requiring cluster-admin access +- Testing cluster-level operations +- Isolating users completely from each other +- Providing a full Kubernetes cluster experience + +vCluster runs as a lightweight virtual cluster within the host cluster, providing complete isolation while sharing the underlying infrastructure. + +### File Server + +The **File Server** provides HTTP access to files in the workshop environment: +- Serving static files and assets +- Downloading workshop resources +- Accessing generated files and outputs +- Sharing files between components + +### Git Server + +The **Git Server** provides Git repository access within the workshop: +- Cloning repositories +- Pushing and pulling changes +- Managing branches and tags +- Integrating with Git workflows + +This enables workshops that involve Git operations and version control. + +### Container Registry + +The **Container Registry** provides container image storage and distribution: +- Storing workshop-specific images +- Building and pushing images during workshops +- Pulling images for deployments +- Managing image versions + +For local development, Educates provides a local registry that simplifies the workflow. + +### Docker Runtime + +The **Docker Runtime** enables: +- Building container images +- Running containers +- Pushing images to registries +- Testing containerized applications + +This is essential for workshops that involve containerization and Docker operations. \ No newline at end of file diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 7df8eeb..60552d7 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -181,9 +181,11 @@ const config: Config = { position: "left", items: [ { to: "/about-educates", label: "Architecture" }, - { to: "/about-educates/concepts", label: "Concepts" }, + { to: "/about-educates/deployment", label: "Deployment" }, + { to: "/about-educates/workshop-capabilities", label: "Workshop Capabilities" }, { to: "/about-educates/workflows", label: "Workflows" }, - { to: "/about-educates/history", label: "History" }, + { to: "/about-educates/local-dev", label: "Local Development" }, + { to: "/about-educates/history", label: "Educates History" }, ], }, { diff --git a/src/components/sections/UseCases.tsx b/src/components/sections/UseCases.tsx index e546bdf..cb54e95 100644 --- a/src/components/sections/UseCases.tsx +++ b/src/components/sections/UseCases.tsx @@ -3,18 +3,16 @@ import Box from '@mui/material/Box'; import Container from '@mui/material/Container'; import Typography from '@mui/material/Typography'; import Grid from '@mui/material/Grid'; -import { IconType } from 'react-icons'; -import { useCases } from '@site/src/data/usecases'; -interface UseCase { - title: string; - description: string; - icon: IconType; -} +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { useCases, UseCase } from '@site/src/data/usecases'; -const UseCaseCard: React.FC = ({ title, description, icon: Icon }) => ( +const UseCaseCard: React.FC void; isSelected: boolean }> = ({ title, description, icon: Icon, onClick, isSelected }) => ( = ({ title, description, icon: Icon }) => ( maxWidth: 400, minWidth: 280, m: 0, + cursor: 'pointer', + transform: isSelected ? 'translateY(-5px)' : 'none', + border: isSelected ? '2px solid' : '2px solid transparent', + borderColor: 'primary.main', '&:hover': { transform: 'translateY(-5px)', boxShadow: 4, }, }} > - + - + {title} - {description} + {description} ); - const UseCases: React.FC<{ sectionType: 'even' | 'odd' }> = ({ sectionType }) => { + const [selectedUseCase, setSelectedUseCase] = React.useState(null); + const theme = useTheme(); + + // Determine number of columns based on breakpoints matching the Grid item sizes + // xs={12} -> 1 col + // md={6} -> 2 cols + // lg={3} -> 4 cols + const isLg = useMediaQuery(theme.breakpoints.up('lg')); + const isMd = useMediaQuery(theme.breakpoints.up('md')); + + const columns = isLg ? 4 : isMd ? 2 : 1; + + // Calculate where to insert the details box + // It should be after the last item in the current row + let insertionIndex = -1; + if (selectedUseCase !== null) { + const currentRow = Math.floor(selectedUseCase / columns); + const itemsInRow = columns; + // The index of the last item in this row + let lastItemInRow = (currentRow + 1) * itemsInRow - 1; + // Cap at the last actual item + insertionIndex = Math.min(lastItemInRow, useCases.length - 1); + } + return ( @@ -49,23 +74,82 @@ const UseCases: React.FC<{ sectionType: 'even' | 'odd' }> = ({ sectionType }) => Use Cases - {useCases.map((useCase, index) => ( - // @ts-ignore - - - - ))} + {useCases.map((useCase, index) => { + const isInsertionPoint = index === insertionIndex; + return ( + + {/* @ts-ignore */} + + setSelectedUseCase(selectedUseCase === index ? null : index)} + isSelected={selectedUseCase === index} + /> + + {isInsertionPoint && selectedUseCase !== null && ( + + + + setSelectedUseCase(null)} + sx={{ + background: 'none', + border: 'none', + cursor: 'pointer', + fontSize: '1.5rem', + lineHeight: 1, + p: 1, + borderRadius: '50%', + '&:hover': { bgcolor: 'action.hover' } + }} + > + × + + + + {useCases[selectedUseCase].title} + + + {useCases[selectedUseCase].details} + + + + )} + + ); + })} @@ -75,4 +159,4 @@ const UseCases: React.FC<{ sectionType: 'even' | 'odd' }> = ({ sectionType }) => ); }; -export default UseCases; \ No newline at end of file +export default UseCases; \ No newline at end of file diff --git a/src/data/usecases.tsx b/src/data/usecases.tsx index 7e6c4ed..83125f9 100644 --- a/src/data/usecases.tsx +++ b/src/data/usecases.tsx @@ -1,34 +1,38 @@ import { IconType } from "react-icons"; -import { FaUsers } from "react-icons/fa"; -import { FaLaptopCode } from "react-icons/fa"; -import { FaRocket } from "react-icons/fa"; -import { FaChalkboardTeacher } from "react-icons/fa"; +import { FaUsers, FaRocket } from "react-icons/fa"; +import { PiExam } from "react-icons/pi"; +import { SiHtmlacademy } from "react-icons/si"; export interface UseCase { title: string; description: string; + details: string; icon: IconType; } export const useCases: UseCase[] = [ - { - title: 'Interactive Workshops', - description: 'Host engaging workshops with hands-on exercises and real-time feedback.', - icon: FaLaptopCode, - }, { title: 'Team Training', description: 'Train your team effectively with customized learning environments.', + details: 'Train your team or customers with the tools or workflows you use regularly. Embed Educates Training into your own Developer Portal.', icon: FaUsers, }, { title: 'Product Demos', description: 'Showcase your products with interactive demonstrations and tutorials.', + details: 'Use this at conferences, customer sites, or online training with a set time and audience.', icon: FaRocket, }, { title: 'Educational Programs', description: 'Create comprehensive educational programs with practical exercises.', - icon: FaChalkboardTeacher, + details: 'Create your own Academy type of experience where users can learn about your product and techonlogies embedding Educates Training into your own web site or Learning Management System.', + icon: SiHtmlacademy, + }, + { + title: 'Exams', + description: 'Exam or certificate people on specific topics.', + details: 'Create exams or certification tests with hands-on exercises and real-time feedback.', + icon: PiExam, }, -]; +]; \ No newline at end of file