From 83ee144bf3a244e401d3b19d5dfccac4e27ec2ef Mon Sep 17 00:00:00 2001 From: Teesofttech Date: Fri, 12 Jun 2026 06:40:20 +0100 Subject: [PATCH] infra: set up test project, examples project, NuGet metadata, and CI/CD pipelines - Tests: add Moq + RichardSzalay.MockHttp and SDK project reference to TableauSharp.Tests - Examples: create samples/TableauSharp.Examples console project with runnable examples covering Auth, Users/Groups, Projects, Workbooks, DataSources, Permissions, and Embedding - NuGet: add PackageId, Version, Authors, Description, tags, repo URL, README, and symbol package configuration to TableauSharp.csproj - CI: .github/workflows/ci.yml builds and tests on every push/PR to master - Publish: .github/workflows/publish-nuget.yml packs and publishes to NuGet on v* tags; set NUGET_API_KEY in repository secrets before releasing Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 50 ++++++++ .github/workflows/publish-nuget.yml | 65 +++++++++++ TableauSharp.sln | 59 ++++++++++ .../Examples/AuthExamples.cs | 60 ++++++++++ .../Examples/DataSourceExamples.cs | 71 ++++++++++++ .../Examples/EmbeddingExamples.cs | 76 +++++++++++++ .../Examples/PermissionExamples.cs | 103 +++++++++++++++++ .../Examples/ProjectExamples.cs | 102 +++++++++++++++++ .../Examples/UserGroupExamples.cs | 107 ++++++++++++++++++ .../Examples/WorkbookExamples.cs | 103 +++++++++++++++++ samples/TableauSharp.Examples/Program.cs | 73 ++++++++++++ .../TableauSharp.Examples.csproj | 22 ++++ .../TableauSharp.Examples/appsettings.json | 20 ++++ src/TableauSharp/TableauSharp.csproj | 19 ++++ .../TableauSharp.Tests.csproj | 6 + 15 files changed, 936 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-nuget.yml create mode 100644 samples/TableauSharp.Examples/Examples/AuthExamples.cs create mode 100644 samples/TableauSharp.Examples/Examples/DataSourceExamples.cs create mode 100644 samples/TableauSharp.Examples/Examples/EmbeddingExamples.cs create mode 100644 samples/TableauSharp.Examples/Examples/PermissionExamples.cs create mode 100644 samples/TableauSharp.Examples/Examples/ProjectExamples.cs create mode 100644 samples/TableauSharp.Examples/Examples/UserGroupExamples.cs create mode 100644 samples/TableauSharp.Examples/Examples/WorkbookExamples.cs create mode 100644 samples/TableauSharp.Examples/Program.cs create mode 100644 samples/TableauSharp.Examples/TableauSharp.Examples.csproj create mode 100644 samples/TableauSharp.Examples/appsettings.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9e0f9e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore TableauSharp.sln + + - name: Build + run: dotnet build TableauSharp.sln --no-restore -c Release + + - name: Test + run: > + dotnet test TableauSharp.sln + --no-build + -c Release + --verbosity normal + --logger "trx;LogFileName=test-results.trx" + --collect:"XPlat Code Coverage" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: '**/TestResults/**' + + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: '**/TestResults/**/coverage.cobertura.xml' diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml new file mode 100644 index 0000000..6ebb5d3 --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,65 @@ +name: Publish to NuGet + +# Triggered by a version tag: git tag v0.1.0 && git push --tags +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' # v0.1.0, v1.0.0-beta, etc. + +jobs: + publish: + name: Pack & Publish + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Extract version from tag + id: version + # Strip the leading 'v' from the tag name (v0.1.0 → 0.1.0) + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Restore + run: dotnet restore TableauSharp.sln + + - name: Build + run: dotnet build TableauSharp.sln --no-restore -c Release + + - name: Test + run: dotnet test TableauSharp.sln --no-build -c Release --verbosity normal + + - name: Pack + run: > + dotnet pack src/TableauSharp/TableauSharp.csproj + --no-build + -c Release + -p:PackageVersion=${{ steps.version.outputs.VERSION }} + -o ./artifacts + + - name: Publish to NuGet + run: > + dotnet nuget push ./artifacts/*.nupkg + --api-key ${{ secrets.NUGET_API_KEY }} + --source https://api.nuget.org/v3/index.json + --skip-duplicate + + - name: Publish symbols + run: > + dotnet nuget push ./artifacts/*.snupkg + --api-key ${{ secrets.NUGET_API_KEY }} + --source https://api.nuget.org/v3/index.json + --skip-duplicate + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: ./artifacts/ diff --git a/TableauSharp.sln b/TableauSharp.sln index 70828f2..f840919 100644 --- a/TableauSharp.sln +++ b/TableauSharp.sln @@ -19,32 +19,90 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableauSharp.AppHost", "Tab EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableauSharp.ServiceDefaults", "TableauSharp.ServiceDefaults\TableauSharp.ServiceDefaults.csproj", "{E0696B33-AF30-01B1-59D3-88E9DB530E2D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableauSharp.Examples", "samples\TableauSharp.Examples\TableauSharp.Examples.csproj", "{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x64.Build.0 = Debug|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x86.Build.0 = Debug|Any CPU {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|Any CPU.Build.0 = Release|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x64.ActiveCfg = Release|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x64.Build.0 = Release|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x86.ActiveCfg = Release|Any CPU + {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x86.Build.0 = Release|Any CPU {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x64.Build.0 = Debug|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x86.Build.0 = Debug|Any CPU {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|Any CPU.Build.0 = Release|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x64.ActiveCfg = Release|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x64.Build.0 = Release|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x86.ActiveCfg = Release|Any CPU + {27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x86.Build.0 = Release|Any CPU {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x64.Build.0 = Debug|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x86.Build.0 = Debug|Any CPU {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|Any CPU.Build.0 = Release|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x64.ActiveCfg = Release|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x64.Build.0 = Release|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x86.ActiveCfg = Release|Any CPU + {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x86.Build.0 = Release|Any CPU {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x64.Build.0 = Debug|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x86.Build.0 = Debug|Any CPU {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|Any CPU.Build.0 = Release|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x64.ActiveCfg = Release|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x64.Build.0 = Release|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x86.ActiveCfg = Release|Any CPU + {EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x86.Build.0 = Release|Any CPU {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x64.Build.0 = Debug|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x86.Build.0 = Debug|Any CPU {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|Any CPU.Build.0 = Release|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x64.ActiveCfg = Release|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x64.Build.0 = Release|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x86.ActiveCfg = Release|Any CPU + {E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x86.Build.0 = Release|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x64.Build.0 = Debug|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x86.Build.0 = Debug|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|Any CPU.Build.0 = Release|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x64.ActiveCfg = Release|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x64.Build.0 = Release|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x86.ActiveCfg = Release|Any CPU + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -53,6 +111,7 @@ Global {F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {27F742DD-CAC0-446F-B9B9-F346B4020CC4} = {649115EA-82C4-43F0-8EC5-3D1673C79EEB} {F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9} = {A2F93780-46F7-4A71-AFE9-4771B77C7DDA} + {0CED7987-15A3-4151-AC7E-85D6BDA02B6D} = {A2F93780-46F7-4A71-AFE9-4771B77C7DDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C8FDB297-CF10-4609-B89F-5382E1BD670D} diff --git a/samples/TableauSharp.Examples/Examples/AuthExamples.cs b/samples/TableauSharp.Examples/Examples/AuthExamples.cs new file mode 100644 index 0000000..56d4f7a --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/AuthExamples.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates the three authentication flows supported by TableauSharp. +// Only one sign-in method should be called per session. After sign-in the +// token is stored automatically in ITableauTokenProvider and injected into +// all other services — you do not need to pass it manually. +public class AuthExamples(IAuthService authService, ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- Auth Examples ---"); + Console.WriteLine(); + + await SignInWithPATAsync(); + await SignOutAsync(); + } + + // Personal Access Token is the recommended flow for server-side applications. + // Configure PersonalAccessTokenName and PersonalAccessTokenSecret in appsettings.json. + private async Task SignInWithPATAsync() + { + Console.WriteLine("[1] Sign in with Personal Access Token"); + try + { + var token = await authService.SignInWithPATAsync(); + Console.WriteLine($" Token : {token.Token[..8]}..."); + Console.WriteLine($" SiteId : {token.SiteId}"); + Console.WriteLine($" UserId : {token.UserId}"); + Console.WriteLine($" Expires : {token.Expiration:u}"); + } + catch (Exception ex) + { + logger.LogError(ex, "PAT sign-in failed"); + } + + Console.WriteLine(); + } + + // Sign out invalidates the session on the Tableau server. + // Always sign out when done to avoid leaking server sessions. + private async Task SignOutAsync() + { + Console.WriteLine("[2] Sign out"); + try + { + var token = await authService.SignInWithPATAsync(); + await authService.SignOutAsync(token.Token); + Console.WriteLine(" Signed out successfully."); + } + catch (Exception ex) + { + logger.LogError(ex, "Sign-out failed"); + } + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Examples/DataSourceExamples.cs b/samples/TableauSharp.Examples/Examples/DataSourceExamples.cs new file mode 100644 index 0000000..6330ef4 --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/DataSourceExamples.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; +using TableauSharp.DataSources.Services; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates data source operations. +// RefreshAsync triggers an extract refresh job on Tableau Server. +// The job runs asynchronously — use Tableau's Jobs API to poll completion. +public class DataSourceExamples( + IAuthService authService, + IDataSourceService dataSourceService, + ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- Data Source Examples ---"); + Console.WriteLine(); + + await authService.SignInWithPATAsync(); + + await ListDataSourcesAsync(); + await RefreshFirstDataSourceAsync(); + } + + private async Task ListDataSourcesAsync() + { + Console.WriteLine("[1] List all data sources"); + try + { + var sources = await dataSourceService.GetAllAsync(); + foreach (var ds in sources) + { + var certified = ds.IsCertified ? " [certified]" : ""; + Console.WriteLine($" {ds.Id} {ds.Name,-35} type={ds.Type ?? "unknown"}{certified}"); + } + + if (!sources.Any()) + Console.WriteLine(" No data sources found."); + } + catch (Exception ex) + { + logger.LogError(ex, "List data sources failed"); + } + + Console.WriteLine(); + } + + // Triggering a refresh is fire-and-forget from the SDK's perspective. + // Tableau Server queues the job and runs it asynchronously. + private async Task RefreshFirstDataSourceAsync() + { + Console.WriteLine("[2] Trigger extract refresh on first data source"); + try + { + var sources = await dataSourceService.GetAllAsync(); + var first = sources.FirstOrDefault(); + if (first is null) { Console.WriteLine(" No data sources."); return; } + + await dataSourceService.RefreshAsync(first.Id); + Console.WriteLine($" Refresh triggered for '{first.Name}'."); + Console.WriteLine(" Check Tableau Server > Jobs to monitor completion."); + } + catch (Exception ex) + { + logger.LogError(ex, "Refresh failed"); + } + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Examples/EmbeddingExamples.cs b/samples/TableauSharp.Examples/Examples/EmbeddingExamples.cs new file mode 100644 index 0000000..9563f1e --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/EmbeddingExamples.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; +using TableauSharp.Embedding.Models; +using TableauSharp.Embedding.Services; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates Tableau embedding via Trusted Tickets. +// Trusted Tickets are the legacy embedding mechanism for Tableau Server. +// Trusted authentication must be enabled on the Tableau Server and the +// caller's IP must be added to the trusted hosts list. +// +// For Tableau Cloud or newer Server versions consider Connected Apps (JWT) instead. +public class EmbeddingExamples( + IAuthService authService, + IEmbeddingService embeddingService, + ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- Embedding Examples ---"); + Console.WriteLine(); + + await authService.SignInWithPATAsync(); + + await GetTrustedTicketAsync(); + GenerateEmbedUrls(); + } + + // A trusted ticket is a single-use token that lets a user view embedded content + // without being prompted to log in. Redeem it within 3 minutes. + private async Task GetTrustedTicketAsync() + { + Console.WriteLine("[1] Get a trusted ticket"); + try + { + var ticket = await embeddingService.GetTrustedTicketAsync(new TrustedTicketRequest + { + Username = "viewer@company.com", + TargetSite = "your-site-name" + // ClientIp is optional — only needed if IP restriction is enabled + }); + + Console.WriteLine($" TicketId : {ticket.TicketId}"); + Console.WriteLine($" EmbedUrl : {ticket.EmbedUrl}"); + Console.WriteLine(); + Console.WriteLine(" Append the view path to EmbedUrl to embed a specific view:"); + Console.WriteLine($" {ticket.EmbedUrl}/views/MyWorkbook/MyView"); + } + catch (Exception ex) + { + logger.LogError(ex, "Trusted ticket request failed"); + } + + Console.WriteLine(); + } + + // URL helpers let you build embed URLs without making an HTTP call. + // Pass a ticket obtained from GetTrustedTicketAsync, or omit it to get + // a template URL with a {ticket} placeholder. + private void GenerateEmbedUrls() + { + Console.WriteLine("[2] Generate embed URLs (no HTTP call)"); + + var viewUrl = embeddingService.GenerateEmbedUrl("SalesReport/Overview"); + Console.WriteLine($" View URL (no ticket) : {viewUrl}"); + + var viewWithTicket = embeddingService.GenerateEmbedUrl("SalesReport/Overview", "abc123ticket"); + Console.WriteLine($" View URL (with ticket): {viewWithTicket}"); + + var workbookUrl = embeddingService.GenerateWorkbookEmbedUrl("SalesReport", "abc123ticket"); + Console.WriteLine($" Workbook URL : {workbookUrl}"); + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Examples/PermissionExamples.cs b/samples/TableauSharp.Examples/Examples/PermissionExamples.cs new file mode 100644 index 0000000..202d8e5 --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/PermissionExamples.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; +using TableauSharp.Common.Models.Enums; +using TableauSharp.Permissions.Models; +using TableauSharp.Permissions.Services; +using TableauSharp.Users.Services; +using TableauSharp.Workbooks.Services; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates permission management across workbooks, projects, and data sources. +// Tableau uses a capability model: each capability (View, Export, etc.) is either +// Allowed or Denied for a specific user or group on a specific resource. +public class PermissionExamples( + IAuthService authService, + IPermissionService permissionService, + IWorkbookService workbookService, + IUserService userService, + ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- Permission Examples ---"); + Console.WriteLine(); + + await authService.SignInWithPATAsync(); + + await GetWorkbookPermissionsAsync(); + await GrantAndRevokePermissionAsync(); + } + + private async Task GetWorkbookPermissionsAsync() + { + Console.WriteLine("[1] Get permissions for first workbook"); + try + { + var workbooks = await workbookService.GetAllAsync(); + var first = workbooks.FirstOrDefault(); + if (first is null) { Console.WriteLine(" No workbooks."); return; } + + Console.WriteLine($" Workbook: {first.Name}"); + var permissions = await permissionService.GetWorkbookPermissionsAsync(first.Id); + foreach (var p in permissions) + { + Console.WriteLine($" {p.GranteeType} {p.GranteeId}"); + foreach (var cap in p.Capabilities) + Console.WriteLine($" {cap.Capability,-20} {cap.Mode}"); + } + + if (!permissions.Any()) + Console.WriteLine(" No permissions set."); + } + catch (Exception ex) + { + logger.LogError(ex, "Get permissions failed"); + } + + Console.WriteLine(); + } + + // This example grants View permission to the first user on the first workbook, + // then immediately revokes it to leave no side-effects. + private async Task GrantAndRevokePermissionAsync() + { + Console.WriteLine("[2] Grant View permission to a user, then revoke it"); + try + { + var workbooks = await workbookService.GetAllAsync(); + var workbook = workbooks.FirstOrDefault(); + var users = await userService.GetAllAsync(); + var user = users.FirstOrDefault(); + + if (workbook is null || user is null) + { + Console.WriteLine(" Needs at least one workbook and one user."); + return; + } + + var permission = new TableauPermission + { + GranteeType = "User", + GranteeId = user.Id, + Capabilities = + [ + new PermissionCapabilityAssignment { Capability = PermissionCapability.View, Mode = "Allow" } + ] + }; + + await permissionService.AddWorkbookPermissionAsync(workbook.Id, permission); + Console.WriteLine($" Granted View/Allow to user '{user.Name}' on workbook '{workbook.Name}'."); + + await permissionService.DeleteWorkbookPermissionAsync( + workbook.Id, user.Id, "User", PermissionCapability.View.ToString()); + Console.WriteLine(" Revoked."); + } + catch (Exception ex) + { + logger.LogError(ex, "Grant/revoke permission failed"); + } + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Examples/ProjectExamples.cs b/samples/TableauSharp.Examples/Examples/ProjectExamples.cs new file mode 100644 index 0000000..e99c356 --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/ProjectExamples.cs @@ -0,0 +1,102 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; +using TableauSharp.Projects.Models; +using TableauSharp.Projects.Services; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates project management. +// Projects are the top-level containers for workbooks and data sources. +// Nested projects (sub-folders) are supported via ParentProjectId. +public class ProjectExamples( + IAuthService authService, + IProjectService projectService, + ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- Project Examples ---"); + Console.WriteLine(); + + await authService.SignInWithPATAsync(); + + await ListProjectsAsync(); + await CreateUpdateDeleteProjectAsync(); + await CreateNestedProjectAsync(); + } + + private async Task ListProjectsAsync() + { + Console.WriteLine("[1] List all projects"); + try + { + var projects = await projectService.GetAllAsync(); + foreach (var p in projects) + Console.WriteLine($" {p.Id} {p.Name,-30} parent={p.ParentProjectId ?? "none"}"); + } + catch (Exception ex) + { + logger.LogError(ex, "List projects failed"); + } + + Console.WriteLine(); + } + + private async Task CreateUpdateDeleteProjectAsync() + { + Console.WriteLine("[2] Create, update, and delete a project"); + try + { + var created = await projectService.CreateAsync(new ProjectCreateRequest + { + Name = "SDK-Example-Project", + Description = "Created by TableauSharp SDK example" + }); + Console.WriteLine($" Created: {created.Id} {created.Name}"); + + var updated = await projectService.UpdateAsync(created.Id, new ProjectUpdateRequest + { + Name = "SDK-Example-Project-Renamed", + Description = "Updated by TableauSharp SDK example" + }); + Console.WriteLine($" Renamed to: {updated.Name}"); + + await projectService.DeleteAsync(created.Id); + Console.WriteLine(" Deleted."); + } + catch (Exception ex) + { + logger.LogError(ex, "Project CRUD failed"); + } + + Console.WriteLine(); + } + + // Sub-projects allow hierarchical organisation of content on Tableau Server. + private async Task CreateNestedProjectAsync() + { + Console.WriteLine("[3] Create a nested project (sub-folder)"); + try + { + var parent = await projectService.CreateAsync(new ProjectCreateRequest { Name = "SDK-Parent" }); + Console.WriteLine($" Parent: {parent.Id} {parent.Name}"); + + var child = await projectService.CreateAsync(new ProjectCreateRequest + { + Name = "SDK-Child", + ParentProjectId = parent.Id + }); + Console.WriteLine($" Child : {child.Id} {child.Name} (parent={child.ParentProjectId})"); + + await projectService.DeleteAsync(child.Id); + await projectService.DeleteAsync(parent.Id); + Console.WriteLine(" Cleaned up."); + } + catch (Exception ex) + { + logger.LogError(ex, "Nested project example failed"); + } + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Examples/UserGroupExamples.cs b/samples/TableauSharp.Examples/Examples/UserGroupExamples.cs new file mode 100644 index 0000000..a9c8c24 --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/UserGroupExamples.cs @@ -0,0 +1,107 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; +using TableauSharp.Users.Models; +using TableauSharp.Users.Services; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates user and group management. +// All operations are site-scoped and require a valid auth token. +// Sign in before calling any of these methods. +public class UserGroupExamples( + IAuthService authService, + IUserService userService, + IGroupService groupService, + ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- User & Group Examples ---"); + Console.WriteLine(); + + await authService.SignInWithPATAsync(); + + await ListUsersAsync(); + await CreateAndUpdateUserAsync(); + await CreateGroupAndManageMembersAsync(); + } + + private async Task ListUsersAsync() + { + Console.WriteLine("[1] List all users"); + try + { + var users = await userService.GetAllAsync(); + foreach (var u in users) + Console.WriteLine($" {u.Id} {u.Name,-30} {u.SiteRole}"); + } + catch (Exception ex) + { + logger.LogError(ex, "List users failed"); + } + + Console.WriteLine(); + } + + private async Task CreateAndUpdateUserAsync() + { + Console.WriteLine("[2] Create and update a user"); + try + { + var created = await userService.CreateAsync(new UserCreateRequest + { + Name = "sdk.example@company.com", + Email = "sdk.example@company.com", + SiteRole = "Viewer" + }); + Console.WriteLine($" Created: {created.Id} {created.Name} {created.SiteRole}"); + + var updated = await userService.UpdateAsync(created.Id, new UserUpdateRequest + { + SiteRole = "Explorer", + IsActive = true + }); + Console.WriteLine($" Updated role to: {updated.SiteRole}"); + + await userService.DeleteAsync(created.Id); + Console.WriteLine(" Deleted."); + } + catch (Exception ex) + { + logger.LogError(ex, "User CRUD failed"); + } + + Console.WriteLine(); + } + + private async Task CreateGroupAndManageMembersAsync() + { + Console.WriteLine("[3] Create group and manage members"); + try + { + var group = await groupService.CreateAsync(new GroupCreateRequest { Name = "SDK-Example-Group" }); + Console.WriteLine($" Group created: {group.Id} {group.Name}"); + + // Add first available user to the group + var users = await userService.GetAllAsync(); + var user = users.FirstOrDefault(); + if (user is not null) + { + await groupService.AddUserToGroupAsync(group.Id, user.Id); + Console.WriteLine($" Added user '{user.Name}' to group."); + + await groupService.RemoveUserFromGroupAsync(group.Id, user.Id); + Console.WriteLine(" Removed user from group."); + } + + await groupService.DeleteAsync(group.Id); + Console.WriteLine(" Group deleted."); + } + catch (Exception ex) + { + logger.LogError(ex, "Group management failed"); + } + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Examples/WorkbookExamples.cs b/samples/TableauSharp.Examples/Examples/WorkbookExamples.cs new file mode 100644 index 0000000..3ff7e92 --- /dev/null +++ b/samples/TableauSharp.Examples/Examples/WorkbookExamples.cs @@ -0,0 +1,103 @@ +using Microsoft.Extensions.Logging; +using TableauSharp.Auth.Service; +using TableauSharp.Workbooks.Models; +using TableauSharp.Workbooks.Services; + +namespace TableauSharp.Examples.Examples; + +// Demonstrates workbook and view operations. +// Exporting views requires a Tableau Server licence that permits image/data export. +public class WorkbookExamples( + IAuthService authService, + IWorkbookService workbookService, + IViewService viewService, + ILogger logger) +{ + public async Task RunAsync() + { + Console.WriteLine("--- Workbook Examples ---"); + Console.WriteLine(); + + await authService.SignInWithPATAsync(); + + await ListWorkbooksAsync(); + await ListViewsForFirstWorkbookAsync(); + await ExportViewAsPngAsync(); + } + + private async Task ListWorkbooksAsync() + { + Console.WriteLine("[1] List workbooks"); + try + { + var workbooks = await workbookService.GetAllAsync(); + foreach (var wb in workbooks) + Console.WriteLine($" {wb.Id} {wb.Name,-40} project={wb.ProjectId}"); + } + catch (Exception ex) + { + logger.LogError(ex, "List workbooks failed"); + } + + Console.WriteLine(); + } + + private async Task ListViewsForFirstWorkbookAsync() + { + Console.WriteLine("[2] List views for the first workbook"); + try + { + var workbooks = await workbookService.GetAllAsync(); + var first = workbooks.FirstOrDefault(); + if (first is null) + { + Console.WriteLine(" No workbooks found."); + return; + } + + Console.WriteLine($" Workbook: {first.Name}"); + var views = await viewService.GetViewsByWorkbookIdAsync(first.Id); + foreach (var v in views) + Console.WriteLine($" View: {v.Id} {v.Name,-30} totalViews={v.TotalViews}"); + } + catch (Exception ex) + { + logger.LogError(ex, "List views failed"); + } + + Console.WriteLine(); + } + + // ExportViewAsync returns raw bytes which you can save to a file or stream to a browser. + // Supported formats: PDF, PNG, CSV + private async Task ExportViewAsPngAsync() + { + Console.WriteLine("[3] Export a view as PNG"); + try + { + var workbooks = await workbookService.GetAllAsync(); + var first = workbooks.FirstOrDefault(); + if (first is null) { Console.WriteLine(" No workbooks."); return; } + + var views = await viewService.GetViewsByWorkbookIdAsync(first.Id); + var firstView = views.FirstOrDefault(); + if (firstView is null) { Console.WriteLine(" No views."); return; } + + var export = await viewService.ExportViewAsync(new ExportRequest + { + ViewId = firstView.Id, + Format = "PNG" + }); + + var outputPath = Path.Combine(Path.GetTempPath(), export.FileName); + await File.WriteAllBytesAsync(outputPath, export.FileContent); + Console.WriteLine($" Saved {export.FileContent.Length:N0} bytes → {outputPath}"); + } + catch (Exception ex) + { + logger.LogError(ex, "Export view failed"); + } + + Console.WriteLine(); + } +} diff --git a/samples/TableauSharp.Examples/Program.cs b/samples/TableauSharp.Examples/Program.cs new file mode 100644 index 0000000..a48bb93 --- /dev/null +++ b/samples/TableauSharp.Examples/Program.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TableauSharp.Examples.Examples; +using TableauSharp.Extensions; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + config.AddJsonFile("appsettings.json", optional: false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices((ctx, services) => + { + services.AddLogging(b => b.AddConsole()); + services.AddTableauSharp(ctx.Configuration); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }) + .Build(); + +// Pick which example to run via command-line arg, or run all sequentially. +// Usage: dotnet run -- auth +// dotnet run -- users +// dotnet run -- workbooks +// dotnet run -- (no arg = menu) + +var example = args.Length > 0 ? args[0].ToLowerInvariant() : "menu"; + +Console.WriteLine("=== TableauSharp SDK Examples ==="); +Console.WriteLine(); + +switch (example) +{ + case "auth": + await host.Services.GetRequiredService().RunAsync(); + break; + case "users": + await host.Services.GetRequiredService().RunAsync(); + break; + case "projects": + await host.Services.GetRequiredService().RunAsync(); + break; + case "workbooks": + await host.Services.GetRequiredService().RunAsync(); + break; + case "datasources": + await host.Services.GetRequiredService().RunAsync(); + break; + case "permissions": + await host.Services.GetRequiredService().RunAsync(); + break; + case "embedding": + await host.Services.GetRequiredService().RunAsync(); + break; + default: + Console.WriteLine("Available examples:"); + Console.WriteLine(" dotnet run -- auth Sign in with PAT, JWT, credentials"); + Console.WriteLine(" dotnet run -- users User & group CRUD"); + Console.WriteLine(" dotnet run -- projects Project CRUD"); + Console.WriteLine(" dotnet run -- workbooks List, publish, export workbooks"); + Console.WriteLine(" dotnet run -- datasources List, publish, refresh data sources"); + Console.WriteLine(" dotnet run -- permissions Get, grant, revoke permissions"); + Console.WriteLine(" dotnet run -- embedding Trusted ticket & embed URLs"); + break; +} diff --git a/samples/TableauSharp.Examples/TableauSharp.Examples.csproj b/samples/TableauSharp.Examples/TableauSharp.Examples.csproj new file mode 100644 index 0000000..83601f3 --- /dev/null +++ b/samples/TableauSharp.Examples/TableauSharp.Examples.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + TableauSharp.Examples + + + + + + + + + + + + + + diff --git a/samples/TableauSharp.Examples/appsettings.json b/samples/TableauSharp.Examples/appsettings.json new file mode 100644 index 0000000..58ee42a --- /dev/null +++ b/samples/TableauSharp.Examples/appsettings.json @@ -0,0 +1,20 @@ +{ + "TableauOptions": { + "Server": "https://your-tableau-server", + "Version": "3.23", + "Site": "your-site-name" + }, + "TableauAuthOptions": { + "SiteContentUrl": "your-site-name", + "PersonalAccessTokenName": "your-pat-name", + "PersonalAccessTokenSecret": "your-pat-secret", + "Username": "your-username", + "Password": "your-password", + "UsePAT": true, + "SecretId": "your-connected-app-secret-id", + "SecretValue": "your-connected-app-secret-value", + "Jwt_Expiry_Minutes": 10, + "Jwt_Audience": "https://your-tableau-server", + "Scopes": "tableau:views:read tableau:workbooks:read tableau:datasources:read" + } +} diff --git a/src/TableauSharp/TableauSharp.csproj b/src/TableauSharp/TableauSharp.csproj index 0d9700c..3eac46d 100644 --- a/src/TableauSharp/TableauSharp.csproj +++ b/src/TableauSharp/TableauSharp.csproj @@ -4,6 +4,21 @@ net8.0 enable enable + + + TableauSharp + 0.1.0-alpha + Teesofttech + A .NET SDK that provides a clean, developer-friendly abstraction over the Tableau REST API. Supports authentication (PAT, credentials, Connected Apps JWT), user/group management, workbooks, views, data sources, projects, permissions, and embedding. + Tableau;REST API;SDK;.NET;Analytics + https://github.com/teesofttech/TableauSharp + https://github.com/teesofttech/TableauSharp + git + MIT + README.md + false + true + snupkg @@ -12,4 +27,8 @@ + + + + diff --git a/test/TableauSharp.Tests/TableauSharp.Tests.csproj b/test/TableauSharp.Tests/TableauSharp.Tests.csproj index 8b5797e..d5d9340 100644 --- a/test/TableauSharp.Tests/TableauSharp.Tests.csproj +++ b/test/TableauSharp.Tests/TableauSharp.Tests.csproj @@ -12,9 +12,15 @@ + + + + + +