Skip to content

Commit 8e9c03b

Browse files
Added c# and react developer examples
1 parent 1b2eae4 commit 8e9c03b

14 files changed

Lines changed: 2430 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "ThoughtSpot C# SDK demo",
3+
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
4+
"features": {
5+
"ghcr.io/devcontainers/features/node:1": {
6+
"version": "20"
7+
}
8+
},
9+
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/rest-api/csharp-sdk",
10+
"postCreateCommand": "cd frontend && npm install",
11+
"forwardPorts": [5000, 5173],
12+
"portsAttributes": {
13+
"5000": {
14+
"label": "Backend (ASP.NET Core API)",
15+
"onAutoForward": "notify"
16+
},
17+
"5173": {
18+
"label": "Frontend (Vite)",
19+
"onAutoForward": "openPreview"
20+
}
21+
},
22+
"customizations": {
23+
"vscode": {
24+
"extensions": [
25+
"ms-dotnettools.csharp",
26+
"dbaeumer.vscode-eslint"
27+
]
28+
}
29+
}
30+
}

rest-api/csharp-sdk/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!-- search-meta
2+
tags: [csharp, dotnet, aspnet-core, rest-api, thoughtspot-client-sdk, react, vite, liveboard-export, tml]
3+
apis: [ThoughtSpotRestApi, ApiClientConfiguration, UserApi, LiveboardApi, ExportApi]
4+
questions:
5+
- How do I build a full-stack ThoughtSpot app with the C# SDK?
6+
- How do I call the ThoughtSpot REST API from an ASP.NET Core backend?
7+
- How do I export a liveboard PDF from a C# backend?
8+
- How do I create a React frontend that talks to a C# ThoughtSpot API?
9+
- How do I search users and liveboards with the ThoughtSpot C# SDK?
10+
-->
11+
12+
# ThoughtSpot C# SDK — full-stack demo
13+
14+
A minimal two-process demo covering user creation, style customization,
15+
search users, search liveboards, export liveboard (PDF), and export TML.
16+
17+
## Try it in GitHub Codespaces
18+
19+
Open this example in a ready-to-run cloud environment:
20+
21+
[Open in GitHub Codespaces](https://github.com/codespaces/new?hide_repo_select=true&ref=SCAL-322334-add-csharp-developer-examples&repo=thoughtspot%2Fdeveloper-examples)
22+
23+
Once the environment starts, open the example folder in the editor with:
24+
25+
```bash
26+
cd /workspaces/developer-examples/rest-api/csharp-sdk
27+
code .
28+
```
29+
30+
Then follow the steps below in two terminals to run the backend and frontend.
31+
32+
```
33+
backend/ ASP.NET Core minimal API (C#) wrapping the ThoughtSpot.Client SDK
34+
frontend/ Vite + React app that calls the backend
35+
```
36+
37+
## Configure
38+
39+
Both processes talk to the same cluster. Set these before running the backend
40+
(defaults point at the same demo cluster used elsewhere in this repo):
41+
42+
```
43+
export TS_HOST=https://<your-cluster>
44+
# use the cluster base URL (for example https://my-company.thoughtspot.cloud)
45+
# not the legacy /v2 path
46+
export TS_USER=<username>
47+
export TS_PASS=<password>
48+
```
49+
50+
## Run
51+
52+
```bash
53+
# terminal 1
54+
cd backend
55+
dotnet run # listens on http://localhost:5000
56+
57+
# terminal 2
58+
cd frontend
59+
npm install
60+
npm run dev # listens on http://localhost:5173
61+
```
62+
63+
Open http://localhost:5173.
64+
65+
## Notes
66+
67+
- The backend authenticates via `ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration { ... })`
68+
once at startup, rather than the legacy `HttpClient`/`HttpClientHandler`
69+
constructors. `CreateAsync` builds its own `SocketsHttpHandler`/`HttpClient`
70+
internally and is what actually gives you `ConnectTimeout`/`ReadTimeout`/
71+
`WriteTimeout`, connection pooling, SSL handling, and automatic bearer-token
72+
fetch + refresh. None of that is wired up if you construct the client from
73+
your own `HttpClient`/`HttpClientHandler`.
74+
- CORS is locked to `http://localhost:5173` in `backend/Program.cs` — update
75+
if you serve the frontend elsewhere.
76+
- "Export PDF" fetches the file as a blob and triggers a normal browser
77+
download; if the export fails (auth, bad liveboard id, etc.) the error
78+
message from the backend is shown in the UI instead of failing silently.
79+
- `frontend/src/LiveboardEmbedView.jsx` is left over from an earlier version
80+
that embedded a live Liveboard via the Visual Embed SDK. It's unused now
81+
(no `@thoughtspot/visual-embed-sdk` credentials/setup here) — safe to
82+
delete if you don't plan to wire embedding back in.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Xunit;
2+
3+
namespace ThoughtSpotBackend.Tests;
4+
5+
public class ConfigurationTests
6+
{
7+
[Theory]
8+
[InlineData("https://foo.thoughtspot.cloud", "https://foo.thoughtspot.cloud")]
9+
[InlineData("https://foo.thoughtspot.cloud/", "https://foo.thoughtspot.cloud")]
10+
[InlineData("https://foo.thoughtspot.cloud/v2", "https://foo.thoughtspot.cloud")]
11+
[InlineData("https://foo.thoughtspot.cloud/v2/", "https://foo.thoughtspot.cloud")]
12+
[InlineData("foo.thoughtspot.cloud", "https://foo.thoughtspot.cloud")]
13+
[InlineData("foo.thoughtspot.cloud/", "https://foo.thoughtspot.cloud")]
14+
public void NormalizeHost_ProducesExpectedBaseUrl(string input, string expected)
15+
{
16+
var normalized = ThoughtSpotConfiguration.NormalizeHost(input);
17+
Assert.Equal(expected, normalized);
18+
}
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
<IsTestProject>true</IsTestProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
12+
<PackageReference Include="xunit" Version="2.9.2" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
<PrivateAssets>all</PrivateAssets>
16+
</PackageReference>
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="../backend/Backend.csproj" />
21+
</ItemGroup>
22+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>ThoughtSpotBackend</RootNamespace>
8+
<!-- Only .NET 8 SDK is used to build, but let the app run against
9+
whatever newer major runtime is actually installed on this machine
10+
(e.g. 10.0.x), instead of failing because 8.0.x isn't present. -->
11+
<RollForward>LatestMajor</RollForward>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<!-- Published SDK package: https://www.nuget.org/packages/ThoughtSpot.Client/0.1.0-beta.4 -->
16+
<PackageReference Include="ThoughtSpot.Client" Version="0.1.0-beta.4" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
using ThoughtSpot.Client;
2+
using ThoughtSpot.Client.Api;
3+
using ThoughtSpot.Client.Client;
4+
using ThoughtSpot.Client.Model;
5+
6+
// ─────────────────────────────────────────────────────────────────────────────
7+
// ThoughtSpot C# SDK — full-stack example (backend)
8+
//
9+
// A tiny ASP.NET Core minimal API that wraps the ThoughtSpot.Client SDK and
10+
// exposes six JSON/file endpoints for the React frontend in ../frontend:
11+
//
12+
// POST /api/users — create a user
13+
// POST /api/style — update style customization
14+
// GET /api/users — search users
15+
// GET /api/liveboards — search liveboards
16+
// GET /api/liveboards/{id}/export — export a liveboard as PDF
17+
// GET /api/liveboards/{id}/tml — export a liveboard's TML
18+
//
19+
// Run with: dotnet run (from this backend/ folder)
20+
// Config via env vars: TS_HOST, TS_USER, TS_PASS (see defaults below).
21+
//
22+
// Uses ThoughtSpotRestApi.CreateAsync(...) instead of the legacy
23+
// HttpClient/HttpClientHandler constructors. CreateAsync builds its own
24+
// SocketsHttpHandler/HttpClient internally, which is what actually gives you
25+
// ConnectTimeout / ReadTimeout / WriteTimeout, connection pooling, SSL
26+
// handling, and automatic bearer-token fetch + refresh (TokenInjectingHandler).
27+
// None of that is wired up if you build the client from your own
28+
// HttpClient/HttpClientHandler via the legacy constructors.
29+
// ─────────────────────────────────────────────────────────────────────────────
30+
31+
string host = ThoughtSpotConfiguration.NormalizeHost(Environment.GetEnvironmentVariable("TS_HOST"));
32+
string user = Environment.GetEnvironmentVariable("TS_USER") ?? "";
33+
string pass = Environment.GetEnvironmentVariable("TS_PASS") ?? "";
34+
35+
var api = await ThoughtSpotRestApi.CreateAsync(new ApiClientConfiguration
36+
{
37+
Host = host,
38+
Username = user,
39+
Password = pass,
40+
TokenValiditySeconds = 3600,
41+
// Self-signed dev/demo clusters. Remove for production.
42+
IgnoreSslErrors = true,
43+
});
44+
45+
var builder = WebApplication.CreateBuilder(args);
46+
builder.Services.AddCors(options =>
47+
{
48+
options.AddDefaultPolicy(policy =>
49+
policy.WithOrigins(
50+
"http://localhost:5173",
51+
"http://127.0.0.1:5173",
52+
"http://0.0.0.0:5173")
53+
.AllowAnyHeader()
54+
.AllowAnyMethod());
55+
});
56+
57+
var app = builder.Build();
58+
app.UseCors();
59+
60+
app.MapGet("/api/health", () => Results.Ok(new { host }));
61+
62+
// 1. User creation ------------------------------------------------------------
63+
app.MapPost("/api/users", (CreateUserBody body) =>
64+
{
65+
var request = new CreateUserRequest(
66+
name: body.Name,
67+
displayName: body.DisplayName,
68+
password: body.Password,
69+
email: body.Email,
70+
accountType: CreateUserRequest.AccountTypeEnum.LOCALUSER,
71+
accountStatus: CreateUserRequest.AccountStatusEnum.ACTIVE,
72+
triggerWelcomeEmail: false,
73+
triggerActivationEmail: false);
74+
75+
try
76+
{
77+
User created = api.CreateUser(request);
78+
return Results.Ok(new { id = created.Id, name = created.Name });
79+
}
80+
catch (ApiException ex)
81+
{
82+
return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode);
83+
}
84+
});
85+
86+
// 2. Style customization -------------------------------------------------------
87+
app.MapPost("/api/style", (StyleBody body) =>
88+
{
89+
try
90+
{
91+
api.UpdateStyleCustomization(
92+
scope: "ORG",
93+
operation: "REPLACE",
94+
navigationPanel: new NavigationPanelInput(
95+
theme: NavigationPanelInput.ThemeEnum.CUSTOM,
96+
baseColor: body.BaseColor),
97+
embeddedFooterText: body.FooterText);
98+
99+
return Results.Ok(new { success = true });
100+
}
101+
catch (ApiException ex)
102+
{
103+
return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode);
104+
}
105+
});
106+
107+
// 3. Search users ---------------------------------------------------------------
108+
app.MapGet("/api/users", (string? query, int size) =>
109+
{
110+
string? namePattern = string.IsNullOrWhiteSpace(query) ? null : $"%{query}%";
111+
var request = new SearchUsersRequest(
112+
namePattern: namePattern ?? null!,
113+
recordSize: size <= 0 ? 10 : size);
114+
115+
List<User> users = api.SearchUsers(request);
116+
return Results.Ok(users.Select(u => new { id = u.Id, name = u.Name, displayName = u.DisplayName }));
117+
});
118+
119+
// 4. Search liveboards ------------------------------------------------------------
120+
app.MapGet("/api/liveboards", (string? query, int size) =>
121+
{
122+
string? namePattern = string.IsNullOrWhiteSpace(query) ? null : $"%{query}%";
123+
var metadataItem = new MetadataListItemInput(
124+
type: MetadataListItemInput.TypeEnum.LIVEBOARD,
125+
namePattern: namePattern ?? null!);
126+
127+
var request = new SearchMetadataRequest(
128+
metadata: new List<MetadataListItemInput> { metadataItem },
129+
recordSize: size <= 0 ? 10 : size);
130+
131+
List<MetadataSearchResponse> results = api.SearchMetadata(request);
132+
return Results.Ok(results.Select(r => new { id = r.MetadataId, name = r.MetadataName }));
133+
});
134+
135+
// 5. Export liveboard (PDF) --------------------------------------------------------
136+
app.MapGet("/api/liveboards/{id}/export", (string id) =>
137+
{
138+
var request = new ExportLiveboardReportRequest(
139+
metadataIdentifier: id,
140+
fileFormat: ExportLiveboardReportRequest.FileFormatEnum.PDF);
141+
142+
try
143+
{
144+
FileParameter file = api.ExportLiveboardReport(request);
145+
using var ms = new MemoryStream();
146+
file.Content.CopyTo(ms);
147+
// Explicit content-length + attachment disposition so the browser's
148+
// fetch()/blob download on the frontend gets a well-formed response
149+
// instead of a chunked stream with no size hint.
150+
var bytes = ms.ToArray();
151+
return Results.Bytes(bytes, "application/pdf", $"liveboard-{id}.pdf");
152+
}
153+
catch (ApiException ex)
154+
{
155+
return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode);
156+
}
157+
});
158+
159+
// 6. Export TML ---------------------------------------------------------------------
160+
app.MapGet("/api/liveboards/{id}/tml", (string id) =>
161+
{
162+
var request = new ExportMetadataTMLRequest(
163+
metadata: new List<ExportMetadataTypeInput>
164+
{
165+
new ExportMetadataTypeInput(type: ExportMetadataTypeInput.TypeEnum.LIVEBOARD, identifier: id)
166+
},
167+
exportFqn: true,
168+
edocFormat: ExportMetadataTMLRequest.EdocFormatEnum.JSON);
169+
170+
try
171+
{
172+
List<Object> tml = api.ExportMetadataTML(request);
173+
return Results.Ok(tml);
174+
}
175+
catch (ApiException ex)
176+
{
177+
return Results.Json(new { error = ex.Message }, statusCode: ex.ErrorCode);
178+
}
179+
});
180+
181+
app.Run();
182+
183+
public static class ThoughtSpotConfiguration
184+
{
185+
public static string NormalizeHost(string? host)
186+
{
187+
if (string.IsNullOrWhiteSpace(host))
188+
{
189+
return "https://try-everywhere.thoughtspot.cloud";
190+
}
191+
192+
var trimmed = host.Trim();
193+
if (!trimmed.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
194+
!trimmed.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
195+
{
196+
trimmed = $"https://{trimmed}";
197+
}
198+
199+
if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri))
200+
{
201+
throw new ArgumentException($"The TS_HOST value '{host}' is not a valid URL.", nameof(host));
202+
}
203+
204+
var path = uri.AbsolutePath.Trim('/');
205+
if (path.Equals("v2", StringComparison.OrdinalIgnoreCase))
206+
{
207+
path = string.Empty;
208+
}
209+
210+
var builder = new UriBuilder(uri)
211+
{
212+
Path = string.IsNullOrEmpty(path) ? string.Empty : $"/{path}",
213+
Query = string.Empty,
214+
Fragment = string.Empty
215+
};
216+
217+
return builder.Uri.GetLeftPart(UriPartial.Authority) + (builder.Path.Length > 1 ? builder.Path : string.Empty);
218+
}
219+
}
220+
221+
record CreateUserBody(string Name, string DisplayName, string Email, string Password);
222+
record StyleBody(string BaseColor, string FooterText);

0 commit comments

Comments
 (0)