diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..3ccb93d --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,44 @@ +name: Build and Test + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --no-build --configuration Release --verbosity normal + env: + TABLEAU_CLOUD_URL: ${{ secrets.TABLEAU_CLOUD_URL }} + TABLEAU_CLOUD_SITE: ${{ secrets.TABLEAU_CLOUD_SITE }} + TABLEAU_CLOUD_TOKEN_NAME: ${{ secrets.TABLEAU_CLOUD_TOKEN_NAME }} + TABLEAU_CLOUD_TOKEN: ${{ secrets.TABLEAU_CLOUD_TOKEN }} + TABLEAU_SERVER_URL: ${{ secrets.TABLEAU_SERVER_URL }} + TABLEAU_SERVER_SITE: "" + TABLEAU_SERVER_TOKEN_NAME: ${{ secrets.TABLEAU_SERVER_TOKEN_NAME }} + TABLEAU_SERVER_TOKEN: ${{ secrets.TABLEAU_SERVER_TOKEN }} + + - name: Run dotnet format + run: dotnet format --verbosity diagnostic + + - name: Check format results + if: failure() + run: echo "Formatting issues found. Please run 'dotnet format' locally and fix the issues." \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1244c08 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Build and Release + +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.x' + + - name: Restore dependencies + run: dotnet restore ./src/MigrationApp.GUI + + - name: Build + run: dotnet build ./src/MigrationApp.GUI --configuration Release --no-restore + + - name: Publish Win-x64 + run: dotnet publish ./src/MigrationApp.GUI -c Release -r win-x64 --self-contained /p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ./build/win_x64 + + - name: Publish OSX-arm64 + run: dotnet publish ./src/MigrationApp.GUI -c Release -r osx-arm64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ./build/osx_arm64 + + - name: Publish OSX-x64 + run: dotnet publish ./src/MigrationApp.GUI -c Release -r osx-x64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ./build/osx_x64 + + - name: Publish Linux-arm64 + run: dotnet publish ./src/MigrationApp.GUI -c Release -r linux-arm64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ./build/linux_arm64 + + - name: Publish Linux-x64 + run: dotnet publish ./src/MigrationApp.GUI -c Release -r linux-arm64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ./build/linux_x64 + + - name: Create ZIP files + run: | + zip -j TableauMigrationApp_win-x64.zip ./build/win_x64/* && + zip -j TableauMigrationApp_osx-arm64.zip ./build/osx_arm64/* && + zip -j TableauMigrationApp_osx-x64.zip ./build/osx_x64/* && + zip -j TableauMigrationApp_linux-arm64.zip ./build/linux_arm64/* && + zip -j TableauMigrationApp_linux-x64.zip ./build/linux_x64/* + + - name: Upload Release Asset Win-x64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./TableauMigrationApp_win-x64.zip + asset_name: TableauMigrationApp_win-x64.zip + asset_content_type: application/zip + + - name: Upload Release Asset OSX-arm64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./TableauMigrationApp_osx-arm64.zip + asset_name: TableauMigrationApp_osx-arm64.zip + asset_content_type: application/zip + + - name: Upload Release Asset OSX-x64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./TableauMigrationApp_osx-x64.zip + asset_name: TableauMigrationApp_osx-x64.zip + asset_content_type: application/zip + + - name: Upload Release Asset Linux-arm64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./TableauMigrationApp_linux-arm64.zip + asset_name: TableauMigrationApp_linux-arm64.zip + asset_content_type: application/zip + + - name: Upload Release Asset Linux-x64 + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./TableauMigrationApp_linux-x64.zip + asset_name: TableauMigrationApp_linux-x64.zip + asset_content_type: application/zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b15be6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Ignore build artifacts +bin/ +obj/ +build/ +Logs/ + +# Ignore Visual Studio Code directories and files +.vscode/ +.vscode/* +.history/ +.vs/ + +# Ignore NuGet packages +packages/ +*.nupkg +*.snupkg + +# Ignore .NET-specific files +project.lock.json +project.fragment.lock.json +artifacts/ + +# Ignore compiled binaries +*.dll +*.exe +*.app +*.iso + +# Ignore OS-generated files/macOS +.DS_Store + +# Ignore Windows-generated files +Thumbs.db +ehthumbs.db +Desktop.ini + +# Third Party Libs +CommandLineParser.2.9.1 + +# Test Results +TestResults/ + +# Ignore local configurations +.env.vars.local + +# Docs +_site diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..dfaf440 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,206 @@ +Apache License Version 2.0 + +Copyright (c) 2024 Salesforce, Inc. +All rights reserved. + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/README.md b/README.md index 82175d9..e4dacde 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,47 @@ # Tableau Migration App ### Table of Contents - [Tableau Migration App](#tableau-migration-app) - - [Table of Contents](#table-of-contents) + - [Table of Contents](#table-of-contents) - [Features](#features) - [Usage](#usage) - - [Tableau Server and Cloud URLs](#tableau-server-and-cloud-urls) - - [Personal Access Tokens](#personal-access-tokens) - - [User Mappings](#user-mappings) - - [Username Already in Email format](#username-already-in-email-format) - - [User has an Associated Email](#user-has-an-associated-email) - - [Default Domain Mapping](#default-domain-mapping) - - [CSV Mapping](#csv-mapping) - - [Outputs](#outputs) - - [Logging](#logging) - - [FAQ](#faq) - - [The application is taking a long time, is it stuck?!](#the-application-is-taking-a-long-time-is-it-stuck) - - [How do I migrate items with more specific logic rules?](#how-do-i-migrate-items-with-more-specific-logic-rules) - - [How do I exclude certain items from being migrated?](#how-do-i-exclude-certain-items-from-being-migrated) - - [What order will my resources get migrated in?](#what-order-will-my-resources-get-migrated-in) + - [Tableau Server and Cloud URLs](#tableau-server-and-cloud-urls) + - [Personal Access Tokens](#personal-access-tokens) + - [User Mappings](#user-mappings) + - [Username Already in Email format](#username-already-in-email-format) + - [User has an Associated Email](#user-has-an-associated-email) + - [Default Domain Mapping](#default-domain-mapping) + - [CSV Mapping](#csv-mapping) + - [Outputs](#outputs) + - [Logging](#logging) + - [FAQ](#faq) + - [The application is taking a long time, is it stuck?!](#the-application-is-taking-a-long-time-is-it-stuck) + - [How do I migrate items with more specific logic rules?](#how-do-i-migrate-items-with-more-specific-logic-rules) + - [How do I exclude certain items from being migrated?](#how-do-i-exclude-certain-items-from-being-migrated) + - [What order will my resources get migrated in?](#what-order-will-my-resources-get-migrated-in) ## Features -The purpose of ths Tabelau Migration App is to provide users with a method to perform simple migrations from their Tableau Server to Tableau Cloud. This app is meant to accompany the [Tableau Manual Migration Guide](https://help.tableau.com/current/guides/migration/en-us/emg_intro.htm) and replace the work required for the migration steps. +The purpose of the Tableau Migration App is to provide users with a method to perform simple migrations from their Tableau Server to Tableau Cloud. This app is meant to accompany the [Tableau Manual Migration Guide](https://help.tableau.com/current/guides/migration/en-us/emg_intro.htm) and replace the work required for the migration steps.
- Tableau Migration App + Tableau Migration App
* The migration app uses the [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk) under the hood, and supports all migration resources that the SDK offers. * At the time of this writing, this includes: - * Users - * Groups - * Projects - * Data Sources - * Workbooks - * Extract Refresh Tasks - * Custom Views + * Users + * Groups + * Projects + * Data Sources + * Workbooks + * Extract Refresh Tasks + * Custom Views * Basic mapping options are provided in the app for username migrations. * A simplified view of ongoing migration progress. ## Usage -### Tableau Server and Cloud URLs ->
Tableau Migration App URL Fields
+### Tableau Server and Cloud URLs +>
Tableau Migration App URL Fields
The fields in this section are for providing the URIs of the Tableau Server you wish to migrate from, and the Tableau Cloud you wish to migrate to. These URIs can be found from your browser address bar when logging into the respective Tableau products, or is what you use when connecting from Tableau Desktop. @@ -51,21 +51,21 @@ These URIs can be found from your browser address bar when logging into the resp **Note**: Multi-site environments will show the `/site/` as part of the URI. If yours does not have one, it is not a problem, and just means that you only have a single site; the `Default` site. ### Personal Access Tokens ->
Tableau Migration App Token Fields
+>
Tableau Migration App Token Fields
-Personal Access Tokens (PATs) are used by the app to programatically access and make changes to the corresponding Tableau Products. +Personal Access Tokens (PATs) are used by the app to programmatically access and make changes to the corresponding Tableau Products. PATs can be generated in your Tableau Environment by going to - > Users > {Your User Here} > Settings > Personal Access Tokens - + > Users > {Your User Here} > Settings > Personal Access Tokens + Where you will be prompted to provide a `Token name` and a `Create Token` button. ->
Tableau Migration PAT
+>
Tableau Migration PAT
Once created, a `secret` will be provided to you for that PAT. ->
Tableau PAT Details
+>
Tableau PAT Details
The `Token Name` is to be filled in the `PAT Name` field of the Migration app, and the `Secret` goes in the `PAT` field. @@ -79,7 +79,7 @@ This will need to be done separately for both the source Tableau Server and the The order of priority of defined mappings for the migration are as such: 1. CSV Defined mapping 2. Username already in email format -3. User has an associated Email +3. User has an associated Email 4. Default Domain Mapping Meaning that if a defined mapping is found for a user in the CSV file, that mapping will be used even if the user has an associated email. @@ -87,26 +87,26 @@ Meaning that if a defined mapping is found for a user in the CSV file, that mapp The following sections will detail more about each mapping option. #### Username Already in Email format -If a username is already in an email format, then nothing will need to be done and the User will be migrated over as is. +If a username is already in an email format, then nothing will need to be done and the User will be migrated over as is. #### User has an Associated Email If a user has an associated email set to the profile, that email will instead be used for the user when migrating. #### Default Domain Mapping ->
Tableau Migration App Token Fields
+>
Tableau Migration App Token Fields
-We also provide a default domain mapping. When this is done, all users will have their names appended with that domain to create an email. +We also provide a default domain mapping. When this is done, all users will have their names appended with that domain to create an email. e.g. If a User with username `PeterP` is to be migrated, and the `Default User Domain` is set to `DailyBugle.com`, then that user will be migrated to Tableau Cloud as `PeterP@DailyBugle.com` -This option can be disabled by clicking the checkbox. In which case, users that do not have a mapping found from the other options will **not** be migrated to Tableau Cloud. +This option can be disabled by clicking the checkbox. In which case, users that do not have a mapping found from the other options will **not** be migrated to Tableau Cloud. **Note** Users containing illegal email characters in their usernames will still fail migration even with a default domain mapped. For example `[]-=@example.com` is not a valid username for Tableau Cloud. **Note2** Users with a space in their Tableau Server usernames such as `Renee Montoya` will have their spaces replaced with a `.` instead, becoming `Renee.Montoya@defaultDomain.here`. #### CSV Mapping ->
Tableau Migration App Token Fields
+>
Tableau Migration App Token Fields
For more fine-grained control over how individual users are migrated, you can define specific user mappings in a CSV file. To do so, the file should contain one user mapping per line, separated by a comma (`,`). @@ -133,28 +133,41 @@ Leonardo, blue@turtle.com ### Outputs Ongoing outputs of the running Migration will be shown in the output window. ->
Tableau Migration App Output
+>
Tableau Migration App Output
-Once migration of a resource is completed, whether successful or not, an associated message will show in the window. +Once migration of a resource is completed, whether successful or not, an associated message will show in the window. ->
Tableau Migration App Output
+>
Tableau Migration App Output
### Logging Application logs can be found in the `Logs` folder from where the application is run. These logs contain all the REST requests made by the underlying [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk) to the Tableau environments to perform the migration. These logs can be used to provide further insight to the ongoing status and/or errors of the migration. +### Cancelling a Migration +When a migration is running, a `Cancel Migration` button will appear. If you want to stop the current migration, you can click this button. Doing so will also present you with a dialog window to save a manifest file. + +>
Tableau Migration App Save Manifest
+ +This manifest can be used to Resume a migration at a later time and continue where you left off. + +### Resuming a Migration +If you have a manifest file saved from a previous run, you can resume a migration by selecting the dropdown arrow on the right side of the `Start Migration` button and selecting `Resume Migration`. + +>
Tableau Migration App Output
+ + ### FAQ - + #### The application is taking a long time, is it stuck?! -> Check the logs. It could be that the current resource is really large and is taking a long time to migrate. -> -> Or that the application has hit its limit on the number of requests it can make in the current time frame, and is currently waiting until it can send more. In this case, the logs messages should mention how much longer it needs to wait for. +> Check the logs. It could be that the current resource is really large and is taking a long time to migrate. +> +> Or that the application has hit its limit on the number of requests it can make in the current time frame, and is currently waiting until it can send more. In this case, the logs messages should mention how much longer it needs to wait for. #### How do I migrate items with more specific logic rules? -> If you want finer control over how migration is handled, such as renaming resources or applying special rules such as re-assigning resources, then it is recommended that you use the [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk) to programatically perform the migration. +> If you want finer control over how migration is handled, such as renaming resources or applying special rules such as re-assigning resources, then it is recommended that you use the [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk) to programmatically perform the migration. #### How do I exclude certain items from being migrated? > You can either delete the items after the migration is performed, or you will have to use the [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk) #### What order will my resources get migrated in? -> Resources are first sorted from largest to smallest, and migrated in that order. \ No newline at end of file +> Resources are first sorted from largest to smallest, and migrated in that order. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..eedd89b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +api diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..6d17ad8 --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,60 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": [ + "**/*.csproj" + ], + } + ], + "dest": "api", + "disableGitFeatures": true + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + }, + { + "files": [ + "readme.md" + ], + "src": "../", + "dest": "index.md" + } + ], + "resource": [ + { + "files": [ + "images/**", + "screenshots/**" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appName": "Tableau Migration App", + "_appTitle": "Tableau Migration App", + "_enableSearch": true, + "_disableContribution": true, + "_gitContribute": { + "branch": "", + "path": "" + }, + "gitUrlPattern": "github", + "pdf": true + } + } +} diff --git a/docs/docs/architecture.md b/docs/docs/architecture.md new file mode 100644 index 0000000..c788b51 --- /dev/null +++ b/docs/docs/architecture.md @@ -0,0 +1,4 @@ +# App Architecture +The application is designed into two overarching modules: +- **The Core Module**: The core logic handling the migration itself using the [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk) +- **The GUI Module**: The graphical user interface presented to the user. Handles the data input and validation. diff --git a/docs/docs/core.md b/docs/docs/core.md new file mode 100644 index 0000000..33f821c --- /dev/null +++ b/docs/docs/core.md @@ -0,0 +1,20 @@ +# Core Module +## Design +The Core module handles the business logic involving performing the migration of Tableau Server to Tableau Cloud. + +The Core module has 4 main folders in it: +- **Entities** - Data structures used to represent data flowing through the application. +- **Hooks** - These are hooks that are passed to the Tableau Migration SDK to trigger whenever certain events occur (e.g. when a resource has completed migration). +- **Interfaces** - Defined interfaces implemented in the library. +- **Services** - The main service for injection is located here. + +## Entrypoint +The Core module implements the [Microsoft Dependency Injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) pattern. + +The core library can by used by configuring the dependency injection with [AddMigrationAppCore](/api/Tableau.Migration.App.Core.ServiceCollectionExtensions.html#Tableau_Migration_App_Core_ServiceCollectionExtensions_AddMigrationAppCore_Microsoft_Extensions_DependencyInjection_IServiceCollection_Microsoft_Extensions_Configuration_IConfiguration_) found in [ServiceCollectionExtensions](/api/Tableau.Migration.App.Core.ServiceCollectionExtensions.html) +. + +## Tableau Migration SDK +The connection to Tableau is handled through the [Tableau Migration SDK](https://github.com/tableau/tableau-migration-sdk). The SDK is added as a part of the configuration when the service is injected. + +The specifics of the interaction with the SDK entities are defined in [TableauMigrationService](/api/Tableau.Migration.App.Core.Services.TableauMigrationService.html). There, the provided migration details such as the authencation information, and user mappings are set before beginning the migration. diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md new file mode 100644 index 0000000..fb7e1f7 --- /dev/null +++ b/docs/docs/getting-started.md @@ -0,0 +1,38 @@ +# Getting Started +## Dependencies +The project is coded in C# using Dotnet. +To build and run this project, you will need to have .NET 8.0 installed. + +## Building The Project +The project can be loaded into Visual Studio using the solution present in the repo. + +Otherwise, it can also be built using the following commands in a terminal instance: +``` +# From the project root folder +$ dotnet restore # download needed dependencies for the project +$ dotnet build # this will build both the Core and GUI modules. +``` + +You can also go into each project folder in `src` and build a singular project. +The GUI module can also be run: +``` +# From /src/Tableau.Migration.App.GUI/ +$ dotnet run # Will build and then run the app. +``` + +## Running Unit Tests +Tests can be run either through Visual Studio, or by the following terminal command: +``` +# From either the project root, or /src/Tableau.Migration.App./ +$ dotnet test +``` + +## Creating Packages +Packages can be created using `dotnet publish` commands. A script named `release.sh` can be found in the `scripts/` folder that holds the command to build a publishing target for +* Win x64 +* OSX ARM64 +* OSX x64 +* Linux x64 +* Linux ARM64 + +*note* For MacOs, there is an additional `/script/macos.sh` script to build the `.app` package by creating the appropriate fodler structure using the published files and the `Info.plist` found in `/src/Tableau.Migration.App.GUI/Assets/`. diff --git a/docs/docs/gui.md b/docs/docs/gui.md new file mode 100644 index 0000000..210eff3 --- /dev/null +++ b/docs/docs/gui.md @@ -0,0 +1,33 @@ +# GUI Module + +## Design +The GUI module is implemented with [Avalonia](https://github.com/AvaloniaUI/Avalonia) in a MVVM pattern. The GUI is in charge of presenting interactable elements for the user to provide their authentication, and configuration data for the migration. This data is then processed and passed over to the Core Module as an dependency injected service to perform the migration. + +The GUI module has the following folders: +- **Assets** - Assets such as icons and data files used for packaging.x +- **Models** - Structures to hold stateful data for the application. +- **Services** - Defined interface and implementations of services to be used in the module. +- **Views** - The axaml definition of GUI screens, as well as their code-behind interactions logic. +- **ViewModels** - The data binding logic associating the model datas with the visible views. + +## Entrypoint +Entrypoint of the module is defined in `Program.cs` found in the root directory of the GUI Module project. This entrypoint loads up the Avalonia services to present the GUI found in `App.axaml`. + +Once loaded, the Avalonia services are configured in the code-behind file `App.axaml.cs`. + +## Services +Currently we have the following defined service interfaces: + +- **Window Provider** - Utility service to retrieve an Avalonia application's main window instance. +- **File Picker** - Service open up a file picker and select files. +- **CSV Parser** - Service to parse CSV files provided for username mappings. + +## Views +The top level view used to hold all visible elements is located in the [MainWindow](/api/Tableau.Migration.App.GUI.Views.MainWindow.html) view. + +## View Models +All ViewModels are named based on their appropriate View name. i.e. the [MainWindow](/api/Tableau.Migration.App.GUI.Views.MainWindow.html) view has an associatedv [MainWindowViewModel](api/Tableau.Migration.App.GUI.ViewModels.MainWindowViewModel.html). + +There exist 2 abstract classes defined in the ViewModel folder: +- [**ViewModelBase**](/api/Tableau.Migration.App.GUI.ViewModels.ViewModelBase.html) - The base class of a ViewModel. +- [**ValidateableViewModelBase**](/api/Tableau.Migration.App.GUI.ViewModels.ValidatableViewModelBase.html) - The base class of a ViewModel that will require some form of validation performed on the data that it holds. diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md new file mode 100644 index 0000000..50635e6 --- /dev/null +++ b/docs/docs/introduction.md @@ -0,0 +1,4 @@ +## Overview +The Tableau Migration Application is an app designed to help users of Tableau Server migrate to Tableau Cloud. + +The usage documentation can be found [here](/). The main purpose of this documentation is to go over the application design details for developers. diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml new file mode 100644 index 0000000..455bcb4 --- /dev/null +++ b/docs/docs/toc.yml @@ -0,0 +1,12 @@ +- name: Overview + href: introduction.md + items: + - name: Getting Started + href: getting-started.md +- name: App Architecture + href: architecture.md + items: + - name: Core Module + href: core.md + - name: GUI Module + href: gui.md diff --git a/docs/screenshots b/docs/screenshots new file mode 120000 index 0000000..2b5b059 --- /dev/null +++ b/docs/screenshots @@ -0,0 +1 @@ +../screenshots \ No newline at end of file diff --git a/screenshots/Resume.png b/screenshots/Resume.png new file mode 100644 index 0000000..06d1199 Binary files /dev/null and b/screenshots/Resume.png differ diff --git a/screenshots/SaveManifest.png b/screenshots/SaveManifest.png new file mode 100644 index 0000000..ddecf7e Binary files /dev/null and b/screenshots/SaveManifest.png differ diff --git a/scripts/macos.sh b/scripts/macos.sh new file mode 100755 index 0000000..1fc8fb8 --- /dev/null +++ b/scripts/macos.sh @@ -0,0 +1,52 @@ +#!/bin/env bash + +# Default architecture +ARCH="" +MAC_APP_SUFFIX="" + +# Parse command line arguments +while [[ "$1" != "" ]]; do + case $1 in + --x64 ) ARCH="osx_x64" + MAC_APP_SUFFIX="osx_x64_macapp" + ;; + --arm64 ) ARCH="osx_arm64" + MAC_APP_SUFFIX="osx_arm64_macapp" + ;; + * ) echo "Usage: $0 [--x64 | --arm64]" + exit 1 + esac + shift +done + +# If no architecture is specified, exit with a message +if [ -z "$ARCH" ]; then + echo "Error: No architecture specified. Use --x64 or --arm64." + exit 1 +fi + +APP_NAME="./build/${MAC_APP_SUFFIX}/TableauMigration.app" +SCRIPT_DIR=$(dirname "$(realpath "$0")") +PROJECT_DIR=$(dirname "$SCRIPT_DIR") + +# Use the chosen architecture +PUBLISH_OUTPUT_DIRECTORY="${PROJECT_DIR}/build/${ARCH}/." + +INFO_PLIST="${PROJECT_DIR}/src/Tableau.Migration.App.GUI/Assets/Info.plist" +ICON_FILE="tableau-migration-app.icns" +ICON_PATH="${PROJECT_DIR}/src/Tableau.Migration.App.GUI/Assets/${ICON_FILE}" + +if [ -d "$APP_NAME" ] +then + rm -rf "$APP_NAME" +fi + +mkdir -p "$APP_NAME" + +mkdir -p "$APP_NAME/Contents" +mkdir -p "$APP_NAME/Contents/MacOS" +mkdir -p "$APP_NAME/Contents/Resources" + +cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist" +cp "$ICON_PATH" "$APP_NAME/Contents/Resources/${ICON_FILE}" +cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS" diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..a6dd44a --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$(dirname "$(realpath "$0")") +PROJECT_DIR=$(dirname "$SCRIPT_DIR") + +dotnet publish ./src/Tableau.Migration.App.GUI -c Release -r win-x64 --self-contained /p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ${PROJECT_DIR}/build/win_x64 + +dotnet publish ./src/Tableau.Migration.App.GUI -c Release -r osx-arm64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ${PROJECT_DIR}/build/osx_arm64 + +dotnet publish ./src/Tableau.Migration.App.GUI -c Release -r osx-x64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ${PROJECT_DIR}/build/osx_x64 + +dotnet publish ./src/Tableau.Migration.App.GUI -c Release -r linux-arm64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ${PROJECT_DIR}/build/linux_arm64 + +dotnet publish ./src/Tableau.Migration.App.GUI -c Release -r linux-arm64 --self-contained -p:PublishSingleFile=true /p:DebugType=None /p:GenerateDocumentationFile=false -o ${PROJECT_DIR}/build/linux_x64 diff --git a/src/Tableau.Migration.App.GUI/Assets/Info.plist b/src/Tableau.Migration.App.GUI/Assets/Info.plist new file mode 100644 index 0000000..f697cec --- /dev/null +++ b/src/Tableau.Migration.App.GUI/Assets/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleIconFile + tableau-migration-app.icns + CFBundleIdentifier + com.tableau.migration.app + CFBundleName + TableauMigrationApp + CFBundleVersion + 1.0.0 + LSMinimumSystemVersion + 10.12 + CFBundleExecutable + TableauMigrationApp + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + NSHighResolutionCapable + + + diff --git a/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_1024x1024.png b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_1024x1024.png new file mode 100644 index 0000000..d5f7c9d Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_1024x1024.png differ diff --git a/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_128x128.png b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_128x128.png new file mode 100644 index 0000000..5758348 Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_128x128.png differ diff --git a/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_16x16.png b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_16x16.png new file mode 100644 index 0000000..92029a2 Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_16x16.png differ diff --git a/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_32x32.png b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_32x32.png new file mode 100644 index 0000000..98d411c Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_32x32.png differ diff --git a/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_512x512.png b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_512x512.png new file mode 100644 index 0000000..9edaaf9 Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_512x512.png differ diff --git a/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_64x64.png b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_64x64.png new file mode 100644 index 0000000..b0fce58 Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/TableauMigrationApp.iconset/icon_64x64.png differ diff --git a/src/Tableau.Migration.App.GUI/Assets/tableau-migration-app.icns b/src/Tableau.Migration.App.GUI/Assets/tableau-migration-app.icns new file mode 100644 index 0000000..1dacf11 Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/tableau-migration-app.icns differ diff --git a/src/Tableau.Migration.App.GUI/Assets/tableau-migration-app.ico b/src/Tableau.Migration.App.GUI/Assets/tableau-migration-app.ico new file mode 100644 index 0000000..1e14ef1 Binary files /dev/null and b/src/Tableau.Migration.App.GUI/Assets/tableau-migration-app.ico differ diff --git a/src/Tableau.Migration.App.GUI/Tableau.Migration.App.GUI.csproj b/src/Tableau.Migration.App.GUI/Tableau.Migration.App.GUI.csproj index 53a7370..f47bd16 100644 --- a/src/Tableau.Migration.App.GUI/Tableau.Migration.App.GUI.csproj +++ b/src/Tableau.Migration.App.GUI/Tableau.Migration.App.GUI.csproj @@ -64,4 +64,8 @@ + + + + diff --git a/src/Tableau.Migration.App.GUI/ViewModels/MainWindowViewModel.cs b/src/Tableau.Migration.App.GUI/ViewModels/MainWindowViewModel.cs index 347ac96..7c1e326 100644 --- a/src/Tableau.Migration.App.GUI/ViewModels/MainWindowViewModel.cs +++ b/src/Tableau.Migration.App.GUI/ViewModels/MainWindowViewModel.cs @@ -18,6 +18,7 @@ namespace Tableau.Migration.App.GUI.ViewModels; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -31,6 +32,7 @@ namespace Tableau.Migration.App.GUI.ViewModels; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; using Tableau.Migration.App.Core.Entities; using Tableau.Migration.App.Core.Hooks.Mappings; using Tableau.Migration.App.Core.Interfaces; @@ -192,32 +194,6 @@ public void CancelMigration(string? manifestSaveFilePath) return; } - /// - /// Resumes a migration process using the specified manifest file path. - /// - /// The file path to the manifest used to resume the migration. - /// - /// This method validates the necessary fields before proceeding. If the validation fails, - /// it logs a message and aborts the migration process. Upon successful validation, it initiates - /// the migration process asynchronously, displaying messages in the UI to indicate that - /// the resume migration process has started. - /// - public void RunResumeMigration(string manifestLoadFilePath) - { - if (!this.AreFieldsValid()) - { - this.logger?.LogInformation("Migration Run failed due to validation errors."); - return; - } - - this.IsMigrating = true; - this.MessageDisplayVM.AddMessage(MigrationMessagesSessionSeperator); - this.MessageDisplayVM.AddMessage("Migration Resumed"); - - this.logger?.LogInformation("Resume Migration Started"); - this.ResumeMigrationTask(manifestLoadFilePath).ConfigureAwait(false); - } - /// /// Callback to update fields when error state is changed. /// @@ -230,8 +206,9 @@ protected virtual void OnErrorsChanged(string propertyName) /// /// Validates fields and starts the migration process if valid. /// + /// A representing the asynchronous operation. [RelayCommand] - private void RunMigration() + private async Task RunMigration() { if (!this.AreFieldsValid()) { @@ -243,7 +220,68 @@ private void RunMigration() this.MessageDisplayVM.AddMessage(MigrationMessagesSessionSeperator); this.MessageDisplayVM.AddMessage("Migration Started"); this.logger?.LogInformation("Migration Started"); - this.RunMigrationTask().ConfigureAwait(false); + await this.RunMigrationTask().ConfigureAwait(false); + } + + /// + /// Asynchronously resumes migration by selecting a manifest file and calling RunResumeMigration with the file path. + /// + [RelayCommand] + private async Task ResumeMigration() + { + if (!this.AreFieldsValid()) + { + this.logger?.LogInformation("Migration Run failed due to validation errors."); + return; + } + + var filePath = await this.SelectManifestFileAsync(); + if (filePath != null) + { + this.IsMigrating = true; + this.MessageDisplayVM.AddMessage(MigrationMessagesSessionSeperator); + this.MessageDisplayVM.AddMessage("Migration Started"); + this.logger?.LogInformation("Migration Started"); + await this.RunMigrationTask().ConfigureAwait(false); + } + } + + /// + /// Opens a file picker dialog to select a manifest file and returns the selected file path. + /// + /// The file path if a file is selected; otherwise, null. + private async Task SelectManifestFileAsync() + { + var options = new FilePickerOpenOptions + { + Title = "Select Manifest File", + AllowMultiple = false, // Allow only one file selection + FileTypeFilter = new List + { + new FilePickerFileType("JSON files") { Patterns = new[] { "*.json" } }, + new FilePickerFileType("All files") { Patterns = new[] { "*.*" } }, + }, + }; + + var window = App.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop + ? desktop.MainWindow + : null; + + if (window?.StorageProvider == null) + { + this.logger?.LogWarning("StorageProvider is null; unable to open file picker dialog."); + return null; + } + + var result = await window.StorageProvider.OpenFilePickerAsync(options); + + if (result == null || result.Count == 0) + { + this.logger?.LogInformation("No file selected in file picker dialog."); + return null; + } + + return result[0].TryGetLocalPath(); } /// diff --git a/src/Tableau.Migration.App.GUI/ViewModels/UserFileMappingsViewModel.cs b/src/Tableau.Migration.App.GUI/ViewModels/UserFileMappingsViewModel.cs index 503ccdc..de38876 100644 --- a/src/Tableau.Migration.App.GUI/ViewModels/UserFileMappingsViewModel.cs +++ b/src/Tableau.Migration.App.GUI/ViewModels/UserFileMappingsViewModel.cs @@ -104,16 +104,23 @@ public bool IsUserMappingFileLoaded set => this.SetProperty(ref this.isUserMappingFileLoaded, value); } + /// + /// This command executes . + /// [RelayCommand] - private void UnLoadUserFile() + public void UnLoadUserFile() { this.ClearCSVLoadedValues(); this.CSVLoadStatus = string.Empty; this.CSVLoadStatusColor = Brushes.Black; } + /// + /// This command executes . + /// + /// A representing the Load User File operation. [RelayCommand] - private async Task LoadUserFile() + public async Task LoadUserFile() { try { diff --git a/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml b/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml index 34bfd7e..7f5cff4 100644 --- a/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml +++ b/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml @@ -34,9 +34,15 @@ - + diff --git a/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml.cs b/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml.cs index 2db7be1..f183bb9 100644 --- a/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml.cs +++ b/src/Tableau.Migration.App.GUI/Views/HelpButton.axaml.cs @@ -49,15 +49,6 @@ public HelpButton() { this.DataContext = this; this.InitializeComponent(); - - this.Initialized += (sender, e) => - { - var linkTextBlock = this.FindControl("LinkTextBlock"); - if (linkTextBlock != null) - { - linkTextBlock.PointerPressed += this.OnLinkClicked; - } - }; } /// diff --git a/src/Tableau.Migration.App.GUI/Views/MainWindow.axaml b/src/Tableau.Migration.App.GUI/Views/MainWindow.axaml index af072ed..a0abe46 100644 --- a/src/Tableau.Migration.App.GUI/Views/MainWindow.axaml +++ b/src/Tableau.Migration.App.GUI/Views/MainWindow.axaml @@ -9,6 +9,7 @@ mc:Ignorable="d" x:Class="Tableau.Migration.App.GUI.Views.MainWindow" x:DataType="vm:MainWindowViewModel" + Icon="avares://TableauMigrationApp/Assets/tableau-migration-app.ico" Title="Tableau Migration App" Width="820" Height="680" MaxWidth="820" @@ -90,17 +91,11 @@ - + + + diff --git a/src/Tableau.Migration.App.GUI/Views/SplitButton.axaml.cs b/src/Tableau.Migration.App.GUI/Views/SplitButton.axaml.cs new file mode 100644 index 0000000..fd9626a --- /dev/null +++ b/src/Tableau.Migration.App.GUI/Views/SplitButton.axaml.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2024, Salesforce, Inc. All rights reserved. +// 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 +// 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.App.GUI.Views; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using Avalonia.VisualTree; +using System.Collections.Generic; +using System.Windows.Input; +using Tableau.Migration.App.GUI.ViewModels; + +/// +/// Split Button component. +/// +public partial class SplitButton : UserControl +{ + /// + /// Dependency property to store the command for primary command. + /// + public static readonly StyledProperty PrimaryCommandProperty = + AvaloniaProperty.Register(nameof(PrimaryCommand)); + + /// + /// Dependency property to store the command for secondary command. + /// + public static readonly StyledProperty SecondaryCommandProperty = + AvaloniaProperty.Register(nameof(SecondaryCommand)); + + /// + /// Dependency property to set the primary button text. + /// + public static readonly StyledProperty PrimaryButtonTextProperty = + AvaloniaProperty.Register(nameof(PrimaryButtonText), "Primary Action"); + + /// + /// Dependency property to set the secondary button text. + /// + public static readonly StyledProperty SecondaryButtonTextProperty = + AvaloniaProperty.Register(nameof(SecondaryButtonText), "Secondary Action"); + + /// + /// Initializes a new instance of the class. + /// + public SplitButton() + { + this.InitializeComponent(); + } + + /// + /// Gets or sets the command for primary action. + /// + public ICommand PrimaryCommand + { + get => this.GetValue(PrimaryCommandProperty); + set => this.SetValue(PrimaryCommandProperty, value); + } + + /// + /// Gets or sets the command for secondary action. + /// + public ICommand SecondaryCommand + { + get => this.GetValue(SecondaryCommandProperty); + set => this.SetValue(SecondaryCommandProperty, value); + } + + /// + /// Gets or sets the text displayed on the main button. + /// + public string PrimaryButtonText + { + get => this.GetValue(PrimaryButtonTextProperty); + set => this.SetValue(PrimaryButtonTextProperty, value); + } + + /// + /// Gets or sets the text displayed on the menu item. + /// + public string SecondaryButtonText + { + get => this.GetValue(SecondaryButtonTextProperty); + set => this.SetValue(SecondaryButtonTextProperty, value); + } +} \ No newline at end of file