From ea65ff25f012c418e0cac521b4aa481c83a4b7b9 Mon Sep 17 00:00:00 2001
From: Pranav Vishnumolakala
Date: Tue, 3 Sep 2024 11:32:08 -0700
Subject: [PATCH] Release 4.3.0
---
.github/workflows/dotnet-test.yml | 4 +-
.gitignore | 10 -
Directory.Build.props | 2 +-
.../Hooks/Filters/SharedCustomViewFilter.cs | 25 +
.../CustomViewDefaultUsersTransformer.cs | 39 +
.../MyMigrationApplication.cs | 8 +
examples/Csharp.ExampleApplication/Program.cs | 8 +
.../Filters/shared_custom_view_filter.py | 11 +
.../Hooks/mappings/project_rename_mapping.py | 2 +-
.../custom_view_default_users_transformer.py | 14 +
.../Python.ExampleApplication.py | 13 +-
.../Python.ExampleApplication.pyproj | 2 +
examples/Python.ExampleApplication/config.ini | 2 +
.../Python.ExampleApplication/print_result.py | 7 +-
.../requirements.txt | 5 -
src/Documentation/api-csharp/index.md | 35 +-
src/Documentation/api-python/index.md | 17 +-
src/Documentation/articles/configuration.md | 14 +
src/Documentation/articles/cv_file.md | 32 +
.../articles/hooks/python_hook_update.md | 4 +-
src/Documentation/articles/index.md | 42 +-
src/Documentation/articles/toc.yml | 2 +
src/Documentation/images/versioning.svg | 1 +
.../includes/csharp-getting-started.md | 34 +
.../includes/python-getting-started.md | 16 +
.../filters/filter_custom_views_by_shared.md | 36 +
.../filters/filter_projects_by_name.md | 5 +-
.../filters/filter_users_by_site_role.md | 5 +-
src/Documentation/samples/filters/index.md | 2 +
src/Documentation/samples/filters/toc.yml | 4 +-
.../samples/mappings/change_projects.md | 5 +-
.../samples/mappings/rename_projects.md | 7 +-
.../log_migration_actions.md | 5 +-
.../custom_view_default_users_transformer.md | 43 +
.../encrypt_extracts_transformer.md | 5 +-
.../samples/transformers/index.md | 3 +-
.../transformers/migrated_tag_transformer.md | 6 +-
.../transformers/start_at_transformer.md | 5 +-
.../samples/transformers/toc.yml | 2 +
src/Python/requirements.txt | 2 +-
src/Python/src/tableau_migration/__init__.py | 3 +
.../tableau_migration/migration_content.py | 161 +++-
src/Python/tests/test_classes.py | 14 +-
src/Python/tests/test_migration_content.py | 112 +++
.../PythonGenerationList.cs | 7 +-
.../appsettings.json | 4 +
src/Tableau.Migration/Api/ApiClient.cs | 1 +
.../Api/ContentApiClientBase.cs | 58 +-
.../Api/CustomViewsApiClient.cs | 352 ++++++++
.../Api/DataSourcesApiClient.cs | 12 +-
.../Api/ICloudTasksApiClient.cs | 1 -
...ContentReferenceFinderFactoryExtensions.cs | 169 ++--
.../Api/ICustomViewsApiClient.cs | 119 +++
.../Api/IServerTasksApiClient.cs | 1 -
.../Api/IServiceCollectionExtensions.cs | 3 +
src/Tableau.Migration/Api/ISitesApiClient.cs | 5 +
.../CustomViewAsUserDefaultViewResult.cs | 41 +
.../ICustomViewAsUserDefaultViewResult.cs | 33 +
.../Api/Models/IPublishCustomViewOptions.cs | 47 ++
.../Api/Models/PublishCustomViewOptions.cs | 62 ++
.../Api/Models/PublishDataSourceOptions.cs | 23 +-
.../Api/Models/PublishFlowOptions.cs | 23 +-
.../Api/Models/PublishWorkbookOptions.cs | 25 +-
.../Api/ProjectsApiClient.cs | 6 +-
.../Api/PublishContentWithFileOptions.cs | 74 ++
.../Api/Publishing/CustomViewPublisher.cs | 86 ++
.../ICustomViewPublisher.cs} | 9 +-
.../CustomViewDefaultUsersResponsePager.cs | 56 ++
.../Api/Rest/Models/ICustomViewType.cs | 61 ++
.../Api/Rest/Models/IWithOwnerType.cs | 2 +-
.../CommitCustomViewPublishRequest.cs | 150 ++++
.../Requests/CommitWorkbookPublishRequest.cs | 2 +-
.../SetCustomViewDefaultUsersRequest.cs | 89 ++
.../Requests/UpdateCustomViewRequest.cs | 108 +++
.../Models/Responses/CreateProjectResponse.cs | 4 +-
.../CustomViewAsUsersDefaultViewResponse.cs | 72 ++
.../Models/Responses/CustomViewResponse.cs | 191 +++++
.../Models/Responses/CustomViewsResponse.cs | 92 ++-
.../Models/Responses/DataSourceResponse.cs | 6 +-
.../Models/Responses/DataSourcesResponse.cs | 8 +-
.../Api/Rest/Models/Responses/FlowResponse.cs | 4 +-
.../Rest/Models/Responses/FlowsResponse.cs | 4 +-
.../Rest/Models/Responses/ProjectsResponse.cs | 4 +-
.../Responses/UpdateCustomViewResponse.cs | 157 ++++
.../Models/Responses/UpdateProjectResponse.cs | 4 +-
...sersWithCustomViewAsDefaultViewResponse.cs | 48 ++
.../Rest/Models/Responses/WorkbookResponse.cs | 8 +-
.../Models/Responses/WorkbooksResponse.cs | 8 +-
.../Rest/Models/Types/CustomViewFileTypes.cs | 31 +
.../Api/Rest/RestUrlPrefixes.cs | 2 +
.../Rest/Api/CustomViewsRestApiSimulator.cs | 106 +++
.../Net/Requests/RestApiRequestMatcher.cs | 4 +-
.../Rest/Net/Requests/RestUrlPatterns.cs | 33 +-
...stCommitCustomViewUploadResponseBuilder.cs | 119 +++
...ustomViewDefaultUsersAddResponseBuilder.cs | 98 +++
.../Api/Simulation/Rest/RestApiSimulator.cs | 12 +-
.../Api/Simulation/SimulatedCustomViewData.cs | 71 ++
.../Api/Simulation/TableauData.cs | 35 +-
src/Tableau.Migration/Api/SitesApiClient.cs | 17 +-
.../Api/WorkbooksApiClient.cs | 12 +-
src/Tableau.Migration/Config/ConfigReader.cs | 18 +-
.../Config/NetworkOptions.cs | 15 +
.../Config/UniqueContentTypesValidator.cs | 51 ++
src/Tableau.Migration/Constants.cs | 18 +-
src/Tableau.Migration/Content/CustomView.cs | 110 +++
.../Files/ContentTypeFilePathResolver.cs | 4 +
src/Tableau.Migration/Content/ICustomView.cs | 60 ++
.../Content/IPublishableCustomView.cs | 32 +
.../IWithWorkbook.cs} | 13 +-
.../Content/PublishableCustomView.cs | 59 ++
.../Schedules/ExtractRefreshContentType.cs | 19 +-
src/Tableau.Migration/ContentLocation.cs | 64 +-
.../Engine/Actions/PreflightAction.cs | 36 +-
.../Engine/Endpoints/IDestinationEndpoint.cs | 12 +
.../TableauApiDestinationEndpoint.cs | 12 +
.../Hooks/Filters/ContentFilterBuilder.cs | 47 +-
.../Hooks/Filters/IContentFilterBuilder.cs | 7 +-
.../Engine/Hooks/IMigrationHookBuilder.cs | 3 +-
.../Hooks/Mappings/ContentMappingBuilder.cs | 31 +-
.../Hooks/Mappings/IContentMappingBuilder.cs | 21 +-
.../Engine/Hooks/MigrationHookBuilder.cs | 54 +-
.../Engine/Hooks/MigrationHookBuilderBase.cs | 40 +
.../CustomViewDefaultUsersPostPublishHook.cs | 61 ++
.../Transformers/ContentTransformerBuilder.cs | 47 +-
...tomViewDefaultUserReferencesTransformer.cs | 90 +++
.../Default/EncryptExtractTransformer.cs | 96 +++
.../Default/GroupUsersTransformer.cs | 35 +-
.../Default/WorkbookReferenceTransformer.cs | 70 ++
.../Engine/IServiceCollectionExtensions.cs | 7 +-
src/Tableau.Migration/Engine/Migration.cs | 13 +-
src/Tableau.Migration/Engine/MigrationPlan.cs | 5 +-
.../Engine/MigrationPlanBuilder.cs | 169 +++-
...ions.cs => MigrationPlanBuilderFactory.cs} | 21 +-
.../CustomMigrationPipelineFactory.cs | 50 ++
.../Pipelines/MigrationPipelineContentType.cs | 29 +-
.../Pipelines/MigrationPipelineFactory.cs | 11 +-
.../ServerToCloudMigrationPipeline.cs | 9 +-
.../ServerToCloudMigrationPlanBuilder.cs | 19 +
src/Tableau.Migration/IMigrationPlan.cs | 6 +
.../IMigrationPlanBuilder.cs | 50 ++
...der.cs => IMigrationPlanBuilderFactory.cs} | 12 +-
src/Tableau.Migration/IMigrationSdk.cs | 9 +-
.../IServiceCollectionExtensions.cs | 30 +-
.../Interop/IServiceCollectionExtensions.cs | 38 +-
.../FailedJobExceptionJsonConverter.cs | 17 +
.../SerializableContentLocation.cs | 2 +-
src/Tableau.Migration/MigrationSdk.cs | 17 +-
src/Tableau.Migration/NameOf.cs | 14 +-
.../Handlers/UserAgentHttpMessageHandler.cs | 9 +-
.../Net/HttpContentExtensions.cs | 3 +
.../Net/HttpContentSerializer.cs | 13 +-
.../Net/IRequestBuilderFactory.cs | 6 +-
.../Net/IServiceCollectionExtensions.cs | 5 +-
.../IUserAgentProvider.cs} | 11 +-
.../Net/MediaTypeHeaderValueExtensions.cs | 2 +
.../Net/RequestBuilderFactory.cs | 4 +-
.../Net/Rest/IRestRequestBuilderFactory.cs | 22 +-
.../Net/Rest/RestRequestBuilderFactory.cs | 38 +-
.../Net/UserAgentProvider.cs | 46 ++
src/Tableau.Migration/PipelineProfile.cs | 9 +-
.../Resources/SharedResourceKeys.cs | 40 +-
.../Resources/SharedResources.resx | 71 +-
.../Python.ExampleApplication.Tests.pyproj | 1 +
...anifestAfterBatchMigrationCompletedHook.cs | 11 +-
.../LogFileHelper.cs | 48 ++
.../Program.cs | 132 +--
.../TestApplication.cs | 4 +-
.../Tableau.Migration.Tests/JsonExtensions.cs | 114 +++
.../Tableau.Migration.Tests/MockExtensions.cs | 4 +
.../ServerToCloudSimulationTestBase.cs | 98 ++-
.../Simulation/TableauDataExtensions.cs | 89 +-
.../Tests/Api/CustomViewsApiClientTests.cs | 99 +++
.../Tests/CustomViewsMigrationTests.cs | 130 +++
.../Tableau.Migration.Tests/TestConfigFile.cs | 109 +++
.../Unit/Api/ApiClientTestDependencies.cs | 4 +
.../Unit/Api/ApiTestBase.cs | 2 +-
.../Unit/Api/CustomViewsApiClientTests.cs | 759 ++++++++++++++++++
.../Unit/Api/GroupsApiClientTests.cs | 10 +
...ntReferenceFinderFactoryExtensionsTests.cs | 203 ++++-
.../Api/IServiceCollectionExtensionsTests.cs | 1 +
.../Models/PublishCustomViewOptionsTests.cs | 65 ++
.../Models/PublishDataSourceOptionsTests.cs | 66 ++
.../Api/Models/PublishFlowOptionsTests.cs | 64 ++
.../Api/Models/PublishWorkbookOptionsTests.cs | 69 ++
.../Publishing/CustomViewPublisherTests.cs | 62 ++
.../Api/Publishing/WorkbookPublisherTests.cs | 1 +
.../Rest/Models/CustomViewResponseTests.cs | 79 ++
.../Rest/Models/CustomViewsResponseTests.cs | 98 +++
.../Requests/UpdateCustomViewRequestTests.cs | 155 ++++
...ustomViewAsUsersDefaultViewResponseTest.cs | 56 ++
.../Models/UpdateCustomViewResponseTests.cs | 79 ++
...ithCustomViewAsDefaultViewResponseTests.cs | 70 ++
.../Unit/Api/SchedulesApiClientTests.cs | 1 -
.../Rest/Net/Requests/RestUrlPatternsTests.cs | 18 +-
.../Unit/Api/UsersApiClientTests.cs | 10 +
.../Unit/Api/WorkbooksApiClientTests.cs | 2 +-
.../Unit/Config/ConfigReaderTests.cs | 1 -
.../Unit/Config/ConfigurationTestContext.cs | 111 +++
.../Unit/Config/ConfigurationTests.cs | 322 +++-----
.../UniqueContentTypesValidatorTests.cs | 78 ++
.../Unit/Content/CustomViewTests.cs | 165 ++++
.../Content/PublishableCustomViewTests.cs | 59 ++
.../Unit/ContentLocationTests.cs | 75 ++
.../Engine/Actions/PreflightActionTests.cs | 21 +-
...ewDefaultUserReferencesTransformerTests.cs | 128 +++
.../Default/EncryptExtractTransformerTests.cs | 103 +++
.../Default/GroupUsersTransformerTests.cs | 18 +-
.../WorkbookReferenceTransformerTests.cs | 84 ++
.../IServiceCollectionExtensionsTests.cs | 6 +
.../MigrationPlanBuilderFactoryTests.cs | 41 +
.../Unit/Engine/MigrationPlanBuilderTests.cs | 187 ++++-
.../Unit/Engine/MigrationTests.cs | 58 +-
.../CustomMigrationPipelineFactoryTests.cs | 59 ++
.../MigrationPipelineContentTypeTests.cs | 57 +-
.../PipelineProfileExtensionsTests.cs | 47 --
.../ServerToCloudMigrationPipelineTests.cs | 3 +-
.../ServerToCloudMigrationPlanBuilderTests.cs | 35 +
.../Unit/IServiceCollectionExtensionsTests.cs | 46 +-
.../IServiceCollectionExtensionsTests.cs | 20 +-
.../Unit/MigrationSdkTests.cs | 35 +
.../Unit/NameOfTests.cs | 86 +-
.../UserAgentHttpMessageHandlerTests.cs | 35 +-
.../Unit/Net/HttpContentExtensionsTests.cs | 34 +
.../Unit/Net/HttpContentSerializerTests.cs | 16 +
.../Net/IServiceCollectionExtensionsTests.cs | 2 +-
.../MediaTypeHeaderValueExtensionsTests.cs | 23 +
.../Rest/RestRequestBuilderFactoryTests.cs | 36 +-
.../Unit/Net/UserAgentProviderTests.cs | 61 ++
.../Unit/SdkUserAgentTests.cs | 69 --
229 files changed, 8970 insertions(+), 1349 deletions(-)
create mode 100644 examples/Csharp.ExampleApplication/Hooks/Filters/SharedCustomViewFilter.cs
create mode 100644 examples/Csharp.ExampleApplication/Hooks/Transformers/CustomViewDefaultUsersTransformer.cs
create mode 100644 examples/Python.ExampleApplication/Hooks/Filters/shared_custom_view_filter.py
create mode 100644 examples/Python.ExampleApplication/Hooks/transformers/custom_view_default_users_transformer.py
create mode 100644 src/Documentation/articles/cv_file.md
create mode 100644 src/Documentation/images/versioning.svg
create mode 100644 src/Documentation/includes/csharp-getting-started.md
create mode 100644 src/Documentation/includes/python-getting-started.md
create mode 100644 src/Documentation/samples/filters/filter_custom_views_by_shared.md
create mode 100644 src/Documentation/samples/transformers/custom_view_default_users_transformer.md
create mode 100644 src/Tableau.Migration/Api/CustomViewsApiClient.cs
create mode 100644 src/Tableau.Migration/Api/ICustomViewsApiClient.cs
create mode 100644 src/Tableau.Migration/Api/Models/CustomViewAsUserDefaultViewResult.cs
create mode 100644 src/Tableau.Migration/Api/Models/ICustomViewAsUserDefaultViewResult.cs
create mode 100644 src/Tableau.Migration/Api/Models/IPublishCustomViewOptions.cs
create mode 100644 src/Tableau.Migration/Api/Models/PublishCustomViewOptions.cs
create mode 100644 src/Tableau.Migration/Api/PublishContentWithFileOptions.cs
create mode 100644 src/Tableau.Migration/Api/Publishing/CustomViewPublisher.cs
rename src/Tableau.Migration/Api/{Rest/Models/IOwnerType.cs => Publishing/ICustomViewPublisher.cs} (72%)
create mode 100644 src/Tableau.Migration/Api/Rest/Models/CustomViewDefaultUsersResponsePager.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/ICustomViewType.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Requests/CommitCustomViewPublishRequest.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Requests/SetCustomViewDefaultUsersRequest.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Requests/UpdateCustomViewRequest.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewAsUsersDefaultViewResponse.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewResponse.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Responses/UpdateCustomViewResponse.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Responses/UsersWithCustomViewAsDefaultViewResponse.cs
create mode 100644 src/Tableau.Migration/Api/Rest/Models/Types/CustomViewFileTypes.cs
create mode 100644 src/Tableau.Migration/Api/Simulation/Rest/Api/CustomViewsRestApiSimulator.cs
create mode 100644 src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCommitCustomViewUploadResponseBuilder.cs
create mode 100644 src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCustomViewDefaultUsersAddResponseBuilder.cs
create mode 100644 src/Tableau.Migration/Api/Simulation/SimulatedCustomViewData.cs
create mode 100644 src/Tableau.Migration/Config/UniqueContentTypesValidator.cs
create mode 100644 src/Tableau.Migration/Content/CustomView.cs
create mode 100644 src/Tableau.Migration/Content/ICustomView.cs
create mode 100644 src/Tableau.Migration/Content/IPublishableCustomView.cs
rename src/Tableau.Migration/{Interop/PythonUserAgentSuffixProvider.cs => Content/IWithWorkbook.cs} (70%)
create mode 100644 src/Tableau.Migration/Content/PublishableCustomView.cs
create mode 100644 src/Tableau.Migration/Engine/Hooks/PostPublish/Default/CustomViewDefaultUsersPostPublishHook.cs
create mode 100644 src/Tableau.Migration/Engine/Hooks/Transformers/Default/CustomViewDefaultUserReferencesTransformer.cs
create mode 100644 src/Tableau.Migration/Engine/Hooks/Transformers/Default/EncryptExtractTransformer.cs
create mode 100644 src/Tableau.Migration/Engine/Hooks/Transformers/Default/WorkbookReferenceTransformer.cs
rename src/Tableau.Migration/Engine/{Pipelines/PipelineProfileExtensions.cs => MigrationPlanBuilderFactory.cs} (57%)
create mode 100644 src/Tableau.Migration/Engine/Pipelines/CustomMigrationPipelineFactory.cs
rename src/Tableau.Migration/{IUserAgentSuffixProvider.cs => IMigrationPlanBuilderFactory.cs} (67%)
rename src/Tableau.Migration/{UserAgentSuffixProvider.cs => Net/IUserAgentProvider.cs} (73%)
create mode 100644 src/Tableau.Migration/Net/UserAgentProvider.cs
create mode 100644 tests/Tableau.Migration.TestApplication/LogFileHelper.cs
create mode 100644 tests/Tableau.Migration.Tests/JsonExtensions.cs
create mode 100644 tests/Tableau.Migration.Tests/Simulation/Tests/Api/CustomViewsApiClientTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Simulation/Tests/CustomViewsMigrationTests.cs
create mode 100644 tests/Tableau.Migration.Tests/TestConfigFile.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/CustomViewsApiClientTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Models/PublishCustomViewOptionsTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Models/PublishDataSourceOptionsTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Models/PublishFlowOptionsTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Models/PublishWorkbookOptionsTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Publishing/CustomViewPublisherTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/CustomViewResponseTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/CustomViewsResponseTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Requests/UpdateCustomViewRequestTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/Responses/CustomViewAsUsersDefaultViewResponseTest.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/UpdateCustomViewResponseTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Api/Rest/Models/UsersWithCustomViewAsDefaultViewResponseTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Config/ConfigurationTestContext.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Config/UniqueContentTypesValidatorTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Content/CustomViewTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Content/PublishableCustomViewTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/CustomViewDefaultUserReferencesTransformerTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/EncryptExtractTransformerTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/WorkbookReferenceTransformerTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Engine/MigrationPlanBuilderFactoryTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Engine/Pipelines/CustomMigrationPipelineFactoryTests.cs
delete mode 100644 tests/Tableau.Migration.Tests/Unit/Engine/Pipelines/PipelineProfileExtensionsTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/MigrationSdkTests.cs
create mode 100644 tests/Tableau.Migration.Tests/Unit/Net/UserAgentProviderTests.cs
delete mode 100644 tests/Tableau.Migration.Tests/Unit/SdkUserAgentTests.cs
diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml
index 1187e00..77e17f7 100644
--- a/.github/workflows/dotnet-test.yml
+++ b/.github/workflows/dotnet-test.yml
@@ -26,7 +26,7 @@ jobs:
run: dotnet build '${{ vars.BUILD_SOLUTION }}' -c ${{ matrix.config }}
- name: Test solution with ${{ matrix.config }} configuration
run: |
- dotnet test '${{ vars.BUILD_SOLUTION }}' --no-build -c ${{ matrix.config }} --verbosity normal --logger trx --results-directory "TestResults-${{ matrix.os }}-${{ matrix.config }}" -- RunConfiguration.TestSessionTimeout=${{ vars.MIGRATIONSDK_TEST_CANCELLATION_TIMEOUT_MILLISECONDS }}
+ dotnet test '${{ vars.BUILD_SOLUTION }}' --no-build -c ${{ matrix.config }} --verbosity normal --logger junit --results-directory "TestResults-${{ matrix.os }}-${{ matrix.config }}" -- RunConfiguration.TestSessionTimeout=${{ vars.MIGRATIONSDK_TEST_CANCELLATION_TIMEOUT_MILLISECONDS }}
- name: Upload test results
# Use always() to always run this step to publish test results when there are test failures
if: ${{ always() }}
@@ -34,4 +34,4 @@ jobs:
with:
name: dotnet-results-${{ matrix.os }}-${{ matrix.config }}
path: TestResults-${{ matrix.os }}-${{ matrix.config }}
- if-no-files-found: error
\ No newline at end of file
+ if-no-files-found: error
diff --git a/.gitignore b/.gitignore
index 8543492..5085114 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,8 +80,6 @@ instance/
# Scrapy stuff:
.scrapy
-# Sphinx documentation
-docs/_build/
# PyBuilder
.pybuilder/
@@ -179,14 +177,6 @@ launchSettings.json
UpgradeLog.htm
*.*.ini
*.*.json
-/src/Python/Documentation/_build
-/src/Python/Documentation/_static
-/src/Python/Python.pyproj.user
-/src/Documentation/_python
-/src/Python/Documentation/generated
-/tests/Python.TestApplication/manifest.json
-/.git2gus
-/src/Python/scripts/publish-package.ps1
# Public repo ignores
.github/pull_request_template.md
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index b170f62..198a9b8 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
enable
true
true
- 4.2.0
+ 4.3.0
Salesforce, Inc.
Salesforce, Inc.
Copyright (c) 2024, Salesforce, Inc. and its licensors
diff --git a/examples/Csharp.ExampleApplication/Hooks/Filters/SharedCustomViewFilter.cs b/examples/Csharp.ExampleApplication/Hooks/Filters/SharedCustomViewFilter.cs
new file mode 100644
index 0000000..7bb7425
--- /dev/null
+++ b/examples/Csharp.ExampleApplication/Hooks/Filters/SharedCustomViewFilter.cs
@@ -0,0 +1,25 @@
+using System;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration.Api.Rest.Models;
+using Tableau.Migration.Content;
+using Tableau.Migration.Engine;
+using Tableau.Migration.Engine.Hooks.Filters;
+using Tableau.Migration.Resources;
+
+namespace Csharp.ExampleApplication.Hooks.Filters
+{
+ #region class
+ public class SharedCustomViewFilter : ContentFilterBase
+ {
+ public SharedCustomViewFilter(
+ ISharedResourcesLocalizer localizer,
+ ILogger> logger)
+ : base(localizer, logger) { }
+
+ public override bool ShouldMigrate(ContentMigrationItem item)
+ {
+ return !item.SourceItem.Shared;
+ }
+ }
+ #endregion
+}
diff --git a/examples/Csharp.ExampleApplication/Hooks/Transformers/CustomViewDefaultUsersTransformer.cs b/examples/Csharp.ExampleApplication/Hooks/Transformers/CustomViewDefaultUsersTransformer.cs
new file mode 100644
index 0000000..070dcef
--- /dev/null
+++ b/examples/Csharp.ExampleApplication/Hooks/Transformers/CustomViewDefaultUsersTransformer.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration;
+using Tableau.Migration.Content;
+using Tableau.Migration.Engine.Hooks.Transformers;
+using Tableau.Migration.Resources;
+
+namespace Csharp.ExampleApplication.Hooks.Transformers
+{
+ #region class
+ public class CustomViewExcludeDefaultUserTransformer(
+ ISharedResourcesLocalizer localizer,
+ ILogger logger)
+ : ContentTransformerBase(localizer, logger)
+ {
+ public IList ExcludeUsernames { get; } = new List() {"User1", "User2"};
+
+
+ private readonly ILogger? _logger = logger;
+
+ public override async Task TransformAsync(IPublishableCustomView itemToTransform, CancellationToken cancel)
+ {
+ var newDefaultUsers = itemToTransform.DefaultUsers.Where(user => !ExcludeUsernames.Contains(user.Name)).ToList();
+
+ itemToTransform.DefaultUsers = newDefaultUsers;
+
+ _logger?.LogInformation(
+ @"Excluding default users {newDefaultUsers}",
+ newDefaultUsers);
+
+ return await Task.FromResult(itemToTransform);
+ }
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/examples/Csharp.ExampleApplication/MyMigrationApplication.cs b/examples/Csharp.ExampleApplication/MyMigrationApplication.cs
index b2cb0bb..bd1b73b 100644
--- a/examples/Csharp.ExampleApplication/MyMigrationApplication.cs
+++ b/examples/Csharp.ExampleApplication/MyMigrationApplication.cs
@@ -113,6 +113,10 @@ public async Task StartAsync(CancellationToken cancel)
#region UnlicensedUsersFilter-Registration
_planBuilder.Filters.Add();
#endregion
+
+ #region SharedCustomViewFilter-Registration
+ _planBuilder.Filters.Add();
+ #endregion
// Add post-publish hooks
#region UpdatePermissionsHook-Registration
@@ -138,6 +142,10 @@ public async Task StartAsync(CancellationToken cancel)
#region StartAtTransformer-Registration
_planBuilder.Transformers.Add, ICloudExtractRefreshTask>();
#endregion
+
+ #region CustomViewDefaultUsersTransformer-Registration
+ _planBuilder.Transformers.Add();
+ #endregion
// Add migration action completed hooks
#region LogMigrationActionsHook-Registration
diff --git a/examples/Csharp.ExampleApplication/Program.cs b/examples/Csharp.ExampleApplication/Program.cs
index 1b56c72..48c1d6c 100644
--- a/examples/Csharp.ExampleApplication/Program.cs
+++ b/examples/Csharp.ExampleApplication/Program.cs
@@ -68,6 +68,10 @@ public static IServiceCollection AddCustomizations(this IServiceCollection servi
#region UnlicensedUsersFilter-DI
services.AddScoped();
#endregion
+
+ #region SharedCustomViewFilter-DI
+ services.AddScoped();
+ #endregion
#region UpdatePermissionsHook-DI
services.AddScoped(typeof(UpdatePermissionsHook<,>));
@@ -90,6 +94,10 @@ public static IServiceCollection AddCustomizations(this IServiceCollection servi
#region StartAtTransformer-DI
services.AddScoped(typeof(SimpleScheduleStartAtTransformer<>));
#endregion
+
+ #region CustomViewDefaultUsersTransformer-DI
+ services.AddScoped();
+ #endregion
#region LogMigrationActionsHook-DI
services.AddScoped();
diff --git a/examples/Python.ExampleApplication/Hooks/Filters/shared_custom_view_filter.py b/examples/Python.ExampleApplication/Hooks/Filters/shared_custom_view_filter.py
new file mode 100644
index 0000000..b7f8acd
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/Filters/shared_custom_view_filter.py
@@ -0,0 +1,11 @@
+from tableau_migration import (
+ ICustomView,
+ ContentMigrationItem,
+ ContentFilterBase)
+
+
+class SharedCustomViewFilter(ContentFilterBase[ICustomView]):
+ def should_migrate(self, item: ContentMigrationItem[ICustomView]) -> bool:
+ if item.source_item.shared == True:
+ return False
+ return True
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py
index 5607abb..ea530ee 100644
--- a/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py
+++ b/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py
@@ -11,6 +11,6 @@ def map(self, ctx: ContentMappingContext[IProject]) -> ContentMappingContext[IPr
new_location = ctx.content_item.location.rename("Production")
- ctx.map_to(new_location)
+ ctx = ctx.map_to(new_location)
return ctx
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Hooks/transformers/custom_view_default_users_transformer.py b/examples/Python.ExampleApplication/Hooks/transformers/custom_view_default_users_transformer.py
new file mode 100644
index 0000000..bd393db
--- /dev/null
+++ b/examples/Python.ExampleApplication/Hooks/transformers/custom_view_default_users_transformer.py
@@ -0,0 +1,14 @@
+from tableau_migration import (
+ ContentTransformerBase,
+ IContentReference,
+ IPublishableCustomView
+)
+
+class CustomViewDefaultUsersTransformer(ContentTransformerBase[IPublishableCustomView]):
+
+ #Pass in list of users retrieved from Users API
+ default_users = []
+
+ def transform(self, itemToTransform: IPublishableCustomView) -> IPublishableCustomView:
+ itemToTransform.default_users = self.default_users
+ return itemToTransform
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/Python.ExampleApplication.py b/examples/Python.ExampleApplication/Python.ExampleApplication.py
index 56e5183..e2bc15c 100644
--- a/examples/Python.ExampleApplication/Python.ExampleApplication.py
+++ b/examples/Python.ExampleApplication/Python.ExampleApplication.py
@@ -8,16 +8,15 @@
import configparser # configuration parser
import os # environment variables
-import sys # system utility
import tableau_migration # Tableau Migration SDK
-import print_result
+from print_result import print_result
from threading import Thread # threading
from tableau_migration import (
MigrationManifestSerializer,
MigrationManifest
- )
+)
serializer = MigrationManifestSerializer()
@@ -40,12 +39,14 @@ def migrate():
server_url = config['SOURCE']['URL'],
site_content_url = config['SOURCE']['SITE_CONTENT_URL'],
access_token_name = config['SOURCE']['ACCESS_TOKEN_NAME'],
- access_token = os.environ.get('TABLEAU_MIGRATION_SOURCE_TOKEN', config['SOURCE']['ACCESS_TOKEN'])) \
+ access_token = os.environ.get('TABLEAU_MIGRATION_SOURCE_TOKEN', config['SOURCE']['ACCESS_TOKEN']),
+ create_api_simulator = os.environ.get('TABLEAU_MIGRATION_SOURCE_SIMULATION', 'False') == 'True') \
.to_destination_tableau_cloud(
pod_url = config['DESTINATION']['URL'],
site_content_url = config['DESTINATION']['SITE_CONTENT_URL'],
access_token_name = config['DESTINATION']['ACCESS_TOKEN_NAME'],
- access_token = os.environ.get('TABLEAU_MIGRATION_DESTINATION_TOKEN', config['DESTINATION']['ACCESS_TOKEN'])) \
+ access_token = os.environ.get('TABLEAU_MIGRATION_DESTINATION_TOKEN', config['DESTINATION']['ACCESS_TOKEN']),
+ create_api_simulator = os.environ.get('TABLEAU_MIGRATION_DESTINATION_SIMULATION', 'False') == 'True') \
.for_server_to_cloud() \
.with_tableau_id_authentication_type() \
.with_tableau_cloud_usernames(config['USERS']['EMAIL_DOMAIN'])
@@ -101,7 +102,7 @@ def load_manifest(manifest_path: str) -> MigrationManifest | None:
while not done:
try:
migration_thread.join(1)
- sys.exit(0)
+ done = True
except KeyboardInterrupt:
# Ctrl+C was caught, request migration to cancel.
print("Caught Ctrl+C, shutting down...")
diff --git a/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj b/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj
index f2f9cbe..459ae6a 100644
--- a/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj
+++ b/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj
@@ -30,6 +30,7 @@
+
@@ -37,6 +38,7 @@
+
diff --git a/examples/Python.ExampleApplication/config.ini b/examples/Python.ExampleApplication/config.ini
index 325a2e8..a7ca03c 100644
--- a/examples/Python.ExampleApplication/config.ini
+++ b/examples/Python.ExampleApplication/config.ini
@@ -2,11 +2,13 @@
URL = http://server
SITE_CONTENT_URL =
ACCESS_TOKEN_NAME = MyServerTokenName
+ACCESS_TOKEN =
[DESTINATION]
URL = https://pod.online.tableau.com
SITE_CONTENT_URL = mycloudsite
ACCESS_TOKEN_NAME = MyCloudTokenName
+ACCESS_TOKEN =
[USERS]
EMAIL_DOMAIN = mycompany.com
diff --git a/examples/Python.ExampleApplication/print_result.py b/examples/Python.ExampleApplication/print_result.py
index aced451..5e6dd00 100644
--- a/examples/Python.ExampleApplication/print_result.py
+++ b/examples/Python.ExampleApplication/print_result.py
@@ -1,11 +1,10 @@
-import tableau_migration
from tableau_migration.migration import PyMigrationResult
from tableau_migration import IMigrationManifestEntry, MigrationManifestEntryStatus
from Tableau.Migration.Engine.Pipelines import ServerToCloudMigrationPipeline
-def print_result(self, result: PyMigrationResult):
+def print_result(result: PyMigrationResult):
"""Prints the result of a migration."""
- self.logger.info(f'Result: {result.status}')
+ print(f'Result: {result.status}')
for pipeline_content_type in ServerToCloudMigrationPipeline.ContentTypes:
content_type = pipeline_content_type.ContentType
@@ -41,4 +40,4 @@ def print_result(self, result: PyMigrationResult):
\t{count_pending}/{count_total} pending
'''
- self.logger.info(output)
\ No newline at end of file
+ print(output)
\ No newline at end of file
diff --git a/examples/Python.ExampleApplication/requirements.txt b/examples/Python.ExampleApplication/requirements.txt
index b7b0300..d8f075b 100644
--- a/examples/Python.ExampleApplication/requirements.txt
+++ b/examples/Python.ExampleApplication/requirements.txt
@@ -1,8 +1,3 @@
-setuptools==70.1.1
configparser==7.0.0
tableau_migration
-cffi==1.16.0
-pycparser==2.22
-pythonnet==3.0.3
-typing_extensions==4.12.2
python-dotenv==1.0.1
diff --git a/src/Documentation/api-csharp/index.md b/src/Documentation/api-csharp/index.md
index e34a3fe..2d7bf32 100644
--- a/src/Documentation/api-csharp/index.md
+++ b/src/Documentation/api-csharp/index.md
@@ -6,40 +6,7 @@ Welcome to the C# API Reference for the Migration SDK.
The following code samples are for writing a simple migration app using the Migration SDK. For details on configuring and customizing the Migration SDK to your specific needs, see [Articles](~/articles/index.md) and [Code Samples](~/samples/index.md).
-### [Program.cs](#tab/program-cs)
-
-[!code-csharp[](../../../examples/Csharp.ExampleApplication/Program.cs#namespace)]
-
-### [Startup code](#tab/startup-cde)
-
-[!code-csharp[](../../../examples/Csharp.ExampleApplication/MyMigrationApplication.cs#namespace)]
-
-### [Config classes](#tab/config-classes)
-
-[!code-csharp[](../../../examples/Csharp.ExampleApplication/Config/MyMigrationApplicationOptions.cs#namespace)]
-
-[!code-csharp[](../../../examples/Csharp.ExampleApplication/Config/EndpointOptions.cs#namespace)]
-
-### [Config file](#tab/appsettings)
-
-```json
-{
- "source": {
- "serverUrl": "http://server",
- "siteContentUrl": "",
- "accessTokenName": "my server token name",
- "accessToken": "my-secret-server-pat"
- },
- "destination": {
- "serverUrl": "https://pod.online.tableau.com",
- "siteContentUrl": "site-name",
- "accessTokenName": "my cloud token name",
- "accessToken": "my-secret-cloud-pat"
- }
-}
-```
-
----
+[!include[](~/includes/csharp-getting-started.md)]
## Suggested Reading
diff --git a/src/Documentation/api-python/index.md b/src/Documentation/api-python/index.md
index e008e3a..55371fa 100644
--- a/src/Documentation/api-python/index.md
+++ b/src/Documentation/api-python/index.md
@@ -24,19 +24,4 @@ There are advanced features of the Migration SDK that the Python Wrapper cannot
The following code samples are for writing a simple migration app using the Migration SDK. For details on configuring and customizing the Migration SDK to your specific needs, see [Articles](~/articles/index.md) and [Code Samples](~/samples/index.md).
-### [Startup Script](#tab/startup)
-
-[!code-python[](../../../examples/Python.ExampleApplication/Python.ExampleApplication.py)]
-
-### [config.ini](#tab/config)
-
-> [!Important]
-> The values below should not be quoted. So ***no*** `'` or `"`.
-
-[!code-ini[](../../../examples/Python.ExampleApplication/config.ini)]
-
-### [requirements.txt](#tab/reqs)
-
-[!code-text[](../../../examples/Python.ExampleApplication/requirements.txt#L3-)]
-
----
+[!include[](~/includes/python-getting-started.md)]
diff --git a/src/Documentation/articles/configuration.md b/src/Documentation/articles/configuration.md
index 02bd091..5f7c6f0 100644
--- a/src/Documentation/articles/configuration.md
+++ b/src/Documentation/articles/configuration.md
@@ -448,6 +448,20 @@ Supported Content Types:
*Description:* The Migration SDK uses [Microsoft.Extensions.Http.Resilience](https://learn.microsoft.com/en-us/dotnet/core/resilience) as a resilience and transient-fault layer. The SDK uses the **PerFileTransferRequestTimeout** property to define the maximum duration of FileTransfer requests.
+
+### Network.UserAgentComment
+
+*Reference:* [`NetworkOptions.UserAgentComment`](xref:Tableau.Migration.Config.NetworkOptions#Tableau_Migration_Config_NetworkOptions_UserAgentComment).
+
+*Default:* [`NetworkOptions.Defaults.USER_AGENT_COMMENT`](xref:Tableau.Migration.Config.NetworkOptions.Defaults#Tableau_Migration_Config_NetworkOptions_Defaults_USER_AGENT_COMMENT).
+
+*Python Environment Variable:* `MigrationSDK__Network__UserAgentComment`
+
+*Reload on Edit?:* **No**. Any changes to this configuration will reflect on the next time the application starts.
+
+*Description:* The Migration SDK appends the **UserAgentComment** property to the User-Agent header in all HTTP requests. This property is only used to assist in server-side debugging and it not typically set.
+
+
### DefaultPermissionsContentTypes.UrlSegments
*Reference:* [`DefaultPermissionsContentTypeOptions.UrlSegments`](xref:Tableau.Migration.Config.DefaultPermissionsContentTypeOptions#Tableau_Migration_Config_DefaultPermissionsContentTypeOptions_UrlSegments).
diff --git a/src/Documentation/articles/cv_file.md b/src/Documentation/articles/cv_file.md
new file mode 100644
index 0000000..9a5a607
--- /dev/null
+++ b/src/Documentation/articles/cv_file.md
@@ -0,0 +1,32 @@
+# Anatomy of a Custom View File
+
+The Migration SDK downloads Custom View definition files during the migration process. There files are in JSON format. This is what a Custom View File looks like:
+
+```json
+[
+ .....
+ {
+ "isSourceView": true,
+ "viewName": "View 1",
+ "tcv": "{base-64 custom view xml content with + signs replaced by -}"
+ }
+ .....
+]
+
+```
+
+## Getting the Custom View Definition from the Custom View File
+
+In the JSON file, the field `tcv` contains the XML definition of a custom view. This field is encoded as a Base64 string, with `+` characters replaced by `-`.
+
+To decode the `tcv` value
+
+1. Replace all the `-` characters in the string with `+`.
+2. Decode the resulting Base64 string.
+3. Get the XML Custom View Definition.
+
+To encode the `tcv` value
+
+1. Make any changes to the Custom View Definition.
+2. Encode the XML into a Base64 string.
+3. Replace all the `+` characters in the string with `-`.
diff --git a/src/Documentation/articles/hooks/python_hook_update.md b/src/Documentation/articles/hooks/python_hook_update.md
index bb6fcbe..862cab1 100644
--- a/src/Documentation/articles/hooks/python_hook_update.md
+++ b/src/Documentation/articles/hooks/python_hook_update.md
@@ -54,8 +54,8 @@ class FilterBob(ContentFilterBase[IUser]):
### Version 3 -> Version 4+ Registration Diff
```diff
-- plan_builder.filters.add(IUser, FilterBob())
-+ plan_builder.filters.add(FilterBob())
+- plan_builder.filters.add(IUser, FilterBob)
++ plan_builder.filters.add(FilterBob)
```
## Mappings
diff --git a/src/Documentation/articles/index.md b/src/Documentation/articles/index.md
index a8f3304..1fa3e76 100644
--- a/src/Documentation/articles/index.md
+++ b/src/Documentation/articles/index.md
@@ -9,6 +9,12 @@ You can develop your migration application using one of the supported languages
- [Python](https://www.python.org/)
- C# using the [.NET Framework](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet-framework)
+## Versioning
+
+The Migration SDK uses [semantic versioning](https://semver.org).
+
+![Versioning](../images/versioning.svg){width=40%}
+
## Prerequisites
To develop your application using the [Migration SDK](https://github.com/tableau/tableau-migration-sdk), you should
@@ -23,30 +29,42 @@ To develop your application using the [Migration SDK](https://github.com/tableau
- Install a [.NET Runtime](https://dotnet.microsoft.com/en-us/download).
- Install the Migration SDK
-## [Python](#tab/Python)
+### [Python](#tab/Python)
+
+Install using PIP
- Install using PIP
- - [PIP CLI](https://pip.pypa.io/en/stable/cli/pip_install): `pip install tableau_migration`
+- [PIP CLI](https://pip.pypa.io/en/stable/cli/pip_install): `pip install tableau_migration`
-## [C#](#tab/CSharp)
+### [C#](#tab/CSharp)
- Install using NuGet
- - [dotnet CLI](https://learn.microsoft.com/en-us/nuget/quickstart/install-and-use-a-package-using-the-dotnet-cli): `dotnet add package Tableau.Migration`
- - [Nuget Package Manager](https://learn.microsoft.com/en-us/nuget/quickstart/install-and-use-a-package-in-visual-studio): Search for `Tableau.Migration`.
+Install using NuGet
+
+- [dotnet CLI](https://learn.microsoft.com/en-us/nuget/quickstart/install-and-use-a-package-using-the-dotnet-cli): `dotnet add package Tableau.Migration`
+- [Nuget Package Manager](https://learn.microsoft.com/en-us/nuget/quickstart/install-and-use-a-package-in-visual-studio): Search for `Tableau.Migration`.
---
-- Use the sample code in one of the [Reference](#getting-started-and-api-reference) sections to get started.
+- Use the sample code in [Example startup code](#example-startup-code) to get started.
- Use the [Resource](#resources) sections to further customize your application.
-## Getting started and API Reference
+## Example startup code
-- [Python API Reference](~/api-python/index.md) : Getting started sample and the complete Python API Reference for comprehensive documentation.
-- [C# API Reference](~/api-csharp/index.md): Getting started sample and the complete C# API Reference for detailed documentation.
+The following code samples are for writing a simple migration app using the Migration SDK. For details on configuring and customizing the Migration SDK to your specific needs, see the other articles and [Code Samples](~/samples/index.md).
+
+### [Python](#tab/Python)
+
+[!include[](~/includes/python-getting-started.md)]
+
+### [C#](#tab/CSharp)
+
+[!include[](~/includes/csharp-getting-started.md)]
+
+---
## Resources
-- [Articles](~/articles/index.md): Articles covering a range of topics, including customization of the Migration SDK.
+- [Python API Reference](~/api-python/index.md) : Getting started sample and the complete Python API Reference for comprehensive documentation.
+- [C# API Reference](~/api-csharp/index.md): Getting started sample and the complete C# API Reference for detailed documentation.
- [Code Samples](~/samples/index.md): Code samples to kickstart your development process.
## Source Code
diff --git a/src/Documentation/articles/toc.yml b/src/Documentation/articles/toc.yml
index a80c5b9..1e818aa 100644
--- a/src/Documentation/articles/toc.yml
+++ b/src/Documentation/articles/toc.yml
@@ -15,6 +15,8 @@
href: hooks/python_hook_update.md
- name: User Authentication
href: user_authentication.md
+- name: Custom View File
+ href: cv_file.md
- name: Dependency Injection
href: dependency_injection.md
- name: Troubleshooting
diff --git a/src/Documentation/images/versioning.svg b/src/Documentation/images/versioning.svg
new file mode 100644
index 0000000..f5d604d
--- /dev/null
+++ b/src/Documentation/images/versioning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Documentation/includes/csharp-getting-started.md b/src/Documentation/includes/csharp-getting-started.md
new file mode 100644
index 0000000..85a7c01
--- /dev/null
+++ b/src/Documentation/includes/csharp-getting-started.md
@@ -0,0 +1,34 @@
+### [Program.cs](#tab/program-cs)
+
+[!code-csharp[](../../../examples/Csharp.ExampleApplication/Program.cs#namespace)]
+
+### [Startup code](#tab/startup-cde)
+
+[!code-csharp[](../../../examples/Csharp.ExampleApplication/MyMigrationApplication.cs#namespace)]
+
+### [Config classes](#tab/config-classes)
+
+[!code-csharp[](../../../examples/Csharp.ExampleApplication/Config/MyMigrationApplicationOptions.cs#namespace)]
+
+[!code-csharp[](../../../examples/Csharp.ExampleApplication/Config/EndpointOptions.cs#namespace)]
+
+### [Config file](#tab/appsettings)
+
+```json
+{
+ "source": {
+ "serverUrl": "http://server",
+ "siteContentUrl": "",
+ "accessTokenName": "my-server-token-name",
+ "accessToken": "my-secret-server-pat"
+ },
+ "destination": {
+ "serverUrl": "https://pod.online.tableau.com",
+ "siteContentUrl": "site-name",
+ "accessTokenName": "my-cloud-token-name",
+ "accessToken": "my-secret-cloud-pat"
+ }
+}
+```
+
+---
diff --git a/src/Documentation/includes/python-getting-started.md b/src/Documentation/includes/python-getting-started.md
new file mode 100644
index 0000000..eae84a7
--- /dev/null
+++ b/src/Documentation/includes/python-getting-started.md
@@ -0,0 +1,16 @@
+### [Startup Script](#tab/startup)
+
+[!code-python[](../../../examples/Python.ExampleApplication/Python.ExampleApplication.py)]
+
+### [config.ini](#tab/config)
+
+> [!Important]
+> The values below should not be quoted. So ***no*** `'` or `"`.
+
+[!code-ini[](../../../examples/Python.ExampleApplication/config.ini)]
+
+### [requirements.txt](#tab/reqs)
+
+[!code-text[](../../../examples/Python.ExampleApplication/requirements.txt#L3-)]
+
+---
diff --git a/src/Documentation/samples/filters/filter_custom_views_by_shared.md b/src/Documentation/samples/filters/filter_custom_views_by_shared.md
new file mode 100644
index 0000000..df28d97
--- /dev/null
+++ b/src/Documentation/samples/filters/filter_custom_views_by_shared.md
@@ -0,0 +1,36 @@
+# Sample: Filter Custom Views by Shared Status
+
+In this example, the custom views currently shared to all users are excluded from migration.
+
+# [Python](#tab/Python)
+
+#### Filter Class
+
+[!code-python[](../../../../examples/Python.ExampleApplication/hooks/filters/shared_custom_view_filter.py)]
+
+#### Registration
+
+[Learn more.](~/samples/index.md?tabs=Python#hook-registration)
+
+[//]: <> (Adding this as code as regions are not supported in Python snippets)
+```Python
+plan_builder.filters.add(SharedCustomViewFilter)
+```
+
+# [C#](#tab/CSharp)
+
+#### Filter Class
+
+[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Hooks/Filters/SharedCustomViewFilter.cs#class)]
+
+#### Registration
+
+[Learn more.](~/samples/index.md?tabs=Python#hook-registration)
+
+[!code-csharp[](../../../../examples/Csharp.ExampleApplication/MyMigrationApplication.cs#SharedCustomViewFilter-Registration)]
+
+#### Dependency Injection
+
+[Learn more.](~/articles/dependency_injection.md)
+
+[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Program.cs#SharedCustomViewFilter-DI)]
diff --git a/src/Documentation/samples/filters/filter_projects_by_name.md b/src/Documentation/samples/filters/filter_projects_by_name.md
index dfffc55..79f0583 100644
--- a/src/Documentation/samples/filters/filter_projects_by_name.md
+++ b/src/Documentation/samples/filters/filter_projects_by_name.md
@@ -10,13 +10,14 @@ In this example, the project named `Default` is filtered out.
#### Registration
-[Learn more.](~/samples/index.md?tabs=Python#hook-registration)
-
[//]: <> (Adding this as code as regions are not supported in Python snippets)
+
```Python
plan_builder.filters.add(DefaultProjectFilter)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
# [C#](#tab/CSharp)
#### Filter Class
diff --git a/src/Documentation/samples/filters/filter_users_by_site_role.md b/src/Documentation/samples/filters/filter_users_by_site_role.md
index 12f2e79..9bba5f5 100644
--- a/src/Documentation/samples/filters/filter_users_by_site_role.md
+++ b/src/Documentation/samples/filters/filter_users_by_site_role.md
@@ -10,13 +10,14 @@ In this example, all unlicensed users are excluded from migration.
#### Registration
-[Learn more.](~/samples/index.md?tabs=Python#hook-registration)
-
[//]: <> (Adding this as code as regions are not supported in Python snippets)
+
```Python
plan_builder.filters.add(UnlicensedUserFilter)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
# [C#](#tab/CSharp)
#### Filter Class
diff --git a/src/Documentation/samples/filters/index.md b/src/Documentation/samples/filters/index.md
index d8e7b6d..ee4641d 100644
--- a/src/Documentation/samples/filters/index.md
+++ b/src/Documentation/samples/filters/index.md
@@ -10,3 +10,5 @@ The following samples cover some common scenarios:
- [Sample: Filter projects by name](~/samples/filters/filter_projects_by_name.md)
- [Sample: Filter users by site role](~/samples/filters/filter_users_by_site_role.md)
+
+- [Sample: Filter custom views by shared setting](~/samples/filters/filter_custom_views_by_shared.md)
diff --git a/src/Documentation/samples/filters/toc.yml b/src/Documentation/samples/filters/toc.yml
index 47de512..7cbe411 100644
--- a/src/Documentation/samples/filters/toc.yml
+++ b/src/Documentation/samples/filters/toc.yml
@@ -1,4 +1,6 @@
- name: Filter projects by name
href: filter_projects_by_name.md
- name: Filter users by SiteRole
- href: filter_users_by_site_role.md
\ No newline at end of file
+ href: filter_users_by_site_role.md
+- name: Filter Custom Views by 'Shared' flag
+ href: filter_custom_views_by_shared.md
\ No newline at end of file
diff --git a/src/Documentation/samples/mappings/change_projects.md b/src/Documentation/samples/mappings/change_projects.md
index bfaa1b0..a3e49a5 100644
--- a/src/Documentation/samples/mappings/change_projects.md
+++ b/src/Documentation/samples/mappings/change_projects.md
@@ -12,14 +12,15 @@ Both the C# and Python mapping classes inherit from a base class that handles mo
#### Registration
-[Learn more.](~/samples/index.md?tabs=Python#hook-registration)
-
[//]: <> (Adding this as code as regions are not supported in Python snippets)
+
```Python
plan_builder.mappings.add(ChangeProjectMappingForWorkbooks)
plan_builder.mappings.add(ChangeProjectMappingForDataSources)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
# [C#](#tab/CSharp)
#### Mapping Class
diff --git a/src/Documentation/samples/mappings/rename_projects.md b/src/Documentation/samples/mappings/rename_projects.md
index fdb178a..2e37c32 100644
--- a/src/Documentation/samples/mappings/rename_projects.md
+++ b/src/Documentation/samples/mappings/rename_projects.md
@@ -10,13 +10,14 @@ In this example, the source project named `Test` is renamed to `Production` on t
#### Registration
-[Learn more.](~/samples/index.md?tabs=Python#hook-registration)
-
[//]: <> (Adding this as code as regions are not supported in Python snippets)
+
```Python
-plan_builder.mappings.add(EmailDomainMapping)
+plan_builder.mappings.add(ProjectRenameMapping)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
# [C#](#tab/CSharp)
#### Mapping Class
diff --git a/src/Documentation/samples/migration-action-completed/log_migration_actions.md b/src/Documentation/samples/migration-action-completed/log_migration_actions.md
index de30f6e..da7d64e 100644
--- a/src/Documentation/samples/migration-action-completed/log_migration_actions.md
+++ b/src/Documentation/samples/migration-action-completed/log_migration_actions.md
@@ -12,14 +12,15 @@ To log migration action statuses in Python, you can utilize the following hook c
### Registration
-For guidance on registering the hook, refer to the [documentation](~/samples/index.md?tabs=Python#hook-registration).
-
[//]: <> (Adding this as code as regions are not supported in python snippets)
+
```Python
plan_builder.hooks.add(LogMigrationActionsHookForUsers)
plan_builder.hooks.add(LogMigrationActionsHookForGroups)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
# [C#](#tab/CSharp)
### Migration Action Completed Hook Class
diff --git a/src/Documentation/samples/transformers/custom_view_default_users_transformer.md b/src/Documentation/samples/transformers/custom_view_default_users_transformer.md
new file mode 100644
index 0000000..0e0ba7e
--- /dev/null
+++ b/src/Documentation/samples/transformers/custom_view_default_users_transformer.md
@@ -0,0 +1,43 @@
+# Sample: Changing Default Users for Custom View
+
+This sample illustrates how to change the default users for a custom view
+
+Both the Python and C# transformer classes inherit from a base class that handles the core functionality, then create versions for `IPublishableCustomView`.
+
+## [Python](#tab/Python)
+
+### Transformer Class
+
+To implement the tag addition in Python, you can utilize the following transformer class:
+
+[!code-python[](../../../../examples/Python.ExampleApplication/hooks/transformers/custom_view_default_users_transformer.py)]
+
+### Registration
+
+For detailed instructions on registering the transformer, refer to the [documentation](~/samples/index.md?tabs=Python#hook-registration).
+
+[//]: <> (Adding this as code as regions are not supported in python snippets)
+```Python
+plan_builder.transformers.add(CustomViewDefaultUsersTransformer)
+```
+
+## [C#](#tab/CSharp)
+
+### Transformer Class
+
+In C#, the transformer class for adding tags is implemented as shown below:
+
+[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Hooks/Transformers/CustomViewDefaultUsersTransformer.cs#class)]
+
+### Registration
+
+To register the transformer in C#, follow the guidance provided in the [documentation](~/samples/index.md?tabs=CSharp#hook-registration).
+
+[!code-csharp[](../../../../examples/Csharp.ExampleApplication/MyMigrationApplication.cs#CustomViewDefaultUsersTransformer-Registration)]
+
+### Dependency Injection
+
+Learn more about dependency injection [here](~/articles/dependency_injection.md).
+
+[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Program.cs#CustomViewDefaultUsersTransformer-DI)]
+
diff --git a/src/Documentation/samples/transformers/encrypt_extracts_transformer.md b/src/Documentation/samples/transformers/encrypt_extracts_transformer.md
index dc0f6ac..3e0224a 100644
--- a/src/Documentation/samples/transformers/encrypt_extracts_transformer.md
+++ b/src/Documentation/samples/transformers/encrypt_extracts_transformer.md
@@ -14,14 +14,15 @@ To encrypt extracts in Python, you can use the following transformer class:
### Registration
-Refer to the [documentation](~/samples/index.md?tabs=Python#hook-registration) for instructions on registering the transformer.
-
[//]: <> (Adding this as code as regions are not supported in python snippets)
+
```Python
plan_builder.transformers.add(EncryptExtractTransformerForDataSources)
plan_builder.transformers.add(EncryptExtractTransformerForWorkbooks)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
## [C#](#tab/CSharp)
### Transformer Class
diff --git a/src/Documentation/samples/transformers/index.md b/src/Documentation/samples/transformers/index.md
index 3d14737..d290ab6 100644
--- a/src/Documentation/samples/transformers/index.md
+++ b/src/Documentation/samples/transformers/index.md
@@ -6,4 +6,5 @@ The following samples cover some common scenarios:
- [Sample: Add tags to content](~/samples/transformers/migrated_tag_transformer.md)
- [Sample: Encrypt Extracts](~/samples/transformers/encrypt_extracts_transformer.md)
-- [Sample: Adjust 'Start At' to Scheduled Tasks](~/samples/transformers/start_at_transformer.md)
\ No newline at end of file
+- [Sample: Adjust 'Start At' to Scheduled Tasks](~/samples/transformers/start_at_transformer.md)
+- [Sample: Change default users for Custom Views](~/samples/transformers/custom_view_default_users_transformer.md)
\ No newline at end of file
diff --git a/src/Documentation/samples/transformers/migrated_tag_transformer.md b/src/Documentation/samples/transformers/migrated_tag_transformer.md
index 59502a5..ebf1a3e 100644
--- a/src/Documentation/samples/transformers/migrated_tag_transformer.md
+++ b/src/Documentation/samples/transformers/migrated_tag_transformer.md
@@ -14,14 +14,15 @@ To implement the tag addition in Python, you can utilize the following transform
### Registration
-For detailed instructions on registering the transformer, refer to the [documentation](~/samples/index.md?tabs=Python#hook-registration).
-
[//]: <> (Adding this as code as regions are not supported in python snippets)
+
```Python
plan_builder.transformers.add(MigratedTagTransformerForDataSources)
plan_builder.transformers.add(MigratedTagTransformerForWorkbooks)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
## [C#](#tab/CSharp)
### Transformer Class
@@ -41,4 +42,3 @@ To register the transformer in C#, follow the guidance provided in the [document
Learn more about dependency injection [here](~/articles/dependency_injection.md).
[!code-csharp[](../../../../examples/Csharp.ExampleApplication/Program.cs#MigratedTagTransformer-DI)]
-
diff --git a/src/Documentation/samples/transformers/start_at_transformer.md b/src/Documentation/samples/transformers/start_at_transformer.md
index efa75fa..921919b 100644
--- a/src/Documentation/samples/transformers/start_at_transformer.md
+++ b/src/Documentation/samples/transformers/start_at_transformer.md
@@ -14,13 +14,14 @@ To adjust the 'Start At' in Python, you can use the following transformer class:
### Registration
-Refer to the [documentation](~/samples/index.md?tabs=Python#hook-registration) for instructions on registering the transformer.
-
[//]: <> (Adding this as code as regions are not supported in python snippets)
+
```Python
plan_builder.transformers.add(SimpleScheduleStartAtTransformer)
```
+See [hook registration](~/samples/index.md?tabs=Python#hook-registration) for more details.
+
## [C#](#tab/CSharp)
### Transformer Class
diff --git a/src/Documentation/samples/transformers/toc.yml b/src/Documentation/samples/transformers/toc.yml
index aa9cdcc..248d958 100644
--- a/src/Documentation/samples/transformers/toc.yml
+++ b/src/Documentation/samples/transformers/toc.yml
@@ -4,3 +4,5 @@
href: encrypt_extracts_transformer.md
- name: Adjust 'Start At' to Scheduled Tasks
href: start_at_transformer.md
+- name: Change default users for Custom Views
+ href: custom_view_default_users_transformer.md
\ No newline at end of file
diff --git a/src/Python/requirements.txt b/src/Python/requirements.txt
index 2185e35..9c48404 100644
--- a/src/Python/requirements.txt
+++ b/src/Python/requirements.txt
@@ -3,4 +3,4 @@ pycparser==2.22
pythonnet==3.0.3
typing_extensions==4.12.2
pytest-env==1.1.3
-build=1.2.1
\ No newline at end of file
+build==1.2.1
\ No newline at end of file
diff --git a/src/Python/src/tableau_migration/__init__.py b/src/Python/src/tableau_migration/__init__.py
index 80e7fc4..a7eba83 100644
--- a/src/Python/src/tableau_migration/__init__.py
+++ b/src/Python/src/tableau_migration/__init__.py
@@ -99,6 +99,7 @@
from tableau_migration.migration_content import PyConnection as IConnection # noqa: E402, F401
from tableau_migration.migration_content import PyConnectionsContent as IConnectionsContent # noqa: E402, F401
from tableau_migration.migration_content import PyContainerContent as IContainerContent # noqa: E402, F401
+from tableau_migration.migration_content import PyCustomView as ICustomView # noqa: E402, F401
from tableau_migration.migration_content import PyDataSource as IDataSource # noqa: E402, F401
from tableau_migration.migration_content import PyDataSourceDetails as IDataSourceDetails # noqa: E402, F401
from tableau_migration.migration_content import PyDescriptionContent as IDescriptionContent # noqa: E402, F401
@@ -107,6 +108,7 @@
from tableau_migration.migration_content import PyGroupUser as IGroupUser # noqa: E402, F401
from tableau_migration.migration_content import PyLabel as ILabel # noqa: E402, F401
from tableau_migration.migration_content import PyProject as IProject # noqa: E402, F401
+from tableau_migration.migration_content import PyPublishableCustomView as IPublishableCustomView # noqa: E402, F401
from tableau_migration.migration_content import PyPublishableDataSource as IPublishableDataSource # noqa: E402, F401
from tableau_migration.migration_content import PyPublishableGroup as IPublishableGroup # noqa: E402, F401
from tableau_migration.migration_content import PyPublishableWorkbook as IPublishableWorkbook # noqa: E402, F401
@@ -118,6 +120,7 @@
from tableau_migration.migration_content import PyWithDomain as IWithDomain # noqa: E402, F401
from tableau_migration.migration_content import PyWithOwner as IWithOwner # noqa: E402, F401
from tableau_migration.migration_content import PyWithTags as IWithTags # noqa: E402, F401
+from tableau_migration.migration_content import PyWithWorkbook as IWithWorkbook # noqa: E402, F401
from tableau_migration.migration_content import PyWorkbook as IWorkbook # noqa: E402, F401
from tableau_migration.migration_content import PyWorkbookDetails as IWorkbookDetails # noqa: E402, F401
from tableau_migration.migration_content_permissions import PyCapability as ICapability # noqa: E402, F401
diff --git a/src/Python/src/tableau_migration/migration_content.py b/src/Python/src/tableau_migration/migration_content.py
index cf562af..c737cc6 100644
--- a/src/Python/src/tableau_migration/migration_content.py
+++ b/src/Python/src/tableau_migration/migration_content.py
@@ -15,6 +15,8 @@
"""Wrapper for classes in Tableau.Migration.Content namespace."""
+from Tableau.Migration import IContentReference # noqa: E402, F401
+
# region _generated
from tableau_migration.migration import PyContentReference # noqa: E402, F401
@@ -37,6 +39,7 @@
IConnection,
IConnectionsContent,
IContainerContent,
+ ICustomView,
IDataSource,
IDataSourceDetails,
IDescriptionContent,
@@ -45,6 +48,7 @@
IGroupUser,
ILabel,
IProject,
+ IPublishableCustomView,
IPublishableDataSource,
IPublishableGroup,
IPublishableWorkbook,
@@ -56,6 +60,7 @@
IWithDomain,
IWithOwner,
IWithTags,
+ IWithWorkbook,
IWorkbook,
IWorkbookDetails
)
@@ -145,6 +150,106 @@ def container(self) -> PyContentReference:
"""Gets the container for the content item. Relocating the content should be done through mapping."""
return None if self._dotnet.Container is None else PyContentReference(self._dotnet.Container)
+class PyWithOwner(PyContentReference):
+ """Interface to be inherited by content items with owner."""
+
+ _dotnet_base = IWithOwner
+
+ def __init__(self, with_owner: IWithOwner) -> None:
+ """Creates a new PyWithOwner object.
+
+ Args:
+ with_owner: A IWithOwner object.
+
+ Returns: None.
+ """
+ self._dotnet = with_owner
+
+ @property
+ def owner(self) -> PyContentReference:
+ """Gets or sets the owner for the content item."""
+ return None if self._dotnet.Owner is None else PyContentReference(self._dotnet.Owner)
+
+ @owner.setter
+ def owner(self, value: PyContentReference) -> None:
+ """Gets or sets the owner for the content item."""
+ self._dotnet.Owner = None if value is None else value._dotnet
+
+class PyWithWorkbook(PyContentReference):
+ """Interface to be inherited by content items with workbook."""
+
+ _dotnet_base = IWithWorkbook
+
+ def __init__(self, with_workbook: IWithWorkbook) -> None:
+ """Creates a new PyWithWorkbook object.
+
+ Args:
+ with_workbook: A IWithWorkbook object.
+
+ Returns: None.
+ """
+ self._dotnet = with_workbook
+
+ @property
+ def workbook(self) -> PyContentReference:
+ """Gets or sets the workbook for the content item."""
+ return None if self._dotnet.Workbook is None else PyContentReference(self._dotnet.Workbook)
+
+ @workbook.setter
+ def workbook(self, value: PyContentReference) -> None:
+ """Gets or sets the workbook for the content item."""
+ self._dotnet.Workbook = None if value is None else value._dotnet
+
+class PyCustomView(PyWithOwner, PyWithWorkbook):
+ """The interface for a custom view content item."""
+
+ _dotnet_base = ICustomView
+
+ def __init__(self, custom_view: ICustomView) -> None:
+ """Creates a new PyCustomView object.
+
+ Args:
+ custom_view: A ICustomView object.
+
+ Returns: None.
+ """
+ self._dotnet = custom_view
+
+ @property
+ def created_at(self) -> str:
+ """Gets the created timestamp."""
+ return self._dotnet.CreatedAt
+
+ @property
+ def updated_at(self) -> str:
+ """Gets the updated timestamp."""
+ return self._dotnet.UpdatedAt
+
+ @property
+ def last_accessed_at(self) -> str:
+ """Gets the last accessed timestamp."""
+ return self._dotnet.LastAccessedAt
+
+ @property
+ def shared(self) -> bool:
+ """Gets or sets whether the custom view is shared with all users (true) or private (false)."""
+ return self._dotnet.Shared
+
+ @shared.setter
+ def shared(self, value: bool) -> None:
+ """Gets or sets whether the custom view is shared with all users (true) or private (false)."""
+ self._dotnet.Shared = value
+
+ @property
+ def base_view_id(self) -> UUID:
+ """Gets the ID of the view that this custom view is based on."""
+ return None if self._dotnet.BaseViewId is None else UUID(self._dotnet.BaseViewId.ToString())
+
+ @property
+ def base_view_name(self) -> str:
+ """Gets the name of the view that this custom view is based on."""
+ return self._dotnet.BaseViewName
+
class PyPublishedContent():
"""Interface for a content item that has metadata around publishing information."""
@@ -281,31 +386,6 @@ def tags(self, value: List[PyTag]) -> None:
dotnet_collection.Add(x._dotnet)
self._dotnet.Tags = dotnet_collection
-class PyWithOwner(PyContentReference):
- """Interface to be inherited by content items with owner."""
-
- _dotnet_base = IWithOwner
-
- def __init__(self, with_owner: IWithOwner) -> None:
- """Creates a new PyWithOwner object.
-
- Args:
- with_owner: A IWithOwner object.
-
- Returns: None.
- """
- self._dotnet = with_owner
-
- @property
- def owner(self) -> PyContentReference:
- """Gets or sets the owner for the content item."""
- return None if self._dotnet.Owner is None else PyContentReference(self._dotnet.Owner)
-
- @owner.setter
- def owner(self, value: PyContentReference) -> None:
- """Gets or sets the owner for the content item."""
- self._dotnet.Owner = None if value is None else value._dotnet
-
class PyDataSource(PyPublishedContent, PyDescriptionContent, PyExtractContent, PyWithTags, PyContainerContent, PyWithOwner):
"""Interface for a data source content item."""
@@ -561,6 +641,37 @@ def parent_project(self) -> PyContentReference:
"""Gets the parent project reference, or null if the project is a top-level project. Should be changed through mapping."""
return None if self._dotnet.ParentProject is None else PyContentReference(self._dotnet.ParentProject)
+class PyPublishableCustomView(PyCustomView):
+ """Interface for the publishable version of ICustomView."""
+
+ _dotnet_base = IPublishableCustomView
+
+ def __init__(self, publishable_custom_view: IPublishableCustomView) -> None:
+ """Creates a new PyPublishableCustomView object.
+
+ Args:
+ publishable_custom_view: A IPublishableCustomView object.
+
+ Returns: None.
+ """
+ self._dotnet = publishable_custom_view
+
+ @property
+ def default_users(self) -> List[PyContentReference]:
+ """The list of users for whom the Custom View is the default."""
+ return [] if self._dotnet.DefaultUsers is None else [PyContentReference(x) for x in self._dotnet.DefaultUsers if x is not None]
+
+ @default_users.setter
+ def default_users(self, value: List[PyContentReference]) -> None:
+ """The list of users for whom the Custom View is the default."""
+ if value is None:
+ self._dotnet.DefaultUsers = DotnetList[IContentReference]()
+ else:
+ dotnet_collection = DotnetList[IContentReference]()
+ for x in filter(None,value):
+ dotnet_collection.Add(x._dotnet)
+ self._dotnet.DefaultUsers = dotnet_collection
+
class PyPublishableDataSource(PyDataSourceDetails, PyConnectionsContent):
"""Interface for a IDataSource that has been downloaded and has full information necessary for re-publishing."""
diff --git a/src/Python/tests/test_classes.py b/src/Python/tests/test_classes.py
index 728c172..90151a8 100644
--- a/src/Python/tests/test_classes.py
+++ b/src/Python/tests/test_classes.py
@@ -257,6 +257,7 @@ def test_overloaded_missing(self):
PyConnection,
PyConnectionsContent,
PyContainerContent,
+ PyCustomView,
PyDataSource,
PyDataSourceDetails,
PyDescriptionContent,
@@ -265,6 +266,7 @@ def test_overloaded_missing(self):
PyGroupUser,
PyLabel,
PyProject,
+ PyPublishableCustomView,
PyPublishableDataSource,
PyPublishableGroup,
PyPublishableWorkbook,
@@ -276,6 +278,7 @@ def test_overloaded_missing(self):
PyWithDomain,
PyWithOwner,
PyWithTags,
+ PyWithWorkbook,
PyWorkbook,
PyWorkbookDetails
)
@@ -339,13 +342,14 @@ def test_overloaded_missing(self):
from Tableau.Migration.Engine.Manifest import MigrationManifestEntryStatus
_generated_class_data = [
- (PyContentLocation, None),
+ (PyContentLocation, [ "ForContentType" ]),
(PyContentReference, None),
(PyResult, [ "CastFailure" ]),
(PyRestIdentifiable, None),
(PyConnection, None),
(PyConnectionsContent, None),
(PyContainerContent, None),
+ (PyCustomView, None),
(PyDataSource, [ "SetLocation" ]),
(PyDataSourceDetails, [ "SetLocation" ]),
(PyDescriptionContent, None),
@@ -354,6 +358,7 @@ def test_overloaded_missing(self):
(PyGroupUser, None),
(PyLabel, None),
(PyProject, [ "Container", "SetLocation" ]),
+ (PyPublishableCustomView, [ "DisposeAsync", "File" ]),
(PyPublishableDataSource, [ "DisposeAsync", "File", "SetLocation" ]),
(PyPublishableGroup, [ "SetLocation" ]),
(PyPublishableWorkbook, [ "ChildPermissionContentItems", "ChildType", "DisposeAsync", "File", "SetLocation", "ShouldMigrateChildPermissions" ]),
@@ -365,6 +370,7 @@ def test_overloaded_missing(self):
(PyWithDomain, None),
(PyWithOwner, None),
(PyWithTags, None),
+ (PyWithWorkbook, None),
(PyWorkbook, [ "SetLocation" ]),
(PyWorkbookDetails, [ "ChildPermissionContentItems", "ChildType", "SetLocation", "ShouldMigrateChildPermissions" ]),
(PyCapability, None),
@@ -409,12 +415,12 @@ def test_overloaded_missing(self):
# endregion
_test_class_data = [
- (PyMigrationPlanBuilder, None),
- (PyServerToCloudMigrationPlanBuilder, None),
+ (PyMigrationPlanBuilder, [ "ForCustomPipeline", "ForCustomPipelineFactory" ]),
+ (PyServerToCloudMigrationPlanBuilder, [ "ForCustomPipeline", "ForCustomPipelineFactory" ]),
(PyMigrationResult, None),
(PyMigrationManifest, None),
(PyMigrator, None),
- (PyMigrationPlan, None),
+ (PyMigrationPlan, [ "PipelineFactoryOverride" ]),
(PyMigrationHookBuilder, None),
(PyContentTransformerBuilder, None),
(PyContentMappingBuilder, None),
diff --git a/src/Python/tests/test_migration_content.py b/src/Python/tests/test_migration_content.py
index e4ab88a..10caabb 100644
--- a/src/Python/tests/test_migration_content.py
+++ b/src/Python/tests/test_migration_content.py
@@ -141,6 +141,7 @@ def test_setter_empty(self):
IConnection,
IConnectionsContent,
IContainerContent,
+ ICustomView,
IDataSource,
IDataSourceDetails,
IDescriptionContent,
@@ -149,6 +150,7 @@ def test_setter_empty(self):
IGroupUser,
ILabel,
IProject,
+ IPublishableCustomView,
IPublishableDataSource,
IPublishableGroup,
IPublishableWorkbook,
@@ -160,6 +162,7 @@ def test_setter_empty(self):
IWithDomain,
IWithOwner,
IWithTags,
+ IWithWorkbook,
IWorkbook,
IWorkbookDetails
)
@@ -168,6 +171,7 @@ def test_setter_empty(self):
PyConnection,
PyConnectionsContent,
PyContainerContent,
+ PyCustomView,
PyDataSource,
PyDataSourceDetails,
PyDescriptionContent,
@@ -176,6 +180,7 @@ def test_setter_empty(self):
PyGroupUser,
PyLabel,
PyProject,
+ PyPublishableCustomView,
PyPublishableDataSource,
PyPublishableGroup,
PyPublishableWorkbook,
@@ -187,6 +192,7 @@ def test_setter_empty(self):
PyWithDomain,
PyWithOwner,
PyWithTags,
+ PyWithWorkbook,
PyWorkbook,
PyWorkbookDetails
)
@@ -262,6 +268,56 @@ def test_container_getter(self):
py = PyContainerContent(dotnet)
assert py.container == None if dotnet.Container is None else PyContentReference(dotnet.Container)
+class TestPyCustomViewGenerated(AutoFixtureTestBase):
+
+ def test_ctor(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py._dotnet == dotnet
+
+ def test_created_at_getter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py.created_at == dotnet.CreatedAt
+
+ def test_updated_at_getter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py.updated_at == dotnet.UpdatedAt
+
+ def test_last_accessed_at_getter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py.last_accessed_at == dotnet.LastAccessedAt
+
+ def test_shared_getter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py.shared == dotnet.Shared
+
+ def test_shared_setter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+
+ # create test data
+ testValue = self.create(Boolean)
+
+ # set property to new test value
+ py.shared = testValue
+
+ # assert value
+ assert py.shared == testValue
+
+ def test_base_view_id_getter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py.base_view_id == None if dotnet.BaseViewId is None else UUID(dotnet.BaseViewId.ToString())
+
+ def test_base_view_name_getter(self):
+ dotnet = self.create(ICustomView)
+ py = PyCustomView(dotnet)
+ assert py.base_view_name == dotnet.BaseViewName
+
class TestPyDataSourceGenerated(AutoFixtureTestBase):
def test_ctor(self):
@@ -524,6 +580,37 @@ def test_parent_project_getter(self):
py = PyProject(dotnet)
assert py.parent_project == None if dotnet.ParentProject is None else PyContentReference(dotnet.ParentProject)
+class TestPyPublishableCustomViewGenerated(AutoFixtureTestBase):
+
+ def test_ctor(self):
+ dotnet = self.create(IPublishableCustomView)
+ py = PyPublishableCustomView(dotnet)
+ assert py._dotnet == dotnet
+
+ def test_default_users_getter(self):
+ dotnet = self.create(IPublishableCustomView)
+ py = PyPublishableCustomView(dotnet)
+ assert len(dotnet.DefaultUsers) != 0
+ assert len(py.default_users) == len(dotnet.DefaultUsers)
+
+ def test_default_users_setter(self):
+ dotnet = self.create(IPublishableCustomView)
+ py = PyPublishableCustomView(dotnet)
+ assert len(dotnet.DefaultUsers) != 0
+ assert len(py.default_users) == len(dotnet.DefaultUsers)
+
+ # create test data
+ dotnetCollection = DotnetList[IContentReference]()
+ dotnetCollection.Add(self.create(IContentReference))
+ dotnetCollection.Add(self.create(IContentReference))
+ testCollection = [] if dotnetCollection is None else [PyContentReference(x) for x in dotnetCollection if x is not None]
+
+ # set property to new test value
+ py.default_users = testCollection
+
+ # assert value
+ assert len(py.default_users) == len(testCollection)
+
class TestPyPublishableGroupGenerated(AutoFixtureTestBase):
def test_ctor(self):
@@ -813,6 +900,31 @@ def test_tags_setter(self):
# assert value
assert len(py.tags) == len(testCollection)
+class TestPyWithWorkbookGenerated(AutoFixtureTestBase):
+
+ def test_ctor(self):
+ dotnet = self.create(IWithWorkbook)
+ py = PyWithWorkbook(dotnet)
+ assert py._dotnet == dotnet
+
+ def test_workbook_getter(self):
+ dotnet = self.create(IWithWorkbook)
+ py = PyWithWorkbook(dotnet)
+ assert py.workbook == None if dotnet.Workbook is None else PyContentReference(dotnet.Workbook)
+
+ def test_workbook_setter(self):
+ dotnet = self.create(IWithWorkbook)
+ py = PyWithWorkbook(dotnet)
+
+ # create test data
+ testValue = self.create(IContentReference)
+
+ # set property to new test value
+ py.workbook = None if testValue is None else PyContentReference(testValue)
+
+ # assert value
+ assert py.workbook == None if testValue is None else PyContentReference(testValue)
+
class TestPyWorkbookGenerated(AutoFixtureTestBase):
def test_ctor(self):
diff --git a/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs b/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs
index e9af5c0..46043b5 100644
--- a/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs
+++ b/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs
@@ -83,6 +83,9 @@ internal static class PythonGenerationList
typeof(IPublishableGroup),
typeof(IWithTags),
typeof(IWithOwner),
+ typeof(IWithWorkbook),
+ typeof(ICustomView),
+ typeof(IPublishableCustomView),
#endregion
@@ -157,14 +160,14 @@ internal static class PythonGenerationList
#endregion
#region - Tableau.Migration.Content.Schedules -
-
+
typeof(IInterval),
typeof(IFrequencyDetails),
typeof(ISchedule),
typeof(ExtractRefreshContentType),
typeof(IExtractRefreshTask<>),
typeof(IWithSchedule<>),
-
+
#endregion
#region - Tableau.Migration.Content.Schedules.Server -
diff --git a/src/Tableau.Migration.PythonGenerator/appsettings.json b/src/Tableau.Migration.PythonGenerator/appsettings.json
index 44bffb6..f3e0605 100644
--- a/src/Tableau.Migration.PythonGenerator/appsettings.json
+++ b/src/Tableau.Migration.PythonGenerator/appsettings.json
@@ -4,6 +4,10 @@
{
"namespace": "Tableau.Migration",
"types": [
+ {
+ "type": "ContentLocation",
+ "excludeMembers": [ "ForContentType" ]
+ },
{
"type": "IResult",
"excludeMembers": [ "CastFailure" ]
diff --git a/src/Tableau.Migration/Api/ApiClient.cs b/src/Tableau.Migration/Api/ApiClient.cs
index 390dcd7..1df5c8f 100644
--- a/src/Tableau.Migration/Api/ApiClient.cs
+++ b/src/Tableau.Migration/Api/ApiClient.cs
@@ -37,6 +37,7 @@ internal sealed class ApiClient : ApiClientBase, IApiClient
private readonly IServerSessionProvider _sessionProvider;
private readonly IHttpContentSerializer _contentSerializer;
internal const string SITES_QUERY_NOT_SUPPORTED = "403069";
+ internal const string EXPERIMENTAL_API_VERSION = "exp";
///
/// Creates a new object.
diff --git a/src/Tableau.Migration/Api/ContentApiClientBase.cs b/src/Tableau.Migration/Api/ContentApiClientBase.cs
index e8e76db..dc0fb27 100644
--- a/src/Tableau.Migration/Api/ContentApiClientBase.cs
+++ b/src/Tableau.Migration/Api/ContentApiClientBase.cs
@@ -15,14 +15,12 @@
// limitations under the License.
//
-using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tableau.Migration.Api.Rest;
using Tableau.Migration.Api.Rest.Models;
-using Tableau.Migration.Content;
using Tableau.Migration.Content.Search;
using Tableau.Migration.Net.Rest;
using Tableau.Migration.Resources;
@@ -33,10 +31,6 @@ internal abstract class ContentApiClientBase : ApiClientBase, IContentApiClient
{
protected readonly IContentReferenceFinderFactory ContentFinderFactory;
- protected readonly Lazy> ProjectFinder;
-
- protected readonly Lazy> UserFinder;
-
public string UrlPrefix { get; }
public ContentApiClientBase(
@@ -50,22 +44,62 @@ public ContentApiClientBase(
UrlPrefix = urlPrefix ?? RestUrlPrefixes.GetUrlPrefix(GetType());
ContentFinderFactory = finderFactory;
- ProjectFinder = new(ContentFinderFactory.ForContentType);
- UserFinder = new(ContentFinderFactory.ForContentType);
}
protected async Task FindProjectAsync(
[NotNull] T? response,
[DoesNotReturnIf(true)] bool throwIfNotFound,
CancellationToken cancel)
- where T : IWithProjectType, INamedContent
- => await ContentFinderFactory.FindProjectAsync(response, Logger, SharedResourcesLocalizer, throwIfNotFound, cancel).ConfigureAwait(false);
+ where T : IWithProjectType, IRestIdentifiable
+ => await ContentFinderFactory
+ .FindProjectAsync(
+ response,
+ Logger,
+ SharedResourcesLocalizer,
+ throwIfNotFound,
+ cancel)
+ .ConfigureAwait(false);
+
+ protected async Task FindUserAsync(
+ [NotNull] T? response,
+ [DoesNotReturnIf(true)] bool throwIfNotFound,
+ CancellationToken cancel)
+ where T : IRestIdentifiable
+ => await ContentFinderFactory
+ .FindUserAsync(
+ response,
+ Logger,
+ SharedResourcesLocalizer,
+ throwIfNotFound,
+ cancel)
+ .ConfigureAwait(false);
protected async Task FindOwnerAsync(
[NotNull] T? response,
[DoesNotReturnIf(true)] bool throwIfNotFound,
CancellationToken cancel)
- where T : IWithOwnerType, INamedContent
- => await ContentFinderFactory.FindOwnerAsync(response, Logger, SharedResourcesLocalizer, throwIfNotFound, cancel).ConfigureAwait(false);
+ where T : IWithOwnerType, IRestIdentifiable
+ => await ContentFinderFactory
+ .FindOwnerAsync(
+ response,
+ Logger,
+ SharedResourcesLocalizer,
+ throwIfNotFound,
+ cancel)
+ .ConfigureAwait(false);
+
+ protected async Task FindWorkbookAsync(
+ [NotNull] T? response,
+ [DoesNotReturnIf(true)] bool throwIfNotFound,
+ CancellationToken cancel)
+ where T : IWithWorkbookReferenceType, IRestIdentifiable
+ => await ContentFinderFactory
+ .FindWorkbookAsync(
+ response,
+ Logger,
+ SharedResourcesLocalizer,
+ throwIfNotFound,
+ cancel)
+ .ConfigureAwait(false);
}
}
diff --git a/src/Tableau.Migration/Api/CustomViewsApiClient.cs b/src/Tableau.Migration/Api/CustomViewsApiClient.cs
new file mode 100644
index 0000000..c608dc6
--- /dev/null
+++ b/src/Tableau.Migration/Api/CustomViewsApiClient.cs
@@ -0,0 +1,352 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration.Api.Models;
+using Tableau.Migration.Api.Publishing;
+using Tableau.Migration.Api.Rest;
+using Tableau.Migration.Api.Rest.Models;
+using Tableau.Migration.Api.Rest.Models.Requests;
+using Tableau.Migration.Api.Rest.Models.Responses;
+using Tableau.Migration.Config;
+using Tableau.Migration.Content;
+using Tableau.Migration.Content.Files;
+using Tableau.Migration.Content.Search;
+using Tableau.Migration.Net;
+using Tableau.Migration.Net.Rest;
+using Tableau.Migration.Paging;
+using Tableau.Migration.Resources;
+
+namespace Tableau.Migration.Api
+{
+
+ internal sealed class CustomViewsApiClient : ContentApiClientBase, ICustomViewsApiClient
+ {
+ private readonly IHttpContentSerializer _serializer;
+ private readonly IConfigReader _configReader;
+ private readonly IContentFileStore _fileStore;
+ private readonly ICustomViewPublisher _customViewPublisher;
+
+ public CustomViewsApiClient(
+ IRestRequestBuilderFactory restRequestBuilderFactory,
+ ILoggerFactory loggerFactory,
+ ISharedResourcesLocalizer sharedResourcesLocalizer,
+ IContentReferenceFinderFactory finderFactory,
+ IHttpContentSerializer serializer,
+ IConfigReader configReader,
+ IContentFileStore fileStore,
+ ICustomViewPublisher customViewPublisher)
+ : base(
+ restRequestBuilderFactory,
+ finderFactory,
+ loggerFactory,
+ sharedResourcesLocalizer,
+ RestUrlPrefixes.CustomViews)
+ {
+ _serializer = serializer;
+ _configReader = configReader;
+ _fileStore = fileStore;
+ _customViewPublisher = customViewPublisher;
+ }
+
+ ///
+ public async Task> GetAllCustomViewsAsync(int pageNumber, int pageSize, CancellationToken cancel)
+ {
+ var getAllCustomViewsResult = await RestRequestBuilderFactory
+ .CreateUri($"{UrlPrefix}")
+ .WithPage(pageNumber, pageSize)
+ .ForGetRequest()
+ .SendAsync(cancel)
+ .ToPagedResultAsync(async (r, c) =>
+ {
+ // Take all items.
+ var results = ImmutableArray.CreateBuilder(r.Items.Length);
+
+ foreach (var item in r.Items)
+ {
+ var workbook = await FindWorkbookAsync(item, false, c).ConfigureAwait(false);
+ var owner = await FindOwnerAsync(item, false, c).ConfigureAwait(false);
+
+ if (workbook is null || owner is null)
+ {
+ Logger.LogWarning(
+ SharedResourcesLocalizer[SharedResourceKeys.CustomViewSkippedMissingReferenceWarning],
+ item.Id,
+ item.Name,
+ item.Workbook!.Id,
+ workbook is null ? SharedResourcesLocalizer[SharedResourceKeys.NotFound] : SharedResourcesLocalizer[SharedResourceKeys.Found],
+ item.Owner!.Id,
+ owner is null ? SharedResourcesLocalizer[SharedResourceKeys.NotFound] : SharedResourcesLocalizer[SharedResourceKeys.Found]);
+ continue;
+ }
+
+ results.Add(new CustomView(item, workbook, owner));
+ }
+
+ // Produce immutable list of type ICustomView and return.
+ return results.ToImmutable();
+ }, SharedResourcesLocalizer, cancel)
+ .ConfigureAwait(false);
+
+ return getAllCustomViewsResult;
+ }
+
+ ///
+ public async Task> UpdateCustomViewAsync(
+ Guid id,
+ CancellationToken cancel,
+ Guid? newOwnerId = null,
+ string? newViewName = null)
+ {
+ var result = await RestRequestBuilderFactory
+ .CreateUri($"{UrlPrefix}/{id.ToUrlSegment()}")
+ .ForPutRequest()
+ .WithXmlContent(new UpdateCustomViewRequest(newOwnerId, newViewName))
+ .SendAsync(cancel)
+ .ToResultAsync(async (r, c) =>
+ {
+ var workbook = await FindWorkbookAsync(r.Item!, true, c).ConfigureAwait(false);
+ var owner = await FindOwnerAsync(r.Item!, true, c).ConfigureAwait(false);
+
+ return new CustomView(r.Item!, workbook, owner);
+ }, SharedResourcesLocalizer, cancel)
+ .ConfigureAwait(false);
+
+ return result;
+ }
+
+ ///
+ public async Task DeleteCustomViewAsync(Guid id, CancellationToken cancel)
+ {
+ var result = await RestRequestBuilderFactory
+ .CreateUri($"{UrlPrefix}/{id.ToUrlSegment()}")
+ .ForDeleteRequest()
+ .SendAsync(cancel)
+ .ToResultAsync(_serializer, SharedResourcesLocalizer, cancel)
+ .ConfigureAwait(false);
+
+ return result;
+ }
+
+ ///
+ public async Task>> GetAllCustomViewDefaultUsersAsync(
+ Guid id,
+ CancellationToken cancel)
+ {
+ IPager pager = new CustomViewDefaultUsersResponsePager(
+ this,
+ id,
+ _configReader.Get().BatchSize);
+
+ return await pager.GetAllPagesAsync(cancel)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ public async Task> GetCustomViewDefaultUsersAsync(
+ Guid id,
+ int pageNumber,
+ int pageSize,
+ CancellationToken cancel)
+ {
+ var result = await RestRequestBuilderFactory
+ .CreateUri($"{UrlPrefix}/{id.ToUrlSegment()}/default/users")
+ .WithPage(pageNumber, pageSize)
+ .ForGetRequest()
+ .SendAsync(cancel)
+ .ToResultAsync(r => r, SharedResourcesLocalizer)
+ .ConfigureAwait(false);
+
+ if (!result.Success)
+ {
+ return PagedResult.Failed(
+ result.Errors);
+ }
+
+ // Take all items.
+ var results = ImmutableArray.CreateBuilder(result.Value.Items.Length);
+
+ foreach (var item in result.Value.Items)
+ {
+ var user = await FindUserAsync(item, false, cancel).ConfigureAwait(false);
+
+ if (user is null)
+ {
+ Logger.LogWarning(
+ SharedResourcesLocalizer[SharedResourceKeys.UserWithCustomViewDefaultSkippedMissingReferenceWarning],
+ item.Id,
+ id);
+ continue;
+ }
+
+ results.Add(user);
+ }
+
+ return PagedResult.Succeeded(
+ results.ToImmutable(),
+ result.Value.Pagination.PageNumber,
+ result.Value.Pagination.PageSize,
+ result.Value.Pagination.TotalAvailable,
+ ((IPageInfo)result.Value).FetchedAllPages);
+ }
+
+ ///
+ public async Task>> SetCustomViewDefaultUsersAsync(
+ Guid id,
+ IEnumerable users,
+ CancellationToken cancel)
+ {
+ if (!users.Any())
+ {
+ return Result>
+ .Succeeded(ImmutableArray.Empty);
+ }
+
+ var setResult = await RestRequestBuilderFactory
+ .CreateUri($"{UrlPrefix}/{id.ToUrlSegment()}/default/users")
+ .ForPostRequest()
+ .WithXmlContent(new SetCustomViewDefaultUsersRequest(users))
+ .SendAsync(cancel)
+ .ToResultAsync>((response) =>
+ response.Items
+ .Select(item => (ICustomViewAsUserDefaultViewResult)new CustomViewAsUserDefaultViewResult(item))
+ .ToImmutableArray(),
+ SharedResourcesLocalizer)
+ .ConfigureAwait(false);
+
+ return setResult;
+ }
+
+ ///
+ public async Task> DownloadCustomViewAsync(
+ Guid customViewId,
+ CancellationToken cancel)
+ {
+ var downloadResult = await RestRequestBuilderFactory
+ .CreateUri($"{UrlPrefix}/{customViewId.ToUrlSegment()}/{RestUrlPrefixes.Content}", true)
+ .ForGetRequest()
+ .DownloadAsync(cancel)
+ .ConfigureAwait(false);
+
+ return downloadResult;
+ }
+
+ #region - IPullApiClient Implementation -
+ ///
+ public async Task> PullAsync(ICustomView contentItem, CancellationToken cancel)
+ {
+ var downloadResult = await DownloadCustomViewAsync(contentItem.Id, cancel).ConfigureAwait(false);
+
+ if (!downloadResult.Success)
+ {
+ return downloadResult.CastFailure();
+ }
+
+ var defaultUsersResult = await GetAllCustomViewDefaultUsersAsync(contentItem.Id, cancel).ConfigureAwait(false);
+
+ if (!defaultUsersResult.Success)
+ {
+ return defaultUsersResult.CastFailure();
+ }
+
+ await using (downloadResult)
+ {
+ var file = await _fileStore.CreateAsync(contentItem, downloadResult.Value, cancel).ConfigureAwait(false);
+
+ var result = new PublishableCustomView(contentItem, [.. defaultUsersResult.Value], file);
+
+ return Result.Succeeded(result);
+ }
+ }
+
+ #endregion
+
+ ///
+ public async Task> PublishCustomViewAsync(
+ IPublishCustomViewOptions options,
+ CancellationToken cancel)
+ => await _customViewPublisher.PublishAsync(options, cancel).ConfigureAwait(false);
+
+ #region - IPublishApiClient Implementation -
+ ///
+ public async Task> PublishAsync(
+ IPublishableCustomView contentItem,
+ CancellationToken cancel)
+ {
+ var fileStream = await contentItem.File.OpenReadAsync(cancel).ConfigureAwait(false);
+ await using (fileStream)
+ {
+ var publishResult = await PublishCustomViewAsync(
+ new PublishCustomViewOptions(contentItem, fileStream.Content),
+ cancel)
+ .ConfigureAwait(false);
+
+ if (!publishResult.Success)
+ {
+ return publishResult.CastFailure();
+ }
+
+ return publishResult;
+ }
+ }
+ #endregion
+
+ #region - IPagedListApiClient Implementation -
+
+ ///
+ public IPager GetPager(int pageSize) => new ApiListPager(this, pageSize);
+
+ #endregion
+
+ #region - IApiPageAccessor Implementation -
+
+ ///
+ public async Task> GetPageAsync(int pageNumber, int pageSize, CancellationToken cancel)
+ => await GetAllCustomViewsAsync(pageNumber, pageSize, cancel).ConfigureAwait(false);
+
+ #endregion
+
+ #region - IReadApiClient Implementation -
+
+ ///
+ public async Task> GetByIdAsync(Guid contentId, CancellationToken cancel)
+ {
+ var getCustomViewResult = await RestRequestBuilderFactory
+ .CreateUri($"/{UrlPrefix}/{contentId.ToUrlSegment()}")
+ .ForGetRequest()
+ .SendAsync(cancel)
+ .ToResultAsync(async (r, c) =>
+ {
+ var workbook = await FindWorkbookAsync(r.Item!, true, c).ConfigureAwait(false);
+ var owner = await FindOwnerAsync(r.Item!, true, c).ConfigureAwait(false);
+
+ return new CustomView(r.Item!, workbook, owner);
+ }, SharedResourcesLocalizer, cancel)
+ .ConfigureAwait(false);
+
+ return getCustomViewResult;
+ }
+ #endregion
+ }
+
+}
diff --git a/src/Tableau.Migration/Api/DataSourcesApiClient.cs b/src/Tableau.Migration/Api/DataSourcesApiClient.cs
index a581d12..ce62931 100644
--- a/src/Tableau.Migration/Api/DataSourcesApiClient.cs
+++ b/src/Tableau.Migration/Api/DataSourcesApiClient.cs
@@ -119,7 +119,17 @@ public async Task> GetAllPublishedDataSourcesAsync(
var owner = await FindOwnerAsync(item, false, cancel).ConfigureAwait(false);
if (project is null || owner is null)
- continue; //Warnings will be logged by prior method calls.
+ {
+ Logger.LogWarning(
+ SharedResourcesLocalizer[SharedResourceKeys.DataSourceSkippedMissingReferenceWarning],
+ item.Id,
+ item.Name,
+ item.Project!.Id,
+ project is null ? SharedResourcesLocalizer[SharedResourceKeys.NotFound] : SharedResourcesLocalizer[SharedResourceKeys.Found],
+ item.Owner!.Id,
+ owner is null ? SharedResourcesLocalizer[SharedResourceKeys.NotFound] : SharedResourcesLocalizer[SharedResourceKeys.Found]);
+ continue;
+ }
results.Add(new DataSource(item, project, owner));
}
diff --git a/src/Tableau.Migration/Api/ICloudTasksApiClient.cs b/src/Tableau.Migration/Api/ICloudTasksApiClient.cs
index c70ac3b..f963750 100644
--- a/src/Tableau.Migration/Api/ICloudTasksApiClient.cs
+++ b/src/Tableau.Migration/Api/ICloudTasksApiClient.cs
@@ -20,7 +20,6 @@
using System.Threading;
using System.Threading.Tasks;
using Tableau.Migration.Api.Models.Cloud;
-using Tableau.Migration.Content.Schedules;
using Tableau.Migration.Content.Schedules.Cloud;
namespace Tableau.Migration.Api
diff --git a/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs b/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs
index 3060f58..5d3be6c 100644
--- a/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs
+++ b/src/Tableau.Migration/Api/IContentReferenceFinderFactoryExtensions.cs
@@ -24,7 +24,6 @@
using Tableau.Migration.Api.Rest.Models;
using Tableau.Migration.Content;
using Tableau.Migration.Content.Schedules;
-using Tableau.Migration.Content.Schedules.Server;
using Tableau.Migration.Content.Search;
using Tableau.Migration.Resources;
@@ -39,26 +38,45 @@ internal static class IContentReferenceFinderFactoryExtensions
ISharedResourcesLocalizer localizer,
[DoesNotReturnIf(true)] bool throwIfNotFound,
CancellationToken cancel)
- where T : IWithProjectType, INamedContent
- {
- Guard.AgainstNull(response, nameof(response));
-
- var project = Guard.AgainstNull(response.Project, () => nameof(response.Project));
- var projectId = Guard.AgainstDefaultValue(project.Id, () => nameof(response.Project.Id));
-
- var projectFinder = finderFactory.ForContentType();
-
- var foundProject = await projectFinder.FindByIdAsync(projectId, cancel).ConfigureAwait(false);
-
- if (foundProject is not null)
- return foundProject;
-
- logger.LogWarning(localizer[SharedResourceKeys.ProjectReferenceNotFoundMessage], response.Project.Name, response.GetType().Name, response.Name);
-
- return throwIfNotFound
- ? throw new InvalidOperationException($"The project with ID {projectId} was not found.")
- : null;
- }
+ where T : IWithProjectType, IRestIdentifiable
+ => await finderFactory
+ .FindAsync(
+ response,
+ r =>
+ {
+ var project = Guard.AgainstNull(r.Project, () => nameof(response.Project));
+ return Guard.AgainstDefaultValue(project.Id, () => nameof(response.Project.Id));
+ },
+ logger,
+ localizer,
+ throwIfNotFound,
+ SharedResourceKeys.ProjectReferenceNotFoundMessage,
+ SharedResourceKeys.ProjectReferenceNotFoundException,
+ cancel)
+ .ConfigureAwait(false);
+
+ public static async Task FindUserAsync(
+ this IContentReferenceFinderFactory finderFactory,
+ [NotNull] T? response,
+ ILogger logger,
+ ISharedResourcesLocalizer localizer,
+ [DoesNotReturnIf(true)] bool throwIfNotFound,
+ CancellationToken cancel)
+ where T : IRestIdentifiable
+ => await finderFactory
+ .FindAsync(
+ response,
+ r =>
+ {
+ return Guard.AgainstDefaultValue(r.Id, () => nameof(response.Id));
+ },
+ logger,
+ localizer,
+ throwIfNotFound,
+ SharedResourceKeys.UserReferenceNotFoundMessage,
+ SharedResourceKeys.UserReferenceNotFoundException,
+ cancel)
+ .ConfigureAwait(false);
public static async Task FindOwnerAsync(
this IContentReferenceFinderFactory finderFactory,
@@ -67,55 +85,98 @@ internal static class IContentReferenceFinderFactoryExtensions
ISharedResourcesLocalizer localizer,
[DoesNotReturnIf(true)] bool throwIfNotFound,
CancellationToken cancel)
- where T : IWithOwnerType, INamedContent
- {
- Guard.AgainstNull(response, nameof(response));
-
- var owner = Guard.AgainstNull(response.Owner, () => nameof(response.Owner));
- var ownerId = Guard.AgainstDefaultValue(owner.Id, () => nameof(response.Owner.Id));
-
- var userFinder = finderFactory.ForContentType();
-
- var foundOwner = await userFinder.FindByIdAsync(ownerId, cancel).ConfigureAwait(false);
+ where T : IWithOwnerType, IRestIdentifiable
+ => await finderFactory
+ .FindAsync(
+ response,
+ r =>
+ {
+ var owner = Guard.AgainstNull(r.Owner, () => nameof(response.Owner));
+ return Guard.AgainstDefaultValue(owner.Id, () => nameof(response.Owner.Id));
+ },
+ logger,
+ localizer,
+ throwIfNotFound,
+ SharedResourceKeys.OwnerNotFoundMessage,
+ SharedResourceKeys.OwnerNotFoundException,
+ cancel)
+ .ConfigureAwait(false);
+
+ public static async Task FindWorkbookAsync(
+ this IContentReferenceFinderFactory finderFactory,
+ [NotNull] T? response,
+ ILogger logger,
+ ISharedResourcesLocalizer localizer,
+ [DoesNotReturnIf(true)] bool throwIfNotFound,
+ CancellationToken cancel)
+ where T : IWithWorkbookReferenceType, IRestIdentifiable
+ => await finderFactory
+ .FindAsync(
+ response,
+ r =>
+ {
+ var workbook = Guard.AgainstNull(r.Workbook, () => nameof(response.Workbook));
+ return Guard.AgainstDefaultValue(workbook.Id, () => nameof(response.Workbook.Id));
+ },
+ logger,
+ localizer,
+ throwIfNotFound,
+ SharedResourceKeys.WorkbookReferenceNotFoundMessage,
+ SharedResourceKeys.WorkbookReferenceNotFoundException,
+ cancel)
+ .ConfigureAwait(false);
- if (foundOwner is not null)
- return foundOwner;
+ public static async Task FindExtractRefreshContentAsync(
+ this IContentReferenceFinderFactory finderFactory,
+ ExtractRefreshContentType contentType,
+ Guid contentId,
+ CancellationToken cancel)
+ {
+ var finder = finderFactory.ForExtractRefreshContent(contentType);
- logger.LogWarning(localizer[SharedResourceKeys.OwnerNotFoundMessage], ownerId, response.GetType().Name, response.Name);
+ var content = await finder.FindByIdAsync(contentId, cancel).ConfigureAwait(false);
- return throwIfNotFound
- ? throw new InvalidOperationException($"The owner with ID {ownerId} was not found.")
- : null;
+ return Guard.AgainstNull(content, nameof(content));
}
- public static async Task FindScheduleAsync(
+ private static async Task FindAsync(
this IContentReferenceFinderFactory finderFactory,
- [NotNull] IWithScheduleReferenceType? response,
+ [NotNull] TResponse? response,
+ Func getResponseId,
+ ILogger logger,
+ ISharedResourcesLocalizer localizer,
+ [DoesNotReturnIf(true)] bool throwIfNotFound,
+ string warningMessageResource,
+ string exceptionMessageResource,
CancellationToken cancel)
+ where TResponse : IRestIdentifiable
+ where TContent : class, IContentReference
{
Guard.AgainstNull(response, nameof(response));
- var schedule = Guard.AgainstNull(response.Schedule, () => nameof(response.Schedule));
- var scheduleId = Guard.AgainstDefaultValue(schedule.Id, () => nameof(response.Schedule.Id));
+ var responseId = getResponseId(response);
- var scheduleFinder = finderFactory.ForContentType();
+ var finder = finderFactory.ForContentType();
- var foundSchedule = await scheduleFinder.FindByIdAsync(scheduleId, cancel).ConfigureAwait(false);
+ var foundContent = await finder.FindByIdAsync(responseId, cancel).ConfigureAwait(false);
- return Guard.AgainstNull(foundSchedule, nameof(foundSchedule));
- }
+ if (foundContent is not null)
+ return foundContent;
- public static async Task FindExtractRefreshContentAsync(
- this IContentReferenceFinderFactory finderFactory,
- ExtractRefreshContentType contentType,
- Guid contentId,
- CancellationToken cancel)
- {
- var finder = finderFactory.ForExtractRefreshContent(contentType);
+ var namedResponse = response as INamedContent;
- var content = await finder.FindByIdAsync(contentId, cancel).ConfigureAwait(false);
+ logger.LogWarning(
+ localizer[warningMessageResource],
+ responseId,
+ response.GetType().Name,
+ namedResponse?.Name ?? string.Empty);
- return Guard.AgainstNull(content, nameof(content));
+ return throwIfNotFound
+ ? throw new InvalidOperationException(
+ string.Format(
+ localizer[exceptionMessageResource],
+ responseId))
+ : null;
}
}
}
diff --git a/src/Tableau.Migration/Api/ICustomViewsApiClient.cs b/src/Tableau.Migration/Api/ICustomViewsApiClient.cs
new file mode 100644
index 0000000..9fe899a
--- /dev/null
+++ b/src/Tableau.Migration/Api/ICustomViewsApiClient.cs
@@ -0,0 +1,119 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Tableau.Migration.Api.Models;
+using Tableau.Migration.Content;
+using Tableau.Migration.Paging;
+
+namespace Tableau.Migration.Api
+{
+ ///
+ /// Interface for API client Custom View operations.
+ ///
+ public interface ICustomViewsApiClient :
+ IContentApiClient,
+ IPagedListApiClient,
+ IApiPageAccessor,
+ IReadApiClient,
+ IPullApiClient,
+ IPublishApiClient
+ {
+ ///
+ /// Gets all custom views in the current site.
+ ///
+ /// The 1-indexed page number.
+ /// The size of the page.
+ /// The cancellation token to obey.
+ /// A list of a page of custom views in the current site.
+ Task> GetAllCustomViewsAsync(int pageNumber, int pageSize, CancellationToken cancel);
+
+ ///
+ /// Changes the owner of an existing custom view.
+ ///
+ /// The ID for the custom view.
+ /// The cancellation token to obey.
+ /// The new owner ID.
+ /// The new view name.
+ ///
+ Task> UpdateCustomViewAsync(
+ Guid id,
+ CancellationToken cancel,
+ Guid? newOwnerId = null,
+ string? newViewName = null);
+
+ ///
+ /// Deletes the specified custom view.
+ ///
+ /// The ID for the custom view.
+ /// The cancellation token to obey.
+ ///
+ Task DeleteCustomViewAsync(
+ Guid id,
+ CancellationToken cancel);
+
+ ///
+ /// Gets the list of user content references whose default view is the specified custom view.
+ ///
+ /// The ID for the custom view.
+ /// The cancellation token to obey.
+ /// A list of all users whose default view is the specified custom view.
+ Task>> GetAllCustomViewDefaultUsersAsync(
+ Guid id,
+ CancellationToken cancel);
+
+ ///
+ /// Gets the paged list of user content references whose default view is the specified custom view.
+ ///
+ /// The ID for the custom view.
+ /// The 1-indexed page number.
+ /// The size of the page.
+ /// The cancellation token to obey.
+ /// A list of a page of users whose default view is the specified custom view.
+ Task> GetCustomViewDefaultUsersAsync(
+ Guid id,
+ int pageNumber,
+ int pageSize,
+ CancellationToken cancel);
+
+ ///
+ /// Sets the specified custom for as the default view for up to 100 specified users. Success or failure for each user is reported in the response body.
+ ///
+ /// The ID for the custom view.
+ /// The list of users.
+ /// The cancellation token to obey.
+ ///
+ Task>> SetCustomViewDefaultUsersAsync(
+ Guid id,
+ IEnumerable users,
+ CancellationToken cancel);
+
+ ///
+ /// Downloads the custom view.
+ ///
+ /// The ID for the custom view.
+ /// The cancellation token to obey.
+ ///
+ Task> DownloadCustomViewAsync(
+ Guid customViewId,
+ CancellationToken cancel);
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Api/IServerTasksApiClient.cs b/src/Tableau.Migration/Api/IServerTasksApiClient.cs
index f486330..1b22789 100644
--- a/src/Tableau.Migration/Api/IServerTasksApiClient.cs
+++ b/src/Tableau.Migration/Api/IServerTasksApiClient.cs
@@ -18,7 +18,6 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
-using Tableau.Migration.Content.Schedules;
using Tableau.Migration.Content.Schedules.Cloud;
using Tableau.Migration.Content.Schedules.Server;
diff --git a/src/Tableau.Migration/Api/IServiceCollectionExtensions.cs b/src/Tableau.Migration/Api/IServiceCollectionExtensions.cs
index a65b1c5..653851a 100644
--- a/src/Tableau.Migration/Api/IServiceCollectionExtensions.cs
+++ b/src/Tableau.Migration/Api/IServiceCollectionExtensions.cs
@@ -73,8 +73,10 @@ internal static IServiceCollection AddMigrationApiClient(this IServiceCollection
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
//API Simulator.
services.AddSingleton();
@@ -85,6 +87,7 @@ internal static IServiceCollection AddMigrationApiClient(this IServiceCollection
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped(typeof(ILabelsApiClient<>), typeof(LabelsApiClient<>));
diff --git a/src/Tableau.Migration/Api/ISitesApiClient.cs b/src/Tableau.Migration/Api/ISitesApiClient.cs
index c3a7f30..b5c9594 100644
--- a/src/Tableau.Migration/Api/ISitesApiClient.cs
+++ b/src/Tableau.Migration/Api/ISitesApiClient.cs
@@ -84,6 +84,11 @@ public interface ISitesApiClient : IAsyncDisposable, IContentApiClient
///
ICloudTasksApiClient CloudTasks { get; }
+ ///
+ /// Gets the API client for custom view operations.
+ ///
+ public ICustomViewsApiClient CustomViews { get; }
+
///
/// Gets the site with the specified ID.
///
diff --git a/src/Tableau.Migration/Api/Models/CustomViewAsUserDefaultViewResult.cs b/src/Tableau.Migration/Api/Models/CustomViewAsUserDefaultViewResult.cs
new file mode 100644
index 0000000..d786175
--- /dev/null
+++ b/src/Tableau.Migration/Api/Models/CustomViewAsUserDefaultViewResult.cs
@@ -0,0 +1,41 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Immutable;
+using Tableau.Migration.Api.Rest.Models.Responses;
+
+namespace Tableau.Migration.Api.Models
+{
+ class CustomViewAsUserDefaultViewResult : ICustomViewAsUserDefaultViewResult
+ {
+ ///
+ public Guid UserId { get; }
+
+ ///
+ public bool Success { get; }
+
+ public CustomViewAsUserDefaultViewResult(CustomViewAsUsersDefaultViewResponse.CustomViewAsUserDefaultViewType response)
+ {
+ var user = Guard.AgainstNull(response.User, () => response.User);
+
+ UserId = Guard.AgainstDefaultValue(user.Id, () => response.User.Id);
+
+ Success = response.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Api/Models/ICustomViewAsUserDefaultViewResult.cs b/src/Tableau.Migration/Api/Models/ICustomViewAsUserDefaultViewResult.cs
new file mode 100644
index 0000000..454dad9
--- /dev/null
+++ b/src/Tableau.Migration/Api/Models/ICustomViewAsUserDefaultViewResult.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Immutable;
+using Tableau.Migration.Api.Rest;
+
+namespace Tableau.Migration.Api.Models
+{
+ ///
+ /// The custom view as user default view result item
+ ///
+ public interface ICustomViewAsUserDefaultViewResult
+ {
+ ///
+ /// Gets the success of set custom view as default for users
+ ///
+ bool Success { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Api/Models/IPublishCustomViewOptions.cs b/src/Tableau.Migration/Api/Models/IPublishCustomViewOptions.cs
new file mode 100644
index 0000000..ac3f75c
--- /dev/null
+++ b/src/Tableau.Migration/Api/Models/IPublishCustomViewOptions.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Tableau.Migration.Api.Models
+{
+ ///
+ /// Interface for API client Custom View publish options.
+ ///
+ public interface IPublishCustomViewOptions : IPublishFileOptions
+ {
+ ///
+ /// The name of the Custom View.
+ ///
+ string Name { get; set; }
+
+ ///
+ /// Flag indicating if the Custom View is shared.
+ ///
+ bool Shared { get; set; }
+
+ ///
+ /// The ID of the Custom View's workbook.
+ ///
+ Guid WorkbookId { get; set; }
+
+ ///
+ /// The Owner ID for the Custom View.
+ ///
+ Guid OwnerId { get; set; }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Models/PublishCustomViewOptions.cs b/src/Tableau.Migration/Api/Models/PublishCustomViewOptions.cs
new file mode 100644
index 0000000..db1dc04
--- /dev/null
+++ b/src/Tableau.Migration/Api/Models/PublishCustomViewOptions.cs
@@ -0,0 +1,62 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.IO;
+using Tableau.Migration.Api.Rest.Models.Types;
+using Tableau.Migration.Content;
+
+namespace Tableau.Migration.Api.Models
+{
+
+ ///
+ public class PublishCustomViewOptions : PublishContentWithFileOptions, IPublishCustomViewOptions
+ {
+ ///
+ public string Name { get; set; }
+
+ ///
+ public bool Shared { get; set; }
+
+ ///
+ public Guid WorkbookId { get; set; }
+
+ ///
+ public Guid OwnerId { get; set; }
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// The publishable custom view information.
+ /// The custom view file as a .
+ /// The type of custom view file. Defaults to .
+ public PublishCustomViewOptions(
+ IPublishableCustomView customView,
+ Stream file,
+ string fileType = CustomViewFileTypes.Json)
+ : base(
+ file,
+ customView.File.OriginalFileName,
+ fileType)
+ {
+ Name = customView.Name;
+ Shared = customView.Shared;
+ WorkbookId = customView.Workbook.Id;
+ OwnerId = customView.Owner.Id;
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Models/PublishDataSourceOptions.cs b/src/Tableau.Migration/Api/Models/PublishDataSourceOptions.cs
index eb7f306..0426b25 100644
--- a/src/Tableau.Migration/Api/Models/PublishDataSourceOptions.cs
+++ b/src/Tableau.Migration/Api/Models/PublishDataSourceOptions.cs
@@ -25,7 +25,7 @@ namespace Tableau.Migration.Api.Models
///
/// Class for API client data source publish options.
///
- public class PublishDataSourceOptions : IPublishDataSourceOptions
+ public class PublishDataSourceOptions : PublishContentWithFileOptions, IPublishDataSourceOptions
{
///
public string Name { get; }
@@ -45,31 +45,26 @@ public class PublishDataSourceOptions : IPublishDataSourceOptions
///
public Guid ProjectId { get; }
- ///
- public Stream File { get; }
-
- ///
- public string FileName { get; }
-
- ///
- public string FileType { get; }
-
///
/// Creates a new instance.
///
/// The publishable data source information.
/// The data source file as a
/// The type of data source file.
- public PublishDataSourceOptions(IPublishableDataSource dataSource, Stream file, string fileType = DataSourceFileTypes.Tdsx)
+ public PublishDataSourceOptions(
+ IPublishableDataSource dataSource,
+ Stream file,
+ string fileType = DataSourceFileTypes.Tdsx)
+ : base(
+ file,
+ dataSource.File.OriginalFileName,
+ fileType)
{
Name = dataSource.Name;
Description = dataSource.Description;
UseRemoteQueryAgent = dataSource.UseRemoteQueryAgent;
EncryptExtracts = dataSource.EncryptExtracts;
ProjectId = ((IContainerContent)dataSource).Container.Id;
- File = file;
- FileName = dataSource.File.OriginalFileName;
- FileType = fileType;
}
}
}
diff --git a/src/Tableau.Migration/Api/Models/PublishFlowOptions.cs b/src/Tableau.Migration/Api/Models/PublishFlowOptions.cs
index a5ad239..5c1384e 100644
--- a/src/Tableau.Migration/Api/Models/PublishFlowOptions.cs
+++ b/src/Tableau.Migration/Api/Models/PublishFlowOptions.cs
@@ -25,7 +25,7 @@ namespace Tableau.Migration.Api.Models
///
/// Class for API client prep flow publish options.
///
- public class PublishFlowOptions : IPublishFlowOptions
+ public class PublishFlowOptions : PublishContentWithFileOptions, IPublishFlowOptions
{
///
public string Name { get; }
@@ -39,29 +39,24 @@ public class PublishFlowOptions : IPublishFlowOptions
///
public Guid ProjectId { get; }
- ///
- public Stream File { get; }
-
- ///
- public string FileName { get; }
-
- ///
- public string FileType { get; }
-
///
/// Creates a new instance.
///
/// The publishable prep flow information.
/// The prep flow file as a
/// The type of prep flow file.
- public PublishFlowOptions(IPublishableFlow flow, Stream file, string fileType = FlowFileTypes.Tflx)
+ public PublishFlowOptions(
+ IPublishableFlow flow,
+ Stream file,
+ string fileType = FlowFileTypes.Tflx)
+ : base(
+ file,
+ flow.File.OriginalFileName,
+ fileType)
{
Name = flow.Name;
Description = flow.Description;
ProjectId = ((IContainerContent)flow).Container.Id;
- File = file;
- FileName = flow.File.OriginalFileName;
- FileType = fileType;
}
}
}
diff --git a/src/Tableau.Migration/Api/Models/PublishWorkbookOptions.cs b/src/Tableau.Migration/Api/Models/PublishWorkbookOptions.cs
index c3ce46f..1e7b164 100644
--- a/src/Tableau.Migration/Api/Models/PublishWorkbookOptions.cs
+++ b/src/Tableau.Migration/Api/Models/PublishWorkbookOptions.cs
@@ -26,7 +26,7 @@ namespace Tableau.Migration.Api.Models
///
/// Class for API client workbook publish options.
///
- public class PublishWorkbookOptions : IPublishWorkbookOptions
+ public class PublishWorkbookOptions : PublishContentWithFileOptions, IPublishWorkbookOptions
{
///
public string Name { get; }
@@ -52,15 +52,6 @@ public class PublishWorkbookOptions : IPublishWorkbookOptions
///
public Guid ProjectId { get; }
- ///
- public Stream File { get; }
-
- ///
- public string FileName { get; }
-
- ///
- public string FileType { get; }
-
///
public IEnumerable HiddenViewNames { get; }
@@ -70,17 +61,21 @@ public class PublishWorkbookOptions : IPublishWorkbookOptions
/// The publishable workbook information.
/// The workbook file as a
/// The type of workbook file.
- public PublishWorkbookOptions(IPublishableWorkbook workbook, Stream file, string fileType = WorkbookFileTypes.Twbx)
+ public PublishWorkbookOptions(
+ IPublishableWorkbook workbook,
+ Stream file,
+ string fileType = WorkbookFileTypes.Twbx)
+ : base(
+ file,
+ workbook.File.OriginalFileName,
+ fileType)
{
Name = workbook.Name;
Description = workbook.Description;
ShowTabs = workbook.ShowTabs;
EncryptExtracts = workbook.EncryptExtracts;
ThumbnailsUserId = workbook.ThumbnailsUserId;
- ProjectId = ((IContainerContent)workbook).Container.Id;
- File = file;
- FileName = workbook.File.OriginalFileName;
- FileType = fileType;
+ ProjectId = ((IContainerContent)workbook).Container.Id;
HiddenViewNames = workbook.HiddenViewNames;
}
}
diff --git a/src/Tableau.Migration/Api/ProjectsApiClient.cs b/src/Tableau.Migration/Api/ProjectsApiClient.cs
index b6f3d3c..d7debec 100644
--- a/src/Tableau.Migration/Api/ProjectsApiClient.cs
+++ b/src/Tableau.Migration/Api/ProjectsApiClient.cs
@@ -78,7 +78,7 @@ public async Task> CreateProjectAsync(ICreateProjectOptions op
.ToResultAsync(async (r, c) =>
{
var response = Guard.AgainstNull(r.Item, () => r.Item);
- var project = await RestProjectBuilder.BuildProjectAsync(response, options.ParentProject, UserFinder.Value, c)
+ var project = await RestProjectBuilder.BuildProjectAsync(response, options.ParentProject, ContentFinderFactory.ForContentType(), c)
.ConfigureAwait(false);
return project;
}, SharedResourcesLocalizer, cancel)
@@ -165,7 +165,7 @@ public async Task DeleteProjectAsync(Guid projectId, CancellationToken
#region - IListApiClient Implementation -
public IPager GetPager(int pageSize)
- => new BreadthFirstPathHierarchyPager(new RestProjectBuilderPager(this, UserFinder.Value, pageSize), pageSize);
+ => new BreadthFirstPathHierarchyPager(new RestProjectBuilderPager(this, ContentFinderFactory.ForContentType(), pageSize), pageSize);
#endregion
@@ -213,7 +213,7 @@ public async Task> PublishAsync(IProject item, CancellationTok
// The owner may be different, though, which we'll want to know for the post-publish hook.
var existingProject = await RestProjectBuilder.BuildProjectAsync(existingProjectResult.Value[0],
- item.ParentProject, UserFinder.Value, cancel).ConfigureAwait(false);
+ item.ParentProject, ContentFinderFactory.ForContentType(), cancel).ConfigureAwait(false);
return Result.Succeeded(existingProject);
}
diff --git a/src/Tableau.Migration/Api/PublishContentWithFileOptions.cs b/src/Tableau.Migration/Api/PublishContentWithFileOptions.cs
new file mode 100644
index 0000000..1d87bdd
--- /dev/null
+++ b/src/Tableau.Migration/Api/PublishContentWithFileOptions.cs
@@ -0,0 +1,74 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.IO;
+using Tableau.Migration.Api;
+
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Tableau.Migration.Api.Models;
+using Tableau.Migration.Api.Rest.Models.Types;
+using Tableau.Migration.Content;
+
+namespace Tableau.Migration.Api
+{
+ ///
+ ///
+ ///
+ public abstract class PublishContentWithFileOptions : IPublishFileOptions
+ {
+ ///
+ /// Constructor to build from File Content and File.
+ ///
+ ///
+ ///
+ ///
+ public PublishContentWithFileOptions(
+ Stream file,
+ string fileName,
+ string fileType)
+ {
+ File = file;
+ FileName = fileName;
+ FileType = fileType;
+ }
+ ///
+ public Stream File { get; set; }
+
+ ///
+ public string FileName { get; set; }
+
+ ///
+ public string FileType { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Api/Publishing/CustomViewPublisher.cs b/src/Tableau.Migration/Api/Publishing/CustomViewPublisher.cs
new file mode 100644
index 0000000..58ca642
--- /dev/null
+++ b/src/Tableau.Migration/Api/Publishing/CustomViewPublisher.cs
@@ -0,0 +1,86 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration.Api.Models;
+using Tableau.Migration.Api.Publishing;
+using Tableau.Migration.Api.Rest;
+using Tableau.Migration.Api.Rest.Models.Requests;
+using Tableau.Migration.Api.Rest.Models.Responses;
+using Tableau.Migration.Content;
+using Tableau.Migration.Content.Search;
+using Tableau.Migration.Net;
+using Tableau.Migration.Net.Rest;
+using Tableau.Migration.Resources;
+
+namespace Tableau.Migration.Api
+{
+ internal class CustomViewPublisher :
+ FilePublisherBase,
+ ICustomViewPublisher
+ {
+ public CustomViewPublisher(
+ IRestRequestBuilderFactory restRequestBuilderFactory,
+ IContentReferenceFinderFactory finderFactory,
+ IServerSessionProvider sessionProvider,
+ ILoggerFactory loggerFactory,
+ ISharedResourcesLocalizer sharedResourcesLocalizer,
+ IHttpStreamProcessor httpStreamProcessor)
+ : base(
+ restRequestBuilderFactory,
+ finderFactory,
+ sessionProvider,
+ loggerFactory,
+ sharedResourcesLocalizer,
+ httpStreamProcessor,
+ RestUrlPrefixes.CustomViews)
+ { }
+
+ protected override CommitCustomViewPublishRequest BuildCommitRequest(IPublishCustomViewOptions options)
+ => new(options);
+
+ protected async override Task> SendCommitRequestAsync(
+ IPublishCustomViewOptions options,
+ string uploadSessionId,
+ MultipartContent content,
+ CancellationToken cancel)
+ {
+ var sendResult = await RestRequestBuilderFactory
+ .CreateUri(ContentTypeUrlPrefix, useExperimental: true)
+ .WithQuery("uploadSessionId", uploadSessionId)
+ .ForPostRequest()
+ .WithContent(content)
+ .SendAsync(cancel)
+ .ConfigureAwait(false);
+
+ var result = sendResult
+ .ToResult(r =>
+ {
+ var workbook = new ContentReferenceStub(r.Item!.Workbook!.Id, string.Empty, new ContentLocation());
+ var owner = new ContentReferenceStub(r.Item!.Owner!.Id, string.Empty, new ContentLocation());
+
+ return new CustomView(r.Item, workbook, owner);
+ },
+ SharedResourcesLocalizer);
+
+ return result;
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/IOwnerType.cs b/src/Tableau.Migration/Api/Publishing/ICustomViewPublisher.cs
similarity index 72%
rename from src/Tableau.Migration/Api/Rest/Models/IOwnerType.cs
rename to src/Tableau.Migration/Api/Publishing/ICustomViewPublisher.cs
index 4647a86..3346611 100644
--- a/src/Tableau.Migration/Api/Rest/Models/IOwnerType.cs
+++ b/src/Tableau.Migration/Api/Publishing/ICustomViewPublisher.cs
@@ -15,11 +15,14 @@
// limitations under the License.
//
-namespace Tableau.Migration.Api.Rest.Models
+using Tableau.Migration.Api.Models;
+using Tableau.Migration.Content;
+
+namespace Tableau.Migration.Api.Publishing
{
///
- /// Interface representing an XML element for the owner of a content item.
+ /// Interface for Custom View publisher classes.
///
- public interface IOwnerType : IRestIdentifiable
+ public interface ICustomViewPublisher : IFilePublisher
{ }
}
diff --git a/src/Tableau.Migration/Api/Rest/Models/CustomViewDefaultUsersResponsePager.cs b/src/Tableau.Migration/Api/Rest/Models/CustomViewDefaultUsersResponsePager.cs
new file mode 100644
index 0000000..d59e824
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/CustomViewDefaultUsersResponsePager.cs
@@ -0,0 +1,56 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Tableau.Migration.Paging;
+
+namespace Tableau.Migration.Api.Rest.Models
+{
+ ///
+ /// Pager implementation that pages users with a custom view as the default view.
+ ///
+ internal sealed class CustomViewDefaultUsersResponsePager
+ : IndexedPagerBase, IPager
+ {
+ private readonly ICustomViewsApiClient _apiClient;
+ private readonly Guid _customViewId;
+
+ public CustomViewDefaultUsersResponsePager(
+ ICustomViewsApiClient apiClient,
+ Guid customViewId,
+ int pageSize)
+ : base(pageSize)
+ {
+ _apiClient = apiClient;
+ _customViewId = customViewId;
+ }
+
+ protected override async Task> GetPageAsync(
+ int pageNumber,
+ int pageSize,
+ CancellationToken cancel)
+ => await _apiClient
+ .GetCustomViewDefaultUsersAsync(
+ _customViewId,
+ pageNumber,
+ pageSize,
+ cancel)
+ .ConfigureAwait(false);
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/ICustomViewType.cs b/src/Tableau.Migration/Api/Rest/Models/ICustomViewType.cs
new file mode 100644
index 0000000..c0ab741
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/ICustomViewType.cs
@@ -0,0 +1,61 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Tableau.Migration.Api.Rest.Models
+{
+ ///
+ /// Interface for a custom view REST response.
+ ///
+ public interface ICustomViewType :
+ IRestIdentifiable,
+ INamedContent,
+ IWithWorkbookReferenceType,
+ IWithOwnerType
+ {
+ ///
+ /// The created timestamp for the response.
+ ///
+ public string? CreatedAt { get; }
+
+ ///
+ /// The updated timestamp for the response.
+ ///
+ public string? UpdatedAt { get; }
+
+ ///
+ /// The lastAccessed timestamp for the response.
+ ///
+ public string? LastAccessedAt { get; }
+
+ ///
+ /// The shared flag for the response.
+ ///
+ public bool Shared { get; }
+
+ ///
+ /// The view ID for the response.
+ ///
+ public Guid? ViewId { get; }
+
+ ///
+ /// The view name for the response.
+ ///
+ public string? ViewName { get; }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/IWithOwnerType.cs b/src/Tableau.Migration/Api/Rest/Models/IWithOwnerType.cs
index fd79564..3239527 100644
--- a/src/Tableau.Migration/Api/Rest/Models/IWithOwnerType.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/IWithOwnerType.cs
@@ -25,6 +25,6 @@ public interface IWithOwnerType
///
/// Gets the owner for the response.
///
- IOwnerType? Owner { get; }
+ IRestIdentifiable? Owner { get; }
}
}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Requests/CommitCustomViewPublishRequest.cs b/src/Tableau.Migration/Api/Rest/Models/Requests/CommitCustomViewPublishRequest.cs
new file mode 100644
index 0000000..b731d55
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Requests/CommitCustomViewPublishRequest.cs
@@ -0,0 +1,150 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Xml.Serialization;
+using Tableau.Migration.Api.Models;
+
+namespace Tableau.Migration.Api.Rest.Models.Requests
+{
+ ///
+ /// Class representing an commit Custom View request.
+ ///
+ [XmlType(XmlTypeName)]
+ public class CommitCustomViewPublishRequest : TableauServerRequest
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public CommitCustomViewPublishRequest()
+ { }
+
+ ///
+ /// The constructor to build from .
+ ///
+ /// The Custom View publish options.
+ public CommitCustomViewPublishRequest(IPublishCustomViewOptions options)
+ {
+ CustomView = new CustomViewType(options);
+ }
+
+ ///
+ /// The Custom View.
+ ///
+ [XmlElement("customView")]
+ public CustomViewType? CustomView { get; set; }
+
+ ///
+ /// The Custom View type in the API request body.
+ ///
+ public class CustomViewType
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public CustomViewType()
+ { }
+
+ ///
+ /// The constructor to build from
+ ///
+ public CustomViewType(IPublishCustomViewOptions options)
+ {
+ Name = options.Name;
+ Shared = options.Shared;
+ Workbook = new WorkbookType(options.WorkbookId);
+ Owner = new OwnerType(options.OwnerId);
+ }
+
+ ///
+ /// The Custom View name.
+ ///
+ [XmlAttribute(AttributeName = "name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Flag to indicate if the Custom View is shared.
+ ///
+ [XmlAttribute(AttributeName = "shared")]
+ public bool Shared { get; set; }
+
+ ///
+ /// The workbook to which this Custom View is linked.
+ ///
+ [XmlElement("workbook")]
+ public WorkbookType? Workbook { get; set; }
+ ///
+ /// The ID of the owner for this Custom View.
+ ///
+ [XmlElement("owner")]
+ public OwnerType? Owner { get; set; }
+
+
+ ///
+ /// The workbook type in the API request body.
+ ///
+ public class WorkbookType
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public WorkbookType()
+ { }
+
+ ///
+ /// The constructor to build from the ID.
+ ///
+ /// The workbook ID.
+ public WorkbookType(Guid id)
+ {
+ Id = id;
+ }
+ ///
+ /// The workbook Id.
+ ///
+ [XmlAttribute(AttributeName = "id")]
+ public Guid Id { get; set; }
+ }
+
+ ///
+ /// The owner type in the API request body.
+ ///
+ public class OwnerType
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public OwnerType()
+ { }
+
+ ///
+ /// The constructor to build from the ID.
+ ///
+ /// The owner ID.
+ public OwnerType(Guid id)
+ {
+ Id = id;
+ }
+ ///
+ /// The Owner Id.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+ }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Requests/CommitWorkbookPublishRequest.cs b/src/Tableau.Migration/Api/Rest/Models/Requests/CommitWorkbookPublishRequest.cs
index 9c62fa3..71d2629 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Requests/CommitWorkbookPublishRequest.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Requests/CommitWorkbookPublishRequest.cs
@@ -125,7 +125,7 @@ public class WorkbookType
/// Gets or sets the views to hide or show in the request
///
[XmlArray("views")]
- [XmlArrayItem("views")]
+ [XmlArrayItem("view")]
public ViewType[] Views { get; set; } = Array.Empty();
#region - Object Specific Types -
diff --git a/src/Tableau.Migration/Api/Rest/Models/Requests/SetCustomViewDefaultUsersRequest.cs b/src/Tableau.Migration/Api/Rest/Models/Requests/SetCustomViewDefaultUsersRequest.cs
new file mode 100644
index 0000000..45fbe7c
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Requests/SetCustomViewDefaultUsersRequest.cs
@@ -0,0 +1,89 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Serialization;
+using Tableau.Migration.Content;
+
+namespace Tableau.Migration.Api.Rest.Models.Requests
+{
+ ///
+ ///
+ /// Class representing a set custom view as default for users request.
+ ///
+ ///
+ /// See Tableau API Reference
+ ///
+ /// documentation for details.
+ ///
+ ///
+ [XmlType(XmlTypeName)]
+ public class SetCustomViewDefaultUsersRequest : TableauServerRequest
+ {
+ ///
+ /// Creates a new instance.
+ ///
+ public SetCustomViewDefaultUsersRequest()
+ { }
+
+ ///
+ /// Creates a new instance.
+ ///
+ public SetCustomViewDefaultUsersRequest(IEnumerable users)
+ {
+ Users = users.Select(user => new UserType(user)).ToArray();
+ }
+
+ ///
+ /// Gets or sets the user for the request.
+ ///
+ [XmlArray("users")]
+ [XmlArrayItem("user")]
+ public UserType[] Users { get; set; } = Array.Empty();
+
+ ///
+ /// The user type in the API request body.
+ ///
+ public class UserType
+ {
+ ///
+ /// Gets or sets the id for the request.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Constructor to build from
+ ///
+ /// The object.
+ public UserType(IContentReference user)
+ {
+ Id = user.Id;
+ }
+
+ ///
+ /// Constructor to build from
+ ///
+ public UserType()
+ { }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Api/Rest/Models/Requests/UpdateCustomViewRequest.cs b/src/Tableau.Migration/Api/Rest/Models/Requests/UpdateCustomViewRequest.cs
new file mode 100644
index 0000000..c98ca28
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Requests/UpdateCustomViewRequest.cs
@@ -0,0 +1,108 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Xml.Serialization;
+
+namespace Tableau.Migration.Api.Rest.Models.Requests
+{
+ ///
+ ///
+ /// Class representing an update custom view request.
+ ///
+ ///
+ /// See Tableau API Reference for documentation.
+ ///
+ ///
+ [XmlType(XmlTypeName)]
+ public class UpdateCustomViewRequest : TableauServerRequest
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public UpdateCustomViewRequest() { }
+
+ ///
+ /// Builds the update request for a custom view.
+ ///
+ /// (Optional) The new owner ID for the custom view.
+ /// (Optional) The new name for the custom view.
+ public UpdateCustomViewRequest(
+ Guid? newOwnerId = null,
+ string? newName = null)
+ {
+ if (newOwnerId is not null &&
+ newOwnerId != Guid.Empty)
+ {
+ CustomView.Owner.Id = newOwnerId.Value;
+ }
+
+ if (!string.IsNullOrWhiteSpace(newName))
+ {
+ CustomView.Name = newName;
+ }
+ }
+
+ ///
+ /// Gets or sets the custom view for the request.
+ ///
+ [XmlElement("customView")]
+ public CustomViewType CustomView { get; set; } = new CustomViewType();
+
+ ///
+ /// The custom view type in the API request body.
+ ///
+ public class CustomViewType
+ {
+ ///
+ /// Gets or sets the name for the request.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; } = null;
+
+ ///
+ /// Gets or sets the owner for the request.
+ ///
+ [XmlElement("owner")]
+ public OwnerType Owner { get; set; } = new OwnerType();
+
+ ///
+ /// The custom view type in the API request body.
+ ///
+ public class OwnerType
+ {
+ private Guid? _id;
+
+ ///
+ /// Gets or sets the ID for the request.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id
+ {
+ get => _id!.Value;
+ set => _id = value;
+ }
+
+ ///
+ /// Defines the serialization for the property .
+ ///
+ [XmlIgnore]
+ public bool IdSpecified => _id.HasValue;
+ }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/CreateProjectResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/CreateProjectResponse.cs
index 99061f9..b671f5d 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/CreateProjectResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/CreateProjectResponse.cs
@@ -93,14 +93,14 @@ public class ProjectType : IProjectType
public OwnerType? Owner { get; set; }
///
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
#region - Object Specific Types -
///
/// Class representing a REST API user response.
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewAsUsersDefaultViewResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewAsUsersDefaultViewResponse.cs
new file mode 100644
index 0000000..9e89c6c
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewAsUsersDefaultViewResponse.cs
@@ -0,0 +1,72 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Xml.Serialization;
+
+namespace Tableau.Migration.Api.Rest.Models.Responses
+{
+ ///
+ /// Class representing a Set Custom View as Default for Users response.
+ ///
+ [XmlType(XmlTypeName)]
+ public class CustomViewAsUsersDefaultViewResponse : TableauServerListResponse
+ {
+ ///
+ /// Gets or sets the groups for the response.
+ ///
+ [XmlArray("customViewAsUserDefaultResults")]
+ [XmlArrayItem("customViewAsUserDefaultViewResult")]
+ public override CustomViewAsUserDefaultViewType[] Items { get; set; } = Array.Empty();
+
+ ///
+ /// Class representing a site response.
+ ///
+ public class CustomViewAsUserDefaultViewType
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("success")]
+ public bool Success { get; set; }
+
+ ///
+ /// Gets or sets the error for the response.
+ ///
+ [XmlElement("error")]
+ public Error? Error { get; set; }
+
+ ///
+ /// Gets or sets the user for the response.
+ ///
+ [XmlElement("user")]
+ public UserType? User { get; set; }
+
+ ///
+ /// Class representing a REST API user response.
+ ///
+ public class UserType
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+ }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewResponse.cs
new file mode 100644
index 0000000..5e007ff
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewResponse.cs
@@ -0,0 +1,191 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Xml.Serialization;
+
+namespace Tableau.Migration.Api.Rest.Models.Responses
+{
+ ///
+ /// Class representing a custom view response.
+ /// See Tableau API Reference for documentation.
+ ///
+ [XmlType(XmlTypeName)]
+ public class CustomViewResponse : TableauServerResponse
+ {
+ ///
+ /// Gets or sets the custom view for the response.
+ ///
+ [XmlElement("customView")]
+ public override CustomViewType? Item { get; set; }
+
+ ///
+ /// Class representing a custom view on the response.
+ ///
+ public class CustomViewType : ICustomViewType
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the createdAt timestamp for the response.
+ ///
+ [XmlAttribute("createdAt")]
+ public string? CreatedAt { get; set; }
+
+ ///
+ /// Gets or sets the updatedAt timestamp for the response.
+ ///
+ [XmlAttribute("updatedAt")]
+ public string? UpdatedAt { get; set; }
+
+ ///
+ /// Gets or sets the lastAccessedAt timestamp for the response.
+ ///
+ [XmlAttribute("lastAccessedAt")]
+ public string? LastAccessedAt { get; set; }
+
+ ///
+ /// Gets or sets the shared flag for the response.
+ ///
+ [XmlAttribute("shared")]
+ public bool Shared { get; set; }
+
+ ///
+ /// Gets or sets the view for the response.
+ ///
+ [XmlElement("view")]
+ public ViewType? View { get; set; }
+
+ Guid? ICustomViewType.ViewId => View?.Id;
+
+ string? ICustomViewType.ViewName => View?.Name;
+
+ ///
+ /// Gets or sets the workbook for the response.
+ ///
+ [XmlElement("workbook")]
+ public WorkbookType? Workbook { get; set; }
+
+ IRestIdentifiable? IWithWorkbookReferenceType.Workbook => Workbook;
+
+ ///
+ /// Gets or sets the owner for the response.
+ ///
+ [XmlElement("owner")]
+ public OwnerType? Owner { get; set; }
+
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
+
+ ///
+ /// Class representing a REST API view on the response.
+ ///
+ public class ViewType : IRestIdentifiable
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public ViewType()
+ { }
+
+ ///
+ /// The constructor to build from ID and Name.
+ ///
+ /// The ID of the base view.
+ /// The name of the base view.
+ public ViewType(Guid id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+ }
+
+ ///
+ /// Class representing a REST API workbook on the response.
+ ///
+ public class WorkbookType : IRestIdentifiable
+ {
+ ///
+ /// The default parameterless constructor.
+ ///
+ public WorkbookType()
+ { }
+
+ ///
+ ///
+ ///
+ ///
+ public WorkbookType(WorkbookResponse.WorkbookType workbook)
+ {
+ Id = workbook.Id;
+ Name = workbook.Name;
+ }
+
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+ }
+
+ ///
+ /// Class representing a REST API owner on the response.
+ ///
+ public class OwnerType : IRestIdentifiable
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+ }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewsResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewsResponse.cs
index e7d45f0..906dbb5 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewsResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/CustomViewsResponse.cs
@@ -22,24 +22,62 @@ namespace Tableau.Migration.Api.Rest.Models.Responses
{
///
/// Class representing a custom views response.
- ///
- /// This is incomplete, there are more attributes than we're saving
+ /// See Tableau API Reference for documentation.
///
[XmlType(XmlTypeName)]
public class CustomViewsResponse : PagedTableauServerResponse
{
///
- /// Gets or sets the groups for the response.
+ /// Gets or sets the custom views for the response.
///
[XmlArray("customViews")]
[XmlArrayItem("customView")]
public override CustomViewResponseType[] Items { get; set; } = Array.Empty();
///
- /// Class representing a site response.
+ /// Class representing a custom view on the response.
///
- public class CustomViewResponseType
+ public class CustomViewResponseType : ICustomViewType
{
+ ///
+ /// The default parameterless constructor.
+ ///
+ public CustomViewResponseType() { }
+
+ ///
+ ///
+ ///
+ ///
+ public CustomViewResponseType(ICustomViewType customView)
+ {
+ Id = customView.Id;
+ Name = customView.Name;
+ CreatedAt = customView.CreatedAt;
+ UpdatedAt = customView.UpdatedAt;
+ LastAccessedAt = customView.LastAccessedAt;
+ Shared = customView.Shared;
+ View = new ViewType()
+ {
+ Id = Guard.AgainstNull(customView.ViewId, () => customView.ViewId),
+ Name = customView.ViewName
+ };
+
+ Guard.AgainstNull(customView.Workbook, () => customView.Workbook);
+
+ Workbook = new WorkbookType()
+ {
+ Id = customView.Workbook.Id
+ };
+
+ Guard.AgainstNull(customView.Owner, () => customView.Owner);
+ Owner = new OwnerType()
+ {
+ Id = customView.Owner.Id
+ };
+
+ }
+
+
///
/// Gets or sets the ID for the response.
///
@@ -53,28 +91,38 @@ public class CustomViewResponseType
public string? Name { get; set; }
///
- /// Gets or sets the created timestamp for the response.
+ /// Gets or sets the createdAt timestamp for the response.
///
[XmlAttribute("createdAt")]
public string? CreatedAt { get; set; }
///
- /// Gets or sets the updated timestamp for the response.
+ /// Gets or sets the updatedAt timestamp for the response.
///
[XmlAttribute("updatedAt")]
public string? UpdatedAt { get; set; }
///
- /// Gets or sets shared flag for the response.
+ /// Gets or sets the lastAccessedAt timestamp for the response.
+ ///
+ [XmlAttribute("lastAccessedAt")]
+ public string? LastAccessedAt { get; set; }
+
+ ///
+ /// Gets or sets the shared flag for the response.
///
[XmlAttribute("shared")]
public bool Shared { get; set; }
///
- /// Gets or sets the owner for the response.
+ /// Gets or sets the view for the response.
///
[XmlElement("view")]
- public ViewType? view { get; set; }
+ public ViewType? View { get; set; }
+
+ Guid? ICustomViewType.ViewId => View?.Id;
+
+ string? ICustomViewType.ViewName => View?.Name;
///
/// Gets or sets the workbook for the response.
@@ -82,16 +130,20 @@ public class CustomViewResponseType
[XmlElement("workbook")]
public WorkbookType? Workbook { get; set; }
+ IRestIdentifiable? IWithWorkbookReferenceType.Workbook => Workbook;
+
///
/// Gets or sets the owner for the response.
///
[XmlElement("owner")]
- public UserType? Owner { get; set; }
+ public OwnerType? Owner { get; set; }
+
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
- /// Class representing a REST API user response.
+ /// Class representing a REST API view on the response.
///
- public class ViewType
+ public class ViewType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
@@ -107,9 +159,9 @@ public class ViewType
}
///
- /// Class representing a REST API user response.
+ /// Class representing a REST API workbook on the response.
///
- public class WorkbookType
+ public class WorkbookType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
@@ -125,10 +177,16 @@ public class WorkbookType
}
///
- /// Class representing a REST API user response.
+ /// Class representing a REST API owner on the response.
///
- public class UserType
+ public class OwnerType : IRestIdentifiable
{
+ ///
+ /// The default parameterless constructor.
+ ///
+ public OwnerType()
+ { }
+
///
/// Gets or sets the ID for the response.
///
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourceResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourceResponse.cs
index a2cc292..e16f304 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourceResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourceResponse.cs
@@ -167,7 +167,7 @@ internal DataSourceType(IDataSourceDetailsType response)
[XmlElement("owner")]
public OwnerType? Owner { get; set; }
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the data source tags for the response.
@@ -230,7 +230,7 @@ public ProjectType(IProjectType project)
}
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
[XmlAttribute("id")]
@@ -246,7 +246,7 @@ public OwnerType()
/// Constructor to build from
///
///
- public OwnerType(IOwnerType owner)
+ public OwnerType(IRestIdentifiable owner)
{
Id = owner.Id;
}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourcesResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourcesResponse.cs
index 1ae4583..70c49ea 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourcesResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/DataSourcesResponse.cs
@@ -121,7 +121,7 @@ public class DataSourceType : IDataSourceType
[XmlElement("owner")]
public OwnerType? Owner { get; set; }
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the data source tags for the response.
@@ -174,7 +174,7 @@ public ProjectType(IProjectReferenceType project)
}
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
[XmlAttribute("id")]
@@ -187,10 +187,10 @@ public OwnerType()
{ }
///
- /// Constructor to build from
+ /// Constructor to build from
///
///
- public OwnerType(IOwnerType owner)
+ public OwnerType(IRestIdentifiable owner)
{
Id = owner.Id;
}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/FlowResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/FlowResponse.cs
index b9ab830..a0f3dd8 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/FlowResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/FlowResponse.cs
@@ -95,7 +95,7 @@ public class FlowType : IFlowType
[XmlElement("owner")]
public OwnerType? Owner { get; set; }
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the tags for the response.
@@ -131,7 +131,7 @@ public class ProjectType : IProjectReferenceType
///
/// Class representing a REST API user response.
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/FlowsResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/FlowsResponse.cs
index 5f01a39..848189c 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/FlowsResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/FlowsResponse.cs
@@ -97,7 +97,7 @@ public class FlowType : IFlowType
[XmlElement("owner")]
public OwnerType? Owner { get; set; }
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the tags for the response.
@@ -133,7 +133,7 @@ public class ProjectType : IProjectReferenceType
///
/// Class representing a REST API user response.
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/ProjectsResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/ProjectsResponse.cs
index caa9a79..720637a 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/ProjectsResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/ProjectsResponse.cs
@@ -115,7 +115,7 @@ public class ProjectType : IProjectType
public UserType? Owner { get; set; }
///
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the contentCounts for the response.
@@ -128,7 +128,7 @@ public class ProjectType : IProjectType
///
/// Class representing a REST API user response.
///
- public class UserType : IOwnerType
+ public class UserType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateCustomViewResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateCustomViewResponse.cs
new file mode 100644
index 0000000..a657245
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateCustomViewResponse.cs
@@ -0,0 +1,157 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Xml.Serialization;
+
+namespace Tableau.Migration.Api.Rest.Models.Responses
+{
+ ///
+ /// Class representing a custom view update response.
+ ///
+ [XmlType(XmlTypeName)]
+ public class UpdateCustomViewResponse : TableauServerResponse
+ {
+ ///
+ /// Gets or sets the custom view for the response.
+ ///
+ [XmlElement("customView")]
+ public override CustomViewResponseType? Item { get; set; }
+
+ ///
+ /// Class representing a custom view on the response.
+ ///
+ public class CustomViewResponseType : ICustomViewType
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the createdAt timestamp for the response.
+ ///
+ [XmlAttribute("createdAt")]
+ public string? CreatedAt { get; set; }
+
+ ///
+ /// Gets or sets the updatedAt timestamp for the response.
+ ///
+ [XmlAttribute("updatedAt")]
+ public string? UpdatedAt { get; set; }
+
+ ///
+ /// Gets or sets the lastAccessedAt timestamp for the response.
+ ///
+ [XmlAttribute("lastAccessedAt")]
+ public string? LastAccessedAt { get; set; }
+
+ ///
+ /// Gets or sets the shared flag for the response.
+ ///
+ [XmlAttribute("shared")]
+ public bool Shared { get; set; }
+
+ ///
+ /// Gets or sets the view for the response.
+ ///
+ [XmlElement("view")]
+ public ViewType? View { get; set; }
+
+ Guid? ICustomViewType.ViewId => View?.Id;
+
+ string? ICustomViewType.ViewName => View?.Name;
+
+ ///
+ /// Gets or sets the workbook for the response.
+ ///
+ [XmlElement("workbook")]
+ public WorkbookType? Workbook { get; set; }
+
+ IRestIdentifiable? IWithWorkbookReferenceType.Workbook => Workbook;
+
+ ///
+ /// Gets or sets the owner for the response.
+ ///
+ [XmlElement("owner")]
+ public OwnerType? Owner { get; set; }
+
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
+
+ ///
+ /// Class representing a REST API view on the response.
+ ///
+ public class ViewType : IRestIdentifiable
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+ }
+
+ ///
+ /// Class representing a REST API workbook on the response.
+ ///
+ public class WorkbookType : IRestIdentifiable
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+ }
+
+ ///
+ /// Class representing a REST API owner on the response.
+ ///
+ public class OwnerType : IRestIdentifiable
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the name for the response.
+ ///
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+ }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateProjectResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateProjectResponse.cs
index a76aa91..4b34728 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateProjectResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/UpdateProjectResponse.cs
@@ -88,14 +88,14 @@ public class ProjectType : IRestIdentifiable, IProjectType
public OwnerType? Owner { get; set; }
///
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
#region - Object Specific Types -
///
/// Class representing a REST API user response.
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
/// Gets or sets the ID for the response.
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/UsersWithCustomViewAsDefaultViewResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/UsersWithCustomViewAsDefaultViewResponse.cs
new file mode 100644
index 0000000..030bf63
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/UsersWithCustomViewAsDefaultViewResponse.cs
@@ -0,0 +1,48 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Xml.Serialization;
+
+namespace Tableau.Migration.Api.Rest.Models.Responses
+{
+ ///
+ /// Class representing a list of users with a custom view as the default view response.
+ ///
+ [XmlType(XmlTypeName)]
+ public class UsersWithCustomViewAsDefaultViewResponse : PagedTableauServerResponse
+ {
+ ///
+ /// Gets or sets the users for the response.
+ ///
+ [XmlArray("users")]
+ [XmlArrayItem("user")]
+ public override UserType[] Items { get; set; } = Array.Empty();
+
+ ///
+ /// Class representing a REST API user response.
+ ///
+ public class UserType : IRestIdentifiable
+ {
+ ///
+ /// Gets or sets the ID for the response.
+ ///
+ [XmlAttribute("id")]
+ public Guid Id { get; set; }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbookResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbookResponse.cs
index 2589b41..b92537b 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbookResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbookResponse.cs
@@ -144,7 +144,7 @@ internal WorkbookType(IWorkbookDetailsType response)
public OwnerType? Owner { get; set; }
///
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the tags for the response.
@@ -239,7 +239,7 @@ public class LocationType : ILocationType
///
/// Class representing a REST API user response.
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
[XmlAttribute("id")]
@@ -252,10 +252,10 @@ public OwnerType()
{ }
///
- /// Constructor to build from
+ /// Constructor to build from
///
/// The owner to copy from.
- public OwnerType(IOwnerType owner)
+ public OwnerType(IRestIdentifiable owner)
{
Id = owner.Id;
}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbooksResponse.cs b/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbooksResponse.cs
index 3c1b5b9..3bd0f22 100644
--- a/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbooksResponse.cs
+++ b/src/Tableau.Migration/Api/Rest/Models/Responses/WorkbooksResponse.cs
@@ -108,7 +108,7 @@ public class WorkbookType : IWorkbookType
public OwnerType? Owner { get; set; }
///
- IOwnerType? IWithOwnerType.Owner => Owner;
+ IRestIdentifiable? IWithOwnerType.Owner => Owner;
///
/// Gets or sets the tags for the response.
@@ -193,7 +193,7 @@ public LocationType(ILocationType response)
///
/// Class representing a REST API user response.
///
- public class OwnerType : IOwnerType
+ public class OwnerType : IRestIdentifiable
{
///
[XmlAttribute("id")]
@@ -206,10 +206,10 @@ public OwnerType()
{ }
///
- /// Constructor to build from
+ /// Constructor to build from
///
/// The owner to copy from.
- public OwnerType(IOwnerType owner)
+ public OwnerType(IRestIdentifiable owner)
{
Id = owner.Id;
}
diff --git a/src/Tableau.Migration/Api/Rest/Models/Types/CustomViewFileTypes.cs b/src/Tableau.Migration/Api/Rest/Models/Types/CustomViewFileTypes.cs
new file mode 100644
index 0000000..71d6c0b
--- /dev/null
+++ b/src/Tableau.Migration/Api/Rest/Models/Types/CustomViewFileTypes.cs
@@ -0,0 +1,31 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace Tableau.Migration.Api.Rest.Models.Types
+{
+ ///
+ /// Class containing custom view file type constants.
+ ///
+ public class CustomViewFileTypes : StringEnum
+ {
+ ///
+ /// Gets the name of the Tde custom view file type.
+ ///
+ public const string Json = "json";
+ }
+}
+
diff --git a/src/Tableau.Migration/Api/Rest/RestUrlPrefixes.cs b/src/Tableau.Migration/Api/Rest/RestUrlPrefixes.cs
index 7614808..3a0381a 100644
--- a/src/Tableau.Migration/Api/Rest/RestUrlPrefixes.cs
+++ b/src/Tableau.Migration/Api/Rest/RestUrlPrefixes.cs
@@ -36,6 +36,7 @@ internal static class RestUrlPrefixes
[typeof(IViewsApiClient)] = Views,
[typeof(IWorkbooksApiClient)] = Workbooks,
[typeof(ITasksApiClient)] = Tasks,
+ [typeof(ICustomViewsApiClient)] = CustomViews
}
.ToImmutableDictionary(InheritedTypeComparer.Instance);
@@ -52,6 +53,7 @@ internal static class RestUrlPrefixes
public const string Workbooks = "workbooks";
public const string Schedules = "schedules";
public const string Tasks = "tasks";
+ public const string CustomViews = "customviews";
public static string GetUrlPrefix()
where TApiClient : IContentApiClient
diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Api/CustomViewsRestApiSimulator.cs b/src/Tableau.Migration/Api/Simulation/Rest/Api/CustomViewsRestApiSimulator.cs
new file mode 100644
index 0000000..680e708
--- /dev/null
+++ b/src/Tableau.Migration/Api/Simulation/Rest/Api/CustomViewsRestApiSimulator.cs
@@ -0,0 +1,106 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using System.Linq;
+using Tableau.Migration.Api.Rest;
+using Tableau.Migration.Api.Rest.Models.Responses;
+using Tableau.Migration.Api.Simulation.Rest.Net;
+using Tableau.Migration.Api.Simulation.Rest.Net.Requests;
+using Tableau.Migration.Api.Simulation.Rest.Net.Responses;
+using Tableau.Migration.Net.Simulation;
+using static Tableau.Migration.Api.Simulation.Rest.Net.Requests.RestUrlPatterns;
+
+namespace Tableau.Migration.Api.Simulation.Rest.Api
+{
+ ///
+ /// Object that defines simulation of Tableau REST API custom view methods.
+ ///
+ public sealed class CustomViewsRestApiSimulator
+ {
+ ///
+ /// Gets the simulated custom view query API method.
+ ///
+ public MethodSimulator QueryCustomViews { get; }
+
+ ///
+ /// Gets the simulated custom view download API method.
+ ///
+ public MethodSimulator DownloadCustomView { get; }
+
+ ///
+ /// Gets the simulated commit custom view upload API method.
+ ///
+ public MethodSimulator CommitCustomViewUpload { get; }
+
+
+ ///
+ /// Gets the simulated get custom view default users API method.
+ ///
+ public MethodSimulator GetCustomViewDefaultUsers { get; }
+
+
+ ///
+ /// Sets the simulated set custom view default users API method.
+ ///
+ public MethodSimulator SetCustomViewDefaultUsers { get; }
+
+ ///
+ /// Creates a new object.
+ ///
+ /// A response simulator to setup with REST API methods.
+ public CustomViewsRestApiSimulator(TableauApiResponseSimulator simulator)
+ {
+ QueryCustomViews = simulator.SetupRestPagedList(
+ SiteUrl(RestUrlPrefixes.CustomViews),
+ (data, request) =>
+ {
+ return data.CustomViews.Select(x => new CustomViewsResponse.CustomViewResponseType(x)).ToList();
+ });
+
+ DownloadCustomView = simulator.SetupRestDownloadById(
+ SiteEntityUrl(
+ postSitePreEntitySuffix: RestUrlPrefixes.CustomViews,
+ postEntitySuffix: "content",
+ useExperimental: true),
+ (data) => data.CustomViewFiles, 4);
+
+ CommitCustomViewUpload = simulator.SetupRestPost(
+ SiteUrl(RestUrlPrefixes.CustomViews, useExperimental: true),
+ new RestCommitCustomViewUploadResponseBuilder(simulator.Data, simulator.Serializer));
+
+ GetCustomViewDefaultUsers = simulator.SetupRestPagedList(
+ SiteEntityUrl(RestUrlPrefixes.CustomViews, "/default/users"),
+ (data, request) =>
+ {
+ var customViewId = request.GetIdAfterSegment(RestUrlPrefixes.CustomViews);
+
+ if (data.CustomViewDefaultUsers == null || customViewId == null)
+ return [];
+
+ data.CustomViewDefaultUsers.TryGetValue(customViewId.Value, out List? defaultUsers);
+
+ return defaultUsers == null ? [] : defaultUsers;
+ });
+
+
+ SetCustomViewDefaultUsers = simulator.SetupRestPost(
+ SiteEntityUrl(RestUrlPrefixes.CustomViews, "/default/users"),
+ new RestCustomViewDefaultUsersAddResponseBuilder(simulator.Data, simulator.Serializer));
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestApiRequestMatcher.cs b/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestApiRequestMatcher.cs
index a74411c..eabec3f 100644
--- a/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestApiRequestMatcher.cs
+++ b/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestApiRequestMatcher.cs
@@ -68,7 +68,9 @@ public virtual bool Matches(HttpRequestMessage request)
}
var restApiVersionGroup = restApiPathMatch.Groups[RestUrlPatterns.VersionGroupName];
- if (restApiVersionGroup is null || !decimal.TryParse(restApiVersionGroup.Value, out var restApiVersion))
+ if (restApiVersionGroup is null
+ || (!decimal.TryParse(restApiVersionGroup.Value, out var restApiVersion)
+ && restApiVersionGroup.Value != ApiClient.EXPERIMENTAL_API_VERSION))
{
return false;
}
diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestUrlPatterns.cs b/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestUrlPatterns.cs
index 7b5a243..caefacf 100644
--- a/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestUrlPatterns.cs
+++ b/src/Tableau.Migration/Api/Simulation/Rest/Net/Requests/RestUrlPatterns.cs
@@ -42,23 +42,44 @@ internal static class RestUrlPatterns
public static readonly string EntityId = IdPattern(EntityIdGroupName);
- public static Regex RestApiUrl(string suffix) => new($"""^/api/(?<{VersionGroupName}>\d+.\d+)/{suffix.TrimPaths()}/?$""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ public static Regex RestApiUrl(string suffix, bool useExperimental = false)
+ {
+ string pattern;
+
+ if (useExperimental)
+ {
+ pattern = $"""^/api/(?<{VersionGroupName}>{ApiClient.EXPERIMENTAL_API_VERSION})/{suffix.TrimPaths()}/?$""";
+ }
+ else
+ {
+ pattern = $"""^/api/(?<{VersionGroupName}>\d+.\d+)/{suffix.TrimPaths()}/?$""";
+
+ }
+
+ return new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ }
- public static Regex SiteUrl(string postSiteSuffix) => RestApiUrl($"""/sites/{SiteId}/{postSiteSuffix.TrimPaths()}""");
+ public static Regex SiteUrl(string postSiteSuffix, bool useExperimental = false)
+ => RestApiUrl($"""/sites/{SiteId}/{postSiteSuffix.TrimPaths()}""", useExperimental);
public static Regex EntityUrl(string preEntitySuffix) => RestApiUrl($"""{preEntitySuffix.TrimPaths()}/{EntityId}""");
- public static Regex SiteEntityUrl(string postSitePreEntitySuffix, string? postEntitySuffix = null)
+ public static Regex SiteEntityUrl(
+ string postSitePreEntitySuffix,
+ string? postEntitySuffix = null,
+ bool useExperimental = false)
{
var trimmedSuffix = postEntitySuffix?.TrimPaths();
trimmedSuffix = string.IsNullOrEmpty(trimmedSuffix) ? string.Empty : $"/{trimmedSuffix}";
- return SiteUrl($"""{postSitePreEntitySuffix.TrimPaths()}/{EntityId}{trimmedSuffix}""");
+ return SiteUrl($"""{postSitePreEntitySuffix.TrimPaths()}/{EntityId}{trimmedSuffix}""", useExperimental);
}
- public static Regex SiteEntityTagsUrl(string postSitePreEntitySuffix, string? postTagsSuffix = null) => SiteEntityUrl(postSitePreEntitySuffix, $"tags/{postTagsSuffix}".TrimEnd('/'));
+ public static Regex SiteEntityTagsUrl(string postSitePreEntitySuffix, string? postTagsSuffix = null)
+ => SiteEntityUrl(postSitePreEntitySuffix, $"tags/{postTagsSuffix}".TrimEnd('/'));
- public static Regex SiteEntityTagUrl(string postSitePreEntitySuffix) => SiteEntityTagsUrl(postSitePreEntitySuffix, new Regex(NamePattern, RegexOptions.IgnoreCase).ToString());
+ public static Regex SiteEntityTagUrl(string postSitePreEntitySuffix)
+ => SiteEntityTagsUrl(postSitePreEntitySuffix, new Regex(NamePattern, RegexOptions.IgnoreCase).ToString());
public static IEnumerable<(string Key, Regex ValuePattern)> SiteCommitFileUploadQueryString(string typeParam)
{
diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCommitCustomViewUploadResponseBuilder.cs b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCommitCustomViewUploadResponseBuilder.cs
new file mode 100644
index 0000000..6cd0bd0
--- /dev/null
+++ b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCommitCustomViewUploadResponseBuilder.cs
@@ -0,0 +1,119 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Linq;
+using System.Text;
+using Tableau.Migration.Api.Rest.Models.Requests;
+using Tableau.Migration.Api.Rest.Models.Responses;
+using Tableau.Migration.Content;
+using Tableau.Migration.Net;
+
+namespace Tableau.Migration.Api.Simulation.Rest.Net.Responses
+{
+ internal class RestCommitCustomViewUploadResponseBuilder
+ : RestCommitUploadResponseBuilder
+ {
+ public RestCommitCustomViewUploadResponseBuilder(
+ TableauData data,
+ IHttpContentSerializer serializer)
+ : base(
+ data,
+ serializer,
+ data => data.CustomViews,
+ data => data.CustomViewFiles,
+ (data, cv, file) => data.AddCustomView(cv, file))
+ { }
+
+ protected override CustomViewResponse.CustomViewType? GetExistingContentItem(
+ CommitCustomViewPublishRequest commitRequest)
+ {
+ var commitCustomView = Guard.AgainstNull(commitRequest.CustomView, () => commitRequest.CustomView);
+
+ var targetCustomView = Data.CustomViews
+ .SingleOrDefault(wb =>
+ string.Equals(wb.Name, commitCustomView.Name, CustomView.NameComparison));
+
+ return targetCustomView;
+ }
+
+ protected override CustomViewResponse.CustomViewType BuildContent(
+ CommitCustomViewPublishRequest commitRequest,
+ ref byte[] commitFileData,
+ CustomViewResponse.CustomViewType? existingContent,
+ UsersResponse.UserType currentUser,
+ bool overwrite)
+ {
+ var commitCustomView = Guard.AgainstNull(commitRequest.CustomView, () => commitRequest.CustomView);
+
+ SimulatedCustomViewData? simulatedFileData;
+ try
+ {
+ simulatedFileData = Encoding.Default.GetString(commitFileData).FromJson();
+ if (simulatedFileData is null)
+ {
+ throw new BuildResponseException(System.Net.HttpStatusCode.BadRequest, 8, "Unable to parse file", "");
+ }
+ }
+ catch (Exception)
+ {
+ throw new BuildResponseException(System.Net.HttpStatusCode.BadRequest, 8, "Unable to parse file", "");
+ }
+
+ var targetCustomView = existingContent ?? new CustomViewResponse.CustomViewType
+ {
+ Id = Guid.NewGuid(),
+ Name = commitCustomView.Name,
+ CreatedAt = DateTime.UtcNow.ToString(),
+ UpdatedAt = DateTime.UtcNow.ToString(),
+ LastAccessedAt = DateTime.UtcNow.ToString(),
+ Shared = true,
+ Owner = new CustomViewResponse.CustomViewType.OwnerType
+ {
+ Id = Guid.NewGuid()
+ },
+ Workbook = new CustomViewResponse.CustomViewType.WorkbookType
+ {
+ Id = Guid.NewGuid()
+ },
+ View = new CustomViewResponse.CustomViewType.ViewType(Guid.NewGuid(), $"ViewName{Guid.NewGuid()}")
+ };
+
+ targetCustomView.Name = commitCustomView.Name;
+ targetCustomView.UpdatedAt = DateTime.UtcNow.ToString();
+ targetCustomView.Shared = commitCustomView.Shared;
+ targetCustomView.Workbook = new CustomViewResponse.CustomViewType.WorkbookType
+ {
+ Id = commitCustomView.Workbook == null ? Guid.NewGuid() : commitCustomView.Workbook.Id
+ };
+
+ targetCustomView.Owner = new()
+ {
+ Id = currentUser.Id
+ };
+
+ targetCustomView.View = new CustomViewResponse.CustomViewType.ViewType(Guid.NewGuid(), $"ViewName{Guid.NewGuid()}");
+
+ commitFileData = Encoding.Default.GetBytes(simulatedFileData.ToJson());
+
+ return targetCustomView;
+ }
+
+ protected override CustomViewResponse.CustomViewType BuildResponse(CustomViewResponse.CustomViewType customView)
+ => customView;
+ }
+}
diff --git a/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCustomViewDefaultUsersAddResponseBuilder.cs b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCustomViewDefaultUsersAddResponseBuilder.cs
new file mode 100644
index 0000000..b3d9ba6
--- /dev/null
+++ b/src/Tableau.Migration/Api/Simulation/Rest/Net/Responses/RestCustomViewDefaultUsersAddResponseBuilder.cs
@@ -0,0 +1,98 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Tableau.Migration.Api.Rest;
+using Tableau.Migration.Api.Rest.Models.Requests;
+using Tableau.Migration.Api.Rest.Models.Responses;
+using Tableau.Migration.Api.Simulation.Rest.Net.Requests;
+using Tableau.Migration.Net;
+
+
+namespace Tableau.Migration.Api.Simulation.Rest.Net.Responses
+{
+ internal class RestCustomViewDefaultUsersAddResponseBuilder : RestApiResponseBuilderBase
+ {
+ public RestCustomViewDefaultUsersAddResponseBuilder(TableauData data, IHttpContentSerializer serializer)
+ : base(data, serializer, requiresAuthentication: true)
+ { }
+
+ protected override ValueTask<(CustomViewAsUsersDefaultViewResponse Response, HttpStatusCode ResponseCode)> BuildResponseAsync(
+ HttpRequestMessage request,
+ CancellationToken cancel)
+ {
+ if (request?.Content is null)
+ return BuildEmptyErrorResponseAsync(HttpStatusCode.BadRequest, 0, "Request or content cannot be null.", "");
+
+ var customViewId = request.GetIdAfterSegment(RestUrlPrefixes.CustomViews);
+ var usersToAdd = request.GetTableauServerRequest()?.Users;
+
+ if (usersToAdd is null || customViewId is null)
+ {
+ return BuildEmptyErrorResponseAsync(
+ HttpStatusCode.BadRequest,
+ 0,
+ $"Request must be of the type {nameof(SetCustomViewDefaultUsersRequest.UserType)} and not null. CustomView ID should not be null",
+ "");
+ }
+
+ var customViewUsers = new List();
+ foreach (var user in usersToAdd)
+ {
+ customViewUsers.Add(new()
+ {
+ Id = user.Id
+ });
+ }
+
+ if (Data.CustomViewDefaultUsers.TryGetValue(
+ customViewId.Value,
+ out List? value))
+ {
+ value.AddRange(customViewUsers);
+ }
+ else
+ {
+ Data.CustomViewDefaultUsers.TryAdd(customViewId.Value, customViewUsers);
+ }
+
+
+ var resultUsers = new List();
+
+ foreach (var user in customViewUsers)
+ {
+ resultUsers.Add(new()
+ {
+ Success = true,
+ User = new()
+ {
+ Id = user.Id
+ }
+ });
+ }
+ return ValueTask.FromResult((new CustomViewAsUsersDefaultViewResponse
+ {
+ Items = [.. resultUsers]
+ },
+ HttpStatusCode.Created));
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Simulation/Rest/RestApiSimulator.cs b/src/Tableau.Migration/Api/Simulation/Rest/RestApiSimulator.cs
index c5cfb74..ff236e5 100644
--- a/src/Tableau.Migration/Api/Simulation/Rest/RestApiSimulator.cs
+++ b/src/Tableau.Migration/Api/Simulation/Rest/RestApiSimulator.cs
@@ -51,12 +51,12 @@ public sealed class RestApiSimulator
/// Gets the simulated job API methods.
///
public JobsRestApiSimulator Jobs { get; }
-
+
///
/// Gets the simulated schedule API methods.
///
public SchedulesRestApiSimulator Schedules { get; }
-
+
///
/// Gets the simulated task API methods.
///
@@ -87,6 +87,11 @@ public sealed class RestApiSimulator
///
public ViewsRestApiSimulator Views { get; }
+ ///
+ /// Gets the simulated custom view API methods.
+ ///
+ public CustomViewsRestApiSimulator CustomViews { get; }
+
///
/// Gets the simulated workbook API methods.
///
@@ -130,7 +135,7 @@ private static ServerSessionResponse.SessionType BuildCurrentSession(TableauData
};
var adminLevel = SiteRoleMapping.GetAdministratorLevel(user.SiteRole);
- if(!AdministratorLevels.IsAMatch(adminLevel, AdministratorLevels.None))
+ if (!AdministratorLevels.IsAMatch(adminLevel, AdministratorLevels.None))
{
response.Site.ExtractEncryptionMode = site.ExtractEncryptionMode;
}
@@ -156,6 +161,7 @@ public RestApiSimulator(TableauApiResponseSimulator simulator)
Workbooks = new(simulator);
Files = new(simulator);
Views = new(simulator);
+ CustomViews = new(simulator);
QueryServerInfo = simulator.SetupRestGet(RestApiUrl("serverinfo"), d => d.ServerInfo, requiresAuthentication: false);
GetCurrentServerSession = simulator.SetupRestGet(RestApiUrl("sessions/current"), BuildCurrentSession);
diff --git a/src/Tableau.Migration/Api/Simulation/SimulatedCustomViewData.cs b/src/Tableau.Migration/Api/Simulation/SimulatedCustomViewData.cs
new file mode 100644
index 0000000..be04b78
--- /dev/null
+++ b/src/Tableau.Migration/Api/Simulation/SimulatedCustomViewData.cs
@@ -0,0 +1,71 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Tableau.Migration.Api.Simulation
+{
+ ///
+ /// Object that holds simulated custom view file data
+ ///
+ public class SimulatedCustomViewData
+ {
+ ///
+ /// Simulated custom views data
+ ///
+ public List CustomViews { get; set; } = new List();
+
+ ///
+ /// Simulated custom view type data
+ ///
+ public class SimulatedCustomViewType
+ {
+ ///
+ /// Constructor for the simulated custom view.
+ ///
+ ///
+ ///
+ ///
+ public SimulatedCustomViewType(bool isSourceView, string viewName, string tcv)
+ {
+ IsSourceView = isSourceView;
+ ViewName = viewName;
+ Tcv = tcv;
+ }
+
+ ///
+ /// Flag that indicates if is the source view
+ /// for the Custom View encoded in .
+ ///
+ [JsonPropertyName("isSourceView")]
+ public bool IsSourceView { get; set; }
+
+ ///
+ /// Name of the view which this custom view is based on.
+ ///
+ [JsonPropertyName("viewName")]
+ public string ViewName { get; set; }
+
+ ///
+ /// The encoded Custom View Definition
+ ///
+ [JsonPropertyName("tcv")]
+ public string Tcv { get; set; }
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Api/Simulation/TableauData.cs b/src/Tableau.Migration/Api/Simulation/TableauData.cs
index b365f92..ee0c239 100644
--- a/src/Tableau.Migration/Api/Simulation/TableauData.cs
+++ b/src/Tableau.Migration/Api/Simulation/TableauData.cs
@@ -95,17 +95,17 @@ public sealed class TableauData
/// Gets or sets the jobs.
///
public ConcurrentSet Jobs { get; set; } = new();
-
+
///
/// Gets or sets the schedules.
///
public ConcurrentSet Schedules { get; set; } = new();
-
+
///
/// Gets or sets the schedules extract refresh tasks.
///
public ConcurrentSet ScheduleExtractRefreshTasks { get; set; } = new();
-
+
///
/// Gets or sets the Tableau Server extract refresh tasks.
///
@@ -171,6 +171,23 @@ public sealed class TableauData
///
public ConcurrentDictionary> Files { get; set; } = new();
+ ///
+ /// Gets or sets the custom views.
+ ///
+ public ConcurrentSet CustomViews { get; set; } = new();
+
+
+ ///
+ /// Gets or sets the custom view fileData contents, by ID.
+ ///
+ public ConcurrentDictionary CustomViewFiles { get; set; } = new();
+
+
+
+ ///
+ /// Gets or sets the custom view default users contents, by ID.
+ ///
+ public ConcurrentDictionary> CustomViewDefaultUsers { get; set; } = new();
#region - Relationships -
@@ -509,6 +526,18 @@ internal void AddView(
Views.Add(view);
}
+ ///
+ /// Adds a custom view to simulated dataset.
+ ///
+ /// The metadata
+ /// A byte array representing the custom view. If null, empty array is used
+ internal void AddCustomView(
+ CustomViewResponse.CustomViewType customView,
+ byte[]? fileData)
+ {
+ CustomViews.Add(customView);
+ CustomViewFiles[customView.Id] = fileData ?? [];
+ }
internal void AddDefaultProjectPermissions(Guid projectId, string contentTypeUrlSegment, PermissionsType permissions)
{
DefaultProjectPermissions.AddOrUpdate(
diff --git a/src/Tableau.Migration/Api/SitesApiClient.cs b/src/Tableau.Migration/Api/SitesApiClient.cs
index b5e0b27..3204ab1 100644
--- a/src/Tableau.Migration/Api/SitesApiClient.cs
+++ b/src/Tableau.Migration/Api/SitesApiClient.cs
@@ -26,7 +26,6 @@
using Tableau.Migration.Api.Rest.Models.Responses;
using Tableau.Migration.Api.Tags;
using Tableau.Migration.Content;
-using Tableau.Migration.Content.Schedules;
using Tableau.Migration.Content.Schedules.Cloud;
using Tableau.Migration.Content.Schedules.Server;
using Tableau.Migration.Content.Search;
@@ -62,6 +61,7 @@ public SitesApiClient(
IViewsApiClient viewsApiClient,
IFlowsApiClient flowsApiClient,
ITasksApiClient tasksApiClient,
+ ICustomViewsApiClient customViewsApiClient,
ISharedResourcesLocalizer sharedResourcesLocalizer)
: base(restRequestBuilderFactory, finderFactory, loggerFactory, sharedResourcesLocalizer)
{
@@ -77,7 +77,8 @@ public SitesApiClient(
Workbooks = workbooksApiClient;
Views = viewsApiClient;
Flows = flowsApiClient;
-
+ CustomViews = customViewsApiClient;
+
_tasksApiClient = tasksApiClient;
}
@@ -92,7 +93,8 @@ public SitesApiClient(
{ typeof(IFlow), client => client.Flows },
{ typeof(IServerSchedule), client => client.Schedules },
{ typeof(IServerExtractRefreshTask), client => client.ServerTasks },
- { typeof(ICloudExtractRefreshTask), client => client.CloudTasks }
+ { typeof(ICloudExtractRefreshTask), client => client.CloudTasks },
+ { typeof(ICustomView), client => client.CustomViews },
}
.ToImmutableDictionary(InheritedTypeComparer.Instance);
@@ -110,7 +112,7 @@ public SitesApiClient(
///
public IJobsApiClient Jobs { get; }
-
+
///
public ISchedulesApiClient Schedules { get; }
@@ -133,11 +135,14 @@ public SitesApiClient(
public IFlowsApiClient Flows { get; }
///
- public IServerTasksApiClient ServerTasks
+ public ICustomViewsApiClient CustomViews { get; }
+
+ ///
+ public IServerTasksApiClient ServerTasks
=> ReturnForInstanceType(TableauInstanceType.Server, _sessionProvider.InstanceType, _tasksApiClient);
///
- public ICloudTasksApiClient CloudTasks
+ public ICloudTasksApiClient CloudTasks
=> ReturnForInstanceType(TableauInstanceType.Cloud, _sessionProvider.InstanceType, _tasksApiClient);
///
diff --git a/src/Tableau.Migration/Api/WorkbooksApiClient.cs b/src/Tableau.Migration/Api/WorkbooksApiClient.cs
index df7999f..ad1b223 100644
--- a/src/Tableau.Migration/Api/WorkbooksApiClient.cs
+++ b/src/Tableau.Migration/Api/WorkbooksApiClient.cs
@@ -116,7 +116,17 @@ public async Task> GetAllWorkbooksAsync(
var owner = await FindOwnerAsync(item, false, c).ConfigureAwait(false);
if (project is null || owner is null)
- continue; //Warnings will be logged by prior method calls.
+ {
+ Logger.LogWarning(
+ SharedResourcesLocalizer[SharedResourceKeys.WorkbookSkippedMissingReferenceWarning],
+ item.Id,
+ item.Name,
+ item.Project!.Id,
+ project is null ? SharedResourcesLocalizer[SharedResourceKeys.NotFound] : SharedResourcesLocalizer[SharedResourceKeys.Found],
+ item.Owner!.Id,
+ owner is null ? SharedResourcesLocalizer[SharedResourceKeys.NotFound] : SharedResourcesLocalizer[SharedResourceKeys.Found]);
+ continue;
+ }
results.Add(new Workbook(item, project, owner));
}
diff --git a/src/Tableau.Migration/Config/ConfigReader.cs b/src/Tableau.Migration/Config/ConfigReader.cs
index 1d99a19..9e75602 100644
--- a/src/Tableau.Migration/Config/ConfigReader.cs
+++ b/src/Tableau.Migration/Config/ConfigReader.cs
@@ -36,8 +36,6 @@ public class ConfigReader : IConfigReader
public ConfigReader(IOptionsMonitor optionsMonitor)
{
_optionsMonitor = optionsMonitor;
- ValidateOptions(Get());
- _optionsMonitor.OnChange(ValidateOptions);
}
///
@@ -71,21 +69,7 @@ public ContentTypesOptions Get()
};
}
- throw new NotSupportedException(
- $"Content type specific options are not supported for {typeof(TContent)} since it is not supported for migration.");
- }
-
- internal void ValidateOptions(MigrationSdkOptions? options)
- {
- options ??= Get();
-
- foreach (var byContentTypeName in options.ContentTypes.GroupBy(v => v.Type))
- {
- if (byContentTypeName.First().IsContentTypeValid() && byContentTypeName.Count() > 1)
- {
- throw new InvalidOperationException($"Duplicate content type names found in {(nameof(MigrationSdkOptions.ContentTypes))} section of the configuration.");
- }
- }
+ throw new NotSupportedException($"Content type specific options are not supported for {typeof(TContent)} since it is not supported for migration.");
}
}
}
diff --git a/src/Tableau.Migration/Config/NetworkOptions.cs b/src/Tableau.Migration/Config/NetworkOptions.cs
index e67d125..b9dd16d 100644
--- a/src/Tableau.Migration/Config/NetworkOptions.cs
+++ b/src/Tableau.Migration/Config/NetworkOptions.cs
@@ -51,6 +51,11 @@ public static class Defaults
/// The default Network Exceptions Logging Flag - Disabled as Default.
///
public const bool LOG_EXCEPTIONS_ENABLED = false;
+
+ ///
+ /// The default omitted user agent comment.
+ ///
+ public const string? USER_AGENT_COMMENT = null;
}
///
@@ -103,6 +108,16 @@ public bool ExceptionsLoggingEnabled
}
private bool? _exceptionsLoggingEnabled;
+ ///
+ /// Gets or sets the comment to include in the HTTP user agent header, or null to omit the user agent comment.
+ ///
+ public string? UserAgentComment
+ {
+ get => _userAgentComment ?? Defaults.USER_AGENT_COMMENT;
+ set => _userAgentComment = value;
+ }
+ private string? _userAgentComment;
+
///
/// Resilience options related to Tableau connections.
/// This configuration adds a transient-fault-handling layer for all communication to Tableau.
diff --git a/src/Tableau.Migration/Config/UniqueContentTypesValidator.cs b/src/Tableau.Migration/Config/UniqueContentTypesValidator.cs
new file mode 100644
index 0000000..5868d1d
--- /dev/null
+++ b/src/Tableau.Migration/Config/UniqueContentTypesValidator.cs
@@ -0,0 +1,51 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.Extensions.Options;
+using Tableau.Migration.Resources;
+
+namespace Tableau.Migration.Config
+{
+ internal sealed class UniqueContentTypesValidator : IValidateOptions
+ {
+ private readonly ISharedResourcesLocalizer _localizer;
+
+ public UniqueContentTypesValidator(ISharedResourcesLocalizer localizer)
+ {
+ _localizer = localizer;
+ }
+
+ public ValidateOptionsResult Validate(string? name, MigrationSdkOptions options)
+ {
+ var duplicateContentErrors = options.ContentTypes
+ .GroupBy(v => v.Type, StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.First().IsContentTypeValid() && g.Count() > 1)
+ .Select(g => string.Format(_localizer[SharedResourceKeys.DuplicateContentTypeConfigurationMessage], g.Key))
+ .ToImmutableArray();
+
+ if (duplicateContentErrors.Any())
+ {
+ return ValidateOptionsResult.Fail(duplicateContentErrors);
+ }
+
+ return ValidateOptionsResult.Success;
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Constants.cs b/src/Tableau.Migration/Constants.cs
index 78eb2bb..8e83427 100644
--- a/src/Tableau.Migration/Constants.cs
+++ b/src/Tableau.Migration/Constants.cs
@@ -96,14 +96,24 @@ public static class Constants
#region - Internal Constants -
///
- /// The default prefix for the user agent string
+ /// The comment for the python user agent.
///
- internal const string USER_AGENT_PREFIX = "TableauMigrationSDK";
+ internal const string PYTHON_USER_AGENT_COMMENT = "Python";
+
+ ///
+ /// The comment for the python user agent.
+ ///
+ internal const string PYTHON_ENVIRONMENT_VARIABLE_PREFIX = "MigrationSDK__";
///
- /// The default suffix for the python user agent string
+ /// The comment for the python user agent.
///
- internal const string USER_AGENT_PYTHON_SUFFIX = "-Python";
+ internal const string PYTHON_USER_AGENT_COMMENT_CONFIG_KEY = PYTHON_ENVIRONMENT_VARIABLE_PREFIX + "Network__UserAgentComment";
+
+ ///
+ /// The default prefix for the user agent string.
+ ///
+ internal const string USER_AGENT_PREFIX = "TableauMigrationSDK";
#endregion
}
diff --git a/src/Tableau.Migration/Content/CustomView.cs b/src/Tableau.Migration/Content/CustomView.cs
new file mode 100644
index 0000000..503acdf
--- /dev/null
+++ b/src/Tableau.Migration/Content/CustomView.cs
@@ -0,0 +1,110 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using Tableau.Migration.Api.Rest.Models;
+
+namespace Tableau.Migration.Content
+{
+ internal class CustomView : ContentBase, ICustomView
+ {
+ ///
+ public string CreatedAt { get; }
+
+ ///
+ public string UpdatedAt { get; }
+
+ ///
+ public string? LastAccessedAt { get; }
+
+ ///
+ public bool Shared { get; set; }
+
+ ///
+ public Guid BaseViewId { get; }
+
+ ///
+ public string BaseViewName { get; }
+
+ ///
+ public IContentReference Workbook { get; set; }
+
+ ///
+ public IContentReference Owner { get; set; }
+
+ internal CustomView(
+ Guid id,
+ string? name,
+ string? createdAt,
+ string? updatedAt,
+ string? lastAccessedAt,
+ bool shared,
+ Guid? viewId,
+ string? viewName,
+ IContentReference workbook,
+ IContentReference owner)
+ : base(new ContentReferenceStub(
+ Guard.AgainstDefaultValue(id, () => id),
+ string.Empty,
+ new(Guard.AgainstNullEmptyOrWhiteSpace(name, () => name))))
+ {
+ CreatedAt = createdAt ?? string.Empty;
+ UpdatedAt = updatedAt ?? string.Empty;
+ LastAccessedAt = lastAccessedAt;
+ Shared = shared;
+
+ Guard.AgainstNull(viewId, () => viewId);
+ BaseViewId = Guard.AgainstDefaultValue(viewId.Value, () => viewId);
+
+ BaseViewName = Guard.AgainstNullEmptyOrWhiteSpace(viewName, () => viewName);
+ Workbook = workbook;
+ Owner = owner;
+ }
+
+ public CustomView(
+ ICustomViewType response,
+ IContentReference workbook,
+ IContentReference owner)
+ : this(
+ response.Id,
+ response.Name,
+ response.CreatedAt,
+ response.UpdatedAt,
+ response.LastAccessedAt,
+ response.Shared,
+ response.ViewId,
+ response.ViewName,
+ workbook,
+ owner)
+ { }
+
+ public CustomView(ICustomView customView)
+ : this(
+ customView.Id,
+ customView.Name,
+ customView.CreatedAt,
+ customView.UpdatedAt,
+ customView.LastAccessedAt,
+ customView.Shared,
+ customView.BaseViewId,
+ customView.BaseViewName,
+ customView.Workbook,
+ customView.Owner)
+ { }
+
+ }
+}
diff --git a/src/Tableau.Migration/Content/Files/ContentTypeFilePathResolver.cs b/src/Tableau.Migration/Content/Files/ContentTypeFilePathResolver.cs
index efa14c3..0b51332 100644
--- a/src/Tableau.Migration/Content/Files/ContentTypeFilePathResolver.cs
+++ b/src/Tableau.Migration/Content/Files/ContentTypeFilePathResolver.cs
@@ -42,6 +42,10 @@ public string ResolveRelativePath(TContent contentItem, string origina
{
return Path.Combine("workbooks", $"workbook-{wb.Id:N}{extension}");
}
+ else if (contentItem is ICustomView cv)
+ {
+ return Path.Combine("customviews", $"customview-{cv.Id:N}{extension}");
+ }
throw new ArgumentException($"Cannot generate a file store path for content type {typeof(TContent).Name}");
}
diff --git a/src/Tableau.Migration/Content/ICustomView.cs b/src/Tableau.Migration/Content/ICustomView.cs
new file mode 100644
index 0000000..b31412a
--- /dev/null
+++ b/src/Tableau.Migration/Content/ICustomView.cs
@@ -0,0 +1,60 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+
+namespace Tableau.Migration.Content
+{
+ ///
+ /// The interface for a custom view content item.
+ ///
+ public interface ICustomView :
+ IContentReference,
+ IWithOwner,
+ IWithWorkbook
+ {
+ ///
+ /// Gets the created timestamp.
+ ///
+ string CreatedAt { get; }
+
+ ///
+ /// Gets the updated timestamp.
+ ///
+ string? UpdatedAt { get; }
+
+ ///
+ /// Gets the last accessed timestamp.
+ ///
+ string? LastAccessedAt { get; }
+
+ ///
+ /// Gets or sets whether the custom view is shared with all users (true) or private (false).
+ ///
+ bool Shared { get; set; }
+
+ ///
+ /// Gets the ID of the view that this custom view is based on.
+ ///
+ Guid BaseViewId { get; }
+
+ ///
+ /// Gets the name of the view that this custom view is based on.
+ ///
+ string BaseViewName { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Content/IPublishableCustomView.cs b/src/Tableau.Migration/Content/IPublishableCustomView.cs
new file mode 100644
index 0000000..9940c37
--- /dev/null
+++ b/src/Tableau.Migration/Content/IPublishableCustomView.cs
@@ -0,0 +1,32 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+
+namespace Tableau.Migration.Content
+{
+ ///
+ /// Interface for the publishable version of .
+ ///
+ public interface IPublishableCustomView : ICustomView, IFileContent
+ {
+ ///
+ /// The list of users for whom the Custom View is the default.
+ ///
+ IList DefaultUsers { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Interop/PythonUserAgentSuffixProvider.cs b/src/Tableau.Migration/Content/IWithWorkbook.cs
similarity index 70%
rename from src/Tableau.Migration/Interop/PythonUserAgentSuffixProvider.cs
rename to src/Tableau.Migration/Content/IWithWorkbook.cs
index bc89092..98ae031 100644
--- a/src/Tableau.Migration/Interop/PythonUserAgentSuffixProvider.cs
+++ b/src/Tableau.Migration/Content/IWithWorkbook.cs
@@ -15,13 +15,16 @@
// limitations under the License.
//
-namespace Tableau.Migration.Interop
+namespace Tableau.Migration.Content
{
- internal class PythonUserAgentSuffixProvider : IUserAgentSuffixProvider
+ ///
+ /// Interface to be inherited by content items with workbook.
+ ///
+ public interface IWithWorkbook : IContentReference
{
///
- /// Python user agent suffix
+ /// Gets or sets the workbook for the content item.
///
- public string UserAgentSuffix { get; init; } = Constants.USER_AGENT_PYTHON_SUFFIX;
+ IContentReference Workbook { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Tableau.Migration/Content/PublishableCustomView.cs b/src/Tableau.Migration/Content/PublishableCustomView.cs
new file mode 100644
index 0000000..c23c6e6
--- /dev/null
+++ b/src/Tableau.Migration/Content/PublishableCustomView.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Tableau.Migration.Content.Files;
+
+namespace Tableau.Migration.Content
+{
+
+ internal class PublishableCustomView : CustomView, IPublishableCustomView
+ {
+ public PublishableCustomView(
+ ICustomView customView,
+ IList defaultUsers,
+ IContentFileHandle file)
+ : base(customView)
+ {
+ DefaultUsers = defaultUsers;
+ File = file;
+ }
+
+ public IList DefaultUsers { get; set; }
+ public IContentFileHandle File { get; set; }
+
+ #region - IAsyncDisposable Implementation -
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting
+ /// unmanaged resources asynchronously.
+ ///
+ /// A task that represents the asynchronous dispose operation.
+ public async ValueTask DisposeAsync()
+ {
+ // Perform async cleanup.
+ await File.DisposeAsync().ConfigureAwait(false);
+
+ // Suppress finalization.
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs b/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs
index b642f95..370c00b 100644
--- a/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs
+++ b/src/Tableau.Migration/Content/Schedules/ExtractRefreshContentType.cs
@@ -1,4 +1,21 @@
-namespace Tableau.Migration.Content.Schedules
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace Tableau.Migration.Content.Schedules
{
///
/// Enum of extract refresh content types.
diff --git a/src/Tableau.Migration/ContentLocation.cs b/src/Tableau.Migration/ContentLocation.cs
index 374ca23..8d2e21a 100644
--- a/src/Tableau.Migration/ContentLocation.cs
+++ b/src/Tableau.Migration/ContentLocation.cs
@@ -19,6 +19,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using Tableau.Migration.Content;
namespace Tableau.Migration
{
@@ -46,6 +47,8 @@ public readonly record struct ContentLocation(ImmutableArray PathSegment
///
public readonly bool IsEmpty { get; } = PathSegments.IsEmpty;
+ #region - Ctor -
+
///
/// Creates a new value.
///
@@ -80,6 +83,10 @@ public ContentLocation(string pathSeparator, IEnumerable segments)
: this(segments.ToImmutableArray(), pathSeparator)
{ }
+ #endregion
+
+ #region - Object Overrides -
+
///
/// Indicates whether this value and a specified value are equal.
///
@@ -101,6 +108,10 @@ public readonly override int GetHashCode()
/// The string representation.
public readonly override string ToString() => Path;
+ #endregion
+
+ #region - Static Factory Methods -
+
///
/// Creates a new value with the standard user/group name separator.
///
@@ -108,7 +119,7 @@ public readonly override int GetHashCode()
/// The user/group name.
/// The newly created .
public static ContentLocation ForUsername(string domain, string username)
- => new(ImmutableArray.Create(domain, username), Constants.DomainNameSeparator);
+ => new([domain, username], Constants.DomainNameSeparator);
///
/// Creates a new value from a string.
@@ -127,6 +138,57 @@ public static ContentLocation FromPath(
.ToImmutableArray(),
pathSeparator);
+ ///
+ /// Creates a new with the appropriate path separator for the content type.
+ ///
+ /// The content type to create the location for.
+ /// The location path segments.
+ ///
+ public static ContentLocation ForContentType(params string[] pathSegments)
+ => ForContentType(typeof(TContent), (IEnumerable)pathSegments);
+
+ ///
+ /// Creates a new with the appropriate path separator for the content type.
+ ///
+ /// The content type to create the location for.
+ /// The location path segments.
+ ///
+ public static ContentLocation ForContentType(IEnumerable pathSegments)
+ => ForContentType(typeof(TContent), pathSegments);
+
+ ///
+ /// Creates a new with the appropriate path separator for the content type.
+ ///
+ /// The content type to create the location for.
+ /// The location path segments.
+ ///
+ public static ContentLocation ForContentType(Type contentType, params string[] pathSegments)
+ => ForContentType(contentType, (IEnumerable)pathSegments);
+
+ ///
+ /// Creates a new with the appropriate path separator for the content type.
+ ///
+ /// The content type to create the location for.
+ /// The location path segments.
+ ///
+ public static ContentLocation ForContentType(Type contentType, IEnumerable pathSegments)
+ {
+ string pathSeparator;
+ switch(contentType)
+ {
+ case Type t when t == typeof(IUser) || t == typeof(IGroup):
+ pathSeparator = Constants.DomainNameSeparator;
+ break;
+ default:
+ pathSeparator = Constants.PathSeparator;
+ break;
+ }
+
+ return new(pathSeparator, pathSegments);
+ }
+
+ #endregion
+
///
/// Compares the current instance with another object of the same type and returns
/// an integer that indicates whether the current instance precedes, follows, or
diff --git a/src/Tableau.Migration/Engine/Actions/PreflightAction.cs b/src/Tableau.Migration/Engine/Actions/PreflightAction.cs
index e5880fe..af99b25 100644
--- a/src/Tableau.Migration/Engine/Actions/PreflightAction.cs
+++ b/src/Tableau.Migration/Engine/Actions/PreflightAction.cs
@@ -19,9 +19,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Tableau.Migration.Api.Rest.Models;
using Tableau.Migration.Config;
-using Tableau.Migration.Content;
using Tableau.Migration.Resources;
namespace Tableau.Migration.Engine.Actions
@@ -43,7 +41,7 @@ public class PreflightAction : IMigrationAction
/// The current migration.
/// A logger.
/// A localizer.
- public PreflightAction(IOptions options, IMigration migration,
+ public PreflightAction(IOptions options, IMigration migration,
ILogger logger, ISharedResourcesLocalizer localizer)
{
_options = options.Value;
@@ -52,31 +50,9 @@ public PreflightAction(IOptions options, IMigration migration,
_localizer = localizer;
}
- private void ValidateExtractEncryptionSetting(ISiteSettings source, ISiteSettings destination)
- {
- /* Any destination value is valid if source has encryption disabled.
- * There won't be extract migration errors and there may be destination extracts encrypted.
- */
- if (ExtractEncryptionModes.IsAMatch(source.ExtractEncryptionMode, ExtractEncryptionModes.Disabled))
- {
- return;
- }
-
- /* We don't care if the destination site is enforced/enabled as long as it supports encrypted extracts.
- * There won't be extract migration errors and the destination gets to keep its preference.
- */
- if (!ExtractEncryptionModes.IsAMatch(destination.ExtractEncryptionMode, ExtractEncryptionModes.Disabled))
- {
- return;
- }
-
- // Warn the user about the potential failure of encrypted extract migration.
- _logger.LogWarning(_localizer[SharedResourceKeys.SiteSettingsExtractEncryptionDisabledLogMessage]);
- }
-
private async ValueTask ManageSettingsAsync(CancellationToken cancel)
{
- if(!_options.ValidateSettings)
+ if (!_options.ValidateSettings)
{
_logger.LogDebug(_localizer[SharedResourceKeys.SiteSettingsSkippedDisabledLogMessage]);
return Result.Succeeded();
@@ -91,7 +67,7 @@ private async ValueTask ManageSettingsAsync(CancellationToken cancel)
var sourceSessionResult = sourceSessionTask.Result;
var destinationSessionResult = destinationSessionTask.Result;
- if(!sourceSessionResult.Success || !destinationSessionResult.Success)
+ if (!sourceSessionResult.Success || !destinationSessionResult.Success)
{
return new ResultBuilder().Add(sourceSessionResult, destinationSessionResult).Build();
}
@@ -100,16 +76,12 @@ private async ValueTask ManageSettingsAsync(CancellationToken cancel)
var sourceSession = sourceSessionResult.Value;
var destinationSession = destinationSessionResult.Value;
- if(!sourceSession.IsAdministrator || !destinationSession.IsAdministrator)
+ if (!sourceSession.IsAdministrator || !destinationSession.IsAdministrator)
{
_logger.LogDebug(_localizer[SharedResourceKeys.SiteSettingsSkippedNoAccessLogMessage]);
return Result.Succeeded();
}
- // Validate supported settings.
-
- ValidateExtractEncryptionSetting(sourceSession.Settings, destinationSession.Settings);
-
/* We currently don't update settings for the user because
* Tableau Cloud returns an error when site administrators update site settings,
* requiring server administrator access that Tableau Cloud users cannot have.
diff --git a/src/Tableau.Migration/Engine/Endpoints/IDestinationEndpoint.cs b/src/Tableau.Migration/Engine/Endpoints/IDestinationEndpoint.cs
index 4ae9718..846322a 100644
--- a/src/Tableau.Migration/Engine/Endpoints/IDestinationEndpoint.cs
+++ b/src/Tableau.Migration/Engine/Endpoints/IDestinationEndpoint.cs
@@ -147,5 +147,17 @@ Task> UpdateConnectionAsync(
/// The cancellation token to obey.
/// The result of the site update operation.
Task> UpdateSiteSettingsAsync(ISiteSettingsUpdate newSiteSettings, CancellationToken cancel);
+
+ ///
+ /// Update the custom view's default users.
+ ///
+ /// The ID of the custom view.
+ /// The list of users who have the custom view as their default.
+ /// The cancellation token to obey.
+ /// The result of the default user list update.
+ Task>> SetCustomViewDefaultUsersAsync(
+ Guid id,
+ IEnumerable users,
+ CancellationToken cancel);
}
}
diff --git a/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs b/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs
index 21ae349..0c33355 100644
--- a/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs
+++ b/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs
@@ -17,6 +17,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
@@ -111,5 +112,16 @@ public async Task> UpdateSiteSettingsAsync(ISiteSettingsUpdate ne
{
return await SiteApi.UpdateSiteAsync(newSiteSettings, cancel).ConfigureAwait(false);
}
+
+ ///
+ public async Task>> SetCustomViewDefaultUsersAsync(
+ Guid id,
+ IEnumerable users,
+ CancellationToken cancel)
+ {
+ return await SiteApi.CustomViews
+ .SetCustomViewDefaultUsersAsync(id, users, cancel)
+ .ConfigureAwait(false);
+ }
}
}
diff --git a/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBuilder.cs b/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBuilder.cs
index fa35cb0..2932c06 100644
--- a/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBuilder.cs
@@ -28,22 +28,6 @@ namespace Tableau.Migration.Engine.Hooks.Filters
///
public class ContentFilterBuilder : ContentTypeHookBuilderBase, IContentFilterBuilder
{
- ///
- /// Creates a new empty object.
- ///
- public ContentFilterBuilder()
- { }
-
- #region - Private Helper Methods -
-
- private IContentFilterBuilder Add(Type filterType, Func initializer)
- {
- AddFactoriesByType(filterType, initializer);
- return this;
- }
-
- #endregion
-
#region - IContentFilterBuilder Implementation -
///
@@ -54,30 +38,13 @@ public virtual IContentFilterBuilder Clear()
}
///
- public virtual IContentFilterBuilder Add(Type genericTransformerType, IEnumerable contentTypes)
- {
- if (!genericTransformerType.IsGenericTypeDefinition)
- throw new ArgumentException($"Type {genericTransformerType.FullName} is not a generic type definition.");
-
- foreach (var contentType in contentTypes)
- {
- var constructedType = genericTransformerType.MakeGenericType(contentType);
-
- object transformerFactory(IServiceProvider services)
- {
- return services.GetRequiredService(constructedType);
- }
-
- Add(constructedType, transformerFactory);
- }
-
- return this;
- }
+ new public virtual IContentFilterBuilder Add(Type genericFilterType, IEnumerable contentTypes)
+ => (IContentFilterBuilder)base.Add(genericFilterType, contentTypes);
///
public virtual IContentFilterBuilder Add(IContentFilter filter)
where TContent : IContentReference
- => Add(typeof(IContentFilter), s => filter);
+ => (IContentFilterBuilder)Add(typeof(IContentFilter), s => filter);
///
public virtual IContentFilterBuilder Add(Func? filterFactory = null)
@@ -85,21 +52,19 @@ public virtual IContentFilterBuilder Add(Func services.GetRequiredService();
- return Add(typeof(IContentFilter), s => filterFactory(s));
+ return (IContentFilterBuilder)Add(typeof(IContentFilter), s => filterFactory(s));
}
///
public virtual IContentFilterBuilder Add(Func>, CancellationToken, Task>?>> callback)
where TContent : IContentReference
- => Add(typeof(IContentFilter),
+ => (IContentFilterBuilder)Add(typeof(IContentFilter),
s => new CallbackHookWrapper, IEnumerable>>(callback));
///
public IContentFilterBuilder Add(Func>, IEnumerable>?> callback)
where TContent : IContentReference
- => Add(
- (ctx, cancel) => Task.FromResult(
- callback(ctx)));
+ => Add((ctx, cancel) => Task.FromResult(callback(ctx)));
#endregion
}
diff --git a/src/Tableau.Migration/Engine/Hooks/Filters/IContentFilterBuilder.cs b/src/Tableau.Migration/Engine/Hooks/Filters/IContentFilterBuilder.cs
index f79c82f..060b33b 100644
--- a/src/Tableau.Migration/Engine/Hooks/Filters/IContentFilterBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/Filters/IContentFilterBuilder.cs
@@ -36,10 +36,10 @@ public interface IContentFilterBuilder : IContentTypeHookBuilder
///
/// Adds a factory to resolve the filter type.
///
- /// The generic type definition for the filter to execute.
+ /// The generic type definition for the filter to execute.
/// The content types used to construct the filter types.
/// The same filter builder object for fluent API calls.
- IContentFilterBuilder Add(Type genericTransformerType, IEnumerable contentTypes);
+ IContentFilterBuilder Add(Type genericFilterType, IEnumerable contentTypes);
///
/// Adds an object to be resolved when you build a filter for the content type.
@@ -76,8 +76,7 @@ IContentFilterBuilder Add(FuncThe content type.
/// A synchronously callback to call for the filter.
/// The same filter builder object for fluent API calls.
- IContentFilterBuilder Add(
- Func>, IEnumerable>?> callback)
+ IContentFilterBuilder Add(Func>, IEnumerable>?> callback)
where TContent : IContentReference;
///
diff --git a/src/Tableau.Migration/Engine/Hooks/IMigrationHookBuilder.cs b/src/Tableau.Migration/Engine/Hooks/IMigrationHookBuilder.cs
index 317fb6f..b7567a6 100644
--- a/src/Tableau.Migration/Engine/Hooks/IMigrationHookBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/IMigrationHookBuilder.cs
@@ -76,8 +76,7 @@ IMigrationHookBuilder Add(FuncThe hook's context type.
/// A synchronous callback to call for the hook.
/// The same hook builder object for fluent API calls.
- IMigrationHookBuilder Add(
- Func callback)
+ IMigrationHookBuilder Add(Func callback)
where THook : IMigrationHook;
///
diff --git a/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBuilder.cs b/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBuilder.cs
index 1494759..4d18a02 100644
--- a/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBuilder.cs
@@ -16,6 +16,7 @@
//
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
@@ -27,22 +28,6 @@ namespace Tableau.Migration.Engine.Hooks.Mappings
///
public class ContentMappingBuilder : ContentTypeHookBuilderBase, IContentMappingBuilder
{
- ///
- /// Creates a new empty object.
- ///
- public ContentMappingBuilder()
- { }
-
- #region - Private Helper Methods -
-
- private IContentMappingBuilder Add(Type mappingType, Func initializer)
- {
- AddFactoriesByType(mappingType, initializer);
- return this;
- }
-
- #endregion
-
#region - IContentMappingBuilder Implementation -
///
@@ -52,10 +37,14 @@ public virtual IContentMappingBuilder Clear()
return this;
}
+ ///
+ new public virtual IContentMappingBuilder Add(Type genericMappingType, IEnumerable contentTypes)
+ => (IContentMappingBuilder)base.Add(genericMappingType, contentTypes);
+
///
public virtual IContentMappingBuilder Add(IContentMapping mapping)
where TContent : IContentReference
- => Add(typeof(IContentMapping), s => mapping);
+ => (IContentMappingBuilder)Add(typeof(IContentMapping), s => mapping);
///
public virtual IContentMappingBuilder Add(Func? mappingFactory = null)
@@ -63,21 +52,19 @@ public virtual IContentMappingBuilder Add(Func services.GetRequiredService();
- return Add(typeof(IContentMapping), s => mappingFactory(s));
+ return (IContentMappingBuilder)Add(typeof(IContentMapping), s => mappingFactory(s));
}
///
public virtual IContentMappingBuilder Add(Func, CancellationToken, Task?>> callback)
where TContent : IContentReference
- => Add(typeof(IContentMapping),
+ => (IContentMappingBuilder)Add(typeof(IContentMapping),
s => new CallbackHookWrapper, ContentMappingContext>(callback));
///
public IContentMappingBuilder Add(Func, ContentMappingContext?> callback)
where TContent : IContentReference
- => Add(
- (ctx, cancel) => Task.FromResult(
- callback(ctx)));
+ => Add((ctx, cancel) => Task.FromResult(callback(ctx)));
#endregion
}
diff --git a/src/Tableau.Migration/Engine/Hooks/Mappings/IContentMappingBuilder.cs b/src/Tableau.Migration/Engine/Hooks/Mappings/IContentMappingBuilder.cs
index 3ba7382..71875fb 100644
--- a/src/Tableau.Migration/Engine/Hooks/Mappings/IContentMappingBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/Mappings/IContentMappingBuilder.cs
@@ -16,6 +16,7 @@
//
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -32,14 +33,21 @@ public interface IContentMappingBuilder : IContentTypeHookBuilder
/// The same mapping builder object for fluent API calls.
IContentMappingBuilder Clear();
+ ///
+ /// Adds a factory to resolve the mapping type.
+ ///
+ /// The generic type definition for the mapping to execute.
+ /// The content types used to construct the mapping types.
+ /// The same mapping builder object for fluent API calls.
+ IContentMappingBuilder Add(Type genericMappingType, IEnumerable contentTypes);
+
///
/// Adds an object to be resolved when you build a mapping for the content type.
///
/// The content type.
/// The mapping to execute.
/// The same mapping builder object for fluent API calls.
- IContentMappingBuilder Add(
- IContentMapping mapping)
+ IContentMappingBuilder Add(IContentMapping mapping)
where TContent : IContentReference;
///
@@ -49,8 +57,7 @@ IContentMappingBuilder Add(
/// The content type.
/// An initializer function to create the object from, potentially from the migration-scoped dependency injection container.
/// The same mapping builder object for fluent API calls.
- IContentMappingBuilder Add(
- Func? mappingFactory = null)
+ IContentMappingBuilder Add(Func? mappingFactory = null)
where TMapping : IContentMapping
where TContent : IContentReference;
@@ -60,8 +67,7 @@ IContentMappingBuilder Add(
/// The content type.
/// A callback to call for the mapping.
/// The same mapping builder object for fluent API calls.
- IContentMappingBuilder Add(
- Func, CancellationToken, Task?>> callback)
+ IContentMappingBuilder Add(Func, CancellationToken, Task?>> callback)
where TContent : IContentReference;
///
@@ -70,8 +76,7 @@ IContentMappingBuilder Add(
/// The content type.
/// A synchronously callback to call for the mapping.
/// The same mapping builder object for fluent API calls.
- IContentMappingBuilder Add(
- Func, ContentMappingContext?> callback)
+ IContentMappingBuilder Add(Func, ContentMappingContext?> callback)
where TContent : IContentReference;
///
diff --git a/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilder.cs b/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilder.cs
index 781517d..54ba6f5 100644
--- a/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilder.cs
@@ -28,22 +28,6 @@ namespace Tableau.Migration.Engine.Hooks
///
public class MigrationHookBuilder : MigrationHookBuilderBase, IMigrationHookBuilder
{
- ///
- /// Creates a new empty object.
- ///
- public MigrationHookBuilder()
- { }
-
- #region - Private Helper Methods -
-
- private IMigrationHookBuilder Add(Type hookType, Func initializer)
- {
- AddFactoriesByType(hookType, initializer);
- return this;
- }
-
- #endregion
-
#region - IMigrationHookBuilder Implementation -
///
@@ -54,55 +38,31 @@ public IMigrationHookBuilder Clear()
}
///
- public virtual IMigrationHookBuilder Add(Type genericHookType, IEnumerable contentTypes)
- {
- if (!genericHookType.IsGenericTypeDefinition)
- throw new ArgumentException($"Type {genericHookType.FullName} is not a generic type definition.");
-
- foreach (var contentType in contentTypes)
- {
- var constructedType = genericHookType.MakeGenericType(contentType);
-
- object hookFactory(IServiceProvider services)
- {
- return services.GetRequiredService(constructedType);
- }
-
- Add(constructedType, hookFactory);
- }
-
- return this;
- }
+ new public virtual IMigrationHookBuilder Add(Type genericHookType, IEnumerable contentTypes)
+ => (IMigrationHookBuilder)base.Add(genericHookType, contentTypes);
///
public virtual IMigrationHookBuilder Add(THook hook)
where THook : notnull
- {
- return Add(typeof(THook), s => hook);
- }
+ => (IMigrationHookBuilder)Add(typeof(THook), s => hook);
///
public virtual IMigrationHookBuilder Add(Func? hookFactory = null)
where THook : notnull
{
- hookFactory ??= services =>
- {
- return services.GetRequiredService();
- };
- return Add(typeof(THook), s => hookFactory(s));
+ hookFactory ??= services => services.GetRequiredService();
+ return (IMigrationHookBuilder)Add(typeof(THook), s => hookFactory(s));
}
///
public virtual IMigrationHookBuilder Add(Func> callback)
where THook : IMigrationHook
- => Add(typeof(THook), s => new CallbackHookWrapper(callback));
+ => (IMigrationHookBuilder)Add(typeof(THook), s => new CallbackHookWrapper(callback));
///
public IMigrationHookBuilder Add(Func callback)
where THook : IMigrationHook
- => Add(
- (ctx, cancel) => Task.FromResult(
- callback(ctx)));
+ => Add((ctx, cancel) => Task.FromResult(callback(ctx)));
#endregion
}
diff --git a/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilderBase.cs b/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilderBase.cs
index 6f1157a..f6bde0d 100644
--- a/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilderBase.cs
+++ b/src/Tableau.Migration/Engine/Hooks/MigrationHookBuilderBase.cs
@@ -19,6 +19,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
namespace Tableau.Migration.Engine.Hooks
{
@@ -112,6 +113,45 @@ void AddForHookInterface(Type hookInterfaceType)
}
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected MigrationHookBuilderBase Add(Type filterType, Func initializer)
+ {
+ AddFactoriesByType(filterType, initializer);
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected MigrationHookBuilderBase Add(Type genericHookType, IEnumerable contentTypes)
+ {
+ if (!genericHookType.IsGenericTypeDefinition)
+ throw new ArgumentException($"Type {genericHookType.FullName} is not a generic type definition.");
+
+ foreach (var contentType in contentTypes)
+ {
+ var constructedType = genericHookType.MakeGenericType(contentType);
+
+ object hookFactory(IServiceProvider services)
+ {
+ return services.GetRequiredService(constructedType);
+ }
+
+ Add(constructedType, hookFactory);
+ }
+
+ return this;
+ }
+
#endregion
///
diff --git a/src/Tableau.Migration/Engine/Hooks/PostPublish/Default/CustomViewDefaultUsersPostPublishHook.cs b/src/Tableau.Migration/Engine/Hooks/PostPublish/Default/CustomViewDefaultUsersPostPublishHook.cs
new file mode 100644
index 0000000..701d783
--- /dev/null
+++ b/src/Tableau.Migration/Engine/Hooks/PostPublish/Default/CustomViewDefaultUsersPostPublishHook.cs
@@ -0,0 +1,61 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Threading;
+using System.Threading.Tasks;
+using Tableau.Migration.Content;
+
+namespace Tableau.Migration.Engine.Hooks.PostPublish.Default
+{
+ ///
+ /// Hook that updates a custom view's default users after publish.
+ ///
+ public class CustomViewDefaultUsersPostPublishHook
+ : ContentItemPostPublishHookBase
+ {
+ private readonly IMigration _migration;
+
+ ///
+ /// Creates a new object.
+ ///
+ /// The current migration.
+ public CustomViewDefaultUsersPostPublishHook(IMigration migration)
+ {
+ _migration = migration;
+ }
+
+ ///
+ public override async Task?> ExecuteAsync(
+ ContentItemPostPublishContext ctx,
+ CancellationToken cancel)
+ {
+ var setResult = await _migration.Destination
+ .SetCustomViewDefaultUsersAsync(
+ ctx.DestinationItem.Id,
+ ctx.PublishedItem.DefaultUsers,
+ cancel)
+ .ConfigureAwait(false);
+
+ if (!setResult.Success)
+ {
+ ctx.ManifestEntry.SetFailed(setResult.Errors);
+ }
+
+ return ctx;
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/ContentTransformerBuilder.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/ContentTransformerBuilder.cs
index dc008af..3fb1f5d 100644
--- a/src/Tableau.Migration/Engine/Hooks/Transformers/ContentTransformerBuilder.cs
+++ b/src/Tableau.Migration/Engine/Hooks/Transformers/ContentTransformerBuilder.cs
@@ -28,22 +28,6 @@ namespace Tableau.Migration.Engine.Hooks.Transformers
///
public class ContentTransformerBuilder : ContentTypeHookBuilderBase, IContentTransformerBuilder
{
- ///
- /// Creates a new empty object.
- ///
- public ContentTransformerBuilder()
- { }
-
- #region - Private Helper Methods -
-
- private IContentTransformerBuilder Add(Type transformerType, Func initializer)
- {
- AddFactoriesByType(transformerType, initializer);
- return this;
- }
-
- #endregion
-
#region - IContentTransformerBuilder Implementation -
///
@@ -54,48 +38,29 @@ public virtual IContentTransformerBuilder Clear()
}
///
- public virtual IContentTransformerBuilder Add(Type genericTransformerType, IEnumerable contentTypes)
- {
- if (!genericTransformerType.IsGenericTypeDefinition)
- throw new ArgumentException($"Type {genericTransformerType.FullName} is not a generic type definition.");
-
- foreach (var contentType in contentTypes)
- {
- var constructedType = genericTransformerType.MakeGenericType(contentType);
-
- object transformerFactory(IServiceProvider services)
- {
- return services.GetRequiredService(constructedType);
- }
-
- Add(constructedType, transformerFactory);
- }
-
- return this;
- }
+ new public virtual IContentTransformerBuilder Add(Type genericTransformerType, IEnumerable contentTypes)
+ => (IContentTransformerBuilder)base.Add(genericTransformerType, contentTypes);
///
public virtual IContentTransformerBuilder Add(IContentTransformer transformer)
- => Add(typeof(IContentTransformer), s => transformer);
+ => (IContentTransformerBuilder)Add(typeof(IContentTransformer), s => transformer);
///
public virtual IContentTransformerBuilder Add(Func? contentTransformerFactory = null)
where TTransformer : IContentTransformer
{
contentTransformerFactory ??= services => services.GetRequiredService();
- return Add(typeof(IContentTransformer), s => contentTransformerFactory(s));
+ return (IContentTransformerBuilder)Add(typeof(IContentTransformer), s => contentTransformerFactory(s));
}
///
public virtual IContentTransformerBuilder Add(Func> callback)
- => Add(typeof(IContentTransformer),
+ => (IContentTransformerBuilder)Add(typeof(IContentTransformer),
s => new CallbackHookWrapper, TPublish>(callback));
///
public IContentTransformerBuilder Add(Func callback)
- => Add(
- (ctx, cancel) => Task.FromResult(
- callback(ctx)));
+ => Add((ctx, cancel) => Task.FromResult(callback(ctx)));
#endregion
}
diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CustomViewDefaultUserReferencesTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CustomViewDefaultUserReferencesTransformer.cs
new file mode 100644
index 0000000..9853f0a
--- /dev/null
+++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/CustomViewDefaultUserReferencesTransformer.cs
@@ -0,0 +1,90 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration.Content;
+using Tableau.Migration.Resources;
+
+namespace Tableau.Migration.Engine.Hooks.Transformers.Default
+{
+ ///
+ /// Transformer that transforms the list of users that have the custom view as default.
+ /// It sets the references of these users to those at the destination.
+ ///
+ public class CustomViewDefaultUserReferencesTransformer
+ : ContentTransformerBase
+ {
+ private readonly IMappedUserTransformer _userTransformer;
+
+ ///
+ /// Creates a new object.
+ ///
+ /// The user transformer.
+ /// The logger used to log messages.
+ /// The string localizer.
+ public CustomViewDefaultUserReferencesTransformer(
+ IMappedUserTransformer userTransformer,
+ ILogger logger,
+ ISharedResourcesLocalizer localizer)
+ : base(localizer, logger)
+ {
+ _userTransformer = userTransformer;
+ }
+
+ ///
+ public override async Task TransformAsync(
+ IPublishableCustomView sourceCustomView,
+ CancellationToken cancel)
+ {
+ var missingUsers = new List();
+
+ for (var i = 0; i < sourceCustomView.DefaultUsers.Count; i++)
+ {
+ var destinationUser = await _userTransformer.ExecuteAsync(sourceCustomView.DefaultUsers[i], cancel)
+ .ConfigureAwait(false);
+
+ if (destinationUser is null)
+ {
+ missingUsers.Add(sourceCustomView.DefaultUsers[i].Name);
+ continue;
+ }
+ sourceCustomView.DefaultUsers[i] = destinationUser;
+ }
+
+ LogMissingUsers(sourceCustomView.Name, missingUsers);
+
+ return sourceCustomView;
+ }
+
+ private void LogMissingUsers(string customViewName, List missingUsers)
+ {
+ if (!missingUsers.Any())
+ {
+ return;
+ }
+
+ Logger.LogDebug(
+ Localizer[SharedResourceKeys.CustomViewDefaultUsersTransformerNoUserRefsDebugMessage],
+ customViewName,
+ string.Join(',', missingUsers));
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/EncryptExtractTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/EncryptExtractTransformer.cs
new file mode 100644
index 0000000..0db37ff
--- /dev/null
+++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/EncryptExtractTransformer.cs
@@ -0,0 +1,96 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration.Api.Rest.Models;
+using Tableau.Migration.Content;
+using Tableau.Migration.Resources;
+
+namespace Tableau.Migration.Engine.Hooks.Transformers.Default
+{
+ ///
+ /// Transformer that encrypts extracts based on the site's encryption mode.
+ ///
+ ///
+ public class EncryptExtractTransformer : ContentTransformerBase where TIExtractContent : IContentReference, IExtractContent
+ {
+ private readonly ILogger _logger;
+ private readonly ISharedResourcesLocalizer _localizer;
+ private readonly IMigration _migration;
+ private string? _siteExtractEncryptionMode = null;
+
+ private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
+
+ ///
+ /// Creates a new EncryptExtractTransformer
+ ///
+ ///
+ ///
+ ///
+ public EncryptExtractTransformer(
+ ISharedResourcesLocalizer localizer,
+ ILogger> logger,
+ IMigration migration)
+ : base(localizer, logger)
+ {
+ _migration = migration;
+ _logger = logger;
+ _localizer = localizer;
+ }
+
+ ///
+ public override async Task TransformAsync(TIExtractContent itemToTransform, CancellationToken cancel)
+ {
+ // This transformer is registered with DI as scoped.
+ // This means a new transformer instance is created per batch per content type.
+ // To limit the number of _migration.Destination.GetSessionAsync calls, we cache the site's encryption mode
+ // after the first call and make it thread safe by wrapping it in a semaphore of count 1 which is async-compatible.
+ await _semaphore.WaitAsync(cancel).ConfigureAwait(false);
+ try
+ {
+ if (_siteExtractEncryptionMode is null)
+ {
+ var session = await _migration.Destination.GetSessionAsync(cancel).ConfigureAwait(false);
+
+ if (!session.Success || session.Value?.Settings?.ExtractEncryptionMode is null)
+ {
+ throw new System.Exception("Unable to determine site data source encryption mode.");
+ }
+
+ _siteExtractEncryptionMode = session.Value.Settings.ExtractEncryptionMode;
+ }
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+
+ if (ExtractEncryptionModes.IsAMatch(_siteExtractEncryptionMode, ExtractEncryptionModes.Enforced))
+ {
+ itemToTransform.EncryptExtracts = true;
+ }
+ else if (ExtractEncryptionModes.IsAMatch(_siteExtractEncryptionMode, ExtractEncryptionModes.Disabled))
+ {
+ itemToTransform.EncryptExtracts = false;
+ }
+
+ return itemToTransform;
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs
index 4870eea..810c34e 100644
--- a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs
+++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs
@@ -15,6 +15,8 @@
// limitations under the License.
//
+using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -30,7 +32,7 @@ namespace Tableau.Migration.Engine.Hooks.Transformers.Default
public class GroupUsersTransformer : ContentTransformerBase
{
private readonly IDestinationContentReferenceFinder _userFinder;
-
+
///
/// Creates a new object.
///
@@ -50,25 +52,38 @@ public GroupUsersTransformer(
IPublishableGroup sourceGroup,
CancellationToken cancel)
{
+ var missingUsers = new List();
+
foreach (var user in sourceGroup.Users)
{
var destinationUser = await _userFinder
.FindBySourceLocationAsync(user.User.Location, cancel)
.ConfigureAwait(false);
- if (destinationUser is not null)
- {
- user.User = destinationUser;
- }
- else
+ if (destinationUser is null)
{
- Logger.LogWarning(
- Localizer[SharedResourceKeys.GroupUsersTransformerCannotAddUserWarning],
- sourceGroup.Name,
- user.User.Location);
+ missingUsers.Add(user.User.Name);
+ continue;
}
+
+ user.User = destinationUser;
}
+
+ LogMissingUsers(sourceGroup.Name, missingUsers);
return sourceGroup;
}
+
+ private void LogMissingUsers(string groupName, List missingUsers)
+ {
+ if (!missingUsers.Any())
+ {
+ return;
+ }
+
+ Logger.LogDebug(
+ Localizer[SharedResourceKeys.GroupUsersTransformerCannotAddUserWarning],
+ groupName,
+ string.Join(',', missingUsers));
+ }
}
}
diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/WorkbookReferenceTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/WorkbookReferenceTransformer.cs
new file mode 100644
index 0000000..9a54c25
--- /dev/null
+++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/WorkbookReferenceTransformer.cs
@@ -0,0 +1,70 @@
+//
+// Copyright (c) 2024, Salesforce, Inc.
+// SPDX-License-Identifier: Apache-2
+//
+// Licensed under the Apache License, Version 2.0 (the "License")
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Tableau.Migration.Content;
+using Tableau.Migration.Engine.Endpoints.Search;
+using Tableau.Migration.Resources;
+
+namespace Tableau.Migration.Engine.Hooks.Transformers.Default
+{
+ ///
+ /// Transformer that changes the workbook reference for a content item.
+ /// It sets the workbook reference to that of the destination.
+ ///
+ public class WorkbookReferenceTransformer
+ : ContentTransformerBase
+ where TContent : IWithWorkbook
+ {
+ private readonly IDestinationContentReferenceFinder _workbookFinder;
+
+ ///
+ /// Creates a new object.
+ ///
+ /// The destination finder factory.
+ /// The logger used to log messages.
+ /// The string localizer.
+ public WorkbookReferenceTransformer(
+ IDestinationContentReferenceFinderFactory destinationFinderFactory,
+ ILogger> logger,
+ ISharedResourcesLocalizer localizer)
+ : base(localizer, logger)
+ {
+ _workbookFinder = destinationFinderFactory.ForDestinationContentType();
+ }
+
+ ///
+ public override async Task TransformAsync(TContent ctx, CancellationToken cancel)
+ {
+ var destinationWorkbook = await _workbookFinder.FindBySourceLocationAsync(
+ ctx.Workbook.Location,
+ cancel)
+ .ConfigureAwait(false);
+
+ if (destinationWorkbook is not null)
+ {
+ ctx.Workbook = destinationWorkbook;
+ return ctx;
+ }
+
+ Logger.LogDebug(Localizer[SharedResourceKeys.SourceWorkbookNotFoundLogMessage], ctx.Name, ctx.Id);
+ return ctx;
+ }
+ }
+}
diff --git a/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs b/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs
index 80a0e43..661df53 100644
--- a/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs
+++ b/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs
@@ -76,6 +76,7 @@ internal static IServiceCollection AddMigrationEngine(this IServiceCollection se
services.AddScoped();
//Plan building.
+ services.AddSingleton();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -138,15 +139,19 @@ internal static IServiceCollection AddMigrationEngine(this IServiceCollection se
services.AddScoped(typeof(CloudScheduleCompatibilityTransformer<>));
services.AddScoped();
services.AddScoped();
+ services.AddScoped(typeof(EncryptExtractTransformer<>));
services.AddScoped();
services.AddScoped();
-
+ services.AddScoped(typeof(WorkbookReferenceTransformer<>));
+ services.AddScoped();
+
services.AddScoped(typeof(OwnerItemPostPublishHook<,>));
services.AddScoped(typeof(PermissionsItemPostPublishHook<,>));
services.AddScoped(typeof(TagItemPostPublishHook<,>));
services.AddScoped();
services.AddScoped(typeof(ChildItemsPermissionsPostPublishHook<,>));
+ services.AddScoped();
//Migration engine file store.
services.AddScoped();
diff --git a/src/Tableau.Migration/Engine/Migration.cs b/src/Tableau.Migration/Engine/Migration.cs
index 474ade4..fc3b0f5 100644
--- a/src/Tableau.Migration/Engine/Migration.cs
+++ b/src/Tableau.Migration/Engine/Migration.cs
@@ -16,6 +16,7 @@
//
using System;
+using Microsoft.Extensions.DependencyInjection;
using Tableau.Migration.Engine.Endpoints;
using Tableau.Migration.Engine.Manifest;
using Tableau.Migration.Engine.Pipelines;
@@ -30,21 +31,21 @@ public class Migration : IMigration
///
/// Creates a new object.
///
- /// The migration input to initialize plan and previous manifest from.
- /// An object to create pipelines with.
- /// An object to create endpoints with.
- /// An object to create manifests with.
- public Migration(IMigrationInput input, IMigrationPipelineFactory pipelineFactory,
- IMigrationEndpointFactory endpointFactory, IMigrationManifestFactory manifestFactory)
+ /// The service provider to use to initialize the migration.
+ public Migration(IServiceProvider services)
{
+ var input = services.GetRequiredService();
Id = input.MigrationId;
Plan = input.Plan;
+ var pipelineFactory = Plan.PipelineFactoryOverride?.Invoke(services) ?? services.GetRequiredService();
Pipeline = pipelineFactory.Create(Plan);
+ var endpointFactory = services.GetRequiredService();
Source = endpointFactory.CreateSource(Plan);
Destination = endpointFactory.CreateDestination(Plan);
+ var manifestFactory = services.GetRequiredService();
Manifest = manifestFactory.Create(input, Id);
}
diff --git a/src/Tableau.Migration/Engine/MigrationPlan.cs b/src/Tableau.Migration/Engine/MigrationPlan.cs
index a3be2cd..9bf3a20 100644
--- a/src/Tableau.Migration/Engine/MigrationPlan.cs
+++ b/src/Tableau.Migration/Engine/MigrationPlan.cs
@@ -20,6 +20,7 @@
using Tableau.Migration.Engine.Endpoints;
using Tableau.Migration.Engine.Hooks;
using Tableau.Migration.Engine.Options;
+using Tableau.Migration.Engine.Pipelines;
namespace Tableau.Migration.Engine
{
@@ -35,6 +36,7 @@ namespace Tableau.Migration.Engine
///
///
///
+ ///
public record MigrationPlan(
Guid PlanId,
[property: EnumDataType(typeof(PipelineProfile))] PipelineProfile PipelineProfile,
@@ -44,7 +46,8 @@ public record MigrationPlan(
IMigrationHookFactoryCollection Hooks,
IMigrationHookFactoryCollection Mappings,
IMigrationHookFactoryCollection Filters,
- IMigrationHookFactoryCollection Transformers
+ IMigrationHookFactoryCollection Transformers,
+ Func? PipelineFactoryOverride
) : IMigrationPlan
{ }
}
diff --git a/src/Tableau.Migration/Engine/MigrationPlanBuilder.cs b/src/Tableau.Migration/Engine/MigrationPlanBuilder.cs
index db04e96..b6dc936 100644
--- a/src/Tableau.Migration/Engine/MigrationPlanBuilder.cs
+++ b/src/Tableau.Migration/Engine/MigrationPlanBuilder.cs
@@ -20,6 +20,7 @@
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
using Tableau.Migration.Api.Simulation;
using Tableau.Migration.Content;
using Tableau.Migration.Content.Schedules;
@@ -46,6 +47,7 @@ public class MigrationPlanBuilder : IMigrationPlanBuilder
private readonly ISharedResourcesLocalizer _localizer;
private readonly ITableauApiSimulatorFactory _simulatorFactory;
+ private Func? _pipelineFactoryOverride;
private ServerToCloudMigrationPlanBuilder? _serverToCloudBuilder;
private PipelineProfile _pipelineProfile;
@@ -98,8 +100,9 @@ public IMigrationPlanBuilder ClearExtensions()
///
public IServerToCloudMigrationPlanBuilder ForServerToCloud()
{
- SetPipelineProfile(PipelineProfile.ServerToCloud);
+ SetPipelineProfile(PipelineProfile.ServerToCloud, ServerToCloudMigrationPipeline.ContentTypes);
+ _pipelineFactoryOverride = null;
_serverToCloudBuilder ??= new(_localizer, this);
ClearExtensions();
@@ -108,6 +111,44 @@ public IServerToCloudMigrationPlanBuilder ForServerToCloud()
.AppendDefaultServerToCloudExtensions();
}
+ ///
+ public IMigrationPlanBuilder ForCustomPipelineFactory(Func pipelineFactoryOverride, params MigrationPipelineContentType[] supportedContentTypes)
+ => ForCustomPipelineFactory(pipelineFactoryOverride, (IEnumerable)supportedContentTypes);
+
+ ///
+ public IMigrationPlanBuilder ForCustomPipelineFactory(Func pipelineFactoryOverride, IEnumerable supportedContentTypes)
+ {
+ SetPipelineProfile(PipelineProfile.Custom, supportedContentTypes);
+
+ _pipelineFactoryOverride = pipelineFactoryOverride;
+ _serverToCloudBuilder = null;
+
+ ClearExtensions();
+ AppendDefaultExtensions();
+
+ return this;
+ }
+
+ ///
+ public IMigrationPlanBuilder ForCustomPipelineFactory(params MigrationPipelineContentType[] supportedContentTypes)
+ where T : IMigrationPipelineFactory
+ => ForCustomPipelineFactory((IEnumerable)supportedContentTypes);
+
+ ///
+ public IMigrationPlanBuilder ForCustomPipelineFactory(IEnumerable supportedContentTypes)
+ where T : IMigrationPipelineFactory
+ => ForCustomPipelineFactory(s => s.GetRequiredService(), supportedContentTypes);
+
+ ///
+ public IMigrationPlanBuilder ForCustomPipeline(params MigrationPipelineContentType[] supportedContentTypes)
+ where T : IMigrationPipeline
+ => ForCustomPipeline((IEnumerable)supportedContentTypes);
+
+ ///
+ public IMigrationPlanBuilder ForCustomPipeline(IEnumerable supportedContentTypes)
+ where T : IMigrationPipeline
+ => ForCustomPipelineFactory>(supportedContentTypes);
+
///
public IMigrationPlanBuilder AppendDefaultExtensions()
{
@@ -117,7 +158,7 @@ public IMigrationPlanBuilder AppendDefaultExtensions()
Filters.Add(typeof(PreviouslyMigratedFilter<>), GetAllContentTypes());
Filters.Add();
Filters.Add(typeof(SystemOwnershipFilter<>), GetContentTypesByInterface());
-
+
//Standard migration transformers.
Transformers.Add();
Transformers.Add();
@@ -126,6 +167,9 @@ public IMigrationPlanBuilder AppendDefaultExtensions()
Transformers.Add();
Transformers.Add();
Transformers.Add(typeof(CloudScheduleCompatibilityTransformer<>), GetPublishTypesByInterface>());
+ Transformers.Add(typeof(WorkbookReferenceTransformer<>), GetPublishTypesByInterface());
+ Transformers.Add();
+ Transformers.Add(typeof(EncryptExtractTransformer<>), GetPublishTypesByInterface());
// Post-publish hooks.
Hooks.Add(typeof(OwnerItemPostPublishHook<,>), GetPostPublishTypesByInterface