From d0b896e12b00a3be5dbc9943c45a8d288bcd5d8f Mon Sep 17 00:00:00 2001 From: Akshit Kujur Date: Mon, 6 Jul 2026 11:41:09 +0530 Subject: [PATCH 1/7] SCAL-322334: Add C# SDK full-stack demo --- .../.devcontainer/devcontainer.json | 30 + rest-api/csharp-sdk/README.md | 54 + rest-api/csharp-sdk/backend/Backend.csproj | 19 + rest-api/csharp-sdk/backend/Program.cs | 179 ++ rest-api/csharp-sdk/frontend/index.html | 11 + .../csharp-sdk/frontend/package-lock.json | 1719 +++++++++++++++++ rest-api/csharp-sdk/frontend/package.json | 19 + rest-api/csharp-sdk/frontend/src/App.jsx | 185 ++ .../frontend/src/LiveboardEmbedView.jsx | 60 + rest-api/csharp-sdk/frontend/src/api.js | 53 + rest-api/csharp-sdk/frontend/src/main.jsx | 9 + rest-api/csharp-sdk/frontend/vite.config.js | 7 + 12 files changed, 2345 insertions(+) create mode 100644 rest-api/csharp-sdk/.devcontainer/devcontainer.json create mode 100644 rest-api/csharp-sdk/README.md create mode 100644 rest-api/csharp-sdk/backend/Backend.csproj create mode 100644 rest-api/csharp-sdk/backend/Program.cs create mode 100644 rest-api/csharp-sdk/frontend/index.html create mode 100644 rest-api/csharp-sdk/frontend/package-lock.json create mode 100644 rest-api/csharp-sdk/frontend/package.json create mode 100644 rest-api/csharp-sdk/frontend/src/App.jsx create mode 100644 rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx create mode 100644 rest-api/csharp-sdk/frontend/src/api.js create mode 100644 rest-api/csharp-sdk/frontend/src/main.jsx create mode 100644 rest-api/csharp-sdk/frontend/vite.config.js diff --git a/rest-api/csharp-sdk/.devcontainer/devcontainer.json b/rest-api/csharp-sdk/.devcontainer/devcontainer.json new file mode 100644 index 0000000..06b5307 --- /dev/null +++ b/rest-api/csharp-sdk/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "ThoughtSpot C# SDK demo", + "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + } + }, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/demos/csharp_sdk", + "postCreateCommand": "cd frontend && npm install", + "forwardPorts": [5000, 5173], + "portsAttributes": { + "5000": { + "label": "Backend (ASP.NET Core API)", + "onAutoForward": "notify" + }, + "5173": { + "label": "Frontend (Vite)", + "onAutoForward": "openPreview" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "dbaeumer.vscode-eslint" + ] + } + } +} diff --git a/rest-api/csharp-sdk/README.md b/rest-api/csharp-sdk/README.md new file mode 100644 index 0000000..fedf908 --- /dev/null +++ b/rest-api/csharp-sdk/README.md @@ -0,0 +1,54 @@ +# ThoughtSpot C# SDK — full-stack demo + +A minimal two-process demo covering user creation, style customization, +search users, search liveboards, export liveboard (PDF), and export TML. + +``` +backend/ ASP.NET Core minimal API (C#) wrapping the ThoughtSpot.Client SDK +frontend/ Vite + React app that calls the backend +``` + +## Configure + +Both processes talk to the same cluster. Set these before running the backend +(defaults point at the same demo cluster used elsewhere in this repo): + +``` +export TS_HOST=https:// +export TS_USER= +export TS_PASS= +``` + +## Run + +```bash +# terminal 1 +cd backend +dotnet run # listens on http://localhost:5000 + +# terminal 2 +cd frontend +npm install +npm run dev # listens on http://localhost:5173 +``` + +Open http://localhost:5173. + +## Notes + +- The backend authenticates via `ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration { ... })` + once at startup, rather than the legacy `HttpClient`/`HttpClientHandler` + constructors. `CreateAsync` builds its own `SocketsHttpHandler`/`HttpClient` + internally and is what actually gives you `ConnectTimeout`/`ReadTimeout`/ + `WriteTimeout`, connection pooling, SSL handling, and automatic bearer-token + fetch + refresh. None of that is wired up if you construct the client from + your own `HttpClient`/`HttpClientHandler`. +- CORS is locked to `http://localhost:5173` in `backend/Program.cs` — update + if you serve the frontend elsewhere. +- "Export PDF" fetches the file as a blob and triggers a normal browser + download; if the export fails (auth, bad liveboard id, etc.) the error + message from the backend is shown in the UI instead of failing silently. +- `frontend/src/LiveboardEmbedView.jsx` is left over from an earlier version + that embedded a live Liveboard via the Visual Embed SDK. It's unused now + (no `@thoughtspot/visual-embed-sdk` credentials/setup here) — safe to + delete if you don't plan to wire embedding back in. diff --git a/rest-api/csharp-sdk/backend/Backend.csproj b/rest-api/csharp-sdk/backend/Backend.csproj new file mode 100644 index 0000000..8b7077c --- /dev/null +++ b/rest-api/csharp-sdk/backend/Backend.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + ThoughtSpotBackend + + LatestMajor + + + + + + + + diff --git a/rest-api/csharp-sdk/backend/Program.cs b/rest-api/csharp-sdk/backend/Program.cs new file mode 100644 index 0000000..9a26355 --- /dev/null +++ b/rest-api/csharp-sdk/backend/Program.cs @@ -0,0 +1,179 @@ +using ThoughtSpot.Client; +using ThoughtSpot.Client.Api; +using ThoughtSpot.Client.Client; +using ThoughtSpot.Client.Model; + +// ───────────────────────────────────────────────────────────────────────────── +// ThoughtSpot C# SDK — full-stack example (backend) +// +// A tiny ASP.NET Core minimal API that wraps the ThoughtSpot.Client SDK and +// exposes six JSON/file endpoints for the React frontend in ../frontend: +// +// POST /api/users — create a user +// POST /api/style — update style customization +// GET /api/users — search users +// GET /api/liveboards — search liveboards +// GET /api/liveboards/{id}/export — export a liveboard as PDF +// GET /api/liveboards/{id}/tml — export a liveboard's TML +// +// Run with: dotnet run (from this backend/ folder) +// Config via env vars: TS_HOST, TS_USER, TS_PASS (see defaults below). +// +// Uses ThoughtSpotRestApi.CreateAsync(...) instead of the legacy +// HttpClient/HttpClientHandler constructors. CreateAsync builds its own +// SocketsHttpHandler/HttpClient internally, which is what actually gives you +// ConnectTimeout / ReadTimeout / WriteTimeout, connection pooling, SSL +// handling, and automatic bearer-token fetch + refresh (TokenInjectingHandler). +// None of that is wired up if you build the client from your own +// HttpClient/HttpClientHandler via the legacy constructors. +// ───────────────────────────────────────────────────────────────────────────── + +string host = Environment.GetEnvironmentVariable("TS_HOST") ?? "https://172.32.25.218:8443"; +string user = Environment.GetEnvironmentVariable("TS_USER") ?? "tsadmin"; +string pass = Environment.GetEnvironmentVariable("TS_PASS") ?? "4Xyc1f%[H^3L"; + +var api = await ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration +{ + Host = host, + Username = user, + Password = pass, + TokenValiditySeconds = 3600, + // Self-signed dev/demo clusters. Remove for production. + IgnoreSslErrors = true, +}); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + policy.WithOrigins("http://localhost:5173") + .AllowAnyHeader() + .AllowAnyMethod()); +}); + +var app = builder.Build(); +app.UseCors(); + +app.MapGet("/api/health", () => Results.Ok(new { host })); + +// 1. User creation ------------------------------------------------------------ +app.MapPost("/api/users", (CreateUserBody body) => +{ + var request = new CreateUserRequest( + name: body.Name, + displayName: body.DisplayName, + password: body.Password, + email: body.Email, + accountType: CreateUserRequest.AccountTypeEnum.LOCALUSER, + accountStatus: CreateUserRequest.AccountStatusEnum.ACTIVE, + triggerWelcomeEmail: false, + triggerActivationEmail: false); + + try + { + User created = api.CreateUser(request); + return Results.Ok(new { id = created.Id, name = created.Name }); + } + catch (ApiException ex) + { + return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); + } +}); + +// 2. Style customization ------------------------------------------------------- +app.MapPost("/api/style", (StyleBody body) => +{ + try + { + api.UpdateStyleCustomization( + scope: "ORG", + operation: "REPLACE", + navigationPanel: new NavigationPanelInput( + theme: NavigationPanelInput.ThemeEnum.CUSTOM, + baseColor: body.BaseColor), + embeddedFooterText: body.FooterText); + + return Results.Ok(new { success = true }); + } + catch (ApiException ex) + { + return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); + } +}); + +// 3. Search users --------------------------------------------------------------- +app.MapGet("/api/users", (string? query, int size) => +{ + var request = new SearchUsersRequest( + namePattern: string.IsNullOrWhiteSpace(query) ? null : $"%{query}%", + recordSize: size <= 0 ? 10 : size); + + List users = api.SearchUsers(request); + return Results.Ok(users.Select(u => new { id = u.Id, name = u.Name, displayName = u.DisplayName })); +}); + +// 4. Search liveboards ------------------------------------------------------------ +app.MapGet("/api/liveboards", (string? query, int size) => +{ + var metadataItem = new MetadataListItemInput( + type: MetadataListItemInput.TypeEnum.LIVEBOARD, + namePattern: string.IsNullOrWhiteSpace(query) ? null : $"%{query}%"); + + var request = new SearchMetadataRequest( + metadata: new List { metadataItem }, + recordSize: size <= 0 ? 10 : size); + + List results = api.SearchMetadata(request); + return Results.Ok(results.Select(r => new { id = r.MetadataId, name = r.MetadataName })); +}); + +// 5. Export liveboard (PDF) -------------------------------------------------------- +app.MapGet("/api/liveboards/{id}/export", (string id) => +{ + var request = new ExportLiveboardReportRequest( + metadataIdentifier: id, + fileFormat: ExportLiveboardReportRequest.FileFormatEnum.PDF); + + try + { + FileParameter file = api.ExportLiveboardReport(request); + using var ms = new MemoryStream(); + file.Content.CopyTo(ms); + // Explicit content-length + attachment disposition so the browser's + // fetch()/blob download on the frontend gets a well-formed response + // instead of a chunked stream with no size hint. + var bytes = ms.ToArray(); + return Results.Bytes(bytes, "application/pdf", $"liveboard-{id}.pdf"); + } + catch (ApiException ex) + { + return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); + } +}); + +// 6. Export TML --------------------------------------------------------------------- +app.MapGet("/api/liveboards/{id}/tml", (string id) => +{ + var request = new ExportMetadataTMLRequest( + metadata: new List + { + new ExportMetadataTypeInput(type: ExportMetadataTypeInput.TypeEnum.LIVEBOARD, identifier: id) + }, + exportFqn: true, + edocFormat: ExportMetadataTMLRequest.EdocFormatEnum.JSON); + + try + { + List tml = api.ExportMetadataTML(request); + return Results.Ok(tml); + } + catch (ApiException ex) + { + return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); + } +}); + +app.Run(); + +record CreateUserBody(string Name, string DisplayName, string Email, string Password); +record StyleBody(string BaseColor, string FooterText); diff --git a/rest-api/csharp-sdk/frontend/index.html b/rest-api/csharp-sdk/frontend/index.html new file mode 100644 index 0000000..fd1a10d --- /dev/null +++ b/rest-api/csharp-sdk/frontend/index.html @@ -0,0 +1,11 @@ + + + + + ThoughtSpot C# SDK Demo + + +
+ + + diff --git a/rest-api/csharp-sdk/frontend/package-lock.json b/rest-api/csharp-sdk/frontend/package-lock.json new file mode 100644 index 0000000..b99c9e5 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/package-lock.json @@ -0,0 +1,1719 @@ +{ + "name": "thoughtspot-sdk-demo-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "thoughtspot-sdk-demo-frontend", + "version": "0.0.1", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", + "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/rest-api/csharp-sdk/frontend/package.json b/rest-api/csharp-sdk/frontend/package.json new file mode 100644 index 0000000..4190854 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "thoughtspot-sdk-demo-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.0" + } +} diff --git a/rest-api/csharp-sdk/frontend/src/App.jsx b/rest-api/csharp-sdk/frontend/src/App.jsx new file mode 100644 index 0000000..cc3894f --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/App.jsx @@ -0,0 +1,185 @@ +import { useState } from "react"; +import { api } from "./api.js"; + +const card = { border: "1px solid #ddd", borderRadius: 8, padding: 16, marginBottom: 16 }; +const input = { display: "block", width: "100%", margin: "6px 0", padding: 6 }; +const button = { padding: "6px 14px", marginTop: 8, cursor: "pointer" }; +const pre = { background: "#f6f6f6", padding: 10, borderRadius: 6, maxHeight: 200, overflow: "auto", fontSize: 12 }; + +export default function App() { + return ( +
+

ThoughtSpot C# SDK — Full-Stack Demo

+ + + + + +
+ ); +} + +function CreateUserCard() { + const [form, setForm] = useState({ name: "", displayName: "", email: "", password: "" }); + const [result, setResult] = useState(null); + + const submit = async (e) => { + e.preventDefault(); + setResult(null); + try { + const created = await api.createUser(form); + setResult(created); + } catch (err) { + setResult({ error: err.message }); + } + }; + + return ( +
+

1. Create user

+
+ setForm({ ...form, name: e.target.value })} required /> + setForm({ ...form, displayName: e.target.value })} required /> + setForm({ ...form, email: e.target.value })} /> + setForm({ ...form, password: e.target.value })} required /> + +
+ {result &&
{JSON.stringify(result, null, 2)}
} +
+ ); +} + +function StyleCard() { + const [baseColor, setBaseColor] = useState("#2359B6"); + const [footerText, setFooterText] = useState("Powered by ThoughtSpot"); + const [result, setResult] = useState(null); + + const submit = async (e) => { + e.preventDefault(); + setResult(null); + try { + const res = await api.updateStyle({ baseColor, footerText }); + setResult(res); + } catch (err) { + setResult({ error: err.message }); + } + }; + + return ( +
+

2. Style customization

+
+ + setBaseColor(e.target.value)} /> + + setFooterText(e.target.value)} /> + +
+ {result &&
{JSON.stringify(result, null, 2)}
} +
+ ); +} + +function SearchUsersCard() { + const [query, setQuery] = useState(""); + const [users, setUsers] = useState([]); + const [error, setError] = useState(null); + + const search = async (e) => { + e.preventDefault(); + setError(null); + try { + setUsers(await api.searchUsers(query)); + } catch (err) { + setError(err.message); + } + }; + + return ( +
+

3. Search users

+
+ setQuery(e.target.value)} /> + +
+ {error &&

{error}

} +
    + {users.map((u) => ( +
  • {u.name} — {u.displayName}
  • + ))} +
+
+ ); +} + +function LiveboardCard() { + const [query, setQuery] = useState(""); + const [liveboards, setLiveboards] = useState([]); + const [tml, setTml] = useState(null); + const [error, setError] = useState(null); + const [exportingId, setExportingId] = useState(null); + + const search = async (e) => { + e.preventDefault(); + setError(null); + try { + const results = await api.searchLiveboards(query); + setLiveboards(results); + } catch (err) { + setError(err.message); + } + }; + + const exportPdf = async (lb) => { + setError(null); + setExportingId(lb.id); + try { + await api.exportLiveboard(lb.id, lb.name); + } catch (err) { + setError(err.message); + } finally { + setExportingId(null); + } + }; + + const loadTml = async (id) => { + setError(null); + setTml(null); + try { + setTml(await api.exportTml(id)); + } catch (err) { + setError(err.message); + } + }; + + return ( +
+

4–6. Search / export / TML a liveboard

+
+ setQuery(e.target.value)} /> + +
+ {error &&

{error}

} + +
    + {liveboards.map((lb) => ( +
  • + {lb.name}{" "} + {" "} + +
  • + ))} +
+ + {tml &&
{JSON.stringify(tml, null, 2)}
} +
+ ); +} diff --git a/rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx b/rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx new file mode 100644 index 0000000..f0b5c67 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx @@ -0,0 +1,60 @@ +import { useEffect, useRef, useState } from "react"; +import { init, AuthType, LiveboardEmbed } from "@thoughtspot/visual-embed-sdk"; +import { api } from "./api.js"; + +// Embeds a live Liveboard using the Visual Embed SDK, authenticated via a +// token minted by the backend's /api/embed-token endpoint (trusted-auth, +// cookieless — no ThoughtSpot login page shown to the end user). +export default function LiveboardEmbedView({ liveboardId }) { + const containerRef = useRef(null); + const [error, setError] = useState(null); + const initedHostRef = useRef(null); + + useEffect(() => { + if (!liveboardId) return; + let cancelled = false; + + async function mount() { + try { + const { token, host, username } = await api.embedToken(); + if (cancelled) return; + + if (initedHostRef.current !== host) { + init({ + thoughtSpotHost: host, + authType: AuthType.TrustedAuthTokenCookieless, + username, + getAuthToken: async () => { + // Re-fetch on every call so re-renders/refresh use a fresh token. + const t = await api.embedToken(); + return t.token; + }, + }); + initedHostRef.current = host; + } + + if (containerRef.current) { + containerRef.current.innerHTML = ""; + } + + const embed = new LiveboardEmbed(containerRef.current, { + liveboardId, + frameParams: { width: "100%", height: "600" }, + }); + embed.render(); + } catch (e) { + setError(e.message); + } + } + + mount(); + return () => { + cancelled = true; + }; + }, [liveboardId]); + + if (!liveboardId) return

Search for a liveboard above and pick one to embed it here.

; + if (error) return

Embed error: {error}

; + + return
; +} diff --git a/rest-api/csharp-sdk/frontend/src/api.js b/rest-api/csharp-sdk/frontend/src/api.js new file mode 100644 index 0000000..eba9b79 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/api.js @@ -0,0 +1,53 @@ +// Thin fetch wrapper around the ASP.NET Core backend in ../backend. +const BASE = "http://localhost:5000"; + +async function handle(res) { + const body = await res.json().catch(() => null); + if (!res.ok) throw new Error(body?.error ?? res.statusText); + return body; +} + +export const api = { + createUser: (payload) => + fetch(`${BASE}/api/users`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }).then(handle), + + updateStyle: (payload) => + fetch(`${BASE}/api/style`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }).then(handle), + + searchUsers: (query, size = 10) => + fetch(`${BASE}/api/users?query=${encodeURIComponent(query ?? "")}&size=${size}`).then(handle), + + searchLiveboards: (query, size = 10) => + fetch(`${BASE}/api/liveboards?query=${encodeURIComponent(query ?? "")}&size=${size}`).then(handle), + + // Fetches the PDF as a blob (rather than a raw navigation) so a + // non-2xx response — auth failure, bad liveboard id, etc. — surfaces as a + // catchable error with the backend's message instead of silently opening + // a blank tab. Triggers a normal browser "Save As" download on success. + exportLiveboard: async (id, name) => { + const res = await fetch(`${BASE}/api/liveboards/${id}/export`); + if (!res.ok) { + const body = await res.json().catch(() => null); + throw new Error(body?.error ?? `Export failed (HTTP ${res.status})`); + } + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${name ?? "liveboard"}.pdf`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + }, + + exportTml: (id) => fetch(`${BASE}/api/liveboards/${id}/tml`).then(handle), +}; diff --git a/rest-api/csharp-sdk/frontend/src/main.jsx b/rest-api/csharp-sdk/frontend/src/main.jsx new file mode 100644 index 0000000..569fdf2 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.jsx"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); diff --git a/rest-api/csharp-sdk/frontend/vite.config.js b/rest-api/csharp-sdk/frontend/vite.config.js new file mode 100644 index 0000000..03069f4 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { port: 5173 }, +}); From 699336ca0f0eded82dae51629f6e2c5d03d56241 Mon Sep 17 00:00:00 2001 From: Akshit Kujur Date: Tue, 7 Jul 2026 10:57:45 +0530 Subject: [PATCH 2/7] SCAL-322334: Fixed devcontainer workspace folder --- rest-api/csharp-sdk/.devcontainer/devcontainer.json | 2 +- rest-api/csharp-sdk/backend/Program.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest-api/csharp-sdk/.devcontainer/devcontainer.json b/rest-api/csharp-sdk/.devcontainer/devcontainer.json index 06b5307..aa9a460 100644 --- a/rest-api/csharp-sdk/.devcontainer/devcontainer.json +++ b/rest-api/csharp-sdk/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "version": "20" } }, - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/demos/csharp_sdk", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/rest-api/csharp-sdk", "postCreateCommand": "cd frontend && npm install", "forwardPorts": [5000, 5173], "portsAttributes": { diff --git a/rest-api/csharp-sdk/backend/Program.cs b/rest-api/csharp-sdk/backend/Program.cs index 9a26355..af78716 100644 --- a/rest-api/csharp-sdk/backend/Program.cs +++ b/rest-api/csharp-sdk/backend/Program.cs @@ -28,9 +28,9 @@ // HttpClient/HttpClientHandler via the legacy constructors. // ───────────────────────────────────────────────────────────────────────────── -string host = Environment.GetEnvironmentVariable("TS_HOST") ?? "https://172.32.25.218:8443"; -string user = Environment.GetEnvironmentVariable("TS_USER") ?? "tsadmin"; -string pass = Environment.GetEnvironmentVariable("TS_PASS") ?? "4Xyc1f%[H^3L"; +string host = Environment.GetEnvironmentVariable("TS_HOST") ?? ""; +string user = Environment.GetEnvironmentVariable("TS_USER") ?? ""; +string pass = Environment.GetEnvironmentVariable("TS_PASS") ?? ""; var api = await ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration { From 1f1bae79641bdcc10aaf2d4d073e7687f3726a0b Mon Sep 17 00:00:00 2001 From: Akshit Kujur Date: Thu, 9 Jul 2026 11:23:36 +0530 Subject: [PATCH 3/7] Simplify C# SDK full-stack demo --- rest-api/csharp-sdk/README.md | 30 +- rest-api/csharp-sdk/backend/.gitignore | 2 + rest-api/csharp-sdk/backend/Program.cs | 289 +++++++++++------- .../csharp-sdk/frontend/package-lock.json | 39 --- rest-api/csharp-sdk/frontend/src/App.jsx | 87 +----- .../frontend/src/LiveboardEmbedView.jsx | 60 ---- rest-api/csharp-sdk/frontend/src/api.js | 35 --- rest-api/csharp-sdk/frontend/vite.config.js | 12 +- 8 files changed, 213 insertions(+), 341 deletions(-) create mode 100644 rest-api/csharp-sdk/backend/.gitignore delete mode 100644 rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx diff --git a/rest-api/csharp-sdk/README.md b/rest-api/csharp-sdk/README.md index fedf908..f43131c 100644 --- a/rest-api/csharp-sdk/README.md +++ b/rest-api/csharp-sdk/README.md @@ -1,7 +1,7 @@ # ThoughtSpot C# SDK — full-stack demo -A minimal two-process demo covering user creation, style customization, -search users, search liveboards, export liveboard (PDF), and export TML. +A minimal two-process demo covering search users, search liveboards, and +exporting a liveboard's TML. ``` backend/ ASP.NET Core minimal API (C#) wrapping the ThoughtSpot.Client SDK @@ -19,6 +19,19 @@ export TS_USER= export TS_PASS= ``` +Or set the `VITE_TS_HOST` / `VITE_TS_USERNAME` / `VITE_TS_PASSWORD` / +`VITE_LIVEBOARD_ID` equivalents in `backend/.env` (loaded automatically at +startup). + +### Why create-user / style-customization / PDF-export aren't here + +They all require admin privileges. On a restricted sandbox account (e.g. a +training/trial cluster), those calls return a 403 "Operation is not allowed" +— a cluster permission limit, not something this demo can route around — so +they were dropped rather than shipped as broken buttons. Search users, +search liveboards, and TML export all work with a regular (non-admin) +account. + ## Run ```bash @@ -45,10 +58,9 @@ Open http://localhost:5173. your own `HttpClient`/`HttpClientHandler`. - CORS is locked to `http://localhost:5173` in `backend/Program.cs` — update if you serve the frontend elsewhere. -- "Export PDF" fetches the file as a blob and triggers a normal browser - download; if the export fails (auth, bad liveboard id, etc.) the error - message from the backend is shown in the UI instead of failing silently. -- `frontend/src/LiveboardEmbedView.jsx` is left over from an earlier version - that embedded a live Liveboard via the Visual Embed SDK. It's unused now - (no `@thoughtspot/visual-embed-sdk` credentials/setup here) — safe to - delete if you don't plan to wire embedding back in. +- TML export (`GET /api/liveboards/{id}/tml`) deserializes through + Newtonsoft.Json internally (that's what the SDK uses), so the backend + re-serializes the response with Newtonsoft before returning it — handing + the raw `List` straight to ASP.NET Core's default System.Text.Json + serializer silently produces near-empty `edoc`/`info` fields instead of an + error, since it doesn't know how to write out Newtonsoft's JToken types. diff --git a/rest-api/csharp-sdk/backend/.gitignore b/rest-api/csharp-sdk/backend/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/rest-api/csharp-sdk/backend/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/rest-api/csharp-sdk/backend/Program.cs b/rest-api/csharp-sdk/backend/Program.cs index af78716..d3edb86 100644 --- a/rest-api/csharp-sdk/backend/Program.cs +++ b/rest-api/csharp-sdk/backend/Program.cs @@ -1,52 +1,123 @@ +using System.IO; using ThoughtSpot.Client; using ThoughtSpot.Client.Api; using ThoughtSpot.Client.Client; using ThoughtSpot.Client.Model; -// ───────────────────────────────────────────────────────────────────────────── -// ThoughtSpot C# SDK — full-stack example (backend) -// -// A tiny ASP.NET Core minimal API that wraps the ThoughtSpot.Client SDK and -// exposes six JSON/file endpoints for the React frontend in ../frontend: -// -// POST /api/users — create a user -// POST /api/style — update style customization -// GET /api/users — search users -// GET /api/liveboards — search liveboards -// GET /api/liveboards/{id}/export — export a liveboard as PDF -// GET /api/liveboards/{id}/tml — export a liveboard's TML -// -// Run with: dotnet run (from this backend/ folder) -// Config via env vars: TS_HOST, TS_USER, TS_PASS (see defaults below). -// -// Uses ThoughtSpotRestApi.CreateAsync(...) instead of the legacy -// HttpClient/HttpClientHandler constructors. CreateAsync builds its own -// SocketsHttpHandler/HttpClient internally, which is what actually gives you -// ConnectTimeout / ReadTimeout / WriteTimeout, connection pooling, SSL -// handling, and automatic bearer-token fetch + refresh (TokenInjectingHandler). -// None of that is wired up if you build the client from your own -// HttpClient/HttpClientHandler via the legacy constructors. -// ───────────────────────────────────────────────────────────────────────────── - -string host = Environment.GetEnvironmentVariable("TS_HOST") ?? ""; -string user = Environment.GetEnvironmentVariable("TS_USER") ?? ""; -string pass = Environment.GetEnvironmentVariable("TS_PASS") ?? ""; - -var api = await ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration + +// Loads key=value lines from a .env file into process environment variables +static void LoadDotEnv(string path = ".env") { - Host = host, - Username = user, - Password = pass, - TokenValiditySeconds = 3600, - // Self-signed dev/demo clusters. Remove for production. - IgnoreSslErrors = true, -}); + if (!File.Exists(path)) + { + return; + } + foreach (var raw in File.ReadAllLines(path)) + { + var line = raw.Trim(); + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + continue; + + var idx = line.IndexOf('='); + if (idx <= 0) + continue; + + var key = line.Substring(0, idx).Trim(); + var val = line.Substring(idx + 1).Trim(); + + if ((val.StartsWith("\"") && val.EndsWith("\"")) || (val.StartsWith("'") && val.EndsWith("'"))) + { + val = val.Substring(1, val.Length - 2); + } + + // Do not overwrite existing environment vars + if (Environment.GetEnvironmentVariable(key) == null) + { + Environment.SetEnvironmentVariable(key, val); + } + } +} + +// Load a local .env file (if present) so developers don't have to export vars. +LoadDotEnv(); + +// Prefer the VITE_* env vars (used by the frontend/dev) but fall back +// to the legacy TS_* names for compatibility. +string hostEnv = Environment.GetEnvironmentVariable("VITE_TS_HOST") ?? Environment.GetEnvironmentVariable("TS_HOST"); +string host = ThoughtSpotConfiguration.NormalizeHost(hostEnv); +string user = Environment.GetEnvironmentVariable("VITE_TS_USERNAME") ?? Environment.GetEnvironmentVariable("TS_USER") ?? ""; +string pass = Environment.GetEnvironmentVariable("VITE_TS_PASSWORD") ?? Environment.GetEnvironmentVariable("TS_PASS") ?? ""; +string liveboardId = Environment.GetEnvironmentVariable("VITE_LIVEBOARD_ID") ?? Environment.GetEnvironmentVariable("TS_LIVEBOARD_ID") ?? ""; + +// Try to create the ThoughtSpot client but don't crash the server if it fails. +ThoughtSpotRestApi? api = null; +try +{ + if (string.IsNullOrWhiteSpace(user) || string.IsNullOrWhiteSpace(pass)) + { + throw new InvalidOperationException("Missing ThoughtSpot credentials (TS_USER/TS_PASS or VITE_TS_USERNAME/VITE_TS_PASSWORD)."); + } + + api = await ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration + { + Host = host, + Username = user, + Password = pass, + TokenValiditySeconds = 3600, + // Self-signed dev/demo clusters. Remove for production. + IgnoreSslErrors = true, + }); + + Console.WriteLine("ThoughtSpot client created successfully."); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Warning: ThoughtSpot client not configured: {ex.Message}"); + Console.Error.WriteLine("Server will continue running; API endpoints will return 503 until configured."); +} + +// Choose a URL to listen on: prefer ASPNETCORE_URLS, else try default ports, else pick an ephemeral port. +int FindFreePort() +{ + // First try 5000..5010 + for (int p = 5000; p <= 5010; p++) + { + try + { + var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, p); + listener.Start(); + listener.Stop(); + return p; + } + catch { } + } + + // Fallback: let OS pick an ephemeral port + var l = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); + l.Start(); + int port = ((System.Net.IPEndPoint)l.LocalEndpoint).Port; + l.Stop(); + return port; +} + +// Determine the URL to listen on and configure the host. var builder = WebApplication.CreateBuilder(args); +string? urls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); +if (string.IsNullOrWhiteSpace(urls)) +{ + int port = FindFreePort(); + urls = $"http://127.0.0.1:{port}"; + Console.WriteLine($"No ASPNETCORE_URLS set; binding to {urls}"); + builder.WebHost.UseUrls(urls); +} builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => - policy.WithOrigins("http://localhost:5173") + policy.WithOrigins( + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://0.0.0.0:5173") .AllowAnyHeader() .AllowAnyMethod()); }); @@ -54,70 +125,45 @@ var app = builder.Build(); app.UseCors(); -app.MapGet("/api/health", () => Results.Ok(new { host })); +app.MapGet("/api/health", () => Results.Ok(new { host, liveboardId })); -// 1. User creation ------------------------------------------------------------ -app.MapPost("/api/users", (CreateUserBody body) => -{ - var request = new CreateUserRequest( - name: body.Name, - displayName: body.DisplayName, - password: body.Password, - email: body.Email, - accountType: CreateUserRequest.AccountTypeEnum.LOCALUSER, - accountStatus: CreateUserRequest.AccountStatusEnum.ACTIVE, - triggerWelcomeEmail: false, - triggerActivationEmail: false); +Func clientNotConfigured = () => Results.Json(new { error = "ThoughtSpot client not configured. See .env.example or set TS_USER/TS_PASS." }, statusCode: 503); - try - { - User created = api.CreateUser(request); - return Results.Ok(new { id = created.Id, name = created.Name }); - } - catch (ApiException ex) - { - return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); - } -}); - -// 2. Style customization ------------------------------------------------------- -app.MapPost("/api/style", (StyleBody body) => +// Some clusters (e.g. restricted sandbox/trial accounts) return a generic +// "Operation is not allowed" 403 for calls that need privileges the +// configured account doesn't have. Surface that as a clear message instead +// of the raw upstream error blob. +string FriendlyError(ApiException ex) { - try - { - api.UpdateStyleCustomization( - scope: "ORG", - operation: "REPLACE", - navigationPanel: new NavigationPanelInput( - theme: NavigationPanelInput.ThemeEnum.CUSTOM, - baseColor: body.BaseColor), - embeddedFooterText: body.FooterText); - - return Results.Ok(new { success = true }); - } - catch (ApiException ex) + if (ex.ErrorCode == 403 && ex.Message.Contains("Operation is not allowed")) { - return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); + return "This operation requires privileges (e.g. admin) that the configured ThoughtSpot account does not have on this cluster."; } -}); -// 3. Search users --------------------------------------------------------------- + return ex.Message; +} + +// 1. Search users --------------------------------------------------------------- app.MapGet("/api/users", (string? query, int size) => { + if (api == null) return clientNotConfigured(); + string? namePattern = string.IsNullOrWhiteSpace(query) ? null : $"%{query}%"; var request = new SearchUsersRequest( - namePattern: string.IsNullOrWhiteSpace(query) ? null : $"%{query}%", + namePattern: namePattern ?? null!, recordSize: size <= 0 ? 10 : size); List users = api.SearchUsers(request); return Results.Ok(users.Select(u => new { id = u.Id, name = u.Name, displayName = u.DisplayName })); }); -// 4. Search liveboards ------------------------------------------------------------ +// 2. Search liveboards ------------------------------------------------------------ app.MapGet("/api/liveboards", (string? query, int size) => { + if (api == null) return clientNotConfigured(); + string? namePattern = string.IsNullOrWhiteSpace(query) ? null : $"%{query}%"; var metadataItem = new MetadataListItemInput( type: MetadataListItemInput.TypeEnum.LIVEBOARD, - namePattern: string.IsNullOrWhiteSpace(query) ? null : $"%{query}%"); + namePattern: namePattern ?? null!); var request = new SearchMetadataRequest( metadata: new List { metadataItem }, @@ -127,33 +173,10 @@ return Results.Ok(results.Select(r => new { id = r.MetadataId, name = r.MetadataName })); }); -// 5. Export liveboard (PDF) -------------------------------------------------------- -app.MapGet("/api/liveboards/{id}/export", (string id) => -{ - var request = new ExportLiveboardReportRequest( - metadataIdentifier: id, - fileFormat: ExportLiveboardReportRequest.FileFormatEnum.PDF); - - try - { - FileParameter file = api.ExportLiveboardReport(request); - using var ms = new MemoryStream(); - file.Content.CopyTo(ms); - // Explicit content-length + attachment disposition so the browser's - // fetch()/blob download on the frontend gets a well-formed response - // instead of a chunked stream with no size hint. - var bytes = ms.ToArray(); - return Results.Bytes(bytes, "application/pdf", $"liveboard-{id}.pdf"); - } - catch (ApiException ex) - { - return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); - } -}); - -// 6. Export TML --------------------------------------------------------------------- +// 3. Export TML --------------------------------------------------------------------- app.MapGet("/api/liveboards/{id}/tml", (string id) => { + if (api == null) return clientNotConfigured(); var request = new ExportMetadataTMLRequest( metadata: new List { @@ -165,15 +188,57 @@ try { List tml = api.ExportMetadataTML(request); - return Results.Ok(tml); + // The SDK deserializes this response body with Newtonsoft.Json, so each + // entry in `tml` is really a Newtonsoft JObject/JArray/JValue. ASP.NET + // Core's default System.Text.Json serializer doesn't understand those + // types and silently writes them out as near-empty objects/arrays. + // Round-trip through Newtonsoft to get back well-formed JSON text. + string json = Newtonsoft.Json.JsonConvert.SerializeObject(tml); + return Results.Content(json, "application/json"); } catch (ApiException ex) { - return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode); + return Results.Json(new { error = FriendlyError(ex) }, statusCode: ex.ErrorCode); } }); app.Run(); -record CreateUserBody(string Name, string DisplayName, string Email, string Password); -record StyleBody(string BaseColor, string FooterText); +public static class ThoughtSpotConfiguration +{ + public static string NormalizeHost(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + { + return "https://try-everywhere.thoughtspot.cloud"; + } + + var trimmed = host.Trim(); + if (!trimmed.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && + !trimmed.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + trimmed = $"https://{trimmed}"; + } + + if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)) + { + throw new ArgumentException($"The TS_HOST value '{host}' is not a valid URL.", nameof(host)); + } + + var path = uri.AbsolutePath.Trim('/'); + if (path.Equals("v2", StringComparison.OrdinalIgnoreCase)) + { + path = string.Empty; + } + + var builder = new UriBuilder(uri) + { + Path = string.IsNullOrEmpty(path) ? string.Empty : $"/{path}", + Query = string.Empty, + Fragment = string.Empty + }; + + return builder.Uri.GetLeftPart(UriPartial.Authority) + (builder.Path.Length > 1 ? builder.Path : string.Empty); + } +} + diff --git a/rest-api/csharp-sdk/frontend/package-lock.json b/rest-api/csharp-sdk/frontend/package-lock.json index b99c9e5..511a750 100644 --- a/rest-api/csharp-sdk/frontend/package-lock.json +++ b/rest-api/csharp-sdk/frontend/package-lock.json @@ -838,9 +838,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -855,9 +852,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -872,9 +866,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -889,9 +880,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -906,9 +894,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -923,9 +908,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -940,9 +922,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -957,9 +936,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -974,9 +950,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -991,9 +964,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1008,9 +978,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1025,9 +992,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1042,9 +1006,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/rest-api/csharp-sdk/frontend/src/App.jsx b/rest-api/csharp-sdk/frontend/src/App.jsx index cc3894f..fea589d 100644 --- a/rest-api/csharp-sdk/frontend/src/App.jsx +++ b/rest-api/csharp-sdk/frontend/src/App.jsx @@ -11,79 +11,12 @@ export default function App() {

ThoughtSpot C# SDK — Full-Stack Demo

- -
); } -function CreateUserCard() { - const [form, setForm] = useState({ name: "", displayName: "", email: "", password: "" }); - const [result, setResult] = useState(null); - - const submit = async (e) => { - e.preventDefault(); - setResult(null); - try { - const created = await api.createUser(form); - setResult(created); - } catch (err) { - setResult({ error: err.message }); - } - }; - - return ( -
-

1. Create user

-
- setForm({ ...form, name: e.target.value })} required /> - setForm({ ...form, displayName: e.target.value })} required /> - setForm({ ...form, email: e.target.value })} /> - setForm({ ...form, password: e.target.value })} required /> - -
- {result &&
{JSON.stringify(result, null, 2)}
} -
- ); -} - -function StyleCard() { - const [baseColor, setBaseColor] = useState("#2359B6"); - const [footerText, setFooterText] = useState("Powered by ThoughtSpot"); - const [result, setResult] = useState(null); - - const submit = async (e) => { - e.preventDefault(); - setResult(null); - try { - const res = await api.updateStyle({ baseColor, footerText }); - setResult(res); - } catch (err) { - setResult({ error: err.message }); - } - }; - - return ( -
-

2. Style customization

-
- - setBaseColor(e.target.value)} /> - - setFooterText(e.target.value)} /> - -
- {result &&
{JSON.stringify(result, null, 2)}
} -
- ); -} - function SearchUsersCard() { const [query, setQuery] = useState(""); const [users, setUsers] = useState([]); @@ -101,7 +34,7 @@ function SearchUsersCard() { return (
-

3. Search users

+

1. Search users

setQuery(e.target.value)} /> @@ -122,7 +55,6 @@ function LiveboardCard() { const [liveboards, setLiveboards] = useState([]); const [tml, setTml] = useState(null); const [error, setError] = useState(null); - const [exportingId, setExportingId] = useState(null); const search = async (e) => { e.preventDefault(); @@ -135,18 +67,6 @@ function LiveboardCard() { } }; - const exportPdf = async (lb) => { - setError(null); - setExportingId(lb.id); - try { - await api.exportLiveboard(lb.id, lb.name); - } catch (err) { - setError(err.message); - } finally { - setExportingId(null); - } - }; - const loadTml = async (id) => { setError(null); setTml(null); @@ -159,7 +79,7 @@ function LiveboardCard() { return (
-

4–6. Search / export / TML a liveboard

+

2–3. Search / TML-export a liveboard

setQuery(e.target.value)} /> @@ -171,9 +91,6 @@ function LiveboardCard() { {liveboards.map((lb) => (
  • {lb.name}{" "} - {" "}
  • ))} diff --git a/rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx b/rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx deleted file mode 100644 index f0b5c67..0000000 --- a/rest-api/csharp-sdk/frontend/src/LiveboardEmbedView.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { init, AuthType, LiveboardEmbed } from "@thoughtspot/visual-embed-sdk"; -import { api } from "./api.js"; - -// Embeds a live Liveboard using the Visual Embed SDK, authenticated via a -// token minted by the backend's /api/embed-token endpoint (trusted-auth, -// cookieless — no ThoughtSpot login page shown to the end user). -export default function LiveboardEmbedView({ liveboardId }) { - const containerRef = useRef(null); - const [error, setError] = useState(null); - const initedHostRef = useRef(null); - - useEffect(() => { - if (!liveboardId) return; - let cancelled = false; - - async function mount() { - try { - const { token, host, username } = await api.embedToken(); - if (cancelled) return; - - if (initedHostRef.current !== host) { - init({ - thoughtSpotHost: host, - authType: AuthType.TrustedAuthTokenCookieless, - username, - getAuthToken: async () => { - // Re-fetch on every call so re-renders/refresh use a fresh token. - const t = await api.embedToken(); - return t.token; - }, - }); - initedHostRef.current = host; - } - - if (containerRef.current) { - containerRef.current.innerHTML = ""; - } - - const embed = new LiveboardEmbed(containerRef.current, { - liveboardId, - frameParams: { width: "100%", height: "600" }, - }); - embed.render(); - } catch (e) { - setError(e.message); - } - } - - mount(); - return () => { - cancelled = true; - }; - }, [liveboardId]); - - if (!liveboardId) return

    Search for a liveboard above and pick one to embed it here.

    ; - if (error) return

    Embed error: {error}

    ; - - return
    ; -} diff --git a/rest-api/csharp-sdk/frontend/src/api.js b/rest-api/csharp-sdk/frontend/src/api.js index eba9b79..61081e4 100644 --- a/rest-api/csharp-sdk/frontend/src/api.js +++ b/rest-api/csharp-sdk/frontend/src/api.js @@ -8,46 +8,11 @@ async function handle(res) { } export const api = { - createUser: (payload) => - fetch(`${BASE}/api/users`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }).then(handle), - - updateStyle: (payload) => - fetch(`${BASE}/api/style`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }).then(handle), - searchUsers: (query, size = 10) => fetch(`${BASE}/api/users?query=${encodeURIComponent(query ?? "")}&size=${size}`).then(handle), searchLiveboards: (query, size = 10) => fetch(`${BASE}/api/liveboards?query=${encodeURIComponent(query ?? "")}&size=${size}`).then(handle), - // Fetches the PDF as a blob (rather than a raw navigation) so a - // non-2xx response — auth failure, bad liveboard id, etc. — surfaces as a - // catchable error with the backend's message instead of silently opening - // a blank tab. Triggers a normal browser "Save As" download on success. - exportLiveboard: async (id, name) => { - const res = await fetch(`${BASE}/api/liveboards/${id}/export`); - if (!res.ok) { - const body = await res.json().catch(() => null); - throw new Error(body?.error ?? `Export failed (HTTP ${res.status})`); - } - const blob = await res.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${name ?? "liveboard"}.pdf`; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - }, - exportTml: (id) => fetch(`${BASE}/api/liveboards/${id}/tml`).then(handle), }; diff --git a/rest-api/csharp-sdk/frontend/vite.config.js b/rest-api/csharp-sdk/frontend/vite.config.js index 03069f4..348be56 100644 --- a/rest-api/csharp-sdk/frontend/vite.config.js +++ b/rest-api/csharp-sdk/frontend/vite.config.js @@ -3,5 +3,15 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], - server: { port: 5173 }, + server: { + host: "0.0.0.0", + port: 5173, + proxy: { + "/api": { + target: "http://localhost:5000", + changeOrigin: true, + secure: false, + }, + }, + }, }); From f83e7fd550f2062574c1ab20ba72a5e5b2d32a51 Mon Sep 17 00:00:00 2001 From: akshit-kujur-ThoughtSpot Date: Thu, 9 Jul 2026 09:10:17 +0000 Subject: [PATCH 4/7] Add C# REST API developer examples --- rest-api/csharp-sdk/README.md | 13 +++++++++++ rest-api/csharp-sdk/backend/Program.cs | 26 +-------------------- rest-api/csharp-sdk/frontend/src/api.js | 3 ++- rest-api/csharp-sdk/frontend/vite.config.js | 4 +++- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/rest-api/csharp-sdk/README.md b/rest-api/csharp-sdk/README.md index f43131c..43a9837 100644 --- a/rest-api/csharp-sdk/README.md +++ b/rest-api/csharp-sdk/README.md @@ -1,5 +1,18 @@ + + # ThoughtSpot C# SDK — full-stack demo +[Open this repo in GitHub Codespaces](https://github.com/thoughtspot/developer-examples/codespaces/new?ref=SCAL-322334-add-csharp-developer-examples) + A minimal two-process demo covering search users, search liveboards, and exporting a liveboard's TML. diff --git a/rest-api/csharp-sdk/backend/Program.cs b/rest-api/csharp-sdk/backend/Program.cs index d3edb86..23c518d 100644 --- a/rest-api/csharp-sdk/backend/Program.cs +++ b/rest-api/csharp-sdk/backend/Program.cs @@ -77,36 +77,12 @@ static void LoadDotEnv(string path = ".env") Console.Error.WriteLine("Server will continue running; API endpoints will return 503 until configured."); } -// Choose a URL to listen on: prefer ASPNETCORE_URLS, else try default ports, else pick an ephemeral port. -int FindFreePort() -{ - // First try 5000..5010 - for (int p = 5000; p <= 5010; p++) - { - try - { - var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, p); - listener.Start(); - listener.Stop(); - return p; - } - catch { } - } - - // Fallback: let OS pick an ephemeral port - var l = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); - l.Start(); - int port = ((System.Net.IPEndPoint)l.LocalEndpoint).Port; - l.Stop(); - return port; -} - // Determine the URL to listen on and configure the host. var builder = WebApplication.CreateBuilder(args); string? urls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); if (string.IsNullOrWhiteSpace(urls)) { - int port = FindFreePort(); + string port = Environment.GetEnvironmentVariable("PORT") ?? "5000"; urls = $"http://127.0.0.1:{port}"; Console.WriteLine($"No ASPNETCORE_URLS set; binding to {urls}"); builder.WebHost.UseUrls(urls); diff --git a/rest-api/csharp-sdk/frontend/src/api.js b/rest-api/csharp-sdk/frontend/src/api.js index 61081e4..006596d 100644 --- a/rest-api/csharp-sdk/frontend/src/api.js +++ b/rest-api/csharp-sdk/frontend/src/api.js @@ -1,5 +1,6 @@ // Thin fetch wrapper around the ASP.NET Core backend in ../backend. -const BASE = "http://localhost:5000"; +// In Vite dev mode, an empty base uses the same origin and is proxied by vite.config.js. +const BASE = import.meta.env.VITE_API_BASE_URL ?? ""; async function handle(res) { const body = await res.json().catch(() => null); diff --git a/rest-api/csharp-sdk/frontend/vite.config.js b/rest-api/csharp-sdk/frontend/vite.config.js index 348be56..780a7da 100644 --- a/rest-api/csharp-sdk/frontend/vite.config.js +++ b/rest-api/csharp-sdk/frontend/vite.config.js @@ -1,6 +1,8 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +const backendTarget = process.env.VITE_BACKEND_URL || "http://127.0.0.1:5000"; + export default defineConfig({ plugins: [react()], server: { @@ -8,7 +10,7 @@ export default defineConfig({ port: 5173, proxy: { "/api": { - target: "http://localhost:5000", + target: backendTarget, changeOrigin: true, secure: false, }, From f98aa9d04ba9899b51ed96424b6af70a0a411252 Mon Sep 17 00:00:00 2001 From: akshit-kujur-ThoughtSpot Date: Thu, 9 Jul 2026 09:25:47 +0000 Subject: [PATCH 5/7] Include C# SDK backend env file --- .gitignore | 3 +++ rest-api/csharp-sdk/backend/.env | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 rest-api/csharp-sdk/backend/.env diff --git a/.gitignore b/.gitignore index 8a7f4b8..e5c9c28 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,9 @@ web_modules/ .env.production.local .env.local +# Allow the C# SDK demo backend env file to be checked in for convenience +!rest-api/csharp-sdk/backend/.env + # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache diff --git a/rest-api/csharp-sdk/backend/.env b/rest-api/csharp-sdk/backend/.env new file mode 100644 index 0000000..24b3d3f --- /dev/null +++ b/rest-api/csharp-sdk/backend/.env @@ -0,0 +1,4 @@ +VITE_TS_HOST=https://training.thoughtspot.cloud +VITE_TS_USERNAME=code-sandbox +VITE_TS_PASSWORD=3mbed+#3xplz +VITE_LIVEBOARD_ID=b504e160-3025-4508-a76a-1beb1f4b5eed \ No newline at end of file From dbfa69f02a6516b41d8059628e901559faa55c01 Mon Sep 17 00:00:00 2001 From: Akshit Kujur Date: Fri, 10 Jul 2026 13:53:49 +0530 Subject: [PATCH 6/7] Restore Spotter and give the demo UI a real design pass Ask Spotter is back as a proper streaming chat view (live tool-call trace, typing indicator, markdown-rendered answers), and the whole app moved off inline styles onto a small design system: gradient accents, entrance animations, the real ThoughtSpot logo in the header, and labeled result lists so search results never look like they auto-opened something. --- rest-api/csharp-sdk/backend/.env | 4 +- rest-api/csharp-sdk/backend/Program.cs | 82 +- rest-api/csharp-sdk/frontend/index.html | 1 + .../csharp-sdk/frontend/public/ts-logo.svg | 6 + rest-api/csharp-sdk/frontend/src/App.css | 698 ++++++++++++++++++ rest-api/csharp-sdk/frontend/src/App.jsx | 119 +-- rest-api/csharp-sdk/frontend/src/api.js | 42 ++ .../src/components/LiveboardsCard.jsx | 128 ++++ .../src/components/SearchUsersCard.jsx | 89 +++ .../frontend/src/components/SpotterCard.jsx | 163 ++++ rest-api/csharp-sdk/frontend/src/icons.jsx | 17 + rest-api/csharp-sdk/frontend/src/markdown.jsx | 97 +++ 12 files changed, 1350 insertions(+), 96 deletions(-) create mode 100644 rest-api/csharp-sdk/frontend/public/ts-logo.svg create mode 100644 rest-api/csharp-sdk/frontend/src/App.css create mode 100644 rest-api/csharp-sdk/frontend/src/components/LiveboardsCard.jsx create mode 100644 rest-api/csharp-sdk/frontend/src/components/SearchUsersCard.jsx create mode 100644 rest-api/csharp-sdk/frontend/src/components/SpotterCard.jsx create mode 100644 rest-api/csharp-sdk/frontend/src/icons.jsx create mode 100644 rest-api/csharp-sdk/frontend/src/markdown.jsx diff --git a/rest-api/csharp-sdk/backend/.env b/rest-api/csharp-sdk/backend/.env index 24b3d3f..ac18b32 100644 --- a/rest-api/csharp-sdk/backend/.env +++ b/rest-api/csharp-sdk/backend/.env @@ -1,4 +1,6 @@ VITE_TS_HOST=https://training.thoughtspot.cloud VITE_TS_USERNAME=code-sandbox VITE_TS_PASSWORD=3mbed+#3xplz -VITE_LIVEBOARD_ID=b504e160-3025-4508-a76a-1beb1f4b5eed \ No newline at end of file +VITE_LIVEBOARD_ID=b504e160-3025-4508-a76a-1beb1f4b5eed +# Worksheet with AI Answer Generation enabled, used by GET /api/spotter/stream +VITE_SPOTTER_WORKSHEET_ID=cd252e5c-b552-49a8-821d-3eadaa049cca \ No newline at end of file diff --git a/rest-api/csharp-sdk/backend/Program.cs b/rest-api/csharp-sdk/backend/Program.cs index 23c518d..a3cb490 100644 --- a/rest-api/csharp-sdk/backend/Program.cs +++ b/rest-api/csharp-sdk/backend/Program.cs @@ -49,6 +49,7 @@ static void LoadDotEnv(string path = ".env") string user = Environment.GetEnvironmentVariable("VITE_TS_USERNAME") ?? Environment.GetEnvironmentVariable("TS_USER") ?? ""; string pass = Environment.GetEnvironmentVariable("VITE_TS_PASSWORD") ?? Environment.GetEnvironmentVariable("TS_PASS") ?? ""; string liveboardId = Environment.GetEnvironmentVariable("VITE_LIVEBOARD_ID") ?? Environment.GetEnvironmentVariable("TS_LIVEBOARD_ID") ?? ""; +string spotterWorksheetId = Environment.GetEnvironmentVariable("VITE_SPOTTER_WORKSHEET_ID") ?? Environment.GetEnvironmentVariable("TS_SPOTTER_WORKSHEET_ID") ?? ""; // Try to create the ThoughtSpot client but don't crash the server if it fails. ThoughtSpotRestApi? api = null; @@ -101,7 +102,7 @@ static void LoadDotEnv(string path = ".env") var app = builder.Build(); app.UseCors(); -app.MapGet("/api/health", () => Results.Ok(new { host, liveboardId })); +app.MapGet("/api/health", () => Results.Ok(new { host, liveboardId, spotterWorksheetId })); Func clientNotConfigured = () => Results.Json(new { error = "ThoughtSpot client not configured. See .env.example or set TS_USER/TS_PASS." }, statusCode: 503); @@ -178,6 +179,85 @@ string FriendlyError(ApiException ex) } }); +// 4. Spotter — ask a natural-language question, streamed live via SSE ----------- +// +// Creates a fresh agent conversation per question, then relays the +// Server-Sent Events from SendAgentConversationMessageStreamingStreamAsync +// straight through to the browser as they arrive (no buffering the full +// answer server-side first). Each upstream `data: [...]` line is +// forwarded verbatim as the default SSE "message" event; a final custom +// "done" event (or "ts-error" on failure) tells the frontend when to stop +// listening. +app.MapGet("/api/spotter/stream", async (HttpContext ctx, string query, string? worksheetId) => +{ + if (api == null) + { + ctx.Response.StatusCode = 503; + await ctx.Response.WriteAsJsonAsync(new { error = "ThoughtSpot client not configured. See .env.example or set TS_USER/TS_PASS." }); + return; + } + + string metadataId = string.IsNullOrWhiteSpace(worksheetId) ? spotterWorksheetId : worksheetId; + if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(metadataId)) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error = "A 'query' and a worksheet id (VITE_SPOTTER_WORKSHEET_ID or ?worksheetId=) are required." }); + return; + } + + ctx.Response.ContentType = "text/event-stream"; + ctx.Response.Headers.CacheControl = "no-cache"; + ctx.Response.Headers["X-Accel-Buffering"] = "no"; + + async Task WriteEventAsync(string? eventName, string data) + { + if (!string.IsNullOrEmpty(eventName)) + { + await ctx.Response.WriteAsync($"event: {eventName}\n", ctx.RequestAborted); + } + await ctx.Response.WriteAsync($"data: {data}\n\n", ctx.RequestAborted); + await ctx.Response.Body.FlushAsync(ctx.RequestAborted); + } + + try + { + // Workaround: ThoughtSpot.Client 0.1.0-beta.4's DataSourceContextInput + // always serializes its unused sibling fields (data_source_identifiers, + // guid) as explicit JSON nulls. This cluster's request validation + // rejects that shape (it falls back to expecting a `worksheet_context` + // payload instead). Bypassing the typed DataSourceContext property and + // writing the minimal object via AdditionalProperties avoids emitting + // those nulls. + var metadataContext = new ContextPayloadV2Input(type: ContextPayloadV2Input.TypeEnum.DATASOURCE); + metadataContext.AdditionalProperties["data_source_context"] = new Dictionary + { + ["data_source_identifier"] = metadataId, + }; + + AgentConversation conversation = api.CreateAgentConversation(new CreateAgentConversationRequest( + metadataContext: metadataContext, + conversationSettings: new ConversationSettingsInput())); + + var streamRequest = new SendAgentConversationMessageStreamingRequest(messages: new List { query }); + + await foreach (var payload in api.SendAgentConversationMessageStreamingStreamAsync( + conversation.ConversationIdentifier, streamRequest, ctx.RequestAborted)) + { + await WriteEventAsync(null, payload); + } + + await WriteEventAsync("done", "{}"); + } + catch (ApiException ex) + { + await WriteEventAsync("ts-error", System.Text.Json.JsonSerializer.Serialize(new { error = FriendlyError(ex) })); + } + catch (OperationCanceledException) + { + // Client navigated away / closed the EventSource — nothing to write. + } +}); + app.Run(); public static class ThoughtSpotConfiguration diff --git a/rest-api/csharp-sdk/frontend/index.html b/rest-api/csharp-sdk/frontend/index.html index fd1a10d..c937fc6 100644 --- a/rest-api/csharp-sdk/frontend/index.html +++ b/rest-api/csharp-sdk/frontend/index.html @@ -2,6 +2,7 @@ + ThoughtSpot C# SDK Demo diff --git a/rest-api/csharp-sdk/frontend/public/ts-logo.svg b/rest-api/csharp-sdk/frontend/public/ts-logo.svg new file mode 100644 index 0000000..c134264 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/public/ts-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/rest-api/csharp-sdk/frontend/src/App.css b/rest-api/csharp-sdk/frontend/src/App.css new file mode 100644 index 0000000..11e3cd1 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/App.css @@ -0,0 +1,698 @@ +:root { + color-scheme: light dark; + --bg: #f2f3fa; + --surface: #ffffff; + --surface-solid: #ffffff; + --surface-hover: #f8f8fd; + --border: #e6e7f0; + --text: #14141c; + --text-muted: #6b6f80; + --accent: #5b5bf6; + --accent-2: #ec4899; + --accent-hover: #4747e0; + --accent-soft: #eeeeff; + --danger: #dc2626; + --danger-soft: rgba(220, 38, 38, 0.08); + --radius-lg: 20px; + --radius-md: 12px; + --radius-sm: 8px; + --shadow-sm: 0 1px 2px rgba(20, 20, 40, 0.05); + --shadow-md: 0 10px 30px rgba(30, 30, 60, 0.10); + --shadow-lg: 0 18px 45px rgba(30, 30, 60, 0.16); + --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0d0d12; + --surface: #1a1a20; + --surface-solid: #1a1a20; + --surface-hover: #202028; + --border: #2c2c36; + --text: #f2f2f5; + --text-muted: #9a9aa8; + --accent: #8484fb; + --accent-2: #f472b6; + --accent-hover: #a0a0ff; + --accent-soft: rgba(132, 132, 251, 0.14); + --danger: #f87171; + --danger-soft: rgba(248, 113, 113, 0.12); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 10px 30px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 18px 45px rgba(0, 0, 0, 0.5); + } +} + +* { + box-sizing: border-box; +} + +html { + background: var(--bg); +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font); + -webkit-font-smoothing: antialiased; + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +/* Soft aurora blobs behind the content — fixed, blurred, low-opacity so text + always stays readable on top. Pure CSS, no images/deps. */ +body::before, +body::after { + content: ""; + position: fixed; + width: 42vw; + height: 42vw; + max-width: 620px; + max-height: 620px; + border-radius: 50%; + filter: blur(90px); + z-index: 0; + pointer-events: none; + opacity: 0.5; +} + +body::before { + top: -14vw; + right: -10vw; + background: radial-gradient(circle, var(--accent), transparent 70%); +} + +body::after { + bottom: -18vw; + left: -12vw; + background: radial-gradient(circle, var(--accent-2), transparent 70%); + opacity: 0.35; +} + +#root { + position: relative; + z-index: 1; +} + +.app { + max-width: 880px; + margin: 0 auto; + padding: 56px 24px 72px; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.app-header { + display: flex; + align-items: center; + gap: 18px; + margin-bottom: 40px; + animation: fadeInUp 0.5s ease both; +} + +.app-badge { + width: 56px; + height: 56px; + border-radius: 15px; + background: #ffffff; + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: var(--shadow-lg); + padding: 10px; +} + +.app-logo { + width: 100%; + height: 100%; + object-fit: contain; +} + +.app-title { + font-size: 23px; + font-weight: 750; + margin: 0; + letter-spacing: -0.01em; +} + +.app-subtitle { + color: var(--text-muted); + font-size: 14px; + margin: 3px 0 0; +} + +.app-pills { + display: flex; + gap: 6px; + margin-top: 10px; +} + +.pill { + font-size: 11px; + font-weight: 600; + padding: 3px 9px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent); + letter-spacing: 0.02em; +} + +.card { + position: relative; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 26px; + margin-bottom: 22px; + box-shadow: var(--shadow-sm); + transition: box-shadow 0.25s ease, border-color 0.25s ease, transform 0.25s ease; + animation: fadeInUp 0.5s ease both; + overflow: hidden; +} + +.card::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 3px; + background: linear-gradient(90deg, var(--accent), var(--accent-2)); + opacity: 0; + transition: opacity 0.25s ease; +} + +.card:hover { + box-shadow: var(--shadow-lg); + border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); + transform: translateY(-2px); +} + +.card:hover::before { + opacity: 1; +} + +.card:nth-of-type(1) { + animation-delay: 0.02s; +} +.card:nth-of-type(2) { + animation-delay: 0.09s; +} +.card:nth-of-type(3) { + animation-delay: 0.16s; +} + +.card-header { + display: flex; + align-items: flex-start; + gap: 14px; +} + +.card-icon { + width: 40px; + height: 40px; + border-radius: 12px; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent); +} + +.card-title { + font-size: 16px; + font-weight: 650; + margin: 0; +} + +.card-desc { + color: var(--text-muted); + font-size: 13px; + margin: 5px 0 0; + line-height: 1.55; +} + +.card-body { + margin-top: 18px; +} + +.field-row { + display: flex; + gap: 10px; +} + +.input-wrap { + position: relative; + flex: 1; + min-width: 0; + display: flex; + align-items: center; +} + +.input-icon { + position: absolute; + left: 12px; + color: var(--text-muted); + pointer-events: none; + display: flex; +} + +.input { + width: 100%; + min-width: 0; + padding: 11px 14px 11px 36px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--surface-solid); + color: var(--text); + font-size: 14px; + font-family: inherit; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-soft); +} + +.btn { + padding: 11px 20px; + border-radius: var(--radius-md); + border: none; + background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 70%, var(--accent-2))); + color: #fff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: transform 0.12s ease, box-shadow 0.2s ease, filter 0.15s ease; + white-space: nowrap; + font-family: inherit; + box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 35%, transparent); +} + +.btn:hover:not(:disabled) { + transform: translateY(-1px); + filter: brightness(1.06); + box-shadow: 0 8px 20px color-mix(in srgb, var(--accent) 45%, transparent); +} + +.btn:active:not(:disabled) { + transform: translateY(0) scale(0.97); +} + +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; + box-shadow: none; +} + +.btn-secondary { + background: var(--accent-soft); + color: var(--accent); + box-shadow: none; +} + +.btn-secondary:hover:not(:disabled) { + background: var(--border); + filter: none; + box-shadow: none; +} + +.btn-sm { + padding: 6px 12px; + font-size: 12.5px; + border-radius: var(--radius-sm); +} + +.error-banner { + background: var(--danger-soft); + color: var(--danger); + border: 1px solid rgba(220, 38, 38, 0.25); + padding: 10px 14px; + border-radius: var(--radius-md); + font-size: 13.5px; + margin-top: 14px; +} + +.section-label { + font-size: 11.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin: 20px 0 8px; +} + +.result-list { + list-style: none; + margin: 16px 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.result-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 14px; + border-radius: var(--radius-md); + background: var(--surface-solid); + border: 1px solid var(--border); + transition: background 0.15s, border-color 0.15s, transform 0.15s, box-shadow 0.15s; + animation: fadeInUp 0.35s ease both; +} + +.result-row:hover { + background: var(--surface-hover); + border-color: var(--accent); + transform: translateX(2px); + box-shadow: var(--shadow-sm); +} + +.result-main { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.avatar { + width: 30px; + height: 30px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent), var(--accent-2)); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; +} + +.result-name { + font-weight: 600; + font-size: 13.5px; +} + +.result-sub { + color: var(--text-muted); + font-size: 12px; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.empty-state { + color: var(--text-muted); + font-size: 13px; + padding: 18px 0 2px; + text-align: center; +} + +.code-block-wrap { + position: relative; + margin-top: 14px; +} + +.code-block-copy { + position: absolute; + top: 8px; + right: 8px; +} + +.code-block { + background: var(--surface-solid); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 14px 16px; + font-family: var(--mono); + font-size: 12px; + max-height: 260px; + overflow: auto; + white-space: pre; +} + +/* Custom scrollbars for the panels that actually scroll (webkit + Firefox) */ +.code-block, +.chat { + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.code-block::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.code-block::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 8px; +} + +/* Spotter chat */ + +.chat { + margin-top: 18px; + display: flex; + flex-direction: column; + gap: 18px; +} + +.chat-empty { + text-align: center; + color: var(--text-muted); + font-size: 13px; + padding: 20px 0 4px; +} + +.exchange { + animation: fadeInUp 0.35s ease both; +} + +.msg-user-row { + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.msg-user { + background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 70%, var(--accent-2))); + color: #fff; + padding: 10px 16px; + border-radius: 14px 14px 4px 14px; + max-width: 80%; + font-size: 14px; + line-height: 1.5; + box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 25%, transparent); +} + +.msg-assistant-row { + display: flex; + align-items: flex-start; + gap: 10px; +} + +.assistant-avatar { + width: 26px; + height: 26px; + border-radius: 8px; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + flex-shrink: 0; + margin-top: 2px; +} + +.msg-assistant { + flex: 1; + min-width: 0; +} + +.trace { + display: flex; + flex-direction: column; + gap: 0; + margin-bottom: 10px; + position: relative; + padding-left: 14px; + border-left: 2px solid var(--border); +} + +.trace-item { + display: flex; + align-items: flex-start; + gap: 8px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.5; + padding: 3px 0; + position: relative; +} + +.trace-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent); + flex-shrink: 0; + margin-top: 5px; + margin-left: -19px; + box-shadow: 0 0 0 3px var(--surface-solid); +} + +.answer-bubble { + background: var(--surface-solid); + border: 1px solid var(--border); + border-radius: 14px 14px 14px 4px; + padding: 14px 18px; + font-size: 14px; + line-height: 1.6; + box-shadow: var(--shadow-sm); +} + +.answer-bubble p { + margin: 6px 0; +} + +.answer-bubble ul { + margin: 6px 0; + padding-left: 20px; +} + +.answer-bubble code { + background: var(--accent-soft); + padding: 1px 5px; + border-radius: 4px; + font-size: 0.9em; +} + +.answer-bubble h4, +.answer-bubble h5, +.answer-bubble h6 { + margin: 12px 0 4px; +} + +.table-wrap { + overflow-x: auto; + margin: 10px 0; +} + +.answer-bubble table { + border-collapse: collapse; + width: 100%; + font-size: 13px; +} + +.answer-bubble th, +.answer-bubble td { + border: 1px solid var(--border); + padding: 6px 10px; + text-align: left; +} + +.answer-bubble th { + background: var(--accent-soft); + font-weight: 650; +} + +.answer-bubble tr:hover td { + background: var(--surface-hover); +} + +.cursor { + display: inline-block; + width: 2px; + height: 1em; + background: var(--accent); + margin-left: 2px; + vertical-align: -2px; + animation: blink 1s step-start infinite; +} + +@keyframes blink { + 50% { + opacity: 0; + } +} + +.typing-dots { + display: inline-flex; + gap: 3px; + align-items: center; + padding: 2px 0; +} + +.typing-dots span { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--text-muted); + animation: bounce 1.2s infinite ease-in-out; +} + +.typing-dots span:nth-child(2) { + animation-delay: 0.15s; +} + +.typing-dots span:nth-child(3) { + animation-delay: 0.3s; +} + +@keyframes bounce { + 0%, + 80%, + 100% { + transform: scale(0.6); + opacity: 0.4; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +.app-footer { + text-align: center; + color: var(--text-muted); + font-size: 12.5px; + margin-top: 40px; + animation: fadeInUp 0.5s ease both; + animation-delay: 0.22s; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + } +} diff --git a/rest-api/csharp-sdk/frontend/src/App.jsx b/rest-api/csharp-sdk/frontend/src/App.jsx index fea589d..a3b8461 100644 --- a/rest-api/csharp-sdk/frontend/src/App.jsx +++ b/rest-api/csharp-sdk/frontend/src/App.jsx @@ -1,102 +1,33 @@ -import { useState } from "react"; -import { api } from "./api.js"; - -const card = { border: "1px solid #ddd", borderRadius: 8, padding: 16, marginBottom: 16 }; -const input = { display: "block", width: "100%", margin: "6px 0", padding: 6 }; -const button = { padding: "6px 14px", marginTop: 8, cursor: "pointer" }; -const pre = { background: "#f6f6f6", padding: 10, borderRadius: 6, maxHeight: 200, overflow: "auto", fontSize: 12 }; +import "./App.css"; +import tsLogo from "/ts-logo.svg"; +import SearchUsersCard from "./components/SearchUsersCard.jsx"; +import LiveboardsCard from "./components/LiveboardsCard.jsx"; +import SpotterCard from "./components/SpotterCard.jsx"; export default function App() { return ( -
    -

    ThoughtSpot C# SDK — Full-Stack Demo

    +
    +
    +
    + ThoughtSpot logo +
    +
    +

    ThoughtSpot C# SDK — Full-Stack Demo

    +

    ASP.NET Core backend + React frontend, wrapping ThoughtSpot.Client

    +
    + C# + ASP.NET Core + React + Server-Sent Events +
    +
    +
    - -
    - ); -} - -function SearchUsersCard() { - const [query, setQuery] = useState(""); - const [users, setUsers] = useState([]); - const [error, setError] = useState(null); + + - const search = async (e) => { - e.preventDefault(); - setError(null); - try { - setUsers(await api.searchUsers(query)); - } catch (err) { - setError(err.message); - } - }; - - return ( -
    -

    1. Search users

    - - setQuery(e.target.value)} /> - - - {error &&

    {error}

    } -
      - {users.map((u) => ( -
    • {u.name} — {u.displayName}
    • - ))} -
    -
    - ); -} - -function LiveboardCard() { - const [query, setQuery] = useState(""); - const [liveboards, setLiveboards] = useState([]); - const [tml, setTml] = useState(null); - const [error, setError] = useState(null); - - const search = async (e) => { - e.preventDefault(); - setError(null); - try { - const results = await api.searchLiveboards(query); - setLiveboards(results); - } catch (err) { - setError(err.message); - } - }; - - const loadTml = async (id) => { - setError(null); - setTml(null); - try { - setTml(await api.exportTml(id)); - } catch (err) { - setError(err.message); - } - }; - - return ( -
    -

    2–3. Search / TML-export a liveboard

    -
    - setQuery(e.target.value)} /> - -
    - {error &&

    {error}

    } - -
      - {liveboards.map((lb) => ( -
    • - {lb.name}{" "} - -
    • - ))} -
    - - {tml &&
    {JSON.stringify(tml, null, 2)}
    } -
    +
    Built on ThoughtSpot.Client — see backend/Program.cs for the REST calls behind each card.
    +
    ); } diff --git a/rest-api/csharp-sdk/frontend/src/api.js b/rest-api/csharp-sdk/frontend/src/api.js index 006596d..fcedc49 100644 --- a/rest-api/csharp-sdk/frontend/src/api.js +++ b/rest-api/csharp-sdk/frontend/src/api.js @@ -16,4 +16,46 @@ export const api = { fetch(`${BASE}/api/liveboards?query=${encodeURIComponent(query ?? "")}&size=${size}`).then(handle), exportTml: (id) => fetch(`${BASE}/api/liveboards/${id}/tml`).then(handle), + + // Opens a live SSE connection to /api/spotter/stream and relays each + // ThoughtSpot agent event to the caller as it arrives — no waiting for + // the full answer before showing anything. + // onEvent(event) — one raw Spotter event object (type: ack | notification | text-chunk | text | answer | ...) + // onError(message) — a fatal error occurred; the stream is already closed + // onDone() — the agent finished responding; the stream is already closed + // Returns a `stop()` function that closes the connection early. + streamSpotter: (query, { onEvent, onError, onDone }) => { + const url = `${BASE}/api/spotter/stream?query=${encodeURIComponent(query)}`; + const source = new EventSource(url); + + source.onmessage = (e) => { + try { + const batch = JSON.parse(e.data); + batch.forEach((evt) => onEvent(evt)); + } catch { + // Ignore malformed/keepalive lines. + } + }; + + source.addEventListener("done", () => { + source.close(); + onDone(); + }); + + source.addEventListener("ts-error", (e) => { + const { error } = JSON.parse(e.data); + source.close(); + onError(error); + }); + + // Fires if the connection drops before a "done"/"ts-error" frame arrives + // (backend crashed, network blip, etc). EventSource retries by default, + // so close it explicitly to stop that and surface the failure instead. + source.onerror = () => { + source.close(); + onError("Lost connection to the backend while streaming."); + }; + + return () => source.close(); + }, }; diff --git a/rest-api/csharp-sdk/frontend/src/components/LiveboardsCard.jsx b/rest-api/csharp-sdk/frontend/src/components/LiveboardsCard.jsx new file mode 100644 index 0000000..2b20e3b --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/components/LiveboardsCard.jsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import { api } from "../api.js"; +import { SearchIcon, CopyIcon } from "../icons.jsx"; + +export default function LiveboardsCard() { + const [query, setQuery] = useState(""); + const [liveboards, setLiveboards] = useState([]); + const [tml, setTml] = useState(null); + const [tmlForId, setTmlForId] = useState(null); + const [error, setError] = useState(null); + const [searching, setSearching] = useState(false); + const [exportingId, setExportingId] = useState(null); + const [searched, setSearched] = useState(false); + const [copied, setCopied] = useState(false); + + const search = async (e) => { + e.preventDefault(); + setError(null); + setSearching(true); + // A previous export shouldn't linger under a fresh set of results — + // it read as if the first result had already been opened for you. + setTml(null); + setTmlForId(null); + try { + setLiveboards(await api.searchLiveboards(query)); + } catch (err) { + setError(err.message); + } finally { + setSearching(false); + setSearched(true); + } + }; + + const loadTml = async (lb) => { + setError(null); + setTml(null); + setCopied(false); + setTmlForId(lb.id); + setExportingId(lb.id); + try { + setTml(await api.exportTml(lb.id)); + } catch (err) { + setError(err.message); + } finally { + setExportingId(null); + } + }; + + const copyTml = async () => { + await navigator.clipboard.writeText(JSON.stringify(tml, null, 2)); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + return ( +
    +
    +
    📊
    +
    +

    Search & export liveboards

    +

    + Search liveboards, then export any result's TML via ExportMetadataTML. +

    +
    +
    + +
    +
    +
    + + setQuery(e.target.value)} + /> +
    + +
    + + {error &&

    {error}

    } + + {liveboards.length > 0 && ( + <> +

    Liveboards ({liveboards.length})

    +
      + {liveboards.map((lb, i) => ( +
    • + {lb.name} + +
    • + ))} +
    + + )} + + {searched && !searching && liveboards.length === 0 && !error && ( +

    No liveboards matched that filter.

    + )} + + {tml && ( +
    + +
    +              {liveboards.find((lb) => lb.id === tmlForId)?.name ? (
    +                
    +                  // {liveboards.find((lb) => lb.id === tmlForId).name}
    +                  {"\n"}
    +                
    +              ) : null}
    +              {JSON.stringify(tml, null, 2)}
    +            
    +
    + )} +
    +
    + ); +} diff --git a/rest-api/csharp-sdk/frontend/src/components/SearchUsersCard.jsx b/rest-api/csharp-sdk/frontend/src/components/SearchUsersCard.jsx new file mode 100644 index 0000000..7efd82a --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/components/SearchUsersCard.jsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { api } from "../api.js"; +import { SearchIcon } from "../icons.jsx"; + +const initials = (name) => + (name ?? "?") + .trim() + .split(/\s+/) + .slice(0, 2) + .map((w) => w[0]?.toUpperCase()) + .join("") || "?"; + +export default function SearchUsersCard() { + const [query, setQuery] = useState(""); + const [users, setUsers] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [searched, setSearched] = useState(false); + + const search = async (e) => { + e.preventDefault(); + setError(null); + setLoading(true); + try { + setUsers(await api.searchUsers(query)); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + setSearched(true); + } + }; + + return ( +
    +
    +
    👤
    +
    +

    Search users

    +

    + Calls SearchUsers against the configured cluster and lists matching accounts. +

    +
    +
    + +
    +
    +
    + + setQuery(e.target.value)} + /> +
    + +
    + + {error &&

    {error}

    } + + {users.length > 0 && ( + <> +

    Users ({users.length})

    +
      + {users.map((u, i) => ( +
    • +
      +
      {initials(u.displayName ?? u.name)}
      +
      +
      {u.displayName ?? u.name}
      +
      {u.name}
      +
      +
      +
    • + ))} +
    + + )} + + {searched && !loading && users.length === 0 && !error && ( +

    No users matched that filter.

    + )} +
    +
    + ); +} diff --git a/rest-api/csharp-sdk/frontend/src/components/SpotterCard.jsx b/rest-api/csharp-sdk/frontend/src/components/SpotterCard.jsx new file mode 100644 index 0000000..5f0b3af --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/components/SpotterCard.jsx @@ -0,0 +1,163 @@ +import { useEffect, useRef, useState } from "react"; +import { api } from "../api.js"; +import { renderMarkdown } from "../markdown.jsx"; +import { SearchIcon } from "../icons.jsx"; + +// Some "thinking" chunks echo raw tool output (dataset schemas, JSON blobs) +// that can run to several KB — fine for a backend log, unreadable as a +// single trace line. Clip it so the trace stays scannable. +const TRACE_CHAR_LIMIT = 160; +const clipTrace = (text) => + text.length > TRACE_CHAR_LIMIT ? `${text.slice(0, TRACE_CHAR_LIMIT).trim()}…` : text; + +// One row of the muted trace above the answer — tool calls and the model's +// "thinking" chunks, so the user can see what Spotter is doing while it works. +function TraceItem({ event }) { + if (event.type === "notification") { + return ( +
    + + {event.metadata?.tool_title ?? event.code} +
    + ); + } + if (event.type === "text-chunk" || event.type === "text") { + return ( +
    + + {clipTrace(event.content)} +
    + ); + } + return null; +} + +function Exchange({ entry }) { + const isAnswering = entry.streaming && entry.answer.length > 0; + const isThinking = entry.streaming && entry.answer.length === 0; + + return ( +
    +
    +
    {entry.query}
    +
    +
    +
    +
    + {entry.events.length > 0 && ( +
    + {entry.events.map((evt, i) => )} +
    + )} + + {isThinking && ( +
    + + + +
    + )} + + {entry.answer && ( +
    + {renderMarkdown(entry.answer)} + {isAnswering && } +
    + )} + + {entry.error &&

    {entry.error}

    } +
    +
    +
    + ); +} + +export default function SpotterCard() { + const [query, setQuery] = useState("total sales by product type"); + const [history, setHistory] = useState([]); + const [streaming, setStreaming] = useState(false); + const stopRef = useRef(null); + const bottomRef = useRef(null); + + // Close any open stream if the user navigates away mid-answer. + useEffect(() => () => stopRef.current?.(), []); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + }, [history]); + + const ask = (e) => { + e.preventDefault(); + stopRef.current?.(); + + const id = `${Date.now()}-${history.length}`; + const askedQuery = query; + setHistory((h) => [...h, { id, query: askedQuery, events: [], answer: "", streaming: true, error: null }]); + setStreaming(true); + + const update = (patch) => + setHistory((h) => h.map((entry) => (entry.id === id ? { ...entry, ...patch(entry) } : entry))); + + stopRef.current = api.streamSpotter(askedQuery, { + onEvent: (evt) => { + const isThinking = evt.metadata?.type === "thinking"; + const isAnswerChunk = (evt.type === "text-chunk" || evt.type === "text") && !isThinking; + update((entry) => + isAnswerChunk + ? { answer: entry.answer + evt.content } + : { events: [...entry.events, evt] } + ); + }, + onError: (message) => { + update(() => ({ error: message, streaming: false })); + setStreaming(false); + }, + onDone: () => { + update(() => ({ streaming: false })); + setStreaming(false); + }, + }); + }; + + return ( +
    +
    +
    +
    +

    Ask Spotter

    +

    + Streams a Spotter agent conversation live via Server-Sent Events, against the + worksheet configured via VITE_SPOTTER_WORKSHEET_ID. +

    +
    +
    + +
    + {history.length === 0 &&

    Ask a question about your data to get started.

    } + + {history.length > 0 && ( +
    + {history.map((entry) => )} +
    +
    + )} + +
    +
    + + setQuery(e.target.value)} + required + /> +
    + +
    +
    +
    + ); +} diff --git a/rest-api/csharp-sdk/frontend/src/icons.jsx b/rest-api/csharp-sdk/frontend/src/icons.jsx new file mode 100644 index 0000000..387bd85 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/icons.jsx @@ -0,0 +1,17 @@ +export function SearchIcon() { + return ( + + + + + ); +} + +export function CopyIcon() { + return ( + + + + + ); +} diff --git a/rest-api/csharp-sdk/frontend/src/markdown.jsx b/rest-api/csharp-sdk/frontend/src/markdown.jsx new file mode 100644 index 0000000..02165f0 --- /dev/null +++ b/rest-api/csharp-sdk/frontend/src/markdown.jsx @@ -0,0 +1,97 @@ +// Minimal Markdown -> React renderer covering what Spotter answers actually +// use (tables, headings, bullet lists, bold/code) — avoids pulling in a full +// markdown library for a demo app. + +function renderInline(text, keyPrefix) { + const parts = []; + const re = /\*\*([^*]+)\*\*|`([^`]+)`/g; + let last = 0, match, i = 0; + while ((match = re.exec(text))) { + if (match.index > last) parts.push(text.slice(last, match.index)); + parts.push( + match[1] !== undefined + ? {match[1]} + : {match[2]} + ); + last = match.index + match[0].length; + } + parts.push(text.slice(last)); + return parts; +} + +const splitTableRow = (line) => + line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim()); + +const isTableSeparator = (line) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line ?? ""); + +export function renderMarkdown(text) { + const lines = text.split("\n"); + const blocks = []; + let i = 0, key = 0; + + while (i < lines.length) { + const line = lines[i]; + + if (!line.trim()) { + i++; + continue; + } + + if (line.includes("|") && isTableSeparator(lines[i + 1])) { + const headers = splitTableRow(line); + i += 2; + const rows = []; + while (i < lines.length && lines[i].trim() && lines[i].includes("|")) { + rows.push(splitTableRow(lines[i])); + i++; + } + blocks.push( +
    + + + {headers.map((h, c) => )} + + + {rows.map((row, r) => ( + {row.map((cell, c) => )} + ))} + +
    {renderInline(h, `h${c}`)}
    {renderInline(cell, `${r}-${c}`)}
    +
    + ); + continue; + } + + const heading = /^(#{1,3})\s+(.*)/.exec(line); + if (heading) { + const Tag = `h${Math.min(heading[1].length + 3, 6)}`; + blocks.push({renderInline(heading[2], `hd${key}`)}); + i++; + continue; + } + + if (/^\s*[-*]\s+/.test(line)) { + const items = []; + while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) { + items.push(lines[i].replace(/^\s*[-*]\s+/, "")); + i++; + } + blocks.push( +
      + {items.map((item, idx) =>
    • {renderInline(item, `li${key}-${idx}`)}
    • )} +
    + ); + continue; + } + + const paraLines = [line]; + i++; + while (i < lines.length && lines[i].trim() && !lines[i].includes("|") && !/^\s*[-*#]/.test(lines[i])) { + paraLines.push(lines[i]); + i++; + } + blocks.push(

    {renderInline(paraLines.join(" "), `p${key}`)}

    ); + } + + return blocks; +} From 5acad5c56a1ef79ee26b696c3f60fbaec7010f89 Mon Sep 17 00:00:00 2001 From: Akshit Kujur Date: Fri, 10 Jul 2026 14:15:08 +0530 Subject: [PATCH 7/7] Move to the published thoughtspot_rest_api_sdk NuGet package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK was republished under this package name/namespace (was ThoughtSpot.Client); bump to 0.1.0-beta.7 and update references accordingly. Verified live against the cluster — search, TML export, and Spotter streaming all still work, and the DataSourceContextInput null-serialization workaround is still needed on this version. --- rest-api/csharp-sdk/README.md | 41 +++++++++++++++++----- rest-api/csharp-sdk/backend/Backend.csproj | 4 +-- rest-api/csharp-sdk/backend/Program.cs | 22 ++++++------ rest-api/csharp-sdk/frontend/src/App.jsx | 4 +-- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/rest-api/csharp-sdk/README.md b/rest-api/csharp-sdk/README.md index 43a9837..3519d61 100644 --- a/rest-api/csharp-sdk/README.md +++ b/rest-api/csharp-sdk/README.md @@ -1,6 +1,6 @@ - + + diff --git a/rest-api/csharp-sdk/backend/Program.cs b/rest-api/csharp-sdk/backend/Program.cs index a3cb490..84a9e3f 100644 --- a/rest-api/csharp-sdk/backend/Program.cs +++ b/rest-api/csharp-sdk/backend/Program.cs @@ -1,8 +1,8 @@ using System.IO; -using ThoughtSpot.Client; -using ThoughtSpot.Client.Api; -using ThoughtSpot.Client.Client; -using ThoughtSpot.Client.Model; +using thoughtspot_rest_api_sdk; +using thoughtspot_rest_api_sdk.Api; +using thoughtspot_rest_api_sdk.Client; +using thoughtspot_rest_api_sdk.Model; // Loads key=value lines from a .env file into process environment variables @@ -221,13 +221,13 @@ async Task WriteEventAsync(string? eventName, string data) try { - // Workaround: ThoughtSpot.Client 0.1.0-beta.4's DataSourceContextInput - // always serializes its unused sibling fields (data_source_identifiers, - // guid) as explicit JSON nulls. This cluster's request validation - // rejects that shape (it falls back to expecting a `worksheet_context` - // payload instead). Bypassing the typed DataSourceContext property and - // writing the minimal object via AdditionalProperties avoids emitting - // those nulls. + // Workaround: thoughtspot_rest_api_sdk's DataSourceContextInput always + // serializes its unused sibling fields (data_source_identifiers, guid) + // as explicit JSON nulls (still true as of 0.1.0-beta.7). This + // cluster's request validation rejects that shape (it falls back to + // expecting a `worksheet_context` payload instead). Bypassing the + // typed DataSourceContext property and writing the minimal object via + // AdditionalProperties avoids emitting those nulls. var metadataContext = new ContextPayloadV2Input(type: ContextPayloadV2Input.TypeEnum.DATASOURCE); metadataContext.AdditionalProperties["data_source_context"] = new Dictionary { diff --git a/rest-api/csharp-sdk/frontend/src/App.jsx b/rest-api/csharp-sdk/frontend/src/App.jsx index a3b8461..c23e13c 100644 --- a/rest-api/csharp-sdk/frontend/src/App.jsx +++ b/rest-api/csharp-sdk/frontend/src/App.jsx @@ -13,7 +13,7 @@ export default function App() {

    ThoughtSpot C# SDK — Full-Stack Demo

    -

    ASP.NET Core backend + React frontend, wrapping ThoughtSpot.Client

    +

    ASP.NET Core backend + React frontend, wrapping thoughtspot_rest_api_sdk

    C# ASP.NET Core @@ -27,7 +27,7 @@ export default function App() { -
    Built on ThoughtSpot.Client — see backend/Program.cs for the REST calls behind each card.
    +
    ); }