diff --git a/MyCompany.MyProject.BackendApi/MyCompany.MyProject.BackendApi.csproj b/MyCompany.MyProject.BackendApi/MyCompany.MyProject.BackendApi.csproj index 55868cd..5ceff23 100644 --- a/MyCompany.MyProject.BackendApi/MyCompany.MyProject.BackendApi.csproj +++ b/MyCompany.MyProject.BackendApi/MyCompany.MyProject.BackendApi.csproj @@ -9,6 +9,7 @@ + diff --git a/MyCompany.MyProject.BackendApi/Program.cs b/MyCompany.MyProject.BackendApi/Program.cs index 083884d..aae554d 100644 --- a/MyCompany.MyProject.BackendApi/Program.cs +++ b/MyCompany.MyProject.BackendApi/Program.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Text.Json; +using Scalar.AspNetCore; // Ensure S8n.Components.Packages assembly is loaded _ = typeof(S8n.Components.Basics.Calculator).Assembly; @@ -16,6 +17,7 @@ var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); + app.MapScalarApiReference(); } app.UseHttpsRedirection(); diff --git a/QuasarFrontend/src/components_s8n/ComponentCalculator.vue b/QuasarFrontend/src/components_s8n/ComponentCalculator.vue index 1578de6..c59dcb5 100644 --- a/QuasarFrontend/src/components_s8n/ComponentCalculator.vue +++ b/QuasarFrontend/src/components_s8n/ComponentCalculator.vue @@ -1,10 +1,6 @@ - - - Simple Calculator - - - + + - - - - - + + + + + + + diff --git a/QuasarFrontend/src/layouts/MainLayout.vue b/QuasarFrontend/src/layouts/MainLayout.vue index 8a02284..e6ed6a9 100644 --- a/QuasarFrontend/src/layouts/MainLayout.vue +++ b/QuasarFrontend/src/layouts/MainLayout.vue @@ -29,6 +29,18 @@ import { ref } from 'vue'; import EssentialLink, { type EssentialLinkProps } from 'components/EssentialLink.vue'; const linksList: EssentialLinkProps[] = [ + { + title: 'Home', + caption: 'Main page', + icon: 'home', + link: '/', + }, + { + title: 'Playground', + caption: 'S8n Components', + icon: 'mdi-cube-outline', + link: '/#/playground', + }, { title: 'Docs', caption: 'quasar.dev', diff --git a/QuasarFrontend/src/pages/IndexPage.vue b/QuasarFrontend/src/pages/IndexPage.vue index 820e982..6346506 100644 --- a/QuasarFrontend/src/pages/IndexPage.vue +++ b/QuasarFrontend/src/pages/IndexPage.vue @@ -5,5 +5,5 @@ diff --git a/QuasarFrontend/src/pages/PlaygroundPage.vue b/QuasarFrontend/src/pages/PlaygroundPage.vue new file mode 100644 index 0000000..cc9673b --- /dev/null +++ b/QuasarFrontend/src/pages/PlaygroundPage.vue @@ -0,0 +1,136 @@ + + + + + + + + Components + Select a component to interact with + + + + + + + + + {{ component.name }} + + {{ component.description }} + + + + + + + + + + Component Info + + + + Code: {{ currentComponentInfo.code }} + + + Class: {{ currentComponentInfo.className }} + + + Method: {{ currentComponentInfo.method }} + + + + + + + + + + {{ currentComponentInfo?.name || 'Select a Component' }} + + {{ currentComponentInfo.description }} + + + + + + + + Welcome to S8n Playground + + Select a component from the sidebar to start interacting with it. + + + + + + + + + + + + diff --git a/QuasarFrontend/src/router/routes.ts b/QuasarFrontend/src/router/routes.ts index 1dbaa36..cba95ed 100644 --- a/QuasarFrontend/src/router/routes.ts +++ b/QuasarFrontend/src/router/routes.ts @@ -4,9 +4,11 @@ const routes: RouteRecordRaw[] = [ { path: '/', component: () => import('layouts/MainLayout.vue'), - children: [{ path: '', component: () => import('pages/IndexPage.vue') }], + children: [ + { path: '', component: () => import('pages/IndexPage.vue') }, + { path: 'playground', component: () => import('pages/PlaygroundPage.vue') }, + ], }, - // Always leave this as last one, // but you can also remove it { diff --git a/S8n.Components.Packages/Basics/HttpRequest.cs b/S8n.Components.Packages/Basics/HttpRequest.cs new file mode 100644 index 0000000..850b7c0 --- /dev/null +++ b/S8n.Components.Packages/Basics/HttpRequest.cs @@ -0,0 +1,121 @@ +using System.Diagnostics; +using System.Text; +using System.Text.Json; + +namespace S8n.Components.Basics; + +public class HttpRequest +{ + private static readonly HttpClient _httpClient = new(); + + private static string EnsureUrlSchema(string url) + { + if (string.IsNullOrWhiteSpace(url)) + return url; + + // Check if the URL already has a scheme + if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && !string.IsNullOrEmpty(uri.Scheme)) + { + return url; // Already has a scheme + } + + // If no scheme, prepend http:// + // Check if it starts with // (protocol-relative URL) + if (url.StartsWith("//")) + { + return "http:" + url; + } + + // Otherwise prepend http:// + return "http://" + url; + } + + public object Execute(string method, string url, Dictionary? headers = null, object? body = null) + { + if (string.IsNullOrWhiteSpace(url)) + { + throw new ArgumentException("URL is required"); + } + + if (string.IsNullOrWhiteSpace(method)) + { + throw new ArgumentException("HTTP method is required"); + } + + var normalizedUrl = EnsureUrlSchema(url); + + var stopwatch = Stopwatch.StartNew(); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Parse(method), normalizedUrl); + + // Add headers if provided + if (headers != null) + { + foreach (var header in headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + // Add body if provided and method supports it + if (body != null && (method.Equals("POST", StringComparison.OrdinalIgnoreCase) || + method.Equals("PUT", StringComparison.OrdinalIgnoreCase) || + method.Equals("PATCH", StringComparison.OrdinalIgnoreCase))) + { + string jsonBody; + if (body is JsonElement jsonElement) + { + jsonBody = jsonElement.GetRawText(); + } + else + { + jsonBody = JsonSerializer.Serialize(body); + } + request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + } + + // Execute request + using var response = _httpClient.SendAsync(request).GetAwaiter().GetResult(); + stopwatch.Stop(); + + // Read response content + var responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + // Extract response headers + var responseHeaders = new Dictionary(); + foreach (var header in response.Headers) + { + responseHeaders[header.Key] = string.Join(", ", header.Value); + } + foreach (var header in response.Content.Headers) + { + responseHeaders[header.Key] = string.Join(", ", header.Value); + } + + return new + { + StatusCode = (int)response.StatusCode, + StatusText = response.StatusCode.ToString(), + Response = responseContent, + Headers = responseHeaders, + Duration = stopwatch.ElapsedMilliseconds + }; + } + catch (HttpRequestException ex) + { + stopwatch.Stop(); + throw new Exception($"HTTP request failed: {ex.Message}"); + } + catch (UriFormatException ex) + { + throw new Exception($"Invalid URL format: {ex.Message}"); + } + catch (Exception ex) when (ex is not ArgumentException) + { + stopwatch.Stop(); + throw new Exception($"Request failed: {ex.Message}"); + } + } +} diff --git a/components b/components index 2ccd4e9..af003c1 160000 --- a/components +++ b/components @@ -1 +1 @@ -Subproject commit 2ccd4e998bce75bb65f9dea4861114522db5ffa0 +Subproject commit af003c180d2a1b5fc6084e001511b70599e39a29 diff --git a/plans/scalar-httprequest-playground.md b/plans/scalar-httprequest-playground.md new file mode 100644 index 0000000..08a367b --- /dev/null +++ b/plans/scalar-httprequest-playground.md @@ -0,0 +1,178 @@ +# Implementation Plan: Replace Swagger with Scalar, Add HttpRequest Component, Create PlaygroundPage + +## Overview +This plan outlines the steps to accomplish three main tasks: +1. Replace the current OpenAPI UI (Swagger) with Scalar API reference UI in the backend. +2. Add a new HttpRequest component (similar to Calculator) for making HTTP requests. +3. Create a PlaygroundPage.vue for dynamic s8n components manipulation. + +## Task 1: Replace Swagger with Scalar + +### Current State +- Backend uses `Microsoft.AspNetCore.OpenApi` package (built-in OpenAPI). +- OpenAPI endpoint is mapped via `app.MapOpenApi()` in development environment. +- No Swashbuckle/Swagger UI currently installed. + +### Requirements +- Integrate Scalar as middleware to serve a modern API reference UI. +- Maintain OpenAPI specification generation (already provided by `AddOpenApi`). +- Scalar UI should be accessible at a dedicated route (e.g., `/scalar`). + +### Implementation Steps + +#### 1.1 Add Scalar.AspNetCore NuGet Package +Add package reference to `MyCompany.MyProject.BackendApi.csproj`: +```xml + +``` + +#### 1.2 Configure Scalar Middleware +Modify `Program.cs`: +- Keep `builder.Services.AddOpenApi()` for OpenAPI spec generation. +- Replace `app.MapOpenApi()` with Scalar middleware in development environment. +- Configure Scalar to use the generated OpenAPI spec. + +Example code: +```csharp +if (app.Environment.IsDevelopment()) +{ + app.MapScalarApiReference(); + // Optional: keep MapOpenApi if needed for raw spec + app.MapOpenApi(); +} +``` + +#### 1.3 Customize Scalar Settings (Optional) +Configure Scalar options (title, theme, etc.) via `AddScalar`. + +### Expected Outcome +- Visiting `/scalar` in browser displays Scalar API reference UI. +- OpenAPI spec remains available at `/openapi/v1.json`. + +## Task 2: Add HttpRequest Component + +### Component Structure + +#### 2.1 C# Backend Component +- **Location**: `S8n.Components.Packages/Basics/HttpRequest.cs` +- **Namespace**: `S8n.Components.Basics` +- **Class**: `HttpRequest` +- **Method**: `Execute` (or `Request`) +- **Inputs**: + - `method` (string): HTTP method (GET, POST, PUT, DELETE, etc.) + - `url` (string): Target URL + - `headers` (Dictionary): Optional request headers + - `body` (object): Optional request body (serialized as JSON) +- **Outputs**: + - `statusCode` (int): HTTP status code + - `response` (string): Response body as string + - `headers` (Dictionary): Response headers + - `duration` (long): Request duration in milliseconds + +Implementation will use `HttpClient` to make the request. + +#### 2.2 Vue Frontend Component +- **Location**: `QuasarFrontend/src/components_s8n/ComponentHttpRequest.vue` +- **Pattern**: Follow `ComponentCalculator.vue` structure. +- **UI Elements**: + - Dropdown for HTTP method (GET, POST, PUT, DELETE, etc.) + - Text input for URL + - Dynamic key-value pair inputs for headers (add/remove) + - JSON editor for request body (textarea with JSON validation) + - Execute button with loading state + - Display response status, headers, body, and duration + +#### 2.3 Component Definition Markdown +- **Location**: `components/basics/httprequest.md` +- **Format**: Similar to `calculator.md` with appropriate inputs/outputs. + +### Implementation Steps + +#### 2.4 Create C# Class +1. Create `HttpRequest.cs` file. +2. Implement method with error handling (timeout, invalid URL, etc.). +3. Ensure JSON serialization/deserialization. + +#### 2.5 Create Vue Component +1. Create `ComponentHttpRequest.vue`. +2. Use `runtime.createExecutor` with appropriate types. +3. Design UI using Quasar components. + +#### 2.6 Create Markdown Definition +1. Create `httprequest.md` with YAML definition. + +#### 2.7 Register Component (if needed) +- The runtime automatically discovers classes via reflection; no explicit registration required. + +## Task 3: Create PlaygroundPage.vue + +### Requirements +- A dedicated page for interacting with s8n components. +- Dynamic component selection and rendering. +- Support for multiple components (Calculator, HttpRequest, future components). + +### Implementation Steps + +#### 3.1 Add Route +Modify `QuasarFrontend/src/router/routes.ts`: +```typescript +{ + path: '/playground', + component: () => import('layouts/MainLayout.vue'), + children: [{ path: '', component: () => import('pages/PlaygroundPage.vue') }], +} +``` + +#### 3.2 Create PlaygroundPage.vue +- **Location**: `QuasarFrontend/src/pages/PlaygroundPage.vue` +- **Structure**: + - Left sidebar: List of available components (hardcoded for now). + - Main content: Dynamic component render area. + - Output display area (could be integrated into each component). + +#### 3.3 Component Registry +Create a component registry file (`src/components_s8n/registry.ts`) that exports: +- Component definitions (name, description, Vue component import). +- Dynamic import of components. + +#### 3.4 Dynamic Component Loading +Use Vue's `` to render selected component. + +#### 3.5 UI Enhancements +- Use Quasar's `q-select` for component selection. +- Provide a clean layout with card containers. + +## Dependencies and Considerations + +### Backend Dependencies +- `Scalar.AspNetCore` (new) +- `Microsoft.AspNetCore.OpenApi` (already present) +- `System.Net.Http` (for HttpRequest component) + +### Frontend Dependencies +- None new; use existing Quasar and Vue. + +### Testing +- Test Scalar UI loads correctly. +- Test HttpRequest component with mock HTTP server. +- Test PlaygroundPage navigation and component switching. + +## Timeline and Priority +1. **Priority 1**: HttpRequest component (core functionality). +2. **Priority 2**: PlaygroundPage (user interface). +3. **Priority 3**: Scalar integration (documentation). + +## Success Criteria +- Scalar UI accessible at `/scalar` (development only). +- HttpRequest component works for basic GET/POST requests. +- PlaygroundPage renders Calculator and HttpRequest components. +- No regression in existing Calculator component. + +## Notes +- Scalar integration may require configuration for production; currently planned for development only. +- HttpRequest component should handle CORS limitations (backend acts as proxy). +- PlaygroundPage can be extended later with component discovery from backend API. + +## Next Steps +1. Review this plan with stakeholders. +2. Proceed to implementation in Code mode. \ No newline at end of file