diff --git a/.gitignore b/.gitignore index 0f20302cf..778fe2465 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ + +#takeoff +takeoff/takeoff_lib/coverage/lcov.info +takeoff/takeoff_cli/takeoff_cli.exe +takeoff/takeoff_gui/.vscode/launch.json +takeoff/takeoff_gui/pubspec.lock +takeoff/takeoff_cli/bin/takeoff_cli.exe # VSCode Config .vscode/* @@ -9,3 +16,4 @@ # secrets folder **/.secrets/** +takeoff/takeoff_lib/test/test.dart diff --git a/scripts/pipelines/gcloud/templates/package/package-setup-environment.sh b/scripts/pipelines/gcloud/templates/package/package-setup-environment.sh index 522857d63..e8101145b 100755 --- a/scripts/pipelines/gcloud/templates/package/package-setup-environment.sh +++ b/scripts/pipelines/gcloud/templates/package/package-setup-environment.sh @@ -10,4 +10,4 @@ then curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip ./aws/install -fi \ No newline at end of file +fi diff --git a/scripts/quickstart/gcloud/quickstart-wayat-backend.sh b/scripts/quickstart/gcloud/quickstart-wayat-backend.sh old mode 100755 new mode 100644 diff --git a/scripts/quickstart/gcloud/quickstart-wayat.sh b/scripts/quickstart/gcloud/quickstart-wayat.sh new file mode 100644 index 000000000..1295e2b92 --- /dev/null +++ b/scripts/quickstart/gcloud/quickstart-wayat.sh @@ -0,0 +1,62 @@ +# # https://github.com/devonfw/hangar/tree/master/setup +# docker build -t hangar -f ./setup/Dockerfile . +# docker run --rm -it -v /Users/$env:UserName/AppData/Roaming/gcloud:/root/.config/gcloud -v /Users/$env:UserName/AppData/Roaming/configstore:/root/.config/configstore -v /Users/$env:UserName/.aws:/root/.aws -v /Users/$env:UserName/.azure:/root/.azure -v /Users/$env:UserName/.kube:/root/.kube -v "/Users/$env:UserName/AppData/Roaming/GitHub CLI:/root/.config/gh" -v /Users/$env:UserName/.ssh:/root/.ssh -v /Users/$env:UserName/hangar_workspace:/hangar_workspace -v /Users/$env:UserName/.gitconfig:/root/.gitconfig -v ./scripts:/scripts hangar bash +gcloud auth login +firebase login --no-localhost +echo "Please write the project ID:" && read project_id +description_project="Quickstart Wayat" +region="europe-west6" +zone="europe-west6-a" +firebase_region="europe-west6" +frontendName="Frontend-Flutter" +backendName="Backend-Python" +frontendLanguage="flutter" +frontendLanguage_version=3.3.4 +backendLanguage="python" +backendLanguage_version=3.10 +echo "Please write your billing account." && read billing_account +workspace_base=/workspace +mkdir -p $workspace_base/$project_id +workspace=$workspace_base/$project_id +sa_name=sa-hangar +# Create project +/scripts/accounts/gcloud/create-project.sh -n "$project_id" -d "$description_project" -b "$billing_account" --firebase +# Create SA +/scripts/accounts/gcloud/setup-principal-account.sh -s "$sa_name" -p "$project_id" -f /scripts/accounts/gcloud/predefined-roles.txt -k "$workspace/key.json" +/scripts/accounts/gcloud/verify-principal-roles-and-permissions.sh -s $sa_name -p "$project_id" -f /scripts/accounts/gcloud/predefined-roles.txt +gcloud auth activate-service-account --key-file="$workspace/key.json" +# Create repos +/scripts/repositories/gcloud/create-repo.sh -a create -s gitflow -p "$project_id" -n "$frontendName" -d "$workspace" +/scripts/repositories/gcloud/create-repo.sh -a create -s gitflow -p "$project_id" -n "$backendName" -d "$workspace" +# Create Sonarqube +mkdir -p $workspace/sonarqube +cd /scripts/sonarqube/gcloud/ +./sonarqube.sh apply --state-folder "$workspace/sonarqube" --service_account_file "$workspace/key.json" --project "$project_id" --region "$region" --zone "$zone" +sonarqube_token=$(grep "sonarqube_token" "$workspace/sonarqube/terraform.tfoutput" | cut -d' ' -f 3 | sed 's/^.//;s/.$//') +sonarqube_url=$(grep "sonarqube_url" "$workspace/sonarqube/terraform.tfoutput" | cut -d' ' -f 3 | sed 's/^.//;s/.$//') +cd /scripts +# Get Cloud Run endpoints +/scripts/quickstart/gcloud/init-cloud-run.sh -p "$project_id" -n "${frontendName,,}" -r "$region" -o "$workspace/$frontendName-url.txt" +/scripts/quickstart/gcloud/init-cloud-run.sh -p "$project_id" -n "${backendName,,}" -r "$region" -o "$workspace/$backendName-url.txt" +# Setup firebase +/scripts/accounts/gcloud/setup-firebase.sh -n "$project_id" -o "$workspace" -r "$firebase_region" --enable-maps --setup-ios --setup-android --setup-web +# Quickstart apps +echo "Manual actions required. Read the info of the last command to more information." +echo "Please write the map secret token." && read map_secret +backend_url=$(<"$workspace/$backendName-url.txt") +frontend_url=$(<"$workspace/$frontendName-url.txt") +./quickstart/gcloud/quickstart-wayat-backend.sh -p "$project_id" -w "$workspace" -d "$workspace/$backendName" --storage-bucket "$project_id".appspot.com +./quickstart/gcloud/quickstart-wayat-frontend.sh -p "$project_id" -w "$workspace" -d "$workspace/$frontendName" --keystore "$workspace/keystore.jks" --backend-url $backend_url --frontend-url $frontend_url --maps-static-secret $map_secret +echo "Manual actions required. Read the info of the last command to more information. Press enter to continue..." && read +# Create backend pipelines +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/build/build-pipeline.cfg -n "${backendName,,}"-build -d "$workspace/$backendName" -l $backendLanguage --language-version $backendLanguage_version -b develop +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/test/test-pipeline.cfg -n "${backendName,,}"-test -d "$workspace/$backendName" -l $backendLanguage --language-version $backendLanguage_version --build-pipeline-name "${backendName,,}"-build -b develop +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/quality/quality-pipeline.cfg -n "${backendName,,}"-quality -d "$workspace/$backendName" -l $backendLanguage --language-version $backendLanguage_version --build-pipeline-name "${backendName,,}"-build --test-pipeline-name "${backendName,,}"-test --sonar-url "$sonarqube_url" --sonar-token "$sonarqube_token" -b develop +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/package/package-pipeline.cfg -i "$region"-docker.pkg.dev/"$project_id"/"${backendName,,}"/"${backendName,,}" -n "${backendName,,}"-package -d "$workspace/$backendName" -l $backendLanguage --language-version $backendLanguage_version --build-pipeline-name "${backendName,,}"-build --quality-pipeline-name "${backendName,,}"-quality -b develop +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/deploy-cloud-run/deploy-cloud-run-pipeline.cfg -n "${backendName,,}"-deploy -d "$workspace/$backendName" -b develop --service-name "${backendName,,}" --gcloud-region "$region" +# Create frontend web pipelines +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/build/build-pipeline.cfg -n "${frontendName,,}"-build -d "$workspace/$frontendName" -l "$frontendLanguage" --language-version "$frontendLanguage_version" --registry-location "$region" -b develop -m E2_HIGHCPU_8 +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/test/test-pipeline.cfg -n "${frontendName,,}"-test -d "$workspace/$frontendName" -l "$frontendLanguage" --language-version "$frontendLanguage_version" --registry-location "$region" --build-pipeline-name "${frontendName,,}"-build -b develop -m E2_HIGHCPU_8 +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/quality/quality-pipeline.cfg -n "${frontendName,,}"-quality -d "$workspace/$frontendName" -l "$frontendLanguage" --language-version "$frontendLanguage_version" --registry-location "$region" --build-pipeline-name "${frontendName,,}"-build --test-pipeline-name "${frontendName,,}"-test --sonar-url "$sonarqube_url" --sonar-token "$sonarqube_token" -b develop -m E2_HIGHCPU_8 +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/package/package-pipeline.cfg -i "$region"-docker.pkg.dev/"$project_id"/"${frontendName,,}"/"${frontendName,,}" -n "${frontendName,,}"-web-package -d "$workspace/$frontendName" -l "$frontendLanguage" --language-version "$frontendLanguage_version" --build-pipeline-name "${frontendName,,}"-build --quality-pipeline-name "${frontendName,,}"-quality -b develop --registry-location "$region" --flutter-web-platform --flutter-android-platform canvaskit -m E2_HIGHCPU_8 +/scripts/pipelines/gcloud/pipeline_generator.sh -c /scripts/pipelines/gcloud/templates/deploy-cloud-run/deploy-cloud-run-pipeline.cfg -n "${frontendName,,}"-web-deploy -d "$workspace/$frontendName" -b develop --service-name "${frontendName,,}" --gcloud-region "$region" --port 80 -m E2_HIGHCPU_8 \ No newline at end of file diff --git a/takeoff/.gitignore b/takeoff/.gitignore new file mode 100644 index 000000000..a878ace4c --- /dev/null +++ b/takeoff/.gitignore @@ -0,0 +1,6 @@ + +# Autogenerated files +*.g.dart +*.gr.dart +*.mocks.dart +takeoff_cli/bin/takeoff_cli.exe diff --git a/takeoff/.vscode/launch.json b/takeoff/.vscode/launch.json new file mode 100644 index 000000000..c02b01ffb --- /dev/null +++ b/takeoff/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "takeoff_cli", + "cwd": "takeoff_cli", + "console": "terminal", + "request": "launch", + "type": "dart", + "program": "bin/takeoff_cli.dart" + }, + { + "name": "takeoff_gui", + "cwd": "takeoff_gui", + "request": "launch", + "type": "dart" + }, + { + "name": "takeoff_gui (profile mode)", + "cwd": "takeoff_gui", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "takeoff_gui (release mode)", + "cwd": "takeoff_gui", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "takeoff_lib", + "cwd": "takeoff_lib", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/takeoff/README.asciidoc b/takeoff/README.asciidoc new file mode 100644 index 000000000..0d98cbd71 --- /dev/null +++ b/takeoff/README.asciidoc @@ -0,0 +1,41 @@ +:url-wayat: https://github.com/devonfw-forge/wayat-flutter-python-mvp + += Takeoff + +image:https://img.shields.io/badge/os-Windows-GREEN.svg[] +image:https://img.shields.io/badge/os-Linux-GREEN.svg[] + +image:https://img.shields.io/badge/Flutter-%2302569B.svg?style=for-the-badge&logo=Flutter&logoColor=white[] +image:https://img.shields.io/badge/GoogleCloud-%234285F4.svg?style=for-the-badge&logo=google-cloud&logoColor=white[] + +image::_docs/rocket_logo.png[] + +A CLI and graphical application for creating new projects in the cloud. + +== What is TakeOff? +TakeOff is a simple and user-friendly graphical and command line interface and to create and projects in the Cloud. + +The main use case is to be able to have a real application created and deployed in the most automated way possible (for Google Cloud, we have chosen {url-wayat}[Wayat]), so any team can take it as a base and start working on the Cloud. We call this "Quickstart". + +It currently only supports Google Cloud, with support for AWS and Azure still being worked on. + +== Requirements +:url-get-docker: https://docs.docker.com/get-docker/ +:url-get-rancher: https://rancherdesktop.io +:url-get-docker-desktop: https://www.docker.com/products/docker-desktop/ + +To use TakeOff, Docker must be previously installed in one of the following ways: + +* {url-get-docker}[Docker] (Linux) +* {url-get-rancher}[Rancher Desktop] (MacOS and Windows) +* {url-get-docker-desktop}[Docker Desktop] (MacOS and Windows) + +image::_docs/diagrams/context_diagram/Context_Diagram.png[600,600] + +== How to use TakeOff + +:url-use-cli: https://github.com/devonfw/hangar/blob/takeoff_develop/takeoff/takeoff_cli/README.asciidoc +:url-use-gui: https://github.com/devonfw/hangar/blob/takeoff_develop/takeoff/takeoff_gui/README.asciidoc + +* {url-use-cli}[TakeOff CLI] +* {url-use-gui}[TakeOff GUI] diff --git a/takeoff/_docs/diagrams/commands_diagram.puml b/takeoff/_docs/diagrams/commands_diagram.puml new file mode 100644 index 000000000..ff548ff08 --- /dev/null +++ b/takeoff/_docs/diagrams/commands_diagram.puml @@ -0,0 +1,27 @@ +@startmindmap CommandsName + +* takeoff +** gc <> +*** create <> +*** run <> +*** init <> +*** list <> +*** clean <> +** aws <> +** azure <> +** quickstart <> +*** wayat <> +*** viplane <> +@endmindmap diff --git a/takeoff/_docs/diagrams/commands_diagram/CommandsName.png b/takeoff/_docs/diagrams/commands_diagram/CommandsName.png new file mode 100644 index 000000000..68a176bbd Binary files /dev/null and b/takeoff/_docs/diagrams/commands_diagram/CommandsName.png differ diff --git a/takeoff/_docs/diagrams/component_diagram.puml b/takeoff/_docs/diagrams/component_diagram.puml new file mode 100644 index 000000000..83fcabb7a --- /dev/null +++ b/takeoff/_docs/diagrams/component_diagram.puml @@ -0,0 +1,51 @@ +@startuml Component_Diagram +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!define FONTAWESOME https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/font-awesome-5 +!include DEVICONS/angular.puml +!include DEVICONS/java.puml +!include DEVICONS/msql_server.puml +!include FONTAWESOME/users.puml +!define osaPuml https://raw.githubusercontent.com/Crashedmind/PlantUML-opensecurityarchitecture2-icons/master +!include osaPuml/Common.puml +!include osaPuml/User/all.puml + +!include +!include +!include +!include +!include +left to right direction + +AddContainerTag("db", $sprite="database_server", $legendText="database container") + + +Person(user, "User", "Interacts with the GUI and CLI to create/deploy on Cloud environments", $sprite="users") +Container_Ext(hangar_cont, "Hangar Container", "Docker","Contains all the hangar scripts and the tools to interact with the Cloud services") + +System_Boundary(c1, "TakeOff") { + Container_Boundary(takeoff_gui, "TakeOff GUI") { + Component(gui, "GUI", $techn="Flutter", $descr="Receives the inputs from the user and calls the appropriate logic") + Component(shared_libary_gui, "Shared business logic library", $techn="Dart package", $descr="Shared package with logic library for both the CLI and GUI") + + } + Container_Boundary(takeoff_cli, "TakeOff CLI") { + Component(cli, "Input Controller", $techn="dart", $descr="Reads the console commands to determine which scripts should be executed") + Component(shared_libary_cli, "Shared business logic library", $techn="Dart package", $descr="Shared package with logic library for both the CLI and GUI") + + } + + ContainerDb(cache, "Cache Store", "Stores user configuration", $tags="db") +} + +Rel(cli, gui, "can launch") +Rel(user, gui, "Uses", "GUI") +Rel_Right(user, cli, "Uses", "CLI") +Rel(gui, shared_libary_gui, "calls") +Rel(cli, shared_libary_cli, "calls") +Rel(shared_libary_cli, hangar_cont, "Executes commands", "Docker CLI") +Rel(shared_libary_gui, hangar_cont, "Executes commands", "Docker CLI") +Rel_Right(shared_libary_cli, cache, "stores/writes") +Rel_Right(shared_libary_gui, cache, "stores/writes") +@enduml \ No newline at end of file diff --git a/takeoff/_docs/diagrams/component_diagram/Component_Diagram.png b/takeoff/_docs/diagrams/component_diagram/Component_Diagram.png new file mode 100644 index 000000000..a260b4f48 Binary files /dev/null and b/takeoff/_docs/diagrams/component_diagram/Component_Diagram.png differ diff --git a/takeoff/_docs/diagrams/container_diagram.puml b/takeoff/_docs/diagrams/container_diagram.puml new file mode 100644 index 000000000..ebd4c84b2 --- /dev/null +++ b/takeoff/_docs/diagrams/container_diagram.puml @@ -0,0 +1,44 @@ +@startuml Container_Diagram +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!define FONTAWESOME https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/font-awesome-5 +!include DEVICONS/angular.puml +!include DEVICONS/java.puml +!include DEVICONS/msql_server.puml +!include FONTAWESOME/users.puml +!define osaPuml https://raw.githubusercontent.com/Crashedmind/PlantUML-opensecurityarchitecture2-icons/master +!include osaPuml/Common.puml +!include osaPuml/User/all.puml + +!include +!include +!include +!include +!include +AddContainerTag("db", $sprite="database_server", $legendText="mounted volume") + + +Person(user, "User", "Interacts with the GUI and CLI to create/deploy on Cloud environments", $sprite="users") +Container_Ext(hangar_cont, "Hangar Container", "Docker","Contains all the hangar scripts and the tools to interact with the Cloud services") + +System_Boundary(c1, "TakeOff") { + Container(flutter_gui, "TakeOff GUI", "Flutter GUI","Simplifies the interaction with TakeOff logic wrapping everything in a UI") + Container(dart_cli, "TakeOff CLI", "Dart CLI","Gets command-line arguments and executes the appropiate scripts against the Hangar container") + ContainerDb(file_cache, "Cache Store", "Stores user configuration", $tags="db") +} + +System_Ext(google, "Google Cloud", "Cloud provider") +System_Ext(azure, "Azure", "Cloud provider") +System_Ext(aws, "AWS", "Cloud Provider") + +Rel(user, flutter_gui, "Uses", "GUI") +Rel(user, dart_cli, "Uses", "CLI") +Rel(dart_cli, hangar_cont, "Executes commands", "Docker CLI") +Rel(flutter_gui, hangar_cont, "Executes commands", "Docker CLI") +Rel(dart_cli, file_cache, "stores/writes") +Rel(flutter_gui, file_cache, "stores/writes") + +Rel(hangar_cont, google, "uses", "HTTPS") +Rel(hangar_cont, aws, "uses", "HTTPS") +Rel(hangar_cont, azure, "uses", "HTTPS") +@enduml \ No newline at end of file diff --git a/takeoff/_docs/diagrams/container_diagram/Container_Diagram.png b/takeoff/_docs/diagrams/container_diagram/Container_Diagram.png new file mode 100644 index 000000000..a90175466 Binary files /dev/null and b/takeoff/_docs/diagrams/container_diagram/Container_Diagram.png differ diff --git a/takeoff/_docs/diagrams/context_diagram.puml b/takeoff/_docs/diagrams/context_diagram.puml new file mode 100644 index 000000000..f148dfa1e --- /dev/null +++ b/takeoff/_docs/diagrams/context_diagram.puml @@ -0,0 +1,25 @@ +@startuml Context_Diagram +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!define FONTAWESOME https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/font-awesome-5 +!include DEVICONS/angular.puml +!include DEVICONS/java.puml +!include DEVICONS/msql_server.puml +!include FONTAWESOME/users.puml + +' LAYOUT_WITH_LEGEND() + +Person(user, "User", "Interacts with the GUI and CLI to create/deploy on Cloud environments", $sprite="users") +System(take_off, "TakeOff", "GUI & CLI program to Create / Manage Cloud Projects") +Container_Ext(hangar_cont, "Hangar Container", "Docker","Contains all the hangar scripts and the tools to interact with the Cloud services") + +System_Ext(google, "Google Cloud", "Cloud provider") +System_Ext(azure, "Azure", "Cloud provider") +System_Ext(aws, "AWS", "Cloud Provider") + +Rel(user, take_off, "Uses", "CLI / GUI") +Rel(take_off, hangar_cont, "Executes commands", "Docker CLI") +Rel(hangar_cont, google, "uses", "HTTPS") +Rel(hangar_cont, aws, "uses", "HTTPS") +Rel(hangar_cont, azure, "uses", "HTTPS") +@endum \ No newline at end of file diff --git a/takeoff/_docs/diagrams/context_diagram/Context_Diagram.png b/takeoff/_docs/diagrams/context_diagram/Context_Diagram.png new file mode 100644 index 000000000..0484c5acf Binary files /dev/null and b/takeoff/_docs/diagrams/context_diagram/Context_Diagram.png differ diff --git a/takeoff/_docs/diagrams/dynamic_diagram.puml b/takeoff/_docs/diagrams/dynamic_diagram.puml new file mode 100644 index 000000000..3616b3a8e --- /dev/null +++ b/takeoff/_docs/diagrams/dynamic_diagram.puml @@ -0,0 +1,104 @@ +@startuml Dynamic_Diagram +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml +!define DEVICONS https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/devicons +!define FONTAWESOME https://raw.githubusercontent.com/tupadr3/plantuml-icon-font-sprites/master/font-awesome-5 +!include DEVICONS/angular.puml +!include DEVICONS/java.puml +!include DEVICONS/msql_server.puml +!include FONTAWESOME/users.puml +!define osaPuml https://raw.githubusercontent.com/Crashedmind/PlantUML-opensecurityarchitecture2-icons/master +!include osaPuml/Common.puml +!include osaPuml/User/all.puml +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml +!include +!include +!include +!include +!include +AddContainerTag("db", $sprite="database_server", $legendText="database container") +left to right direction + +' LAYOUT_WITH_LEGEND() + +Person(user, "User", "Interacts with the GUI and CLI to create/deploy on Cloud environments", $sprite="users") +System_Boundary(s_takeoff, "TakeOff") { + Container_Boundary(takeoff_gui, "TakeOff GUI") { + Component(gui, "GUI", $techn="Flutter", $descr="Receives the inputs from the user and calls the appropriate logic") + } + Container_Boundary(takeoff_cli, "TakeOff CLI") { + Component(cli, "Input Controller", $techn="Dart", $descr="Reads the console commands to determine which scripts should be executed") + } + + Container_Boundary(c_takeoff, "Shared Business logic library"){ + Component(façade, "Logic Façade", "Abstraction Layer", "Automation of Hangar scripts") + Component(project_controller, "Project Controller") + Component(repository_controller, "Repository Controller") + Component(account_controller, "Account Controller") + Component(pipeline_controller, "Pipeline Controller") + Component(cache_controller, "Cache Controller") + } + + ContainerDb(db, "Cache Store", "User Configuration", $tags="db") +} + +System_Boundary(s_hangar, "Hangar") { + Container_Boundary(hangar, "Hangar") { + Component(create_project, "create-project.sh") + Component(setup_sonar, "setup-sonar.sh") + Component(verify_roles, "verify-roles.sh") + Component(setup_service_account, "setup-principal-account.sh") + Component(create_repo, "create-repo.sh") + Component(pipeline_generator, "pipeline-generator.sh") + } + + + Container_Boundary(hangar_clis, "Cloud CLIs") { + Component_Ext(gcloud, "GCloud CLI") + + } + +} + +System_Boundary(host, "Host OS"){ + ContainerDb(dbcli, "CLI Configs", "Self CLI Configuration", $tags="db") +} + + + +System_Boundary(cloud, "Cloud"){ + System_Ext(google_cloud, "Google Cloud") +} + +Rel(user, gui, "uses") +Rel(user, cli, "uses") + +Rel(gui, façade, "calls") +Rel(cli, façade, "calls") + +Rel(project_controller, create_project, "Interacts") +Rel(project_controller, setup_sonar, "Interacts") +Rel(account_controller, setup_service_account, "Interacts") +Rel(account_controller, verify_roles, "Interacts") +Rel(pipeline_controller, pipeline_generator, "Interacts") +Rel(repository_controller, create_repo, "Interacts") +Rel_Up(cache_controller, db, "Stores / Reads") + +Rel(façade, project_controller, "Uses") +Rel(façade, repository_controller, "Uses") +Rel(façade, account_controller, "Uses") +Rel(façade, pipeline_controller, "Uses") +Rel(façade, cache_controller, "Uses") + + +Rel(create_project, gcloud, "Interacts") +Rel(setup_sonar, gcloud, "Interacts") +Rel(setup_service_account, gcloud, "Interacts") +Rel(verify_roles, gcloud, "Interacts") +Rel(pipeline_generator, gcloud, "Interacts") +Rel(create_repo, gcloud, "Interacts") + +Rel(gcloud, dbcli, "Stores / Reads") + +Rel(gcloud, google_cloud, "Communicates with", "HTTPS") + +@enduml \ No newline at end of file diff --git a/takeoff/_docs/diagrams/dynamic_diagram/Dynamic_Diagram.png b/takeoff/_docs/diagrams/dynamic_diagram/Dynamic_Diagram.png new file mode 100644 index 000000000..f21de2215 Binary files /dev/null and b/takeoff/_docs/diagrams/dynamic_diagram/Dynamic_Diagram.png differ diff --git a/takeoff/_docs/diagrams/quickstart_flow_diagram.puml b/takeoff/_docs/diagrams/quickstart_flow_diagram.puml new file mode 100644 index 000000000..af66a0cfd --- /dev/null +++ b/takeoff/_docs/diagrams/quickstart_flow_diagram.puml @@ -0,0 +1,36 @@ +@startuml QuickstartFlowDiagram + +start +#red:Firebase Login; +:Google login; +#orange:Create project GCloud Flag Firebase; +:Create service account; +:Set up account & add roles; +:Verify roles; +:Activate service account; +:Create repos; +:Set Up Sonarqube; +' Creates a file with the backend cloud run url +#orange: Init Cloud Run Back; +' Creates a file with the frontend cloud run url +#orange: Init Cloud Run Front; +' Generates files for quickstart back & front +' Generates message that tells you to copy a secret from a URL (manual) +' Generates keystore file +#FF7276: Set Up Firebase; +#orange: Quickstart Backend; +' Needs the frontend url +' Needs the secret from the user in set up firebase (manual) +' Needs the keystore file +#red: Quickstart Frontend; +:Set Up backend pipelines; +#orange: Set up frontent pipelines web; +#orange: Set up frontent pipelines android; +#orange: trigger pipelines; + +end +@enduml diff --git a/takeoff/_docs/diagrams/quickstart_flow_diagram/QuickstartFlowDiagram.png b/takeoff/_docs/diagrams/quickstart_flow_diagram/QuickstartFlowDiagram.png new file mode 100644 index 000000000..c14d16243 Binary files /dev/null and b/takeoff/_docs/diagrams/quickstart_flow_diagram/QuickstartFlowDiagram.png differ diff --git a/takeoff/_docs/rocket_logo.png b/takeoff/_docs/rocket_logo.png new file mode 100644 index 000000000..2b06ec3ac Binary files /dev/null and b/takeoff/_docs/rocket_logo.png differ diff --git a/takeoff/takeoff_cli/.gitignore b/takeoff/takeoff_cli/.gitignore new file mode 100644 index 000000000..3c8a15727 --- /dev/null +++ b/takeoff/takeoff_cli/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/takeoff/takeoff_cli/CHANGELOG.md b/takeoff/takeoff_cli/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/takeoff/takeoff_cli/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/takeoff/takeoff_cli/README.asciidoc b/takeoff/takeoff_cli/README.asciidoc new file mode 100644 index 000000000..272baec37 --- /dev/null +++ b/takeoff/takeoff_cli/README.asciidoc @@ -0,0 +1,68 @@ += TakeOff CLI + +``` +takeoff_cli [arguments] +``` + +image::documentation/assets/takeoff_cli.png[800,800] + +== Commands +``` + aws Using AWS Cloud Services. + azure Using Azure Cloud Services. + gc Using Google Cloud Services. + quickstart +``` + +== Subcommands +``` + init --account [account] Initialize the account which will use the selected cloud provider. + create [arguments] Creates a new project in the specified cloud provider and sets up the environment. + run --project [projectId] Creates a shell with the selected project and the TakeOff service account. + list List all the projects created from TakeOff with the selected Cloud Provider. + web --project [projectId] [arguments] Open in web browser project resource. + clean --id [projectId] Removes all the local data of the provided project. + This will not delete the project in the cloud provider. +``` + +== Create arguments +``` + -h, --help + -n, --name Project name / ProjectId + -a, --billing-account Billing account + -b, --backend-language The technology for the BackEnd. Required if no frontend-language is specified. + [node, python, quarkus, quarkus-jvm] + --backend-version The version for the backend-language. This will have no effect unless backend-language is specified. + -f, --frontend-language The technology for the FrontEnd. Required if no backend-language is specified. + [angular, flutter] + --frontend-version The version for the frontend-language. This will have no effect unless frontend-language is specified. + -r, --region Cloud region in which the project will be created. +``` + +== Web arguments +``` + -h, --help + -p, --project [Required] ProjectId which resource need open. + -r, --resource [Required] Resource type [ide, pipeline, frontend repository, backend repository]. +``` + +== QuickStart commands +``` + viplane Automatically creates and deploys all the necessary services and resources to have VipLane on the cloud + wayat Automatically creates and deploys all the necessary services and resources to have Wayat on the cloud. +``` + +== Examples +* Windows: +``` + .\takeoff_cli gc init --account user@email.com + .\takeoff_cli gc create -n takeoff-test-project -a [billing-account] -b python 3.10 -f flutter 3.3.7 -r europe-west1 + .\takeoff_cli gc web -p takeoff-test-project -r pipeline +``` + +* Linux: +``` + ./takeoff_cli gc init --account user@email.com + ./takeoff_cli gc create -n takeoff-test-project -a [billing-account] -b python 3.10 -f flutter 3.3.7 -r europe-west1 + ./takeoff_cli gc web -p takeoff-test-project -r pipeline +``` diff --git a/takeoff/takeoff_cli/analysis_options.yaml b/takeoff/takeoff_cli/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/takeoff/takeoff_cli/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/takeoff/takeoff_cli/bin/takeoff_cli.dart b/takeoff/takeoff_cli/bin/takeoff_cli.dart new file mode 100644 index 000000000..975e78527 --- /dev/null +++ b/takeoff/takeoff_cli/bin/takeoff_cli.dart @@ -0,0 +1,11 @@ +import 'package:stack_trace/stack_trace.dart'; +import 'package:takeoff_cli/takeoff_cli.dart'; + +void main(List arguments) async { + // Avoid display the trace + Chain.capture(() { + TakeOffCli().run(arguments); + }, onError: (error, stackChain) { + print(error); + }); +} diff --git a/takeoff/takeoff_cli/bin/takeoff_cli.exe b/takeoff/takeoff_cli/bin/takeoff_cli.exe new file mode 100644 index 000000000..5823a86ea Binary files /dev/null and b/takeoff/takeoff_cli/bin/takeoff_cli.exe differ diff --git a/takeoff/takeoff_cli/coverage/lcov.info b/takeoff/takeoff_cli/coverage/lcov.info new file mode 100644 index 000000000..a91f96847 --- /dev/null +++ b/takeoff/takeoff_cli/coverage/lcov.info @@ -0,0 +1,54 @@ +SF:lib\services\project_service.dart +DA:5,1 +DA:9,0 +DA:10,0 +DA:13,1 +DA:14,1 +DA:16,3 +DA:17,3 +DA:21,2 +DA:23,1 +DA:24,3 +DA:27,3 +DA:28,2 +DA:29,1 +DA:33,1 +DA:35,2 +DA:38,1 +DA:47,2 +DA:55,1 +DA:56,2 +DA:60,1 +DA:64,2 +DA:66,1 +DA:67,2 +DA:71,1 +DA:73,1 +DA:75,3 +DA:76,0 +DA:80,2 +DA:82,1 +DA:83,1 +DA:84,2 +DA:88,2 +DA:89,0 +DA:91,2 +DA:95,1 +DA:100,1 +DA:102,2 +DA:104,3 +DA:105,3 +DA:109,1 +DA:110,1 +DA:114,2 +DA:116,1 +DA:117,1 +DA:118,2 +DA:124,2 +DA:125,0 +DA:126,2 +DA:128,1 +DA:129,2 +LF:50 +LH:45 +end_of_record diff --git a/takeoff/takeoff_cli/documentation/assets/takeoff_cli.png b/takeoff/takeoff_cli/documentation/assets/takeoff_cli.png new file mode 100644 index 000000000..c9698b63f Binary files /dev/null and b/takeoff/takeoff_cli/documentation/assets/takeoff_cli.png differ diff --git a/takeoff/takeoff_cli/documentation/diagrams/takeoff_diagram.png b/takeoff/takeoff_cli/documentation/diagrams/takeoff_diagram.png new file mode 100644 index 000000000..3e86d38f0 Binary files /dev/null and b/takeoff/takeoff_cli/documentation/diagrams/takeoff_diagram.png differ diff --git a/takeoff/takeoff_cli/lib/input/commands/aws/aws_command.dart b/takeoff/takeoff_cli/lib/input/commands/aws/aws_command.dart new file mode 100644 index 000000000..a374bc980 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/aws/aws_command.dart @@ -0,0 +1,20 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class AwsCommand extends Command { + final ProjectsService service; + @override + String get description => + "Contains all commands related to Amazon Web Services"; + + @override + String get name => "aws"; + + AwsCommand(this.service); + + @override + void run() { + Log.warning("AWS is currently not supported"); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/azure/azure_command.dart b/takeoff/takeoff_cli/lib/input/commands/azure/azure_command.dart new file mode 100644 index 000000000..b2d4a8432 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/azure/azure_command.dart @@ -0,0 +1,19 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class AzureCommand extends Command { + final ProjectsService service; + @override + String get description => "Contains all commands related to Azure"; + + @override + String get name => "azure"; + + AzureCommand(this.service); + + @override + void run() { + Log.warning("Azure is currently not supported"); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/common/clean_command.dart b/takeoff/takeoff_cli/lib/input/commands/common/clean_command.dart new file mode 100644 index 000000000..51c022277 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/common/clean_command.dart @@ -0,0 +1,23 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class CleanCommand extends Command { + final ProjectsService service; + @override + final String name = "clean"; + @override + final String description = + "Removes all the local data of the provided project. This will not delete" + " the project in the cloud provider."; + final CloudProviderId cloudProvider; + + CleanCommand(this.service, this.cloudProvider) { + argParser.addOption('id', mandatory: true); + } + + @override + void run() { + service.cleanProject(cloudProvider, argResults?["id"]); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/common/init_command.dart b/takeoff/takeoff_cli/lib/input/commands/common/init_command.dart new file mode 100644 index 000000000..c3940f0f3 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/common/init_command.dart @@ -0,0 +1,22 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class InitCommand extends Command { + final ProjectsService service; + @override + final String name = "init"; + @override + final String description = + "Initialize the account which will use the selected cloud provider."; + final CloudProviderId cloudProvider; + + InitCommand(this.service, this.cloudProvider) { + argParser.addOption('account', mandatory: true); + } + + @override + void run() { + service.initAccount(cloudProvider, argResults?["account"]); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/common/list_command.dart b/takeoff/takeoff_cli/lib/input/commands/common/list_command.dart new file mode 100644 index 000000000..fa02d4e6c --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/common/list_command.dart @@ -0,0 +1,20 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class ListCommand extends Command { + final ProjectsService service; + @override + final String name = "list"; + @override + final String description = + "List all the projects created from TakeOff with the selected Cloud Provider"; + final CloudProviderId cloudProvider; + + ListCommand(this.service, this.cloudProvider); + + @override + void run() { + service.listProjects(cloudProvider); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/common/run_command.dart b/takeoff/takeoff_cli/lib/input/commands/common/run_command.dart new file mode 100644 index 000000000..a4eb833e3 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/common/run_command.dart @@ -0,0 +1,22 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class RunCommand extends Command { + final ProjectsService service; + @override + final String name = "run"; + @override + final String description = + "Creates a shell with the selected project and the TakeOff service account"; + final CloudProviderId cloudProvider; + + RunCommand(this.service, this.cloudProvider) { + argParser.addOption("project", mandatory: true); + } + + @override + void run() { + service.runProject(argResults?["project"], cloudProvider); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/gcloud/create_gcloud_command.dart b/takeoff/takeoff_cli/lib/input/commands/gcloud/create_gcloud_command.dart new file mode 100644 index 000000000..9385d6470 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/gcloud/create_gcloud_command.dart @@ -0,0 +1,58 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class CreateGCloudCommand extends Command { + final ProjectsService service; + @override + final String name = "create"; + @override + final String description = + "Creates a new project in the specified cloud provider and sets up the environment"; + + CreateGCloudCommand(this.service) { + argParser.addOption('name', abbr: 'n', mandatory: true); + argParser.addOption('billing-account', abbr: 'a', mandatory: true); + argParser.addOption('backend-language', + abbr: 'b', + allowed: [ + Language.node.name, + Language.python.name, + Language.quarkus.name, + Language.quarkusJVM.name, + ], + help: + "The technology for the BackEnd. Required if no frontend-language is specified."); + argParser.addOption("backend-version", + help: + "The version for the backend-language. This will have no effect unless backend-language is specified."); + argParser.addOption('frontend-language', + abbr: 'f', + allowed: [ + Language.angular.name, + Language.flutter.name, + ], + help: + "The technology for the FrontEnd. Required if no backend-language is specified."); + argParser.addOption("frontend-version", + help: + "The version for the frontend-language. This will have no effect unless frontend-language is specified."); + argParser.addOption('region', + abbr: 'r', + mandatory: true, + allowed: googleCloudRegions, + help: "Google Cloud region in which the project will be created."); + } + + @override + void run() { + service.createGoogleProject( + projectName: argResults?["name"], + billingAccount: argResults?["billing-account"], + backendLanguage: Language.fromString(argResults?["backend-language"]), + backendVersion: argResults?["backend-version"], + frontendLanguage: Language.fromString(argResults?["frontend-language"]), + frontendVersion: argResults?["frontend-version"], + googleCloudRegion: argResults?["region"]); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/gcloud/gcloud_command.dart b/takeoff/takeoff_cli/lib/input/commands/gcloud/gcloud_command.dart new file mode 100644 index 000000000..2b8275ce2 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/gcloud/gcloud_command.dart @@ -0,0 +1,27 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/input/commands/common/clean_command.dart'; +import 'package:takeoff_cli/input/commands/common/init_command.dart'; +import 'package:takeoff_cli/input/commands/common/list_command.dart'; +import 'package:takeoff_cli/input/commands/common/run_command.dart'; +import 'package:takeoff_cli/input/commands/gcloud/create_gcloud_command.dart'; +import 'package:takeoff_cli/input/commands/gcloud/open_gcloud_command.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class GCloudCommand extends Command { + final ProjectsService service; + @override + String get description => "Contains all commands related to Google Cloud"; + + @override + String get name => "gc"; + + GCloudCommand(this.service) { + addSubcommand(CreateGCloudCommand(service)); + addSubcommand(CleanCommand(service, CloudProviderId.gcloud)); + addSubcommand(ListCommand(service, CloudProviderId.gcloud)); + addSubcommand(InitCommand(service, CloudProviderId.gcloud)); + addSubcommand(RunCommand(service, CloudProviderId.gcloud)); + addSubcommand(OpenGCloudCommand(service, CloudProviderId.gcloud)); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/gcloud/open_gcloud_command.dart b/takeoff/takeoff_cli/lib/input/commands/gcloud/open_gcloud_command.dart new file mode 100644 index 000000000..945f0e8e4 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/gcloud/open_gcloud_command.dart @@ -0,0 +1,43 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class OpenGCloudCommand extends Command { + final ProjectsService service; + + @override + final String name = "web"; + + final String resource = "--resource"; + + @override + final String description = "Open a project resources in web"; + final CloudProviderId cloudProvider; + + OpenGCloudCommand(this.service, this.cloudProvider) { + argParser.addOption('project', + abbr: 'p', + mandatory: true, + help: + "Add project name or create it -> execute takeoff create [project name] [arguments]"); + argParser.addOption('resource', + abbr: 'r', + mandatory: true, + allowed: [ + Resource.ide.name, + Resource.pipeline.name, + Resource.feRepo.name, + Resource.beRepo.name, + ], + help: + "Choose resource type which needs to open: ide, pipeline, fe repo, be repo."); + } + + @override + void run() { + service.openResource( + projectId: argResults?["project"], + cloudProviderId: cloudProvider, + resource: Resource.fromString(argResults?["resource"])); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_command.dart b/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_command.dart new file mode 100644 index 000000000..ab8ba5f5c --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_command.dart @@ -0,0 +1,19 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/input/commands/quickstart/quickstart_viplane_command.dart'; +import 'package:takeoff_cli/input/commands/quickstart/quickstart_wayat_command.dart'; +import 'package:takeoff_cli/services/project_service.dart'; + +class QuickstartCommand extends Command { + final ProjectsService service; + @override + final String name = "quickstart"; + @override + final String description = + "Automatically creates and deploys all the necessary" + " services and resources to have either Wayat or VipLane on the cloud."; + + QuickstartCommand(this.service) { + addSubcommand(QuickstartWayatCommand(service)); + addSubcommand(QuickstartVipLaneCommand(service)); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_viplane_command.dart b/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_viplane_command.dart new file mode 100644 index 000000000..6f63f3185 --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_viplane_command.dart @@ -0,0 +1,20 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class QuickstartVipLaneCommand extends Command { + final ProjectsService service; + @override + final String name = "viplane"; + @override + final String description = + "Automatically creates and deploys all the necessary" + " services and resources to have VipLane on the cloud."; + + QuickstartVipLaneCommand(this.service); + + @override + void run() { + Log.error("VipLane is not currently supported in quickstart"); + } +} diff --git a/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_wayat_command.dart b/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_wayat_command.dart new file mode 100644 index 000000000..4f193495c --- /dev/null +++ b/takeoff/takeoff_cli/lib/input/commands/quickstart/quickstart_wayat_command.dart @@ -0,0 +1,26 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class QuickstartWayatCommand extends Command { + final ProjectsService service; + @override + final String name = "wayat"; + @override + final String description = + "Automatically creates and deploys all the necessary" + " services and resources to have Wayat on the cloud."; + + QuickstartWayatCommand(this.service) { + argParser.addOption("billing-account", abbr: "b", mandatory: true); + argParser.addOption("google-cloud-region", + abbr: "r", mandatory: true, allowed: firebaseRegions); + } + + @override + void run() { + service.quickstartWayat( + billingAccount: argResults?["billing-account"], + googleCloudRegion: argResults?["google-cloud-region"]); + } +} diff --git a/takeoff/takeoff_cli/lib/services/project_service.dart b/takeoff/takeoff_cli/lib/services/project_service.dart new file mode 100644 index 000000000..6ffda1e92 --- /dev/null +++ b/takeoff/takeoff_cli/lib/services/project_service.dart @@ -0,0 +1,132 @@ +import 'package:takeoff_lib/takeoff_lib.dart'; + +class ProjectsService { + final TakeOffFacade _takeOffFacade; + ProjectsService( + this._takeOffFacade, + ); + + Future initAccount(CloudProviderId cloudProvider, String email) async { + await _takeOffFacade.init(email, cloudProvider, useStdin: true); + } + + Future listProjects(CloudProviderId cloudProvider) async { + CloudProvider provider = CloudProvider.fromId(cloudProvider); + + if ((await _takeOffFacade.getCurrentAccount(cloudProvider)).isEmpty) { + Log.error("You have not logged in with ${provider.name}"); + return; + } + + List projects = await _takeOffFacade.getProjects(cloudProvider); + + if (projects.isEmpty) { + Log.warning("No projects created with ${provider.name}"); + return; + } + print("Projects from ${provider.name}:"); + for (var element in projects) { + print(element); + } + } + + Future runProject( + String projectId, CloudProviderId cloudProvider) async { + _takeOffFacade.runProject(projectId, cloudProvider); + } + + Future createGoogleProject( + {required String projectName, + required String billingAccount, + required Language backendLanguage, + String? backendVersion, + required Language frontendLanguage, + String? frontendVersion, + required String googleCloudRegion}) async { + try { + await _takeOffFacade.createProjectGCloud( + projectName: projectName, + billingAccount: billingAccount, + backendLanguage: backendLanguage, + backendVersion: backendVersion, + frontendLanguage: frontendLanguage, + frontendVersion: frontendVersion, + googleCloudRegion: googleCloudRegion); + } on CreateProjectException catch (e) { + Log.error(e.message); + } + } + + Future quickstartWayat( + {required String billingAccount, + required String googleCloudRegion}) async { + try { + await _takeOffFacade.quickstartWayat( + billingAccount: billingAccount, googleCloudRegion: googleCloudRegion); + } on CreateProjectException catch (e) { + Log.error(e.message); + } + } + + Future cleanProject( + CloudProviderId cloudProviderId, String projectId) async { + CloudProvider provider = CloudProvider.fromId(cloudProviderId); + + if ((await _takeOffFacade.getCurrentAccount(cloudProviderId)).isEmpty) { + Log.error("You have not logged in with ${provider.name}"); + return; + } + + List projects = await _takeOffFacade.getProjects(cloudProviderId); + + if (!projects.contains(projectId)) { + Log.error( + "Project $projectId does not exist in TakeOff for ${provider.name}"); + return; + } + + if (!await _takeOffFacade.cleanProject(cloudProviderId, projectId)) { + Log.error("There was an error removing project $projectId"); + } else { + Log.success("Cleaned all data from project $projectId"); + } + } + + Future openResource( + {required String projectId, + required CloudProviderId cloudProviderId, + required Resource resource, + UrlLaucher? urlLaucher}) async { + CloudProvider provider = CloudProvider.fromId(cloudProviderId); + + String account = await _takeOffFacade.getCurrentAccount(cloudProviderId); + + if ((await _takeOffFacade.getCurrentAccount(cloudProviderId)).isEmpty) { + Log.error("You have not logged in with ${provider.name}"); + return; + } + + if (projectId.isEmpty) { + Log.error("Project ID cannot be empty"); + return; + } + + List projects = await _takeOffFacade.getProjects(cloudProviderId); + + if (!projects.contains(projectId)) { + Log.error( + "Project $projectId does not exist in TakeOff for ${provider.name}"); + return; + } + + try { + Uri url = + _takeOffFacade.getResource(projectId, cloudProviderId, resource); + UrlLaucher launcher = urlLaucher ?? UrlLaucher(); + launcher.launch(url.toString()); + } catch (e) { + Log.error( + "Error opening resource ${resource.name} of project $projectId"); + } + } +} diff --git a/takeoff/takeoff_cli/lib/takeoff_cli.dart b/takeoff/takeoff_cli/lib/takeoff_cli.dart new file mode 100644 index 000000000..66a84e2ed --- /dev/null +++ b/takeoff/takeoff_cli/lib/takeoff_cli.dart @@ -0,0 +1,34 @@ +import 'package:args/command_runner.dart'; +import 'package:takeoff_cli/input/commands/aws/aws_command.dart'; +import 'package:takeoff_cli/input/commands/azure/azure_command.dart'; +import 'package:takeoff_cli/input/commands/gcloud/gcloud_command.dart'; +import 'package:takeoff_cli/input/commands/quickstart/quickstart_command.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class TakeOffCli { + void run(List args) async { + print(title); + + TakeOffFacade facade = TakeOffFacade(); + await facade.initialize(); + + ProjectsService projectsService = ProjectsService(facade); + CommandRunner("takeoff", "A CLI to easily create a new cloud environment.") + ..addCommand(QuickstartCommand(projectsService)) + ..addCommand(GCloudCommand(projectsService)) + ..addCommand(AwsCommand(projectsService)) + ..addCommand(AzureCommand(projectsService)) + ..run(args); + } + + String title = " | \n" + " / \\ \n" + " / _ \\ \n" + " |.o '.| _______ _ ____ __ __\n" + " |'._.'| |__ __| | | / __ \\ / _|/ _|\n" + " | | | | __ _| | _____| | | | |_| |_ \n" + " ,'| | |`. | |/ _` | |/ / _ \\ | | | _| _|\n" + "/ | | | \\ | | (_| | < __/ |__| | | | | \n" + "|,-'--|--'-.| |_|\\__,_|_|\\_\\___|\\____/|_| |_| \n"; +} diff --git a/takeoff/takeoff_cli/pubspec.lock b/takeoff/takeoff_cli/pubspec.lock new file mode 100644 index 000000000..6cc089ef7 --- /dev/null +++ b/takeoff/takeoff_cli/pubspec.lock @@ -0,0 +1,495 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "50.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" + args: + dependency: "direct main" + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.10.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.7" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.2" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.4" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + get_it: + dependency: "direct dev" + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + lints: + dependency: "direct dev" + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + logger: + dependency: transitive + description: + name: logger + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.13" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path: + dependency: "direct dev" + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + sembast: + dependency: "direct dev" + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.6" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.1" + stack_trace: + dependency: "direct main" + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+3" + takeoff_lib: + dependency: "direct main" + description: + path: "../takeoff_lib" + relative: true + source: path + version: "1.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.22.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.16" + test_core: + dependency: transitive + description: + name: test_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.20" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "9.4.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.2 <3.0.0" diff --git a/takeoff/takeoff_cli/pubspec.yaml b/takeoff/takeoff_cli/pubspec.yaml new file mode 100644 index 000000000..8b6175818 --- /dev/null +++ b/takeoff/takeoff_cli/pubspec.yaml @@ -0,0 +1,22 @@ +name: takeoff_cli +description: A sample command-line application. +version: 1.0.0 +publish_to: 'none' + +environment: + sdk: '>=2.18.2 <3.0.0' + +dependencies: + args: ^2.3.1 + stack_trace: ^1.11.0 + takeoff_lib: + path: '../takeoff_lib' + +dev_dependencies: + lints: ^2.0.0 + test: ^1.16.0 + get_it: ^7.2.0 + mockito: ^5.3.2 + build_runner: ^2.3.2 + sembast: ^3.3.1 + path: ^1.8.0 \ No newline at end of file diff --git a/takeoff/takeoff_cli/test/services/project_service_test.dart b/takeoff/takeoff_cli/test/services/project_service_test.dart new file mode 100644 index 000000000..b4634dfb1 --- /dev/null +++ b/takeoff/takeoff_cli/test/services/project_service_test.dart @@ -0,0 +1,441 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:takeoff_cli/services/project_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/persistence/database/database_factory.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:test/test.dart'; +import 'package:path/path.dart'; + +import 'project_service_test.mocks.dart'; + +List log = []; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + late FoldersService foldersService; + TakeOffFacade facade = TakeOffFacade(); + + setUpAll(() async { + GetIt.I.registerSingleton(PlatformService()); + foldersService = FoldersService(); + GetIt.I.registerSingleton(foldersService); + facade.googleController = GoogleCloudControllerImpl(); + await databaseFactoryIo.deleteDatabase("project_service_test.db"); + }); + + setUp(() async { + log.clear(); + GetIt.I.registerSingleton( + await DbFactory(dbPath: "project_service_test.db").create()); + }); + + test( + "listProjects prints the correct message if not logged with Google Cloud", + overridePrint(() async { + ProjectsService projectsService = ProjectsService(facade); + await projectsService.listProjects(CloudProviderId.gcloud); + + expect(log.length, 1); + expect( + log.first.contains("You have not logged in with Google Cloud"), true); + })); + + test( + "listProjects prints the correct message if no projects are created with Google Cloud", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com"; + await cacheRepository.saveGoogleEmail(email); + ProjectsService projectsService = ProjectsService(facade); + await projectsService.listProjects(CloudProviderId.gcloud); + + expect(log.length, 1); + expect(log.first.contains("No projects created with Google Cloud"), true); + })); + + test( + "listProjects prints the correct messages if there are projects created with Google Cloud", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + List projects = + List.generate(15, (_) => Random().nextInt(1000000).toString()); + for (String elem in projects) { + await cacheRepository.saveGoogleProjectId(elem); + } + + ProjectsService projectsService = ProjectsService(facade); + + await projectsService.listProjects(CloudProviderId.gcloud); + + expect(log.length, 16); + expect(log.first.contains("Projects from Google Cloud:"), true); + expect(log.sublist(1), projects); + })); + + test( + "cleanProject prints the correct message if the project does not exist with Google Cloud", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + ProjectsService projectsService = ProjectsService(facade); + await projectsService.cleanProject(CloudProviderId.gcloud, "projectId"); + + expect(log.length, 1); + expect( + log.first.contains( + "Project projectId does not exist in TakeOff for Google Cloud"), + true); + })); + + test( + "cleanProject prints the correct message if the project does not exist with Google Cloud", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + List projects = + List.generate(15, (_) => Random().nextInt(1000000).toString()); + for (String elem in projects) { + await cacheRepository.saveGoogleProjectId(elem); + } + + ProjectsService projectsService = ProjectsService(facade); + await projectsService.cleanProject(CloudProviderId.gcloud, "projectId"); + + expect(log.length, 1); + expect( + log.first.contains( + "Project projectId does not exist in TakeOff for Google Cloud"), + true); + })); + + test( + "cleanProject prints the correct message if the project exist with Google Cloud", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + List projects = + List.generate(15, (_) => Random().nextInt(1000000).toString()); + for (String elem in projects) { + await cacheRepository.saveGoogleProjectId(elem); + } + await cacheRepository.saveGoogleProjectId("projectId"); + + ProjectsService projectsService = ProjectsService(facade); + await projectsService.cleanProject(CloudProviderId.gcloud, "projectId"); + + expect(log.length, 1); + expect(log.first.contains("Cleaned all data from project projectId"), true); + })); + + test("cleanProject cleans the data", overridePrint(() async { + String project = "project_${Random().nextInt(100000)}"; + + Directory directory = + Directory(join(foldersService.getHostFolders()["workspace"]!, project)); + if (directory.existsSync()) { + fail("Project directory $project already existed"); + } + directory.createSync(); + + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + List projects = + List.generate(15, (_) => Random().nextInt(1000000).toString()); + for (String elem in projects) { + await cacheRepository.saveGoogleProjectId(elem); + } + await cacheRepository.saveGoogleProjectId(project); + + ProjectsService projectsService = ProjectsService(facade); + await projectsService.cleanProject(CloudProviderId.gcloud, project); + + expect(log.length, 1); + expect(log.first.contains("Cleaned all data from project $project"), true); + expect( + (await cacheRepository.getGoogleProjectIds()).contains(project), false); + expect(directory.existsSync(), false); + })); + + test("runProject calls the correct method in the facade", () async { + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + String project = "project_${Random().nextInt(100000)}"; + int providerIndex = Random().nextInt(3); + List cloudProvider = [ + CloudProviderId.aws, + CloudProviderId.azure, + CloudProviderId.gcloud + ]; + + await projectsService.runProject(project, cloudProvider[providerIndex]); + + verify(mockTakeOffFacade.runProject(project, cloudProvider[providerIndex])) + .called(1); + }); + + test("createGoogleProject calls the correct method in the facade", () async { + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + String project = "project_${Random().nextInt(100000)}"; + + await projectsService.createGoogleProject( + projectName: project, + billingAccount: "billing", + backendLanguage: Language.node, + backendVersion: "1", + frontendLanguage: Language.flutter, + frontendVersion: "2", + googleCloudRegion: googleCloudRegions.first); + + verify(mockTakeOffFacade.createProjectGCloud( + projectName: project, + billingAccount: "billing", + backendLanguage: Language.node, + backendVersion: "1", + frontendLanguage: Language.flutter, + frontendVersion: "2", + googleCloudRegion: googleCloudRegions.first)) + .called(1); + }); + + test("createGoogleProject logs an error if it fails", overridePrint(() async { + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + String project = "project_${Random().nextInt(100000)}"; + + when(mockTakeOffFacade.createProjectGCloud( + projectName: anyNamed("projectName"), + billingAccount: anyNamed("billingAccount"), + googleCloudRegion: anyNamed("googleCloudRegion"), + backendLanguage: anyNamed("backendLanguage"), + backendVersion: anyNamed("backendVersion"), + frontendLanguage: anyNamed("frontendLanguage"), + frontendVersion: anyNamed("frontendVersion"))) + .thenThrow(CreateProjectException("Error creating project $project")); + + await projectsService.createGoogleProject( + projectName: project, + billingAccount: "billing", + backendLanguage: Language.node, + backendVersion: "1", + frontendLanguage: Language.flutter, + frontendVersion: "2", + googleCloudRegion: googleCloudRegions.first); + + expect(log.length, 1); + expect(log.first.contains("Error creating project $project"), true); + })); + + test("quickstartWayat calls the correct method in the facade", () async { + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + String billingAccount = "${Random().nextInt(100000)}"; + + await projectsService.quickstartWayat( + billingAccount: billingAccount, + googleCloudRegion: googleCloudRegions.first); + + verify(mockTakeOffFacade.quickstartWayat( + billingAccount: billingAccount, + googleCloudRegion: googleCloudRegions.first)) + .called(1); + }); + + test("quickstartWayat logs an error if it fails", overridePrint(() async { + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + String billingAccount = "${Random().nextInt(100000)}"; + + when(mockTakeOffFacade.quickstartWayat( + billingAccount: anyNamed("billingAccount"), + googleCloudRegion: anyNamed("googleCloudRegion"))) + .thenThrow(CreateProjectException( + "Error quickstarting wayat billing: $billingAccount")); + + await projectsService.quickstartWayat( + billingAccount: billingAccount, + googleCloudRegion: googleCloudRegions.first); + + expect(log.length, 1); + expect( + log.first + .contains("Error quickstarting wayat billing: $billingAccount"), + true); + })); + + test("openResouce throws an error if not logged in", overridePrint(() async { + ProjectsService projectsService = ProjectsService(facade); + + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.removeGoogleEmail(); + + String project = "project_${Random().nextInt(100000)}"; + int resourceIndex = Random().nextInt(Resource.values.length); + String googleCloudName = CloudProvider.fromId(CloudProviderId.gcloud).name; + + await projectsService.openResource( + projectId: project, + cloudProviderId: CloudProviderId.gcloud, + resource: Resource.values[resourceIndex]); + String logg = log.first; + + expect(log.length, 1); + expect(log.first.contains("You have not logged in with $googleCloudName"), + true); + })); + + test("openResouce throws an error if the ID is empty", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + ProjectsService projectsService = ProjectsService(facade); + + int resourceIndex = Random().nextInt(Resource.values.length); + + await projectsService.openResource( + projectId: "", + cloudProviderId: CloudProviderId.gcloud, + resource: Resource.values[resourceIndex]); + + expect(log.length, 1); + expect(log.first.contains("Project ID cannot be empty"), true); + })); + + test("openResouce throws an error if the project does not exist", + overridePrint(() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "test${Random().nextInt(10000)}@mail.com}"; + await cacheRepository.saveGoogleEmail(email); + + ProjectsService projectsService = ProjectsService(facade); + + String project = "project_${Random().nextInt(100000)}"; + int resourceIndex = Random().nextInt(Resource.values.length); + String googleCloudName = CloudProvider.fromId(CloudProviderId.gcloud).name; + + await projectsService.openResource( + projectId: project, + cloudProviderId: CloudProviderId.gcloud, + resource: Resource.values[resourceIndex]); + + expect(log.length, 1); + expect( + log.first.contains( + "Project $project does not exist in TakeOff for $googleCloudName"), + true); + })); + + test("openResouce calls the correct method in the facade", () async { + MockUrlLaucher mockUrlLaucher = MockUrlLaucher(); + when(mockUrlLaucher.launch(any)) + .thenAnswer((_) async => ProcessResult(1, 0, "", "")); + + String email = "test${Random().nextInt(10000)}@mail.com}"; + String project = "project_${Random().nextInt(100000)}"; + + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + when(mockTakeOffFacade.getCurrentAccount(any)) + .thenAnswer((_) async => email); + when(mockTakeOffFacade.getProjects(any)).thenAnswer((_) async => [project]); + + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + int resourceIndex = Random().nextInt(Resource.values.length); + + await projectsService.openResource( + projectId: project, + cloudProviderId: CloudProviderId.gcloud, + resource: Resource.values[resourceIndex], + urlLaucher: mockUrlLaucher); + + verify(mockTakeOffFacade.getResource( + project, CloudProviderId.gcloud, Resource.values[resourceIndex])) + .called(1); + verify(mockUrlLaucher.launch(any)).called(1); + }); + + test("openResouce logs an error if it cannot get the resource", + overridePrint(() async { + String email = "test${Random().nextInt(10000)}@mail.com}"; + String project = "project_${Random().nextInt(100000)}"; + + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + when(mockTakeOffFacade.getCurrentAccount(any)) + .thenAnswer((_) async => email); + when(mockTakeOffFacade.getProjects(any)).thenAnswer((_) async => [project]); + when(mockTakeOffFacade.getResource(any, any, any)).thenThrow(Exception(e)); + + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + int resourceIndex = Random().nextInt(Resource.values.length); + + await projectsService.openResource( + projectId: project, + cloudProviderId: CloudProviderId.gcloud, + resource: Resource.values[resourceIndex], + ); + + expect(log.length, 1); + expect( + log.first.contains( + "Error opening resource ${Resource.values[resourceIndex].name} of project $project"), + true); + })); + + test("initAccount calls the correct method in the facade", () async { + String email = "test${Random().nextInt(10000)}@mail.com}"; + + MockTakeOffFacade mockTakeOffFacade = MockTakeOffFacade(); + + ProjectsService projectsService = ProjectsService(mockTakeOffFacade); + + await projectsService.initAccount(CloudProviderId.gcloud, email); + + verify(mockTakeOffFacade.init(email, CloudProviderId.gcloud, + useStdin: true)) + .called(1); + }); + + tearDown(() async { + await GetIt.I.unregister(); + await databaseFactoryIo.deleteDatabase("project_service_test.db"); + }); +} + +void Function() overridePrint(void Function() testFn) => () { + var spec = ZoneSpecification(print: (_, __, ___, String msg) { + // Add to log instead of printing to stdout + log.add(msg); + }); + return Zone.current.fork(specification: spec).run(testFn); + }; diff --git a/takeoff/takeoff_cli/test/takeoff_cli_test.dart b/takeoff/takeoff_cli/test/takeoff_cli_test.dart new file mode 100644 index 000000000..33442f0d8 --- /dev/null +++ b/takeoff/takeoff_cli/test/takeoff_cli_test.dart @@ -0,0 +1,3 @@ +// ignore: depend_on_referenced_packages + +void main() {} diff --git a/takeoff/takeoff_gui/.gitignore b/takeoff/takeoff_gui/.gitignore new file mode 100644 index 000000000..d0d37d5d5 --- /dev/null +++ b/takeoff/takeoff_gui/.gitignore @@ -0,0 +1,49 @@ +# Unsupported platforms +/android +/ios +/web + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/takeoff/takeoff_gui/.metadata b/takeoff/takeoff_gui/.metadata new file mode 100644 index 000000000..6423aca06 --- /dev/null +++ b/takeoff/takeoff_gui/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: android + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: ios + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: linux + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: macos + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: web + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: windows + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/takeoff/takeoff_gui/README.asciidoc b/takeoff/takeoff_gui/README.asciidoc new file mode 100644 index 000000000..238ac8f4a --- /dev/null +++ b/takeoff/takeoff_gui/README.asciidoc @@ -0,0 +1,17 @@ += TakeOff GUI + +image::documentation/assets/takeoff_main.png[700,700] + +== Usage + +* Create a new project that will import and deploy a real application in your account almost automatically with Quickstart + +image::documentation/assets/takeoff_quickstart.png[700,700] + +* Create a new empty project (no preexisting code imported) using the `Create` option + +image::documentation/assets/takeoff_create.png[700,700] + +* Manage projects created with TakeOff with the resource screen to open the cloud IDE, pipelines, Frontend repository or Backend repository in your browser + +image::documentation/assets/takeoff_resources.png[700,700] diff --git a/takeoff/takeoff_gui/analysis_options.yaml b/takeoff/takeoff_gui/analysis_options.yaml new file mode 100644 index 000000000..61b6c4de1 --- /dev/null +++ b/takeoff/takeoff_gui/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/takeoff/takeoff_gui/assets/gifs/rocket.gif b/takeoff/takeoff_gui/assets/gifs/rocket.gif new file mode 100644 index 000000000..05fae6391 Binary files /dev/null and b/takeoff/takeoff_gui/assets/gifs/rocket.gif differ diff --git a/takeoff/takeoff_gui/assets/images/angular_logo.png b/takeoff/takeoff_gui/assets/images/angular_logo.png new file mode 100644 index 000000000..1e17062fb Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/angular_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/aws_logo.png b/takeoff/takeoff_gui/assets/images/aws_logo.png new file mode 100644 index 000000000..f4c059910 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/aws_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/azure_devops_logo.png b/takeoff/takeoff_gui/assets/images/azure_devops_logo.png new file mode 100644 index 000000000..fb4319c85 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/azure_devops_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/azure_logo.png b/takeoff/takeoff_gui/assets/images/azure_logo.png new file mode 100644 index 000000000..8406b3cc3 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/azure_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/flutter_logo.png b/takeoff/takeoff_gui/assets/images/flutter_logo.png new file mode 100644 index 000000000..3ae48b218 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/flutter_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/github_logo.png b/takeoff/takeoff_gui/assets/images/github_logo.png new file mode 100644 index 000000000..57bc3fe29 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/github_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/google_cloud_logo.png b/takeoff/takeoff_gui/assets/images/google_cloud_logo.png new file mode 100644 index 000000000..1e278b5d9 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/google_cloud_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/google_logo.png b/takeoff/takeoff_gui/assets/images/google_logo.png new file mode 100644 index 000000000..6b8a61a22 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/google_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/java_logo.png b/takeoff/takeoff_gui/assets/images/java_logo.png new file mode 100644 index 000000000..f91c11f56 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/java_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/python_logo.png b/takeoff/takeoff_gui/assets/images/python_logo.png new file mode 100644 index 000000000..848a7ef19 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/python_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/rocket_logo.png b/takeoff/takeoff_gui/assets/images/rocket_logo.png new file mode 100644 index 000000000..2b06ec3ac Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/rocket_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/viplane_logo.png b/takeoff/takeoff_gui/assets/images/viplane_logo.png new file mode 100644 index 000000000..01e40ce96 Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/viplane_logo.png differ diff --git a/takeoff/takeoff_gui/assets/images/wayat_logo.png b/takeoff/takeoff_gui/assets/images/wayat_logo.png new file mode 100644 index 000000000..88a4219ee Binary files /dev/null and b/takeoff/takeoff_gui/assets/images/wayat_logo.png differ diff --git a/takeoff/takeoff_gui/coverage/lcov.info b/takeoff/takeoff_gui/coverage/lcov.info new file mode 100644 index 000000000..4f8716eee --- /dev/null +++ b/takeoff/takeoff_gui/coverage/lcov.info @@ -0,0 +1,1145 @@ +SF:lib\domain\project.dart +DA:8,3 +LF:1 +LH:1 +end_of_record +SF:lib\features\home\controllers\projects_controller.dart +DA:40,0 +DA:42,0 +DA:43,0 +DA:50,1 +DA:54,1 +DA:58,1 +DA:60,2 +DA:61,4 +DA:62,3 +DA:63,4 +DA:64,1 +DA:65,1 +DA:70,1 +DA:71,2 +DA:72,1 +DA:75,1 +DA:76,1 +DA:77,2 +DA:78,2 +DA:81,0 +DA:82,0 +DA:84,0 +DA:88,0 +DA:89,0 +DA:91,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:108,0 +LF:33 +LH:16 +end_of_record +SF:lib\features\home\controllers\projects_controller.g.dart +DA:14,0 +DA:16,0 +DA:18,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:36,1 +DA:37,2 +DA:39,1 +DA:41,2 +DA:42,1 +DA:45,1 +DA:47,4 +DA:48,1 +DA:52,1 +DA:53,2 +DA:55,1 +DA:57,2 +DA:58,1 +DA:61,0 +DA:63,0 +DA:64,0 +DA:68,1 +DA:69,2 +DA:71,1 +DA:73,2 +DA:74,1 +DA:77,0 +DA:79,0 +DA:80,0 +DA:84,1 +DA:85,2 +DA:87,1 +DA:89,1 +DA:90,3 +DA:93,1 +DA:94,2 +DA:96,1 +DA:98,2 +DA:101,1 +DA:103,2 +DA:107,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +LF:51 +LH:29 +end_of_record +SF:lib\common\custom_button.dart +DA:8,3 +DA:14,3 +DA:16,3 +DA:18,3 +DA:19,6 +DA:20,3 +DA:21,3 +DA:25,3 +DA:27,3 +DA:28,6 +DA:30,6 +LF:11 +LH:11 +end_of_record +SF:lib\features\home\pages\home_page.dart +DA:12,1 +DA:17,1 +DA:19,2 +DA:20,1 +DA:21,1 +DA:22,1 +DA:23,3 +DA:25,1 +DA:26,1 +DA:27,1 +DA:29,1 +DA:30,2 +DA:31,1 +DA:32,2 +DA:34,3 +DA:35,0 +DA:36,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:44,3 +DA:47,1 +DA:48,2 +DA:50,3 +DA:52,0 +DA:53,0 +DA:54,3 +DA:56,1 +DA:57,2 +DA:58,3 +DA:60,0 +DA:61,0 +DA:62,3 +LF:33 +LH:24 +end_of_record +SF:lib\features\home\widgets\cloud_projects_list.dart +DA:12,2 +DA:21,2 +DA:23,2 +DA:25,2 +DA:26,2 +DA:27,2 +DA:29,2 +DA:30,2 +DA:31,2 +DA:32,2 +DA:33,2 +DA:38,6 +DA:39,1 +DA:41,1 +DA:43,2 +DA:44,1 +DA:45,1 +DA:46,2 +LF:18 +LH:18 +end_of_record +SF:lib\common\monitor\controllers\monitor_controller.g.dart +DA:14,0 +DA:16,0 +DA:18,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +LF:20 +LH:0 +end_of_record +SF:lib\common\monitor\controllers\monitor_controller.dart +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:39,0 +DA:41,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +LF:20 +LH:0 +end_of_record +SF:lib\features\create\utils\create_message.dart +DA:8,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:19,0 +LF:10 +LH:0 +end_of_record +SF:lib\common\monitor\pages\monitor_dialog.dart +DA:13,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:40,0 +DA:41,0 +DA:46,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:87,0 +DA:89,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:109,0 +LF:55 +LH:0 +end_of_record +SF:lib\common\monitor\pages\user_interaction_dialog.dart +DA:12,0 +DA:14,0 +DA:15,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:48,0 +DA:49,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:86,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:96,0 +LF:39 +LH:0 +end_of_record +SF:lib\features\create\controllers\create_controller.g.dart +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:52,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:90,0 +DA:92,0 +DA:93,0 +DA:100,0 +DA:102,0 +DA:103,0 +DA:106,0 +DA:108,0 +DA:109,0 +DA:116,0 +DA:118,0 +DA:119,0 +DA:122,0 +DA:124,0 +DA:125,0 +DA:132,0 +DA:134,0 +DA:137,0 +DA:139,0 +DA:143,0 +DA:145,0 +DA:148,0 +DA:150,0 +DA:154,0 +DA:156,0 +DA:159,0 +DA:161,0 +DA:165,0 +DA:167,0 +DA:170,0 +DA:172,0 +DA:176,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +LF:74 +LH:0 +end_of_record +SF:lib\features\create\controllers\create_controller.dart +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:51,0 +DA:53,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:83,0 +DA:85,0 +DA:86,0 +DA:89,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +LF:41 +LH:0 +end_of_record +SF:lib\features\create\utils\cloud_providers_comb.dart +DA:5,0 +DA:6,0 +DA:11,0 +DA:12,0 +LF:4 +LH:0 +end_of_record +SF:lib\features\create\utils\languages_versions.dart +DA:4,0 +DA:10,0 +DA:18,0 +DA:19,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +LF:10 +LH:0 +end_of_record +SF:lib\features\create\controllers\project_form_controllers\aws_form_controller.g.dart +DA:14,0 +DA:15,0 +DA:17,0 +DA:22,0 +DA:24,0 +DA:27,0 +DA:29,0 +DA:33,0 +DA:36,0 +DA:37,0 +LF:10 +LH:0 +end_of_record +SF:lib\features\create\controllers\project_form_controllers\aws_form_controller.dart +DA:16,0 +DA:25,0 +DA:28,0 +DA:32,0 +LF:4 +LH:0 +end_of_record +SF:lib\features\create\controllers\project_form_controllers\azure_form_controller.dart +DA:16,0 +DA:25,0 +DA:28,0 +DA:32,0 +LF:4 +LH:0 +end_of_record +SF:lib\features\create\controllers\project_form_controllers\azure_form_controller.g.dart +DA:14,0 +DA:15,0 +DA:17,0 +DA:22,0 +DA:24,0 +DA:27,0 +DA:29,0 +DA:33,0 +DA:36,0 +DA:37,0 +LF:10 +LH:0 +end_of_record +SF:lib\features\create\controllers\project_form_controllers\google_form_controller.g.dart +DA:14,0 +DA:15,0 +DA:17,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:70,0 +DA:72,0 +DA:75,0 +DA:77,0 +DA:81,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +LF:31 +LH:0 +end_of_record +SF:lib\features\create\controllers\project_form_controllers\google_form_controller.dart +DA:27,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:43,0 +DA:48,0 +DA:51,0 +DA:53,0 +DA:56,0 +DA:57,0 +DA:58,0 +LF:11 +LH:0 +end_of_record +SF:lib\features\create\pages\create_dialog.dart +DA:12,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:27,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:35,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +LF:29 +LH:0 +end_of_record +SF:lib\features\create\widgets\backend_form.dart +DA:13,0 +DA:15,0 +DA:17,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +LF:24 +LH:0 +end_of_record +SF:lib\features\create\widgets\dropdown_field.dart +DA:8,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:31,0 +LF:12 +LH:0 +end_of_record +SF:lib\features\create\widgets\cloud_selector.dart +DA:14,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:41,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:59,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:77,0 +LF:37 +LH:0 +end_of_record +SF:lib\features\create\widgets\frontend_form.dart +DA:12,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +LF:24 +LH:0 +end_of_record +SF:lib\features\create\widgets\project_form.dart +DA:10,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +LF:10 +LH:0 +end_of_record +SF:lib\features\create\widgets\project_forms\aws_form.dart +DA:7,0 +DA:9,0 +DA:11,0 +LF:3 +LH:0 +end_of_record +SF:lib\features\create\widgets\project_forms\azure_form.dart +DA:7,0 +DA:9,0 +DA:11,0 +LF:3 +LH:0 +end_of_record +SF:lib\features\create\widgets\project_forms\google_form.dart +DA:9,0 +DA:11,0 +DA:13,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:68,0 +LF:35 +LH:0 +end_of_record +SF:lib\features\create\widgets\repo_selector.dart +DA:14,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:43,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:58,0 +DA:59,0 +DA:66,0 +DA:67,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:87,0 +DA:89,0 +LF:46 +LH:0 +end_of_record +SF:lib\features\home\widgets\floating_action_menu.dart +DA:13,2 +DA:15,2 +DA:17,2 +DA:19,2 +DA:21,2 +DA:22,2 +DA:23,4 +DA:25,4 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:39,2 +DA:40,4 +DA:42,4 +DA:43,0 +DA:45,0 +DA:46,0 +LF:18 +LH:11 +end_of_record +SF:lib\features\home\widgets\google_login_dialog.dart +DA:13,1 +DA:15,1 +DA:17,1 +DA:18,2 +DA:20,1 +DA:23,2 +DA:24,1 +DA:27,1 +DA:28,1 +DA:35,1 +DA:40,1 +DA:41,1 +DA:49,1 +DA:50,2 +DA:51,1 +DA:52,1 +DA:53,1 +DA:57,1 +DA:58,0 +DA:59,0 +DA:63,1 +DA:64,1 +DA:67,0 +DA:68,0 +DA:69,0 +DA:78,1 +DA:79,1 +DA:80,3 +DA:81,1 +DA:82,1 +DA:83,2 +DA:84,2 +DA:85,2 +DA:86,2 +DA:87,1 +DA:89,2 +DA:99,2 +LF:37 +LH:32 +end_of_record +SF:lib\features\home\widgets\auto_closing_dialog.dart +DA:11,2 +DA:17,2 +DA:18,2 +DA:23,2 +DA:25,4 +DA:26,2 +DA:29,2 +DA:31,4 +DA:33,0 +DA:38,4 +DA:39,2 +DA:40,1 +DA:41,1 +DA:43,2 +DA:44,1 +DA:45,1 +DA:47,2 +DA:48,2 +DA:49,2 +DA:52,2 +DA:54,6 +DA:55,6 +DA:56,2 +DA:57,2 +DA:58,2 +DA:61,1 +DA:62,2 +DA:63,2 +LF:28 +LH:27 +end_of_record +SF:lib\features\home\widgets\cloud_project_item.dart +DA:9,2 +DA:12,2 +DA:16,2 +DA:18,2 +DA:20,2 +DA:23,2 +DA:25,2 +DA:26,2 +DA:27,8 +DA:29,0 +DA:30,0 +DA:31,0 +LF:12 +LH:9 +end_of_record +SF:lib\features\home\widgets\cloud_provider_header.dart +DA:5,3 +DA:11,3 +DA:18,3 +DA:20,3 +DA:21,3 +DA:22,3 +DA:23,3 +DA:24,6 +DA:25,6 +DA:26,2 +DA:29,3 +DA:31,3 +DA:34,3 +DA:38,3 +DA:39,3 +DA:40,11 +LF:16 +LH:16 +end_of_record +SF:lib\features\quickstart\controllers\quickstart_controller.dart +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:37,0 +DA:39,0 +DA:40,0 +LF:11 +LH:0 +end_of_record +SF:lib\features\quickstart\controllers\quickstart_controller.g.dart +DA:14,0 +DA:16,0 +DA:18,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:47,0 +DA:48,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:71,0 +DA:73,0 +DA:76,0 +DA:78,0 +DA:82,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:89,0 +LF:31 +LH:0 +end_of_record +SF:lib\features\quickstart\pages\quickstart_dialog.dart +DA:7,7 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:30,0 +DA:42,0 +LF:12 +LH:1 +end_of_record +SF:lib\features\quickstart\widgets\quickstart_card.dart +DA:16,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:43,0 +DA:44,0 +DA:46,0 +DA:48,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:57,0 +DA:60,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +LF:25 +LH:0 +end_of_record +SF:lib\features\quickstart\widgets\quickstart_form.dart +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +LF:8 +LH:0 +end_of_record +SF:lib\features\quickstart\widgets\viplane_form.dart +DA:9,0 +DA:11,0 +LF:2 +LH:0 +end_of_record +SF:lib\features\quickstart\widgets\wayat_form.dart +DA:11,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:25,0 +DA:28,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:42,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:61,0 +LF:28 +LH:0 +end_of_record +SF:lib\mocks\mock_projects.dart +DA:5,6 +DA:6,2 +DA:7,2 +DA:8,2 +DA:9,2 +DA:10,2 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +LF:28 +LH:6 +end_of_record diff --git a/takeoff/takeoff_gui/documentation/assets/takeoff_create.png b/takeoff/takeoff_gui/documentation/assets/takeoff_create.png new file mode 100644 index 000000000..09fa928be Binary files /dev/null and b/takeoff/takeoff_gui/documentation/assets/takeoff_create.png differ diff --git a/takeoff/takeoff_gui/documentation/assets/takeoff_main.png b/takeoff/takeoff_gui/documentation/assets/takeoff_main.png new file mode 100644 index 000000000..1ddc5d2b5 Binary files /dev/null and b/takeoff/takeoff_gui/documentation/assets/takeoff_main.png differ diff --git a/takeoff/takeoff_gui/documentation/assets/takeoff_quickstart.png b/takeoff/takeoff_gui/documentation/assets/takeoff_quickstart.png new file mode 100644 index 000000000..be8d1ddfd Binary files /dev/null and b/takeoff/takeoff_gui/documentation/assets/takeoff_quickstart.png differ diff --git a/takeoff/takeoff_gui/documentation/assets/takeoff_resources.png b/takeoff/takeoff_gui/documentation/assets/takeoff_resources.png new file mode 100644 index 000000000..55f47a726 Binary files /dev/null and b/takeoff/takeoff_gui/documentation/assets/takeoff_resources.png differ diff --git a/takeoff/takeoff_gui/documentation/diagrams/takeoff_diagram.png b/takeoff/takeoff_gui/documentation/diagrams/takeoff_diagram.png new file mode 100644 index 000000000..3e86d38f0 Binary files /dev/null and b/takeoff/takeoff_gui/documentation/diagrams/takeoff_diagram.png differ diff --git a/takeoff/takeoff_gui/l10n.yaml b/takeoff/takeoff_gui/l10n.yaml new file mode 100644 index 000000000..b75725652 --- /dev/null +++ b/takeoff/takeoff_gui/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/takeoff/takeoff_gui/lib/README.md b/takeoff/takeoff_gui/lib/README.md new file mode 100644 index 000000000..63b913cba --- /dev/null +++ b/takeoff/takeoff_gui/lib/README.md @@ -0,0 +1,40 @@ +To test create project without using the facade methods, +add the next function in features/create/controllers/create_controller.dart: +``` +Future fakeProcess(StreamController outputStream, + StreamController inputStream) async { + String receivedData; + await Future.delayed(const Duration(seconds: 1)); + + outputStream.add(GuiMessage.info("test1")); + await Future.delayed(const Duration(seconds: 4)); + + outputStream.add(GuiMessage.browser("test2", "https://www.google.com")); + receivedData = await inputStream.stream.take(1).last; + await Future.delayed(const Duration(seconds: 5)); + + outputStream.add( + GuiMessage.input("test3 $receivedData. text field", InputType.text)); + receivedData = await inputStream.stream.take(1).last; + await Future.delayed(const Duration(seconds: 5)); + + outputStream.add( + GuiMessage.input("test3 $receivedData number field", InputType.number)); + receivedData = await inputStream.stream.take(1).last; + await Future.delayed(const Duration(seconds: 5)); + + outputStream.add(GuiMessage.success( + "Success building your project", "https://www.google.com")); + } +``` + + and replace the createProject method with the following lines: + +``` +void createProject() async { + monitorController.monitorProcess(() async => fakeProcess( + monitorController.outputChannel, monitorController.inputChannel)); +} +``` + +remember to import dart:async \ No newline at end of file diff --git a/takeoff/takeoff_gui/lib/common/custom_button.dart b/takeoff/takeoff_gui/lib/common/custom_button.dart new file mode 100644 index 000000000..f9d035052 --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/custom_button.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class CustomButton extends StatelessWidget { + final String text; + final IconData icon; + final Function? onPressed; + final Color? color; + const CustomButton({ + Key? key, + required this.text, + required this.icon, + required this.onPressed, + this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed == null ? null : () => onPressed!(), + style: ElevatedButton.styleFrom( + backgroundColor: color, + minimumSize: const Size(150, 50), + maximumSize: const Size(170, 50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon), + const SizedBox(width: 10), + Text(text), + ], + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/common/custom_scroll_behaviour.dart b/takeoff/takeoff_gui/lib/common/custom_scroll_behaviour.dart new file mode 100644 index 000000000..8978e826c --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/custom_scroll_behaviour.dart @@ -0,0 +1,12 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class MyCustomScrollBehavior extends MaterialScrollBehavior { + // Override behavior methods and getters like dragDevices + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.trackpad, + }; +} diff --git a/takeoff/takeoff_gui/lib/common/error_loading_page.dart b/takeoff/takeoff_gui/lib/common/error_loading_page.dart new file mode 100644 index 000000000..feeefaedc --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/error_loading_page.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ErrorLoadingPage extends StatelessWidget { + final String message; + const ErrorLoadingPage({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: AppLocalizations.of(context)!.errorTitle, + home: Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.errorMessage), + backgroundColor: Colors.red, + ), + body: Center( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.warning_amber_outlined, + color: Colors.red, + size: 150, + ), + const SizedBox(height: 50), + Text( + message, + style: const TextStyle(fontSize: 30), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/common/icon_text_button.dart b/takeoff/takeoff_gui/lib/common/icon_text_button.dart new file mode 100644 index 000000000..0365b47db --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/icon_text_button.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class IconTextButton extends StatelessWidget { + final String text; + final IconData icon; + final double? size; + final Color? color; + final void Function()? onPressed; + const IconTextButton({ + Key? key, + required this.text, + required this.icon, + this.onPressed, + this.color, + this.size, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onPressed, + child: SizedBox( + child: Column( + children: [ + Icon( + icon, + color: color ?? Colors.white, + size: size != null ? (size! - 20) : 30, + ), + Text( + text, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: color ?? Colors.white), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/common/loading_page.dart b/takeoff/takeoff_gui/lib/common/loading_page.dart new file mode 100644 index 000000000..6aebc1f8a --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/loading_page.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class LoadingPage extends StatelessWidget { + final String message; + const LoadingPage({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 300, + width: 300, + child: Image(image: AssetImage("assets/gifs/rocket.gif")), + ), + const SizedBox(height: 20), + Text( + message, + style: const TextStyle(fontSize: 20), + ) + ], + ), + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/common/monitor/controllers/monitor_controller.dart b/takeoff/takeoff_gui/lib/common/monitor/controllers/monitor_controller.dart new file mode 100644 index 000000000..bc34e658b --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/monitor/controllers/monitor_controller.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/features/create/utils/create_message.dart'; +import 'package:takeoff_gui/features/create/utils/type_message.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +part 'monitor_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class MonitorController = _MonitorController with _$MonitorController; + +abstract class _MonitorController with Store { + StreamController outputChannel = StreamController.broadcast(); + StreamController inputChannel = StreamController.broadcast(); + + @observable + ObservableList steps = ObservableList.of([]); + + @observable + String projectUrl = ""; + + @computed + bool get hasFinished { + List messages = + steps.map((element) => element.typeMessage).toList(); + return messages.contains(TypeMessage.error) || + messages.contains(TypeMessage.success); + } + + Future monitorProcess(Function process) async { + outputChannel.stream.listen((event) { + if (event.type == MessageType.success) { + projectUrl = event.url!; + } + steps.add(CreateMessage.fromGuiMessage(event)); + }); + try { + await process(); + } catch (e) { + steps.add(CreateMessage(TypeMessage.error, e.toString())); + } + } + + bool get isSuccess { + List messages = + steps.map((element) => element.typeMessage).toList(); + return messages.contains(TypeMessage.success); + } + + void resetChannel() { + outputChannel.close(); + inputChannel.close(); + inputChannel = StreamController.broadcast(); + outputChannel = StreamController.broadcast(); + steps = ObservableList.of([]); + } +} diff --git a/takeoff/takeoff_gui/lib/common/monitor/pages/monitor_dialog.dart b/takeoff/takeoff_gui/lib/common/monitor/pages/monitor_dialog.dart new file mode 100644 index 000000000..2703d9414 --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/monitor/pages/monitor_dialog.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/monitor/controllers/monitor_controller.dart'; +import 'package:takeoff_gui/common/monitor/pages/user_interaction_dialog.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class MonitorDialog extends StatelessWidget { + final MonitorController controller = GetIt.I.get(); + MonitorDialog({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: controller.outputChannel.stream, + builder: (context, snapshot) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (snapshot.hasData) { + _generateDialog(snapshot.data as GuiMessage, context); + } + }); + return AlertDialog( + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: SingleChildScrollView( + child: Column( + children: [ + Observer( + builder: (_) => Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + controller.hasFinished + ? AppLocalizations.of(context)! + .projectCreationFinishedMessage + : AppLocalizations.of(context)! + .creatingProjectMessage, + style: const TextStyle(fontSize: 30)), + if (!controller.hasFinished) + const CircularProgressIndicator(), + ], + ), + ), + const SizedBox(height: 50), + SizedBox( + height: 600, + width: 600, + child: DecoratedBox( + decoration: const BoxDecoration(color: Colors.black), + child: Observer( + builder: (_) => Padding( + padding: const EdgeInsets.all(8.0), + child: ListView.builder( + itemCount: controller.steps.length, + itemBuilder: ((context, index) => Text( + controller.steps[index].message, + style: TextStyle( + color: controller + .steps[index].typeMessage.color), + )), + ), + ), + ), + ), + ), + ], + ), + ), + ), + actions: [ + Observer(builder: (_) { + if (controller.hasFinished) { + return CustomButton( + onPressed: () { + if (controller.isSuccess) { + launchUrl(Uri.parse(controller.projectUrl)); + controller.projectUrl = ""; + GetIt.I + .get() + .updateInitAccounts(); + } + controller.resetChannel(); + Navigator.of(context).pop(); + }, + icon: Icons.browser_updated_outlined, + text: controller.isSuccess + ? AppLocalizations.of(context)!.openProjectButton + : AppLocalizations.of(context)!.closeButton); + } + return Container(); + }) + ], + ); + }); + } + + _generateDialog(GuiMessage message, BuildContext context) { + switch (message.type) { + case MessageType.info: + break; + case MessageType.error: + break; + case MessageType.success: + break; + case MessageType.input: + case MessageType.browser: + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => UserInteractionDialog(message: message), + ); + break; + } + } +} diff --git a/takeoff/takeoff_gui/lib/common/monitor/pages/user_interaction_dialog.dart b/takeoff/takeoff_gui/lib/common/monitor/pages/user_interaction_dialog.dart new file mode 100644 index 000000000..b2d9f92e0 --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/monitor/pages/user_interaction_dialog.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_gui/common/monitor/controllers/monitor_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class UserInteractionDialog extends StatefulWidget { + final GuiMessage message; + + const UserInteractionDialog({super.key, required this.message}); + + @override + State createState() => _UserInteractionDialogState(); +} + +class _UserInteractionDialogState extends State { + final MonitorController controller = GetIt.I.get(); + + final TextEditingController textController = TextEditingController(); + late bool linkTapped; + + @override + void initState() { + linkTapped = widget.message.url == null; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + AppLocalizations.of(context)!.followStepsMessage, + style: const TextStyle(fontSize: 30), + ), + ], + ), + const SizedBox(height: 50), + SelectableText( + widget.message.message, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + if (widget.message.inputType != null) + Row( + children: [ + Expanded( + child: TextField( + controller: textController, + inputFormatters: + widget.message.inputType == InputType.number + ? [FilteringTextInputFormatter.digitsOnly] + : [], + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + ], + ) + ], + ), + ), + ), + actions: [ + if (widget.message.url != null) + CustomButton( + text: AppLocalizations.of(context)!.openLink, + icon: Icons.check_box, + onPressed: () { + launchUrl(Uri.parse(widget.message.url!)); + setState(() { + linkTapped = true; + }); + }, + ), + CustomButton( + text: AppLocalizations.of(context)!.confirm, + icon: Icons.check_box, + onPressed: (linkTapped) + ? () { + if (widget.message.inputType != null) { + controller.inputChannel.add(textController.text); + } else if (widget.message.url != null) { + controller.inputChannel.add("true"); + } + Navigator.of(context).pop(); + } + : null, + ) + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/common/tooltip.dart b/takeoff/takeoff_gui/lib/common/tooltip.dart new file mode 100644 index 000000000..0a6964ccb --- /dev/null +++ b/takeoff/takeoff_gui/lib/common/tooltip.dart @@ -0,0 +1,34 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:flutter/material.dart'; + +class TooltipMessage extends StatelessWidget { + final String message; + final Widget child; + const TooltipMessage({ + Key? key, + required this.message, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: message, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + gradient: const LinearGradient( + colors: [Colors.black87, Colors.black87]), + ), + height: 30, + padding: const EdgeInsets.all(8.0), + preferBelow: false, + textStyle: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + showDuration: const Duration(seconds: 1), + waitDuration: const Duration(seconds: 1), + child: child, + ); + } +} diff --git a/takeoff/takeoff_gui/lib/domain/project.dart b/takeoff/takeoff_gui/lib/domain/project.dart new file mode 100644 index 000000000..7023b5b28 --- /dev/null +++ b/takeoff/takeoff_gui/lib/domain/project.dart @@ -0,0 +1,12 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/takeoff_lib.dart'; + +class Project { + String name; + CloudProviderId cloud; + + Project({ + required this.name, + required this.cloud, + }); +} diff --git a/takeoff/takeoff_gui/lib/features/create/controllers/create_controller.dart b/takeoff/takeoff_gui/lib/features/create/controllers/create_controller.dart new file mode 100644 index 000000000..4b368dff9 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/controllers/create_controller.dart @@ -0,0 +1,102 @@ +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/common/monitor/controllers/monitor_controller.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/create_form_controller.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/project_form_controllers.dart'; +import 'package:takeoff_gui/features/create/utils/cloud_providers_comb.dart'; +import 'package:takeoff_gui/features/create/utils/languages_versions.dart'; +import 'package:takeoff_gui/features/create/utils/provider_ci_cd.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +part 'create_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class CreateController = _CreateController with _$CreateController; + +abstract class _CreateController with Store { + final MonitorController monitorController = GetIt.I.get(); + + @computed + CreateFormController get formController { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return GetIt.I.get(); + case CloudProviderId.aws: + return GetIt.I.get(); + case CloudProviderId.azure: + return GetIt.I.get(); + } + } + + @observable + CloudProviderId cloudProvider = CloudProviderId.gcloud; + + @observable + ProviderCICD repoProvider = ProviderCICD.gcloud; + + @observable + Language frontendLanguage = LanguagesVersions.frontendLanguages[0]; + + @observable + String frontendVersion = LanguagesVersions + .versionsLanguages[LanguagesVersions.frontendLanguages[0]]![0]; + + @observable + Language backendLanguage = LanguagesVersions.backendLanguages[0]; + + @observable + String backendVersion = LanguagesVersions + .versionsLanguages[LanguagesVersions.backendLanguages[0]]![0]; + + @computed + List get providersCICD => + CloudProvidersComb.cicd[cloudProvider]!; + + @computed + bool get isValid { + return formController.isValid && + (backendLanguage != Language.none || frontendLanguage != Language.none); + } + + @action + void setCloudProvider(CloudProviderId cloud) { + cloudProvider = cloud; + repoProvider = CloudProvidersComb.cicd[cloudProvider]![0]; + } + + void createProject() async { + monitorController.monitorProcess(() async => formController.create( + backendLanguage: backendLanguage, + backendVersion: backendVersion, + frontendLanguage: frontendLanguage, + frontendVersion: frontendVersion, + outputStream: monitorController.outputChannel, + inputStream: monitorController.inputChannel)); + } + + @action + void setFrontendLanguage(Language lang) { + frontendLanguage = lang; + frontendVersion = LanguagesVersions.versionsLanguages[frontendLanguage]![0]; + } + + @action + void setBackendLanguage(Language lang) { + backendLanguage = lang; + backendVersion = LanguagesVersions.versionsLanguages[backendLanguage]![0]; + } + + @action + void resetForm() { + cloudProvider = CloudProviderId.gcloud; + repoProvider = ProviderCICD.gcloud; + frontendLanguage = LanguagesVersions.frontendLanguages[0]; + frontendVersion = LanguagesVersions + .versionsLanguages[LanguagesVersions.frontendLanguages[0]]![0]; + backendLanguage = LanguagesVersions.backendLanguages[0]; + backendVersion = LanguagesVersions + .versionsLanguages[LanguagesVersions.backendLanguages[0]]![0]; + formController.resetForm(); + monitorController.resetChannel(); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/aws_form_controller.dart b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/aws_form_controller.dart new file mode 100644 index 000000000..445ae6405 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/aws_form_controller.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/create_form_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +part 'aws_form_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class AwsFormController = _AwsFormController with _$AwsFormController; + +abstract class _AwsFormController with Store implements CreateFormController { + final TakeOffFacade facade = GetIt.I.get(); + + @override + Future create({ + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + StreamController? outputStream, + StreamController? inputStream, + }) { + return Future.value(); + } + + @computed + @override + bool get isValid => false; + + @action + @override + void resetForm() {} +} diff --git a/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/azure_form_controller.dart b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/azure_form_controller.dart new file mode 100644 index 000000000..a794d941b --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/azure_form_controller.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/create_form_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +part 'azure_form_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class AzureFormController = _AzureFormController with _$AzureFormController; + +abstract class _AzureFormController with Store implements CreateFormController { + final TakeOffFacade facade = GetIt.I.get(); + + @override + Future create({ + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + StreamController? outputStream, + StreamController? inputStream, + }) { + return Future.value(); + } + + @computed + @override + bool get isValid => false; + + @action + @override + void resetForm() {} +} diff --git a/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/create_form_controller.dart b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/create_form_controller.dart new file mode 100644 index 000000000..3960634a5 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/create_form_controller.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:takeoff_lib/takeoff_lib.dart'; + +abstract class CreateFormController { + bool get isValid; + + Future create({ + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + StreamController? outputStream, + StreamController? inputStream, + }); + + void resetForm(); +} diff --git a/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/google_form_controller.dart b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/google_form_controller.dart new file mode 100644 index 000000000..48f54b96d --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/google_form_controller.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/create_form_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +part 'google_form_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class GoogleFormController = _GoogleFormController with _$GoogleFormController; + +abstract class _GoogleFormController + with Store + implements CreateFormController { + final TakeOffFacade facade = GetIt.I.get(); + + @observable + String projectName = ""; + + @observable + String region = ""; + + @observable + String billingAccount = ""; + + @override + Future create({ + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + StreamController? outputStream, + StreamController? inputStream, + }) { + return facade.createProjectGCloud( + projectName: projectName, + billingAccount: billingAccount, + backendLanguage: backendLanguage, + backendVersion: backendVersion, + frontendLanguage: frontendLanguage, + frontendVersion: frontendVersion, + googleCloudRegion: region, + outputStream: outputStream, + inputStream: inputStream); + } + + @computed + @override + bool get isValid => + projectName.isNotEmpty && billingAccount.isNotEmpty && region.isNotEmpty; + + @action + @override + void resetForm() { + projectName = ""; + billingAccount = ""; + region = ""; + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/project_form_controllers.dart b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/project_form_controllers.dart new file mode 100644 index 000000000..38694c51f --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/controllers/project_form_controllers/project_form_controllers.dart @@ -0,0 +1,3 @@ +export 'package:takeoff_gui/features/create/controllers/project_form_controllers/aws_form_controller.dart'; +export 'package:takeoff_gui/features/create/controllers/project_form_controllers/azure_form_controller.dart'; +export 'package:takeoff_gui/features/create/controllers/project_form_controllers/google_form_controller.dart'; diff --git a/takeoff/takeoff_gui/lib/features/create/pages/create_dialog.dart b/takeoff/takeoff_gui/lib/features/create/pages/create_dialog.dart new file mode 100644 index 000000000..b93973bbc --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/pages/create_dialog.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/tooltip.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/common/monitor/pages/monitor_dialog.dart'; +import 'package:takeoff_gui/features/create/widgets/widgets.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CreateDialog extends StatelessWidget { + final CreateController controller = GetIt.I.get(); + CreateDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.createProject, + style: const TextStyle(fontSize: 30), + ), + CloudSelector(), + const SizedBox(height: 10), + RepoSelector(), + const SizedBox(height: 10), + ProjectForm(), + const SizedBox(height: 10), + FrontendForm(), + const SizedBox(height: 10), + BackendForm(), + ], + ), + )), + actions: [ + Observer( + builder: (_) => TooltipMessage( + message: AppLocalizations.of(context)!.createButtonTooltip, + child: CustomButton( + text: AppLocalizations.of(context)!.createButton, + icon: Icons.add, + onPressed: !controller.isValid + ? null + : () { + Navigator.of(context).pop(); + controller.createProject(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => MonitorDialog(), + ); + }, + ), + ), + ), + TooltipMessage( + message: AppLocalizations.of(context)!.closeButtonTooltip, + child: CustomButton( + text: AppLocalizations.of(context)!.closeButton, + icon: Icons.close, + color: Colors.red, + onPressed: () { + Navigator.of(context).pop(); + controller.resetForm(); + }, + ), + ) + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/utils/cloud_providers_comb.dart b/takeoff/takeoff_gui/lib/features/create/utils/cloud_providers_comb.dart new file mode 100644 index 000000000..48f83a631 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/utils/cloud_providers_comb.dart @@ -0,0 +1,14 @@ +import 'package:takeoff_gui/features/create/utils/provider_ci_cd.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class CloudProvidersComb { + static Map> cicd = { + CloudProviderId.gcloud: [ + ProviderCICD.gcloud, + ProviderCICD.azureDevOps, + ProviderCICD.github + ], + CloudProviderId.azure: [ProviderCICD.azureDevOps, ProviderCICD.github], + CloudProviderId.aws: [ProviderCICD.azureDevOps, ProviderCICD.github], + }; +} diff --git a/takeoff/takeoff_gui/lib/features/create/utils/create_message.dart b/takeoff/takeoff_gui/lib/features/create/utils/create_message.dart new file mode 100644 index 000000000..8a407b61b --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/utils/create_message.dart @@ -0,0 +1,22 @@ +import 'package:takeoff_gui/features/create/utils/type_message.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class CreateMessage { + final TypeMessage typeMessage; + final String message; + + CreateMessage(this.typeMessage, this.message); + + factory CreateMessage.fromGuiMessage(GuiMessage message) { + switch (message.type) { + case MessageType.info: + return CreateMessage(TypeMessage.info, message.message); + case MessageType.success: + return CreateMessage(TypeMessage.success, message.message); + case MessageType.error: + return CreateMessage(TypeMessage.error, message.message); + default: + return CreateMessage(TypeMessage.action, message.message); + } + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/utils/languages_versions.dart b/takeoff/takeoff_gui/lib/features/create/utils/languages_versions.dart new file mode 100644 index 000000000..f8a5593b6 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/utils/languages_versions.dart @@ -0,0 +1,35 @@ +import 'package:takeoff_lib/takeoff_lib.dart'; + +class LanguagesVersions { + static List frontendLanguages = [ + Language.flutter, + Language.angular, + Language.none, + ]; + + static List backendLanguages = [ + Language.python, + Language.node, + Language.quarkus, + Language.quarkusJVM, + Language.none, + ]; + + static Map> versionsLanguages = { + Language.flutter: [ + "3.0.0", + "3.0.5", + "3.3.2", + "3.3.4", + "3.3.5", + "3.3.6", + "3.3.7" + ], + Language.angular: ["latest"], + Language.python: ["3.9", "3.10", "3.11"], + Language.node: ["latest"], + Language.quarkus: ["latest"], + Language.quarkusJVM: ["latest"], + Language.none: ["Not available"], + }; +} diff --git a/takeoff/takeoff_gui/lib/features/create/utils/provider_ci_cd.dart b/takeoff/takeoff_gui/lib/features/create/utils/provider_ci_cd.dart new file mode 100644 index 000000000..69fcea965 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/utils/provider_ci_cd.dart @@ -0,0 +1 @@ +enum ProviderCICD { github, azureDevOps, gcloud } diff --git a/takeoff/takeoff_gui/lib/features/create/utils/type_message.dart b/takeoff/takeoff_gui/lib/features/create/utils/type_message.dart new file mode 100644 index 000000000..95b1776b3 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/utils/type_message.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +enum TypeMessage { + info(color: Colors.blue), + action(color: Colors.grey), + success(color: Colors.green), + error(color: Colors.red); + + const TypeMessage({required this.color}); + + final Color color; +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/backend_form.dart b/takeoff/takeoff_gui/lib/features/create/widgets/backend_form.dart new file mode 100644 index 000000000..bb80b5eeb --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/backend_form.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/features/create/utils/languages_versions.dart'; +import 'package:takeoff_gui/features/create/widgets/dropdown_field.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + + +class BackendForm extends StatelessWidget { + final CreateController controller = GetIt.I.get(); + BackendForm({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.backendTechnology), + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: Observer( + builder: (context) => DropdownField( + callback: controller.setBackendLanguage, + dropdownValue: controller.backendLanguage, + values: LanguagesVersions.backendLanguages, + )), + ), + const SizedBox(width: 20), + Expanded( + child: Observer( + builder: (context) => DropdownField( + callback: (String? s) => + controller.backendVersion = s ?? "", + disable: controller.backendLanguage == Language.none, + dropdownValue: controller.backendVersion, + values: LanguagesVersions.versionsLanguages[ + controller.backendLanguage] ?? + [] as List, + )), + ), + ], + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/cloud_selector.dart b/takeoff/takeoff_gui/lib/features/create/widgets/cloud_selector.dart new file mode 100644 index 000000000..5eed1b98b --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/cloud_selector.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CloudSelector extends StatelessWidget { + final BoxBorder border = Border.all(color: Colors.grey, width: 3); + final BoxBorder selectedBorder = + Border.all(color: Colors.indigoAccent, width: 5); + final double squareSize = 130; + final CreateController controller = GetIt.I.get(); + CloudSelector({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.selectProvider), + const SizedBox(height: 15), + Row( + children: [ + GestureDetector( + child: Observer( + builder: (_) => Container( + height: squareSize, + width: squareSize, + decoration: BoxDecoration( + border: controller.cloudProvider == CloudProviderId.gcloud + ? selectedBorder + : border, + image: const DecorationImage( + fit: BoxFit.scaleDown, + image: + AssetImage("assets/images/google_cloud_logo.png")), + ), + ), + ), + onTap: () => controller.setCloudProvider(CloudProviderId.gcloud), + ), + const SizedBox(width: 20), + GestureDetector( + child: Observer( + builder: (_) => Container( + height: squareSize, + width: squareSize, + decoration: BoxDecoration( + border: controller.cloudProvider == CloudProviderId.aws + ? selectedBorder + : border, + image: const DecorationImage( + fit: BoxFit.scaleDown, + image: AssetImage("assets/images/aws_logo.png")), + ), + ), + ), + onTap: () => controller.setCloudProvider(CloudProviderId.aws), + ), + const SizedBox(width: 20), + GestureDetector( + child: Observer( + builder: (_) => Container( + height: squareSize, + width: squareSize, + decoration: BoxDecoration( + border: controller.cloudProvider == CloudProviderId.azure + ? selectedBorder + : border, + image: const DecorationImage( + fit: BoxFit.scaleDown, + image: AssetImage("assets/images/azure_logo.png")), + ), + ), + ), + onTap: () => controller.setCloudProvider(CloudProviderId.azure), + ), + ], + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/dropdown_field.dart b/takeoff/takeoff_gui/lib/features/create/widgets/dropdown_field.dart new file mode 100644 index 000000000..72dd31f8a --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/dropdown_field.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class DropdownField extends StatelessWidget { + final T? dropdownValue; + final void Function(T) callback; + final List values; + final bool? disable; + const DropdownField({ + Key? key, + required this.dropdownValue, + required this.values, + required this.callback, + this.disable, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + value: dropdownValue, + elevation: 16, + onChanged: disable != null && disable == true + ? null + : (value) { + callback(value as T); + }, + items: values.map>((T value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList(), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/frontend_form.dart b/takeoff/takeoff_gui/lib/features/create/widgets/frontend_form.dart new file mode 100644 index 000000000..5d4c27e4b --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/frontend_form.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/features/create/utils/languages_versions.dart'; +import 'package:takeoff_gui/features/create/widgets/dropdown_field.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class FrontendForm extends StatelessWidget { + final CreateController controller = GetIt.I.get(); + FrontendForm({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.frontendTechnology), + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: Observer( + builder: (context) => DropdownField( + callback: controller.setFrontendLanguage, + dropdownValue: controller.frontendLanguage, + values: LanguagesVersions.frontendLanguages, + )), + ), + const SizedBox(width: 20), + Expanded( + child: Observer( + builder: (context) => DropdownField( + callback: (String? s) => + controller.frontendVersion = s ?? "", + disable: controller.frontendLanguage == Language.none, + dropdownValue: controller.frontendVersion, + values: LanguagesVersions.versionsLanguages[ + controller.frontendLanguage] ?? + [] as List, + )), + ), + ], + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/project_form.dart b/takeoff/takeoff_gui/lib/features/create/widgets/project_form.dart new file mode 100644 index 000000000..a807a1ca1 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/project_form.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/features/create/widgets/project_forms/project_forms.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class ProjectForm extends StatelessWidget { + final CreateController controller = GetIt.I.get(); + ProjectForm({super.key}); + + @override + Widget build(BuildContext context) { + return Observer(builder: (_) { + switch (controller.cloudProvider) { + case CloudProviderId.gcloud: + return GoogleForm(); + case CloudProviderId.aws: + return AwsForm(); + case CloudProviderId.azure: + return AzureForm(); + } + }); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/aws_form.dart b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/aws_form.dart new file mode 100644 index 000000000..0449a7d8c --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/aws_form.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/aws_form_controller.dart'; + +class AwsForm extends StatelessWidget { + final AwsFormController controller = GetIt.I.get(); + AwsForm({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/azure_form.dart b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/azure_form.dart new file mode 100644 index 000000000..6f14c2e3c --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/azure_form.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/azure_form_controller.dart'; + +class AzureForm extends StatelessWidget { + final AzureFormController controller = GetIt.I.get(); + AzureForm({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/google_form.dart b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/google_form.dart new file mode 100644 index 000000000..0681f86cb --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/google_form.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/google_form_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class GoogleForm extends StatelessWidget { + final GoogleFormController controller = GetIt.I.get(); + GoogleForm({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.projectData), + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: Observer( + builder: (_) => TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.projectName, + errorText: controller.projectName.isEmpty + ? AppLocalizations.of(context)!.fieldRequired + : null), + onChanged: (value) => controller.projectName = value, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Observer( + builder: (_) => TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.billingAccount, + errorText: controller.billingAccount.isEmpty + ? AppLocalizations.of(context)!.fieldRequired + : null), + onChanged: (value) => controller.billingAccount = value, + ), + ), + ), + ], + ), + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: Observer(builder: (_) { + if (controller.region.isEmpty) { + controller.region = googleCloudRegions.first; + } + + return DropdownButtonFormField( + decoration: InputDecoration( + label: Text(AppLocalizations.of(context)!.region), + border: const OutlineInputBorder(), + ), + items: googleCloudRegions + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + value: controller.region, + onChanged: (value) => controller.region = value!); + }), + ), + const SizedBox(width: 20), + Expanded(child: Container()), + ], + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/project_forms.dart b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/project_forms.dart new file mode 100644 index 000000000..63ef51b9d --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/project_forms/project_forms.dart @@ -0,0 +1,3 @@ +export 'package:takeoff_gui/features/create/widgets/project_forms/aws_form.dart'; +export 'package:takeoff_gui/features/create/widgets/project_forms/azure_form.dart'; +export 'package:takeoff_gui/features/create/widgets/project_forms/google_form.dart'; diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/repo_selector.dart b/takeoff/takeoff_gui/lib/features/create/widgets/repo_selector.dart new file mode 100644 index 000000000..f112ef510 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/repo_selector.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/features/create/utils/provider_ci_cd.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class RepoSelector extends StatelessWidget { + final BoxBorder border = Border.all(color: Colors.grey, width: 3); + final BoxBorder selectedBorder = + Border.all(color: Colors.indigoAccent, width: 5); + final double squareSize = 130; + final CreateController controller = GetIt.I.get(); + RepoSelector({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.selectRepoCiCdProvider), + const SizedBox(height: 15), + Row( + children: [ + GestureDetector( + child: Observer( + builder: (_) => + (controller.providersCICD.contains(ProviderCICD.gcloud)) + ? Container( + height: squareSize, + width: squareSize, + decoration: BoxDecoration( + border: + controller.repoProvider == ProviderCICD.gcloud + ? selectedBorder + : border, + image: const DecorationImage( + fit: BoxFit.scaleDown, + image: AssetImage( + "assets/images/google_cloud_logo.png")), + ), + ) + : SizedBox(width: squareSize, height: squareSize), + ), + onTap: () => controller.repoProvider = ProviderCICD.gcloud, + ), + const SizedBox(width: 20), + GestureDetector( + child: Observer( + builder: (_) => (controller.providersCICD + .contains(ProviderCICD.azureDevOps)) + ? Container( + height: squareSize, + width: squareSize, + decoration: BoxDecoration( + border: controller.repoProvider == + ProviderCICD.azureDevOps + ? selectedBorder + : border, + image: const DecorationImage( + fit: BoxFit.scaleDown, + image: AssetImage( + "assets/images/azure_devops_logo.png")), + ), + ) + : SizedBox(width: squareSize, height: squareSize)), + onTap: () => controller.repoProvider = ProviderCICD.azureDevOps, + ), + const SizedBox(width: 20), + GestureDetector( + child: Observer( + builder: (_) => (controller.providersCICD + .contains(ProviderCICD.azureDevOps)) + ? Container( + height: squareSize, + width: squareSize, + decoration: BoxDecoration( + border: controller.repoProvider == ProviderCICD.github + ? selectedBorder + : border, + image: const DecorationImage( + fit: BoxFit.scaleDown, + image: + AssetImage("assets/images/github_logo.png")), + ), + ) + : SizedBox(width: squareSize, height: squareSize), + ), + onTap: () => controller.repoProvider = ProviderCICD.github, + ), + ], + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/create/widgets/widgets.dart b/takeoff/takeoff_gui/lib/features/create/widgets/widgets.dart new file mode 100644 index 000000000..bb26022e5 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/create/widgets/widgets.dart @@ -0,0 +1,7 @@ +export 'package:takeoff_gui/features/create/widgets/backend_form.dart'; +export 'package:takeoff_gui/features/create/widgets/project_form.dart'; +export 'package:takeoff_gui/features/create/widgets/project_forms/google_form.dart'; +export 'package:takeoff_gui/features/create/widgets/cloud_selector.dart'; +export 'package:takeoff_gui/features/create/widgets/dropdown_field.dart'; +export 'package:takeoff_gui/features/create/widgets/frontend_form.dart'; +export 'package:takeoff_gui/features/create/widgets/repo_selector.dart'; diff --git a/takeoff/takeoff_gui/lib/features/details/pages/project_details.dart b/takeoff/takeoff_gui/lib/features/details/pages/project_details.dart new file mode 100644 index 000000000..f55b35a87 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/details/pages/project_details.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_gui/features/details/pages/resource_details.dart'; +import 'package:takeoff_gui/features/details/widgets/side_bar.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ProjectDetails extends StatelessWidget { + final Project project = GetIt.I.get().selectedProject!; + ProjectDetails({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + SideBar(), + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO add dropdown to select project here + const SizedBox(height: 40), + Text( + "${project.name} ${AppLocalizations.of(context)!.projectResources}", + style: const TextStyle(fontSize: 30), + ), + const SizedBox(height: 40), + ResourceDetails(), + ], + ), + ) + ], + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/details/pages/resource_details.dart b/takeoff/takeoff_gui/lib/features/details/pages/resource_details.dart new file mode 100644 index 000000000..7a96d9c4d --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/details/pages/resource_details.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_gui/common/tooltip.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ResourceDetails extends StatelessWidget { + ResourceDetails({super.key}); + final BoxBorder border = Border.all(color: Colors.black87, width: 2); + final BoxBorder selectedBorder = + Border.all(color: Colors.indigoAccent, width: 4); + final ProjectsController controller = GetIt.I.get(); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200, + height: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TooltipMessage( + message: AppLocalizations.of(context)!.openIdeButtonTooltip, + child: CustomButton( + text: AppLocalizations.of(context)!.openIdeButton, + onPressed: () => controller.openResource(Resource.ide), + icon: Icons.code), + ), + const SizedBox(height: 20), + TooltipMessage( + message: AppLocalizations.of(context)!.openPipelineButtonTooltip, + child: CustomButton( + text: AppLocalizations.of(context)!.openPipelineButton, + onPressed: () => controller.openResource(Resource.pipeline), + icon: Icons.cloud_sync_outlined), + ), + const SizedBox(height: 20), + TooltipMessage( + message: AppLocalizations.of(context)!.openFeRepoTooltip, + child: CustomButton( + text: AppLocalizations.of(context)!.openFeRepo, + onPressed: () => controller.openResource(Resource.feRepo), + icon: Icons.account_tree_outlined), + ), + const SizedBox(height: 20), + TooltipMessage( + message: AppLocalizations.of(context)!.openBeRepoTooltip, + child: CustomButton( + text: AppLocalizations.of(context)!.openBeRepo, + onPressed: () => controller.openResource(Resource.beRepo), + icon: Icons.account_tree_outlined), + ), + ], + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/details/widgets/clean_dialog.dart b/takeoff/takeoff_gui/lib/features/details/widgets/clean_dialog.dart new file mode 100644 index 000000000..0f0a83129 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/details/widgets/clean_dialog.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CleanDialog extends StatelessWidget { + final ProjectsController controller = GetIt.I.get(); + CleanDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: Colors.red.shade200, + title: Text( + AppLocalizations.of(context)!.removeProject, + style: TextStyle(fontSize: 30), + ), + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.projectWillBeDeleted, + ), + Text( + AppLocalizations.of(context)!.onceRemovedProject, + ), + Text( + AppLocalizations.of(context)!.confirmationDeleteProject, + ), + ], + ), + )), + actions: [ + CustomButton( + text: AppLocalizations.of(context)!.removeButton, + icon: Icons.remove, + color: Colors.red.shade600, + onPressed: () { + context.go("/"); + controller.clean(); + }, + ), + CustomButton( + text: AppLocalizations.of(context)!.closeButton, + icon: Icons.close, + color: Colors.grey, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/details/widgets/side_bar.dart b/takeoff/takeoff_gui/lib/features/details/widgets/side_bar.dart new file mode 100644 index 000000000..b0f28325c --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/details/widgets/side_bar.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; +import 'package:takeoff_gui/common/icon_text_button.dart'; +import 'package:takeoff_gui/common/tooltip.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/features/create/pages/create_dialog.dart'; +import 'package:takeoff_gui/features/details/widgets/clean_dialog.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:takeoff_gui/features/quickstart/pages/quickstart_dialog.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SideBar extends StatelessWidget { + final ProjectsController controller = GetIt.I.get(); + SideBar({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Material( + elevation: 10, + child: Container( + width: 80, + decoration: BoxDecoration(color: Theme.of(context).primaryColor), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + TooltipMessage( + message: + AppLocalizations.of(context)!.quickStartButtonTooltip, + child: IconTextButton( + text: AppLocalizations.of(context)!.quickstartButton, + icon: Icons.rocket_launch, + onPressed: () => showDialog( + context: context, + builder: ((context) { + GetIt.I.get().resetForm(); + return const QuickstartDialog(); + }), + ), + ), + ), + TooltipMessage( + message: + AppLocalizations.of(context)!.createButtonTooltip, + child: IconTextButton( + text: AppLocalizations.of(context)!.createButton, + icon: Icons.add_box, + onPressed: () => showDialog( + context: context, + builder: ((context) { + GetIt.I.get().resetForm(); + return CreateDialog(); + }), + ), + ), + ), + ], + ), + Column( + children: [ + TooltipMessage( + message: AppLocalizations.of(context)!.cliButtonTooltip, + child: IconTextButton( + text: AppLocalizations.of(context)!.cliButton, + icon: Icons.terminal, + onPressed: () => controller.openCLI(), + ), + ), + TooltipMessage( + message: AppLocalizations.of(context)!.cleanButtonTooltip, + child: IconTextButton( + text: AppLocalizations.of(context)!.cleanButton, + icon: Icons.cleaning_services, + onPressed: () => showDialog( + context: context, + builder: ((context) => CleanDialog()), + ), + ), + ), + TooltipMessage( + message: AppLocalizations.of(context)!.homeButtonTooltip, + child: IconTextButton( + text: AppLocalizations.of(context)!.homeButton, + icon: Icons.home, + onPressed: () => context.go("/"), + ), + ), + ], + ) + ], + ), + ), + ), + //const VerticalDivider(thickness: 1, width: 1, color: Colors.black), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/controllers/projects_controller.dart b/takeoff/takeoff_gui/lib/features/home/controllers/projects_controller.dart new file mode 100644 index 000000000..acd8ae394 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/controllers/projects_controller.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:url_launcher/url_launcher.dart'; + +part 'projects_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class ProjectsController = _ProjectsController with _$ProjectsController; + +abstract class _ProjectsController with Store { + final TakeOffFacade facade = GetIt.I.get(); + + StreamController> channel = StreamController(); + + @observable + Project? selectedProject; + + @observable + bool waitForToken = false; + + @observable + ObservableMap> projects = ObservableMap.of({ + CloudProviderId.aws: [], + CloudProviderId.azure: [], + CloudProviderId.gcloud: [] + }); + + @observable + ObservableMap accounts = ObservableMap.of({ + CloudProviderId.aws: "", + CloudProviderId.azure: "", + CloudProviderId.gcloud: "" + }); + + @computed + bool get isLogged { + for (CloudProviderId cloud in accounts.keys) { + if (accounts[cloud]!.isNotEmpty) { + return true; + } + } + return false; + } + + @action + Future initAccount(String email, CloudProviderId cloud) { + late Future exitStatus = + facade.init(email, CloudProviderId.gcloud, stdinStream: channel.stream); + waitForToken = true; + return exitStatus; + } + + @action + Future updateInitAccounts() async { + for (CloudProviderId cloud in CloudProviderId.values) { + accounts[cloud] = await facade.getCurrentAccount(cloud); + if (accounts[cloud]!.isNotEmpty) { + projects[cloud] = (await facade.getProjects(cloud)) + .map((String e) => Project(name: e, cloud: cloud)) + .toList(); + } + } + } + + Future logOut(CloudProviderId cloud) async { + await facade.logOut(CloudProviderId.gcloud); + await updateInitAccounts(); + } + + void resetChannel() { + waitForToken = false; + channel.close(); + channel = StreamController(); + } + + void openCLI() { + Project? project = selectedProject; + if (project != null) { + facade.runProject(project.name, project.cloud); + } + } + + void clean() { + Project? project = selectedProject; + if (project != null) { + facade.cleanProject(project.cloud, project.name); + } + } + + void openResource(Resource resource) async { + Project? project = selectedProject; + if (project != null) { + Uri url = facade.getResource(project.name, project.cloud, resource); + await _launchUrl(url); + } + } + + Future _launchUrl(Uri url) async { + if (await canLaunchUrl(url)) { + launchUrl(url); + return true; + } else { + throw 'Could not launch $url'; + } + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/pages/home_page.dart b/takeoff/takeoff_gui/lib/features/home/pages/home_page.dart new file mode 100644 index 000000000..d1e0815d4 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/pages/home_page.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_projects_list.dart'; +import 'package:takeoff_gui/features/home/widgets/floating_action_menu.dart'; +import 'package:takeoff_gui/features/home/widgets/google_login_dialog.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class HomePage extends StatelessWidget { + HomePage({super.key}); + final ProjectsController projectsController = + GetIt.I.get(); + final ScrollController scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + projectsController.updateInitAccounts(); + return Scaffold( + floatingActionButton: FloatingActionMenu(), + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.appTitle), + ), + body: SingleChildScrollView( + controller: scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Observer(builder: (context) { + return CloudProjectsList( + name: AppLocalizations.of(context)!.gc, + projects: + projectsController.projects[CloudProviderId.gcloud] ?? [], + authenticateCallback: () { + showDialog( + context: context, + builder: (BuildContext context) => GoogleLoginDialog(), + ); + }, + logOutCallback: () => + projectsController.logOut(CloudProviderId.gcloud), + authAccount: + projectsController.accounts[CloudProviderId.gcloud]!, + ); + }), + CloudProjectsList( + name: AppLocalizations.of(context)!.az, + projects: + projectsController.projects[CloudProviderId.azure] ?? [], + // TODO Add loggin method + authenticateCallback: () => print("Authenticating on Azure"), + logOutCallback: () => print("LogOut on Azure"), + authAccount: projectsController.accounts[CloudProviderId.azure]!, + ), + CloudProjectsList( + name: AppLocalizations.of(context)!.aws, + projects: projectsController.projects[CloudProviderId.aws] ?? [], + // TODO Add loggin method + authenticateCallback: () => print("Authenticating on AWS"), + logOutCallback: () => print("LogOut on AWS"), + authAccount: projectsController.accounts[CloudProviderId.aws]!, + ), + ], + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/utils/type_dialog.dart b/takeoff/takeoff_gui/lib/features/home/utils/type_dialog.dart new file mode 100644 index 000000000..bd3d199f4 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/utils/type_dialog.dart @@ -0,0 +1,5 @@ +enum TypeDialog { + info, + success, + error, +} diff --git a/takeoff/takeoff_gui/lib/features/home/widgets/auto_closing_dialog.dart b/takeoff/takeoff_gui/lib/features/home/widgets/auto_closing_dialog.dart new file mode 100644 index 000000000..4b9f33727 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/widgets/auto_closing_dialog.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:takeoff_gui/features/home/utils/type_dialog.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AutoClosingDialog extends StatefulWidget { + final TypeDialog typeDialog; + final String title; + final String message; + + const AutoClosingDialog( + {super.key, + required this.typeDialog, + required this.title, + required this.message}); + + @override + State createState() => _AutoClosingDialogState(); +} + +class _AutoClosingDialogState extends State { + late Timer timer; + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + timer = Timer( + const Duration(seconds: 3), + (() => Navigator.of(context).pop()), + ); + + Color backgroundColor; + Color buttonColor; + switch (widget.typeDialog) { + case TypeDialog.info: + backgroundColor = Colors.blue.shade50; + buttonColor = Colors.blue.shade400; + break; + case TypeDialog.success: + backgroundColor = Colors.green.shade100; + buttonColor = Colors.green.shade500; + break; + case TypeDialog.error: + backgroundColor = Colors.red.shade200; + buttonColor = Colors.red.shade600; + break; + } + return AlertDialog( + backgroundColor: backgroundColor, + title: Text(widget.title), + content: Text(widget.message), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: buttonColor, + ), + onPressed: () { + timer.cancel(); + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context)!.closeButton), + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/widgets/cloud_project_item.dart b/takeoff/takeoff_gui/lib/features/home/widgets/cloud_project_item.dart new file mode 100644 index 000000000..69af85aac --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/widgets/cloud_project_item.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; + +class CloudProjectItem extends StatelessWidget { + final ProjectsController controller = GetIt.I.get(); + CloudProjectItem({ + Key? key, + required this.project, + }) : super(key: key); + + final Project project; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 200, + width: 200, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + child: Card( + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Center(child: Text(project.name))), + ), + onTap: () { + controller.selectedProject = project; + context.go("/project/${project.cloud}/${project.name}"); + }, + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/widgets/cloud_projects_list.dart b/takeoff/takeoff_gui/lib/features/home/widgets/cloud_projects_list.dart new file mode 100644 index 000000000..a7c58307e --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/widgets/cloud_projects_list.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_provider_header.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_project_item.dart'; + +class CloudProjectsList extends StatelessWidget { + final String name; + final List projects; + final String authAccount; + final Function authenticateCallback; + final Function logOutCallback; + const CloudProjectsList({ + super.key, + required this.name, + required this.projects, + required this.authenticateCallback, + required this.authAccount, + required this.logOutCallback, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: CloudProviderHeader( + name: name, + authenticateCallback: authenticateCallback, + authAccount: authAccount, + logOutCallback: logOutCallback, + ), + ), + const SizedBox(height: 10), + // List of projects + if (authAccount.isNotEmpty && projects.isNotEmpty) + SizedBox( + height: 200, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: projects.length, + itemBuilder: (context, index) { + return CloudProjectItem( + project: projects[index], + ); + }, + scrollDirection: Axis.horizontal, + ), + ), + ), + ], + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/widgets/cloud_provider_header.dart b/takeoff/takeoff_gui/lib/features/home/widgets/cloud_provider_header.dart new file mode 100644 index 000000000..87a986cf8 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/widgets/cloud_provider_header.dart @@ -0,0 +1,60 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:flutter/material.dart'; +import 'package:takeoff_gui/common/tooltip.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class CloudProviderHeader extends StatelessWidget { + const CloudProviderHeader({ + Key? key, + required this.name, + required this.authAccount, + required this.authenticateCallback, + required this.logOutCallback, + }) : super(key: key); + + final String name; + final String authAccount; + final Function authenticateCallback; + final Function logOutCallback; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Text(name), + authAccount.isNotEmpty + ? TooltipMessage( + message: AppLocalizations.of(context)! + .logOutCloudProviderTooltip + .replaceAll("PROVIDER", name), + child: IconButton( + icon: const Icon(Icons.logout_outlined), + splashRadius: 16, + onPressed: () => logOutCallback(), + ), + ) + : TooltipMessage( + message: AppLocalizations.of(context)! + .logInCloudProviderTooltip + .replaceAll("PROVIDER", name), + child: IconButton( + icon: const Icon(Icons.login_outlined), + splashRadius: 16, + onPressed: () => authenticateCallback(), + ), + ) + ], + ), + Row( + children: [ + Text(authAccount.isNotEmpty + ? authAccount + : AppLocalizations.of(context)!.notAuthenticated), + ], + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/widgets/floating_action_menu.dart b/takeoff/takeoff_gui/lib/features/home/widgets/floating_action_menu.dart new file mode 100644 index 000000000..77103206d --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/widgets/floating_action_menu.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/tooltip.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/features/create/pages/create_dialog.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:takeoff_gui/features/quickstart/pages/quickstart_dialog.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class FloatingActionMenu extends StatelessWidget { + final ProjectsController controller = GetIt.I.get(); + FloatingActionMenu({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Observer( + builder: (_) => TooltipMessage( + message: AppLocalizations.of(context)!.createButtonTooltip, + child: CustomButton( + icon: Icons.add_box_outlined, + onPressed: controller.isLogged + ? () => showDialog( + context: context, + builder: ((context) { + GetIt.I.get().resetForm(); + return CreateDialog(); + }), + barrierDismissible: false, + ) + : null, + text: AppLocalizations.of(context)!.createButton), + ), + ), + const SizedBox(width: 10), + Observer( + builder: (_) => TooltipMessage( + message: AppLocalizations.of(context)!.quickStartButtonTooltip, + child: CustomButton( + icon: Icons.rocket_launch, + onPressed: controller.isLogged + ? () => showDialog( + context: context, + builder: ((context) { + GetIt.I.get().resetForm(); + return const QuickstartDialog(); + }), + ) + : null, + text: AppLocalizations.of(context)!.quickstartButton, + ), + ), + ), + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/home/widgets/google_login_dialog.dart b/takeoff/takeoff_gui/lib/features/home/widgets/google_login_dialog.dart new file mode 100644 index 000000000..1c8b855bf --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/home/widgets/google_login_dialog.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/utils/type_dialog.dart'; +import 'package:takeoff_gui/features/home/widgets/auto_closing_dialog.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class GoogleLoginDialog extends StatelessWidget { + final TextEditingController controller = TextEditingController(); + final ProjectsController projectsController = + GetIt.I.get(); + GoogleLoginDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Observer( + builder: (context) => AlertDialog( + title: Text(AppLocalizations.of(context)!.loginGoogleMessage), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: !projectsController.waitForToken + ? [ + Text(AppLocalizations.of(context)!.enterGoogleAccountMessage), + const SizedBox(height: 10), + TextField( + controller: controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'i.e. example@gmail.com', + ), + ), + ] + : [ + Text(AppLocalizations.of(context)!.openTabMessage), + Text(AppLocalizations.of(context)!.enterTokenMessage), + const SizedBox(height: 10), + TextField( + controller: controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'ahjkfdsyui32hcdh4uD', + ), + ), + ], + ), + actions: [ + !projectsController.waitForToken + ? ElevatedButton( + onPressed: () { + _doLogin(context); + }, + child: Text(AppLocalizations.of(context)!.loginButton), + ) + : ElevatedButton( + onPressed: () { + projectsController.channel.add(controller.text.codeUnits); + }, + child: Text(AppLocalizations.of(context)!.confirmTokenButton), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + onPressed: () { + projectsController.resetChannel(); + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context)!.closeButton), + ), + ], + ), + ); + } + + void _doLogin(context) { + projectsController + .initAccount(controller.text, CloudProviderId.gcloud) + .then( + (value) { + projectsController.updateInitAccounts(); + projectsController.waitForToken = false; + Navigator.of(context).pop(); + projectsController.resetChannel(); + showDialog( + context: context, + builder: (BuildContext context) => AutoClosingDialog( + typeDialog: value ? TypeDialog.success : TypeDialog.error, + title: AppLocalizations.of(context)!.loginButton, + message: value + ? AppLocalizations.of(context)!.loggedInMessage + : AppLocalizations.of(context)!.loginFailedMessage, + ), + ); + }, + ); + controller.clear(); + } +} diff --git a/takeoff/takeoff_gui/lib/features/quickstart/controllers/quickstart_controller.dart b/takeoff/takeoff_gui/lib/features/quickstart/controllers/quickstart_controller.dart new file mode 100644 index 000000000..e88ba1151 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/controllers/quickstart_controller.dart @@ -0,0 +1,42 @@ +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart'; +import 'package:takeoff_gui/common/monitor/controllers/monitor_controller.dart'; +import 'package:takeoff_gui/features/quickstart/utils/apps.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +part 'quickstart_controller.g.dart'; + +// ignore: library_private_types_in_public_api +class QuickstartController = _QuickstartController with _$QuickstartController; + +abstract class _QuickstartController with Store { + final TakeOffFacade facade = GetIt.I.get(); + + final MonitorController monitorController = GetIt.I.get(); + + @observable + Apps app = Apps.wayat; + + @observable + String billingAccount = ""; + + @observable + String region = ""; + + @computed + bool get isValidForm => billingAccount.isNotEmpty && region.isNotEmpty; + + void createWayat() { + monitorController.monitorProcess(() async => await facade.quickstartWayat( + billingAccount: billingAccount, + googleCloudRegion: region, + outputStream: monitorController.outputChannel, + inputStream: monitorController.inputChannel)); + } + + @action + void resetForm() { + billingAccount = ""; + region = ""; + } +} diff --git a/takeoff/takeoff_gui/lib/features/quickstart/pages/quickstart_dialog.dart b/takeoff/takeoff_gui/lib/features/quickstart/pages/quickstart_dialog.dart new file mode 100644 index 000000000..271eede64 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/pages/quickstart_dialog.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:takeoff_gui/features/quickstart/utils/apps.dart'; +import 'package:takeoff_gui/features/quickstart/widgets/quickstart_card.dart'; +import 'package:takeoff_gui/features/quickstart/widgets/quickstart_form.dart'; + +class QuickstartDialog extends StatelessWidget { + const QuickstartDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + QuickstartCard( + appImage: const AssetImage("assets/images/wayat_logo.png"), + technologiesImages: const [ + AssetImage("assets/images/google_logo.png"), + AssetImage("assets/images/flutter_logo.png"), + AssetImage("assets/images/python_logo.png") + ], + appTypeSelect: Apps.wayat, + ), + QuickstartCard( + appImage: + const AssetImage("assets/images/viplane_logo.png"), + technologiesImages: const [ + AssetImage("assets/images/azure_logo.png"), + AssetImage("assets/images/angular_logo.png"), + AssetImage("assets/images/java_logo.png") + ], + appTypeSelect: Apps.viplane, + ), + ], + ), + const SizedBox(height: 20), + QuickstartForm() + ], + ), + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/quickstart/utils/apps.dart b/takeoff/takeoff_gui/lib/features/quickstart/utils/apps.dart new file mode 100644 index 000000000..a23111243 --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/utils/apps.dart @@ -0,0 +1 @@ +enum Apps { wayat, viplane } diff --git a/takeoff/takeoff_gui/lib/features/quickstart/widgets/quickstart_card.dart b/takeoff/takeoff_gui/lib/features/quickstart/widgets/quickstart_card.dart new file mode 100644 index 000000000..f3bface6c --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/widgets/quickstart_card.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:takeoff_gui/features/quickstart/utils/apps.dart'; + +class QuickstartCard extends StatelessWidget { + final BoxBorder border = Border.all(color: Colors.grey, width: 3); + final BoxBorder selectedBorder = + Border.all(color: Colors.indigoAccent, width: 5); + final double squareSize = 130; + final QuickstartController controller = GetIt.I.get(); + final ImageProvider appImage; + final List> technologiesImages; + final Apps appTypeSelect; + QuickstartCard( + {super.key, + required this.appImage, + required this.technologiesImages, + required this.appTypeSelect}); + + @override + Widget build(BuildContext context) { + List rowChildren = []; + for (ImageProvider image in technologiesImages) { + rowChildren.add( + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + image: DecorationImage(fit: BoxFit.contain, image: image), + ), + ), + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Observer( + builder: (_) => GestureDetector( + child: SizedBox( + height: 250, + width: 250, + child: DecoratedBox( + decoration: BoxDecoration( + border: + controller.app == appTypeSelect ? selectedBorder : border, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 100, + width: 150, + decoration: BoxDecoration( + image: + DecorationImage(fit: BoxFit.contain, image: appImage), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: rowChildren, + ) + ], + ), + ), + ), + onTap: () { + if (controller.app != appTypeSelect) { + controller.resetForm(); + controller.app = appTypeSelect; + } + }, + ), + ), + ); + } +} diff --git a/takeoff/takeoff_gui/lib/features/quickstart/widgets/quickstart_form.dart b/takeoff/takeoff_gui/lib/features/quickstart/widgets/quickstart_form.dart new file mode 100644 index 000000000..2dc3188ba --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/widgets/quickstart_form.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:takeoff_gui/features/quickstart/utils/apps.dart'; +import 'package:takeoff_gui/features/quickstart/widgets/viplane_form.dart'; +import 'package:takeoff_gui/features/quickstart/widgets/wayat_form.dart'; + +class QuickstartForm extends StatelessWidget { + final QuickstartController controller = GetIt.I.get(); + QuickstartForm({super.key}); + + @override + Widget build(BuildContext context) { + return Observer(builder: (_) { + switch (controller.app) { + case Apps.wayat: + return WayatForm(); + case Apps.viplane: + return VipLaneForm(); + } + }); + } +} diff --git a/takeoff/takeoff_gui/lib/features/quickstart/widgets/viplane_form.dart b/takeoff/takeoff_gui/lib/features/quickstart/widgets/viplane_form.dart new file mode 100644 index 000000000..007caf64b --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/widgets/viplane_form.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class VipLaneForm extends StatelessWidget { + final QuickstartController controller = GetIt.I.get(); + final TakeOffFacade facade = GetIt.I.get(); + VipLaneForm({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + AppLocalizations.of(context)!.comingSoonButton, + style: const TextStyle(fontSize: 20), + )); + } +} diff --git a/takeoff/takeoff_gui/lib/features/quickstart/widgets/wayat_form.dart b/takeoff/takeoff_gui/lib/features/quickstart/widgets/wayat_form.dart new file mode 100644 index 000000000..b3ceecf5b --- /dev/null +++ b/takeoff/takeoff_gui/lib/features/quickstart/widgets/wayat_form.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/monitor/pages/monitor_dialog.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_gui/common/tooltip.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class WayatForm extends StatelessWidget { + final QuickstartController controller = GetIt.I.get(); + + WayatForm({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Expanded( + child: Observer( + builder: (_) => TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.billingAccount, + errorText: controller.billingAccount.isEmpty + ? AppLocalizations.of(context)!.fieldRequired + : null), + onChanged: (value) => controller.billingAccount = value, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Observer(builder: (_) { + if (controller.region.isEmpty) { + controller.region = firebaseRegions.first; + } + + return DropdownButtonFormField( + decoration: InputDecoration( + label: Text(AppLocalizations.of(context)!.region), + border: const OutlineInputBorder(), + ), + hint: Text(AppLocalizations.of(context)!.region), + items: firebaseRegions + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + value: controller.region, + onChanged: (value) => controller.region = value!); + }), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Observer( + builder: (_) => TooltipMessage( + message: AppLocalizations.of(context)!.quickStartButtonTooltip, + child: CustomButton( + onPressed: controller.isValidForm + ? () { + Navigator.of(context).pop(); + controller.createWayat(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => + MonitorDialog(), + ); + } + : null, + icon: Icons.add_box_outlined, + text: AppLocalizations.of(context)!.quickstartButton), + ), + ) + ], + ) + ], + ); + } +} diff --git a/takeoff/takeoff_gui/lib/l10n/app_en.arb b/takeoff/takeoff_gui/lib/l10n/app_en.arb new file mode 100644 index 000000000..7224269db --- /dev/null +++ b/takeoff/takeoff_gui/lib/l10n/app_en.arb @@ -0,0 +1,67 @@ +{ + "appTitle": "Take Off", + "gc": "Google Cloud", + "az": "Azure", + "aws": "AWS", + "loginGoogleMessage": "Login on Google", + "enterGoogleAccountMessage": "Enter your google account:", + "openTabMessage": "A tab will open in your browser. Please follow the steps.", + "enterTokenMessage": "Enter your token:", + "loginButton": "Login", + "confirmTokenButton": "Confirm token", + "loggedInMessage": "You're logged in with Google", + "loginFailedMessage": "Something happened! Check your credentialss", + "createProject": "Create a project", + "projectCreationFinishedMessage": "Project creation finished", + "creatingProjectMessage": "Creating project...", + "openProjectButton": "Open project", + "quickstartButton": "QuickStart", + "createButton": "Create", + "closeButton": "Close", + "removeButton": "Remove", + "cleanButton": "Clean", + "cliButton": "CLI", + "homeButton": "Home", + "errorTitle": "Error", + "comingSoonButton": "Coming soon", + "urlError": "Could not launch URL", + "errorMessage": "Some error happened", + "projectResources": "project resources", + "openIdeButton": "Open IDE", + "openPipelineButton": "Open Pipeline", + "openFeRepo": "Open FE Repo", + "openBeRepo": "Open BE Repo", + "openLink": "Open link", + "confirm": "Confirm", + "loadingPageMessage": "Launching the app, please wait while checking the requirements...", + "errorDockerDaemon": "Some unexpected error happened, check docker daemon or try reinstalling the app.", + "errorContainerNotDetected": "A valid container runtime was not detected.\nRun either dockerd or containerd and restart TakeOff.", + "projectData": "Project data", + "projectName": "Project name", + "fieldRequired": "This field is required", + "billingAccount": "Billing Account", + "region": "Region", + "backendTechnology": "Backend technology", + "frontendTechnology": "Frontend technology", + "selectProvider": "Select a cloud provider", + "selectRepoCiCdProvider": "Select a repo & CI/CD provider", + "removeProject": "Remove project", + "projectWillBeDeleted": "The project will be deleted from local cache, but not remove from cloud.", + "onceRemovedProject": "Once removed, you won't be able to add it again.", + "confirmationDeleteProject": "Do you want to remove it?", + "followStepsMessage": "Please, follow these steps", + "notAuthenticated": "Not authenticated", + + "createButtonTooltip": "Create a new empty project (no preexisting code imported) which will be deployed in the Cloud", + "quickStartButtonTooltip": "Create and deploy Wayat in Google Cloud or VipLane on Azure", + "cliButtonTooltip": "Open the interactive Cloud provider's CLI in the context of the project", + "cleanButtonTooltip": "Remove the local data of the project.\nThis WILL NOT delete the project in the cloud", + "homeButtonTooltip": "Return to the home screen", + "openIdeButtonTooltip": "Open the cloud provider's IDE with the project in the browser", + "openPipelineButtonTooltip": "Open the project's pipelines in the browser", + "openFeRepoTooltip": "Open the FrontEnd repository in the browser", + "openBeRepoTooltip": "Open the BackEnd repository in the browser", + "logInCloudProviderTooltip": "Log in PROVIDER account", + "logOutCloudProviderTooltip": "Log out PROVIDER account", + "closeButtonTooltip": "Close the creation dialog" +} \ No newline at end of file diff --git a/takeoff/takeoff_gui/lib/l10n/locale_constants.dart b/takeoff/takeoff_gui/lib/l10n/locale_constants.dart new file mode 100644 index 000000000..526a3e22a --- /dev/null +++ b/takeoff/takeoff_gui/lib/l10n/locale_constants.dart @@ -0,0 +1,22 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; + +class LocaleConstants { + /// Get system language code + static String defaultLanguage = Platform.localeName.substring(0, 2); + + /// Return saved locale + static Locale getLocale() { + return locale(defaultLanguage); + } + + @visibleForTesting + static Locale locale(String languageCode) { + switch (languageCode) { + case 'en': + return const Locale('en', 'US'); + default: + return const Locale('en', 'US'); + } + } +} \ No newline at end of file diff --git a/takeoff/takeoff_gui/lib/main.dart b/takeoff/takeoff_gui/lib/main.dart new file mode 100644 index 000000000..bc0ecb2e6 --- /dev/null +++ b/takeoff/takeoff_gui/lib/main.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:takeoff_gui/common/custom_scroll_behaviour.dart'; +import 'package:takeoff_gui/common/monitor/controllers/monitor_controller.dart'; +import 'package:takeoff_gui/features/create/controllers/create_controller.dart'; +import 'package:takeoff_gui/common/error_loading_page.dart'; +import 'package:takeoff_gui/common/loading_page.dart'; +import 'package:takeoff_gui/features/create/controllers/project_form_controllers/project_form_controllers.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/quickstart/controllers/quickstart_controller.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:takeoff_gui/l10n/locale_constants.dart'; +import 'package:takeoff_gui/navigation/app_router.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:desktop_window/desktop_window.dart'; + +void main() async { + await registerSingletons(); + runApp(const MyApp()); + await DesktopWindow.setMinWindowSize(const Size(1200, 800)); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: GetIt.I.get().initialize(), + builder: (context, snapshot) { + final localization = + lookupAppLocalizations(LocaleConstants.getLocale()); + if (snapshot.hasError) { + return ErrorLoadingPage(message: localization.errorDockerDaemon); + } else if (snapshot.hasData) { + if (!snapshot.data!) { + return ErrorLoadingPage( + message: localization.errorContainerNotDetected, + ); + } + return MaterialApp.router( + onGenerateTitle: (context) => + AppLocalizations.of(context)!.appTitle, + scrollBehavior: MyCustomScrollBehavior(), + debugShowCheckedModeBanner: false, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: AppRouter().router, + ); + } else { + return LoadingPage( + message: localization.loadingPageMessage, + ); + } + }); + } +} + +Future registerSingletons() async { + TakeOffFacade facade = TakeOffFacade(); + GetIt.I.registerSingleton(facade); + GetIt.I.registerSingleton(ProjectsController()); + GetIt.I.registerLazySingleton(() => MonitorController()); + GetIt.I.registerLazySingleton(() => CreateController()); + GetIt.I.registerLazySingleton( + () => QuickstartController()); + GetIt.I.registerLazySingleton( + () => GoogleFormController()); + GetIt.I.registerLazySingleton(() => AwsFormController()); + GetIt.I + .registerLazySingleton(() => AzureFormController()); +} diff --git a/takeoff/takeoff_gui/lib/mocks/mock_projects.dart b/takeoff/takeoff_gui/lib/mocks/mock_projects.dart new file mode 100644 index 000000000..14f966384 --- /dev/null +++ b/takeoff/takeoff_gui/lib/mocks/mock_projects.dart @@ -0,0 +1,38 @@ +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class MockProjects { + static List projectsAWS = [ + Project(name: "AWS Fake1", cloud: CloudProviderId.aws), + Project(name: "AWS Fake2", cloud: CloudProviderId.aws), + Project(name: "AWS Fake3", cloud: CloudProviderId.aws), + Project(name: "AWS Fake4", cloud: CloudProviderId.aws), + Project(name: "AWS Fake5", cloud: CloudProviderId.aws), + ]; + + static List projectsGCloud = [ + Project(name: "GCloud Fake1", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake2", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake3", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake4", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake5", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake6", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake7", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake8", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake9", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake10", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake11", cloud: CloudProviderId.gcloud), + Project(name: "GCloud Fake12", cloud: CloudProviderId.gcloud), + ]; + + static List projectsAzure = [ + Project(name: "Azure Fake1", cloud: CloudProviderId.azure), + Project(name: "Azure Fake2", cloud: CloudProviderId.azure), + Project(name: "Azure Fake3", cloud: CloudProviderId.azure), + Project(name: "Azure Fake4", cloud: CloudProviderId.azure), + Project(name: "Azure Fake5", cloud: CloudProviderId.azure), + Project(name: "Azure Fake6", cloud: CloudProviderId.azure), + Project(name: "Azure Fake7", cloud: CloudProviderId.azure), + Project(name: "Azure Fake8", cloud: CloudProviderId.azure), + ]; +} diff --git a/takeoff/takeoff_gui/lib/navigation/app_router.dart b/takeoff/takeoff_gui/lib/navigation/app_router.dart new file mode 100644 index 000000000..fd6515210 --- /dev/null +++ b/takeoff/takeoff_gui/lib/navigation/app_router.dart @@ -0,0 +1,20 @@ +import 'package:go_router/go_router.dart'; +import 'package:takeoff_gui/features/details/pages/project_details.dart'; +import 'package:takeoff_gui/features/home/pages/home_page.dart'; + +class AppRouter { + late final GoRouter router = GoRouter( + initialLocation: "/", + debugLogDiagnostics: true, + routes: [ + GoRoute(path: "/", builder: (context, state) => HomePage(), routes: [ + GoRoute( + path: "project/:cloud/:id", + builder: (context, state) { + return ProjectDetails(); + }, + ), + ]), + ], + ); +} diff --git a/takeoff/takeoff_gui/linux/.gitignore b/takeoff/takeoff_gui/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/takeoff/takeoff_gui/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/takeoff/takeoff_gui/linux/CMakeLists.txt b/takeoff/takeoff_gui/linux/CMakeLists.txt new file mode 100644 index 000000000..ef9d36423 --- /dev/null +++ b/takeoff/takeoff_gui/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "takeoff_gui") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.takeoff_gui") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/takeoff/takeoff_gui/linux/flutter/CMakeLists.txt b/takeoff/takeoff_gui/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/takeoff/takeoff_gui/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/takeoff/takeoff_gui/linux/flutter/generated_plugin_registrant.cc b/takeoff/takeoff_gui/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..ae5025a8c --- /dev/null +++ b/takeoff/takeoff_gui/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWindowPlugin"); + desktop_window_plugin_register_with_registrar(desktop_window_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/takeoff/takeoff_gui/linux/flutter/generated_plugin_registrant.h b/takeoff/takeoff_gui/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/takeoff/takeoff_gui/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/takeoff/takeoff_gui/linux/flutter/generated_plugins.cmake b/takeoff/takeoff_gui/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..93a953228 --- /dev/null +++ b/takeoff/takeoff_gui/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_window + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/takeoff/takeoff_gui/linux/main.cc b/takeoff/takeoff_gui/linux/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/takeoff/takeoff_gui/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/takeoff/takeoff_gui/linux/my_application.cc b/takeoff/takeoff_gui/linux/my_application.cc new file mode 100644 index 000000000..e06e040e3 --- /dev/null +++ b/takeoff/takeoff_gui/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "takeoff_gui"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "takeoff_gui"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/takeoff/takeoff_gui/linux/my_application.h b/takeoff/takeoff_gui/linux/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/takeoff/takeoff_gui/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/takeoff/takeoff_gui/macos/.gitignore b/takeoff/takeoff_gui/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/takeoff/takeoff_gui/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/takeoff/takeoff_gui/macos/Flutter/Flutter-Debug.xcconfig b/takeoff/takeoff_gui/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..c2efd0b60 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/takeoff/takeoff_gui/macos/Flutter/Flutter-Release.xcconfig b/takeoff/takeoff_gui/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..c2efd0b60 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/takeoff/takeoff_gui/macos/Flutter/GeneratedPluginRegistrant.swift b/takeoff/takeoff_gui/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..dde1ad2f2 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import desktop_window +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/takeoff/takeoff_gui/macos/Runner.xcodeproj/project.pbxproj b/takeoff/takeoff_gui/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..3db7c3896 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* takeoff_gui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "takeoff_gui.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* takeoff_gui.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* takeoff_gui.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/takeoff/takeoff_gui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/takeoff/takeoff_gui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/takeoff/takeoff_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/takeoff/takeoff_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..bb524668b --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/takeoff/takeoff_gui/macos/Runner.xcworkspace/contents.xcworkspacedata b/takeoff/takeoff_gui/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/takeoff/takeoff_gui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/takeoff/takeoff_gui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/takeoff/takeoff_gui/macos/Runner/AppDelegate.swift b/takeoff/takeoff_gui/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..d53ef6437 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/takeoff/takeoff_gui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/takeoff/takeoff_gui/macos/Runner/Base.lproj/MainMenu.xib b/takeoff/takeoff_gui/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/takeoff/takeoff_gui/macos/Runner/Configs/AppInfo.xcconfig b/takeoff/takeoff_gui/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..ea6bd5bb9 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = takeoff_gui + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.spikeTakeoffGui + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/takeoff/takeoff_gui/macos/Runner/Configs/Debug.xcconfig b/takeoff/takeoff_gui/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/takeoff/takeoff_gui/macos/Runner/Configs/Release.xcconfig b/takeoff/takeoff_gui/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/takeoff/takeoff_gui/macos/Runner/Configs/Warnings.xcconfig b/takeoff/takeoff_gui/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/takeoff/takeoff_gui/macos/Runner/DebugProfile.entitlements b/takeoff/takeoff_gui/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..dddb8a30c --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/takeoff/takeoff_gui/macos/Runner/Info.plist b/takeoff/takeoff_gui/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/takeoff/takeoff_gui/macos/Runner/MainFlutterWindow.swift b/takeoff/takeoff_gui/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..2722837ec --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/takeoff/takeoff_gui/macos/Runner/Release.entitlements b/takeoff/takeoff_gui/macos/Runner/Release.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/takeoff/takeoff_gui/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/takeoff/takeoff_gui/pubspec.lock b/takeoff/takeoff_gui/pubspec.lock new file mode 100644 index 000000000..07c65445b --- /dev/null +++ b/takeoff/takeoff_gui/pubspec.lock @@ -0,0 +1,654 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "50.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.5" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.7" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.2" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.4" + desktop_window: + dependency: "direct main" + description: + name: desktop_window + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: "direct main" + description: + name: flutter_mobx + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6+4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + get_it: + dependency: "direct main" + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + go_router: + dependency: "direct main" + description: + name: go_router + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.5" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.2" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + logger: + dependency: transitive + description: + name: logger + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + mobx: + dependency: "direct main" + description: + name: mobx + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + mockito: + dependency: "direct dev" + description: + name: mockito + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + sembast: + dependency: transitive + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.6" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+3" + takeoff_lib: + dependency: "direct main" + description: + path: "../takeoff_lib" + relative: true + source: path + version: "1.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.12" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.7" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.22" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.2 <3.0.0" + flutter: ">=3.3.0" diff --git a/takeoff/takeoff_gui/pubspec.yaml b/takeoff/takeoff_gui/pubspec.yaml new file mode 100644 index 000000000..128fef8ef --- /dev/null +++ b/takeoff/takeoff_gui/pubspec.yaml @@ -0,0 +1,44 @@ +name: takeoff_gui +description: A new Flutter project. +publish_to: 'none' +version: 1.0.0+1 +environment: + sdk: '>=2.18.2 <3.0.0' +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + takeoff_lib: + path: '../takeoff_lib' + go_router: ^5.1.5 + get_it: ^7.2.0 + mobx: ^2.0.7+7 + flutter_mobx: ^2.0.6+3 + desktop_window: ^0.4.0 + url_launcher: ^6.1.7 + flutter_localizations: + sdk: flutter + intl: ^0.17.0 + flutter_launcher_icons: ^0.11.0 + + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.2.1 + mobx_codegen: ^2.0.7+2 + flutter_lints: ^2.0.0 + # Testing mocks + mockito: ^5.3.0 +flutter: + generate: true + uses-material-design: true + assets: + - assets/images/ + - assets/gifs/ + +flutter_icons: + windows: + generate: true + image_path: "assets/images/rocket_logo.png" + icon_size: 48 # min:48, max:256, default: 48 \ No newline at end of file diff --git a/takeoff/takeoff_gui/test/common/test_widget.dart b/takeoff/takeoff_gui/test/common/test_widget.dart new file mode 100644 index 000000000..5b8113174 --- /dev/null +++ b/takeoff/takeoff_gui/test/common/test_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:takeoff_gui/common/custom_scroll_behaviour.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class TestWidget extends StatelessWidget { + final Widget child; + final Widget? floatingButton; + const TestWidget({super.key, required this.child, this.floatingButton}); + + @override + Widget build(BuildContext context) { + final GoRouter router = GoRouter( + initialLocation: "/", + debugLogDiagnostics: true, + routes: [ + GoRoute( + path: "/", + builder: (context, state) => Scaffold( + body: child, + floatingActionButton: floatingButton, + )), + ], + ); + + return MaterialApp.router( + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, + scrollBehavior: MyCustomScrollBehavior(), + debugShowCheckedModeBanner: false, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: router, + ); + } +} diff --git a/takeoff/takeoff_gui/test/domain/project_test.dart b/takeoff/takeoff_gui/test/domain/project_test.dart new file mode 100644 index 000000000..695db584a --- /dev/null +++ b/takeoff/takeoff_gui/test/domain/project_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +// @GenerateMocks([]) +void main() async { + setUpAll(() async {}); + + test('Check project constructor and name', () async { + String name = "Project Name"; + Project project = Project(name: name, cloud: CloudProviderId.gcloud); + expect(project.name, name); + }); +} diff --git a/takeoff/takeoff_gui/test/features/common/custom_button_test.dart b/takeoff/takeoff_gui/test/features/common/custom_button_test.dart new file mode 100644 index 000000000..9aaf88968 --- /dev/null +++ b/takeoff/takeoff_gui/test/features/common/custom_button_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; + +// @GenerateMocks([ClassToMock]) +void main() async { + setUpAll(() async {}); + + Widget createApp(Widget floatingMenu) { + return MaterialApp( + home: Scaffold( + body: const Text(""), + floatingActionButton: floatingMenu, + ), + ); + } + + testWidgets('Widget test', (tester) async { + // Avoid overflow due to test conditions + FlutterError.onError = null; + String buttonText = "TestText"; + IconData icon = Icons.access_alarm; + bool testVar = false; + await tester.pumpWidget(createApp( + CustomButton( + text: buttonText, + icon: icon, + onPressed: () => testVar = true, + ), + )); + await tester.tap(find.byType(CustomButton)); + await tester.pumpAndSettle(); + + expect(find.text(buttonText), findsOneWidget); + expect(find.byIcon(icon), findsOneWidget); + expect(testVar, true); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/controllers/projects_controller_test.dart b/takeoff/takeoff_gui/test/features/home/controllers/projects_controller_test.dart new file mode 100644 index 000000000..c56cf5eaa --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/controllers/projects_controller_test.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import './projects_controller_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + late ProjectsController controller; + MockTakeOffFacade facade = MockTakeOffFacade(); + setUpAll(() async { + GetIt.I.registerSingleton(facade); + controller = ProjectsController(); + }); + + test('Test initAccount google cloud', () async { + String testEmail = "test@mail.com"; + CloudProviderId cloud = CloudProviderId.gcloud; + controller.initAccount(testEmail, cloud); + verify(facade.init(testEmail, cloud, stdinStream: anyNamed("stdinStream"))) + .called(1); + expect(controller.waitForToken, true); + }); + + test('Test initAccount AWS', () async { + String testEmail = "test@mail.com"; + CloudProviderId cloud = CloudProviderId.aws; + when(facade.init(testEmail, cloud)) + .thenAnswer((realInvocation) => Future.value(false)); + bool result = await controller.initAccount(testEmail, cloud); + expect(result, false); + }); + + test('Test initAccount Azure', () async { + String testEmail = "test@mail.com"; + CloudProviderId cloud = CloudProviderId.azure; + when(facade.init(testEmail, cloud)) + .thenAnswer((realInvocation) => Future.value(false)); + bool result = await controller.initAccount(testEmail, cloud); + expect(result, false); + }); + + test('Test updateInitAccounts', () async { + CloudProviderId cloud = CloudProviderId.gcloud; + String account = "test@mail.com"; + when(facade.getCurrentAccount(cloud)) + .thenAnswer((realInvocation) => Future.value("test@mail.com")); + await controller.updateInitAccounts(); + expect(controller.accounts[cloud], account); + }); + + test('Test resetChannel', () async { + controller.waitForToken = true; + StreamController oldChannel = StreamController(); + controller.resetChannel(); + expect(controller.waitForToken, false); + expect(oldChannel != controller.channel, true); + }); + + test('Test logOut', () async { + await controller.logOut(CloudProviderId.aws); + verify(facade.getCurrentAccount(any)).called(greaterThan(0)); + + await controller.logOut(CloudProviderId.azure); + verify(facade.getCurrentAccount(any)).called(greaterThan(0)); + + await controller.logOut(CloudProviderId.gcloud); + verify(facade.logOut(CloudProviderId.gcloud)).called(greaterThan(0)); + verify(facade.getCurrentAccount(any)).called(greaterThan(0)); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/pages/home_page_test.dart b/takeoff/takeoff_gui/test/features/home/pages/home_page_test.dart new file mode 100644 index 000000000..2afe9ca0f --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/pages/home_page_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mobx/mobx.dart' as mobx; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:takeoff_gui/domain/project.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/pages/home_page.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_projects_list.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'home_page_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + MockProjectsController controller = MockProjectsController(); + // Avoid overflow due to test conditions + setUpAll(() async { + GetIt.I.registerSingleton(controller); + }); + + Widget createApp(Widget body) { + return MaterialApp( + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + localeResolutionCallback: + (Locale? locale, Iterable supportedLocales) { + for (Locale supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale?.languageCode) { + return supportedLocale; + } + } + return const Locale("en", "US"); + }, + home: Scaffold( + body: body, + ), + ); + } + + testWidgets('Widget test', (tester) async { + FlutterError.onError = null; + when(controller.accounts) + .thenReturn(mobx.ObservableMap.of({ + CloudProviderId.aws: "", + CloudProviderId.azure: "", + CloudProviderId.gcloud: "", + })); + + when(controller.projects) + .thenReturn(mobx.ObservableMap>.of({ + CloudProviderId.aws: [], + CloudProviderId.azure: [], + CloudProviderId.gcloud: [], + })); + await tester.pumpWidget(createApp(HomePage())); + + expect(find.byType(CloudProjectsList), findsNWidgets(3)); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/widgets/auto_closing_dialog_test.dart b/takeoff/takeoff_gui/test/features/home/widgets/auto_closing_dialog_test.dart new file mode 100644 index 000000000..455d02095 --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/widgets/auto_closing_dialog_test.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/utils/type_dialog.dart'; +import 'package:takeoff_gui/features/home/widgets/auto_closing_dialog.dart'; + +import '../../../common/test_widget.dart'; +import 'auto_closing_dialog_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + MockProjectsController controller = MockProjectsController(); + setUpAll(() async { + GetIt.I.registerSingleton(controller); + }); + + testWidgets('Dialog has data', (tester) async { + String title = "Test Title"; + String message = "Test message"; + await tester.pumpWidget(TestWidget( + child: AutoClosingDialog( + typeDialog: TypeDialog.info, title: title, message: message))); + + expect(find.text(title), findsOneWidget); + expect(find.text(message), findsOneWidget); + expect(find.text(title), findsOneWidget); + await tester.pumpAndSettle(); + }); + + testWidgets('Dialog info shows blue color', (tester) async { + String title = "Test Title"; + String message = "Test message"; + await tester.pumpWidget(TestWidget( + child: AutoClosingDialog( + typeDialog: TypeDialog.info, title: title, message: message))); + + ElevatedButton button = tester.widget(find.byType(ElevatedButton)); + AlertDialog dialog = tester.widget(find.byType(AlertDialog)); + expect(button.style?.backgroundColor?.resolve({}), + Colors.blue.shade400); + expect(dialog.backgroundColor, Colors.blue.shade50); + }); + + testWidgets('Dialog error shows red color', (tester) async { + String title = "Test Title"; + String message = "Test message"; + await tester.pumpWidget(TestWidget( + child: AutoClosingDialog( + typeDialog: TypeDialog.error, title: title, message: message))); + + ElevatedButton button = tester.widget(find.byType(ElevatedButton)); + AlertDialog dialog = tester.widget(find.byType(AlertDialog)); + expect(button.style?.backgroundColor?.resolve({}), + Colors.red.shade600); + expect(dialog.backgroundColor, Colors.red.shade200); + }); + + testWidgets('Dialog success shows green color', (tester) async { + String title = "Test Title"; + String message = "Test message"; + await tester.pumpWidget(TestWidget( + child: AutoClosingDialog( + typeDialog: TypeDialog.success, title: title, message: message))); + + ElevatedButton button = tester.widget(find.byType(ElevatedButton)); + AlertDialog dialog = tester.widget(find.byType(AlertDialog)); + expect(button.style?.backgroundColor?.resolve({}), + Colors.green.shade500); + expect(dialog.backgroundColor, Colors.green.shade100); + }); + + testWidgets('Checks closes dialog', (tester) async { + String title = "Test Title"; + String message = "Test message"; + await tester.pumpWidget(TestWidget(child: Builder(builder: (context) { + return ElevatedButton( + child: const Text("openDialog"), + onPressed: () => showDialog( + context: context, + builder: (context) => AutoClosingDialog( + typeDialog: TypeDialog.success, + title: title, + message: message)), + ); + }))); + + await tester.tap(find.widgetWithText(ElevatedButton, "openDialog")); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(ElevatedButton, "Close")); + await tester.pumpAndSettle(); + expect(find.byType(AutoClosingDialog), findsNothing); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/widgets/cloud_projects_list_test.dart b/takeoff/takeoff_gui/test/features/home/widgets/cloud_projects_list_test.dart new file mode 100644 index 000000000..4627662bf --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/widgets/cloud_projects_list_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_projects_list.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_project_item.dart'; +import 'package:takeoff_gui/mocks/mock_projects.dart'; +import '../../../common/test_widget.dart'; +import 'cloud_projects_list_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + setUpAll(() { + MockProjectsController mockController = MockProjectsController(); + GetIt.I.registerSingleton(mockController); + }); + + testWidgets('Show projects with account', (tester) async { + String nameList = "AWS"; + + await tester.pumpWidget(TestWidget( + child: CloudProjectsList( + name: nameList, + authAccount: "test@mail.com", + projects: MockProjects.projectsAWS, + authenticateCallback: () => true, + logOutCallback: () => true))); + await tester.pumpAndSettle(); + expect(find.text(nameList), findsOneWidget); + expect(find.byType(CloudProjectItem), findsWidgets); + }); + + testWidgets('Hide projects if not logged', (tester) async { + String nameList = "AWS"; + + await tester.pumpWidget(TestWidget( + child: CloudProjectsList( + name: nameList, + authAccount: "", + projects: MockProjects.projectsAWS, + authenticateCallback: () => true, + logOutCallback: () => true))); + expect(find.text(nameList), findsOneWidget); + expect(find.byType(CloudProjectItem), findsNothing); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/widgets/cloud_provider_header_test.dart b/takeoff/takeoff_gui/test/features/home/widgets/cloud_provider_header_test.dart new file mode 100644 index 000000000..937816b0d --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/widgets/cloud_provider_header_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_provider_header.dart'; + +import '../../../common/test_widget.dart'; + +void main() async { + setUpAll(() async {}); + + testWidgets('Headers has IconButton', (tester) async { + String providerName = "testProviderName"; + await tester.pumpWidget(TestWidget( + child: CloudProviderHeader( + authAccount: "", + name: providerName, + authenticateCallback: () => true, + logOutCallback: () => true))); + + expect(find.byIcon(Icons.login_outlined), findsOneWidget); + }); + testWidgets('Headers without authenticated user', (tester) async { + String providerName = "testProviderName"; + await tester.pumpWidget(TestWidget( + child: CloudProviderHeader( + authAccount: "", + name: providerName, + authenticateCallback: () => true, + logOutCallback: () => true))); + + expect(find.text("Not authenticated"), findsOneWidget); + }); + + testWidgets('Headers with authenticated user', (tester) async { + String providerName = "testProviderName"; + String userAccount = "user@mail.com"; + await tester.pumpWidget(TestWidget( + child: CloudProviderHeader( + name: providerName, + authAccount: userAccount, + authenticateCallback: () => true, + logOutCallback: () => true))); + + expect(find.text(userAccount), findsOneWidget); + }); + + testWidgets('Auth callback is called', (tester) async { + String providerName = ""; + String userAccount = ""; + bool testVar = false; + await tester.pumpWidget(TestWidget( + child: CloudProviderHeader( + name: providerName, + authAccount: userAccount, + authenticateCallback: () => testVar = true, + logOutCallback: () => testVar = false))); + + expect(testVar, false); + + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + + expect(testVar, true); + }); + + testWidgets('LogOut callback is called', (tester) async { + String providerName = "testProviderName"; + String userAccount = "user@mail.com"; + bool testVar = false; + await tester.pumpWidget(TestWidget( + child: CloudProviderHeader( + name: providerName, + authAccount: userAccount, + authenticateCallback: () => testVar = false, + logOutCallback: () => testVar = true))); + + expect(testVar, false); + + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + + expect(testVar, true); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/widgets/cloud_provider_item_test.dart b/takeoff/takeoff_gui/test/features/home/widgets/cloud_provider_item_test.dart new file mode 100644 index 000000000..33139b1f5 --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/widgets/cloud_provider_item_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/widgets/cloud_project_item.dart'; +import 'package:takeoff_gui/mocks/mock_projects.dart'; +import 'cloud_provider_item_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + setUpAll(() async { + MockProjectsController mockController = MockProjectsController(); + GetIt.I.registerSingleton(mockController); + }); + + Widget createApp(Widget body) { + return MaterialApp( + home: Scaffold( + body: body, + ), + ); + } + + testWidgets('Widget test', (tester) async { + await tester.pumpWidget( + createApp(CloudProjectItem(project: MockProjects.projectsAWS[0]))); + + expect(find.text(MockProjects.projectsAWS[0].name), findsOneWidget); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/widgets/floating_action_menu_test.dart b/takeoff/takeoff_gui/test/features/home/widgets/floating_action_menu_test.dart new file mode 100644 index 000000000..0264d0d2a --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/widgets/floating_action_menu_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +// import 'package:mockito/mockito.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/common/custom_button.dart'; +import 'package:takeoff_gui/features/home/widgets/floating_action_menu.dart'; +import '../../../common/test_widget.dart'; +import 'floating_action_menu_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + setUp(() { + MockProjectsController controller = MockProjectsController(); + GetIt.I.registerSingleton(controller); + }); + + testWidgets('Two action buttons for create and quickstart', (tester) async { + // Avoid overflow due to test conditions + FlutterError.onError = null; + await tester.pumpWidget( + TestWidget(floatingButton: FloatingActionMenu(), child: Container())); + expect(find.byType(CustomButton), findsNWidgets(2)); + expect(find.text("Create"), findsOneWidget); + expect(find.text("QuickStart"), findsOneWidget); + }); +} diff --git a/takeoff/takeoff_gui/test/features/home/widgets/google_login_dialog_test.dart b/takeoff/takeoff_gui/test/features/home/widgets/google_login_dialog_test.dart new file mode 100644 index 000000000..756032068 --- /dev/null +++ b/takeoff/takeoff_gui/test/features/home/widgets/google_login_dialog_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:takeoff_gui/features/home/controllers/projects_controller.dart'; +import 'package:takeoff_gui/features/home/widgets/google_login_dialog.dart'; + +import '../../../common/test_widget.dart'; +import 'google_login_dialog_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() async { + MockProjectsController controller = MockProjectsController(); + setUpAll(() async { + GetIt.I.registerSingleton(controller); + }); + + testWidgets('First step of login dialog', (tester) async { + when(controller.waitForToken).thenReturn(false); + await tester.pumpWidget(TestWidget(child: GoogleLoginDialog())); + + expect(find.text("Enter your google account:"), findsOneWidget); + expect(find.byType(TextField), findsOneWidget); + expect(find.widgetWithText(ElevatedButton, "Login"), findsOneWidget); + expect(find.widgetWithText(ElevatedButton, "Close"), findsOneWidget); + }); + + testWidgets('Second step of login dialog', (tester) async { + when(controller.waitForToken).thenReturn(true); + await tester.pumpWidget(TestWidget(child: GoogleLoginDialog())); + + expect(find.text("Enter your token:"), findsOneWidget); + expect(find.byType(TextField), findsOneWidget); + expect( + find.widgetWithText(ElevatedButton, "Confirm token"), findsOneWidget); + expect(find.widgetWithText(ElevatedButton, "Close"), findsOneWidget); + }); + + testWidgets('Check doLogin', (tester) async { + when(controller.waitForToken).thenReturn(false); + await tester.pumpWidget(TestWidget(child: Builder(builder: (context) { + return ElevatedButton( + child: const Text("openDialog"), + onPressed: () => showDialog( + context: context, builder: (context) => GoogleLoginDialog()), + ); + }))); + + await tester.tap(find.widgetWithText(ElevatedButton, "openDialog")); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(ElevatedButton, "Login")); + await tester.pumpAndSettle(); + + verify(controller.initAccount(any, any)).called(1); + verify(controller.updateInitAccounts()).called(1); + }); +} diff --git a/takeoff/takeoff_gui/windows/.gitignore b/takeoff/takeoff_gui/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/takeoff/takeoff_gui/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/takeoff/takeoff_gui/windows/CMakeLists.txt b/takeoff/takeoff_gui/windows/CMakeLists.txt new file mode 100644 index 000000000..22bb91eb7 --- /dev/null +++ b/takeoff/takeoff_gui/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(takeoff_gui LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "takeoff_gui") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/takeoff/takeoff_gui/windows/flutter/CMakeLists.txt b/takeoff/takeoff_gui/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..930d2071a --- /dev/null +++ b/takeoff/takeoff_gui/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/takeoff/takeoff_gui/windows/flutter/generated_plugin_registrant.cc b/takeoff/takeoff_gui/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..33bc361ef --- /dev/null +++ b/takeoff/takeoff_gui/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWindowPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/takeoff/takeoff_gui/windows/flutter/generated_plugin_registrant.h b/takeoff/takeoff_gui/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/takeoff/takeoff_gui/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/takeoff/takeoff_gui/windows/flutter/generated_plugins.cmake b/takeoff/takeoff_gui/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..d3bb57851 --- /dev/null +++ b/takeoff/takeoff_gui/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_window + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/takeoff/takeoff_gui/windows/runner/CMakeLists.txt b/takeoff/takeoff_gui/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..17411a8ab --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/takeoff/takeoff_gui/windows/runner/Runner.rc b/takeoff/takeoff_gui/windows/runner/Runner.rc new file mode 100644 index 000000000..817edbb20 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "takeoff_gui" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "takeoff_gui" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "takeoff_gui.exe" "\0" + VALUE "ProductName", "takeoff_gui" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/takeoff/takeoff_gui/windows/runner/flutter_window.cpp b/takeoff/takeoff_gui/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..b43b9095e --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/takeoff/takeoff_gui/windows/runner/flutter_window.h b/takeoff/takeoff_gui/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/takeoff/takeoff_gui/windows/runner/main.cpp b/takeoff/takeoff_gui/windows/runner/main.cpp new file mode 100644 index 000000000..6d0924a3a --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"takeoff_gui", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/takeoff/takeoff_gui/windows/runner/resource.h b/takeoff/takeoff_gui/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/takeoff/takeoff_gui/windows/runner/resources/app_icon.ico b/takeoff/takeoff_gui/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..57ea15a08 Binary files /dev/null and b/takeoff/takeoff_gui/windows/runner/resources/app_icon.ico differ diff --git a/takeoff/takeoff_gui/windows/runner/runner.exe.manifest b/takeoff/takeoff_gui/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..a42ea7687 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/takeoff/takeoff_gui/windows/runner/utils.cpp b/takeoff/takeoff_gui/windows/runner/utils.cpp new file mode 100644 index 000000000..f5bf9fa0f --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/takeoff/takeoff_gui/windows/runner/utils.h b/takeoff/takeoff_gui/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/takeoff/takeoff_gui/windows/runner/win32_window.cpp b/takeoff/takeoff_gui/windows/runner/win32_window.cpp new file mode 100644 index 000000000..c10f08dc7 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/takeoff/takeoff_gui/windows/runner/win32_window.h b/takeoff/takeoff_gui/windows/runner/win32_window.h new file mode 100644 index 000000000..17ba43112 --- /dev/null +++ b/takeoff/takeoff_gui/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/takeoff/takeoff_lib/.gitignore b/takeoff/takeoff_lib/.gitignore new file mode 100644 index 000000000..4af528361 --- /dev/null +++ b/takeoff/takeoff_lib/.gitignore @@ -0,0 +1,10 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build outputs. +build/ + +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock \ No newline at end of file diff --git a/takeoff/takeoff_lib/CHANGELOG.md b/takeoff/takeoff_lib/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/takeoff/takeoff_lib/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/takeoff/takeoff_lib/README.asciidoc b/takeoff/takeoff_lib/README.asciidoc new file mode 100644 index 000000000..0c12347ff --- /dev/null +++ b/takeoff/takeoff_lib/README.asciidoc @@ -0,0 +1,32 @@ += TakeOff LIB + +Export any libraries intended for clients of this package. + +== Lib contains: +``` + export 'src/takeoff_facade.dart'; + export 'src/utils/logger/log.dart'; + export 'src/domain/cloud_provider.dart'; + export 'src/domain/cloud_provider_id.dart'; + export 'src/domain/language.dart'; + export 'src/domain/resource.dart'; + export 'src/domain/gui_message/gui_message.dart'; + export 'src/domain/gui_message/input_type.dart'; + export 'src/domain/gui_message/message_type.dart'; + export 'src/controllers/cloud/common/hangar/project/create_project_exception.dart'; + export 'src/utils/url_launcher/url_launcher.dart'; + export 'src/domain/resource.dart'; +``` + +== Using +``` + import 'package:takeoff_lib/src/[folder]/[subfolder]/[filename].dart' +``` + +== Example +``` + import 'package:takeoff_lib/src/utils/logger/log.dart' + import 'package:takeoff_lib/src/domain/gui_message/gui_message.dart'; + import 'package:takeoff_lib/src/controllers/cloud/common/hangar/project/create_project_exception.dart'; + +``` diff --git a/takeoff/takeoff_lib/analysis_options.yaml b/takeoff/takeoff_lib/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/takeoff/takeoff_lib/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/takeoff/takeoff_lib/example/takeoff_lib_example.dart b/takeoff/takeoff_lib/example/takeoff_lib_example.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/takeoff/takeoff_lib/example/takeoff_lib_example.dart @@ -0,0 +1 @@ + diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/auth/auth_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/auth/auth_controller.dart new file mode 100644 index 000000000..d025be549 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/auth/auth_controller.dart @@ -0,0 +1,8 @@ +import 'package:takeoff_lib/src/domain/cloud_provider.dart'; + +/// Defines the interface of the authentication controllers for each Cloud Provider +abstract class AuthController { + Future authenticate(String email); + Future getCurrentAccount(); + Future logOut(); +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/account/account_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/account/account_controller.dart new file mode 100644 index 000000000..0baae5fc6 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/account/account_controller.dart @@ -0,0 +1,3 @@ +abstract class AccountController { + Future setUpAccountAndVerifyRoles(); +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/account/account_exception.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/account/account_exception.dart new file mode 100644 index 000000000..68a2be469 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/account/account_exception.dart @@ -0,0 +1,8 @@ +class AccountException implements Exception { + final String message; + const AccountException(this.message); + @override + String toString() { + return "AccountException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/pipeline/create_pipeline_exception.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/pipeline/create_pipeline_exception.dart new file mode 100644 index 000000000..17a7ffa39 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/pipeline/create_pipeline_exception.dart @@ -0,0 +1,9 @@ +/// Exception launched when a pipeline fails to be created. +class CreatePipelineException implements Exception { + final String message; + const CreatePipelineException(this.message); + @override + String toString() { + return "CreatePipelineException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/pipeline/pipeline_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/pipeline/pipeline_controller.dart new file mode 100644 index 000000000..aaf7b83b7 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/pipeline/pipeline_controller.dart @@ -0,0 +1,19 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart'; + +/// Executes the create pipeline scripts +class PipelineController { + /// Executes the pipeline [script]. + /// + /// Whether or not the process succeed. + Future execute(T script) async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand([], script.toCommand())) { + return false; + } + + return true; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/project/create_project_exception.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/project/create_project_exception.dart new file mode 100644 index 000000000..c50ccfc0f --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/project/create_project_exception.dart @@ -0,0 +1,9 @@ +/// Exception launched when something fails in [GoogleCloudController.createProject]. +class CreateProjectException implements Exception { + final String message; + const CreateProjectException(this.message); + @override + String toString() { + return "CreateProjectException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/project/project_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/project/project_controller.dart new file mode 100644 index 000000000..66cd8b07f --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/project/project_controller.dart @@ -0,0 +1,7 @@ +/// Interface for the controllers that will create the projects in each provider. +abstract class ProjectController { + /// Creates the project. + /// + /// Whether or not the process succeed. + Future createProject(); +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/repository/repository_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/repository/repository_controller.dart new file mode 100644 index 000000000..99450b2ee --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/repository/repository_controller.dart @@ -0,0 +1,16 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/repo/create_repo.dart'; + +/// Controller for the repository operations in every provider. +class RepositoryController { + /// Creates a repository using the passed script in any cloud provider. + Future createRepository(T script) async { + DockerController controller = GetIt.I.get(); + if (!await controller.executeCommand([], script.toCommand())) { + return false; + } + + return true; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/sonar/sonarqube_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/sonar/sonarqube_controller.dart new file mode 100644 index 000000000..35d55376e --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/common/hangar/sonar/sonarqube_controller.dart @@ -0,0 +1,16 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/sonarqube/setup_sonar.dart'; + +class SonarqubeController { + Future execute(SetUpSonar script, String cloud) async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand( + ["--workdir", "/scripts/sonarqube/$cloud"], script.toCommand())) { + return false; + } + + return true; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/auth/gcloud_auth_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/auth/gcloud_auth_controller.dart new file mode 100644 index 000000000..a47bd8b44 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/auth/gcloud_auth_controller.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/auth/auth_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/domain/gcloud.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/utils/logger/log.dart'; +import 'package:takeoff_lib/src/utils/url_launcher/url_launcher.dart'; + +/// Specific implementation of the authentication process for Google Cloud +/// +/// When [stdinStream] is not null, it will use the data from that stream to +/// pass it as the Google Cloud token. +/// +/// If [useStdin] is true, it will read a line from the standard input to +/// pass it as the Google Cloud token, +/// +/// If [useStdin] is `false` and [stdinStream] is `null`, that means that you +/// expect that the user is already authenticated and that Google Cloud has +/// to reuse the credentials. This is, for example, in the beginning of the +/// `createProject`, `quickstartWayat` or `run` processes, when you need to +/// make sure that the account used is the user's and not a previously +/// set service account. This arguments will not work to log in, only to +/// set the CLI to the logged account. +/// +/// [useStdin] cannot be `true` if [stdinStream] is not `null`. +class GCloudAuthController implements AuthController { + Stream>? stdinStream; + bool useStdin; + + GCloudAuthController({this.useStdin = false, this.stdinStream}) { + assert((useStdin && stdinStream == null) || (!useStdin)); + } + + @override + Future authenticate(String email) async { + DockerController dockerController = GetIt.I.get(); + + List volumeMappings = dockerController.getVolumeMappings(); + + List args = ["run", "--rm", "-i"] + + volumeMappings + + [DockerController.imageName] + + ["gcloud", "auth", "login", email]; + + Log.info("Authenticating with Google Cloud"); + Log.info("Launching ${dockerController.command} + $args"); + Process gCloudProcess = + await Process.start(dockerController.command, args, runInShell: true); + + bool openedUrl = false; + + StreamSubscription> stderrHandler = + gCloudProcess.stderr.listen((event) async { + String message = String.fromCharCodes(event).trim(); + if (!openedUrl && !message.startsWith("WARNING")) { + String url = message.split("\n").last.trim(); + if (Uri.tryParse(url) != null) { + Log.info("Opening Google Authentication in the browser"); + await UrlLaucher().launch(url); + if (useStdin) { + String? line = stdin.readLineSync(); + gCloudProcess.stdin.writeln(line?.trim()); + } + } + openedUrl = true; + } else { + stdout.writeln(message); + } + }); + + StreamSubscription>? stdinHandler; + + StreamSubscription> stdoutHandler = + gCloudProcess.stdout.listen((event) { + stdout.writeln(String.fromCharCodes(event)); + }); + + if (stdinStream != null) { + stdinHandler = stdinStream!.listen((event) { + gCloudProcess.stdin.writeln(String.fromCharCodes(event).trim()); + }); + } + + int exitCode = await gCloudProcess.exitCode; + + await stderrHandler.cancel(); + await stdinHandler?.cancel(); + await stdoutHandler.cancel(); + + if (exitCode != 0) { + Log.error( + "The hangar docker process exited with an exit code of $exitCode"); + return false; + } + + Log.info("Login succesful with $email"); + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.saveGoogleEmail(email); + + return true; + } + + @override + Future getCurrentAccount() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + return await cacheRepository.getGoogleEmail(); + } + + @override + Future logOut() async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + return await cacheRepository.removeGoogleEmail(); + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/gcloud_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/gcloud_controller.dart new file mode 100644 index 000000000..61d9004f7 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/gcloud_controller.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:takeoff_lib/src/controllers/cloud/gcloud/auth/gcloud_auth_controller.dart'; +import 'package:takeoff_lib/src/domain/gui_message/gui_message.dart'; +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:takeoff_lib/src/domain/resource.dart'; + +abstract class GoogleCloudController { + Future createProject({ + required String projectName, + required String billingAccount, + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + required String googleCloudRegion, + StreamController? inputStream, + StreamController? outputStream, + }); + + /// Logs in with Google Cloud. + /// + /// Receives the [email] to log in, an optional [GCloudAuthController] for + /// testing purposes and a stdin stream for the GUI client to be able to write + /// to the authentication process. + Future init(String email, + {bool useStdin = false, + GCloudAuthController? controller, + Stream>? stdinStream}); + + /// Runs the Google Cloud CLI with the specified project and service account + Future run(String projectId); + + /// Returns the current logged Google Account or an empty String if there is none + Future getAccount( + {GCloudAuthController? controller, Stream>? stdinStream}); + + /// Removes the current account from the TakeOff cache + Future logOut( + {GCloudAuthController? controller, Stream>? stdinStream}); + + /// Removes the project ID from the cache DB and the correspondent workspace folder + Future cleanProject(String projectId); + + /// Creates and deploys wayat in Google Cloud + Future wayatQuickstart( + {required String billingAccount, + required String googleCloudRegion, + StreamController? inputStream, + StreamController? outputStream}); + + Uri getGCloudResourceUrl(String project, Resource resource); +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart new file mode 100644 index 000000000..5d0401a42 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart @@ -0,0 +1,682 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/auth/gcloud_auth_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/account/account_controller_gcloud.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_exception.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/firebase_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/account/account_exception.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/setup_firebase_exception.dart'; +import 'package:takeoff_lib/src/domain/application_end.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/pipeline/create_pipeline_exception.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/pipeline/pipeline_controller_gcloud.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/project/project_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/project/project_controller_gcloud.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_exception.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/repository/repository_controller.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/repo/branch_strategy.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_web_renderer.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_step.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_steps_output.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/wayat_frontend.dart'; +import 'package:takeoff_lib/src/domain/sonar_output.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/sonar/sonarqube_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/repo/repo_action.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/sonarqube/setup_sonar.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/project/create_project.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/account/setup_principal_account.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/init_cloud_run.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/setup_firebase.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/wayat_backend.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/repo/create_repo.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/url_launcher/gcloud_url.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +/// Centralizes all the operations related with Google Cloud, such as +/// creating a project, quickstarting wayat, account management or list projects +class GoogleCloudControllerImpl implements GoogleCloudController { + FoldersService foldersService = GetIt.I.get(); + + @override + Future createProject({ + required String projectName, + required String billingAccount, + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + required String googleCloudRegion, + StreamController? inputStream, + StreamController? outputStream, + String backRepoName = "Backend", + String frontRepoName = "Frontend", + RepoAction frontRepoAction = RepoAction.create, + RepoAction backRepoAction = RepoAction.create, + String? frontImportUrl, + String? backImportUrl, + String? frontRepoSubpath, + String? backRepoSubpath, + String? deployFrontServiceName, + String? deployBackServiceName, + bool firebase = false, + bool wayat = false, + }) async { + if (backendLanguage == null && frontendLanguage == null) { + throw CreateProjectException( + "To create a project specify at least a BackEnd or FrontEnd language"); + } + + await _checkAuthentication(); + _logAndStream( + GuiMessage.info( + "Creating folder ${FoldersService.containerFolders["workspace"]}/$projectName"), + outputStream); + + Directory projectDir = await _createWorkspaceFolder(projectName); + ProjectController projectController = ProjectControllerGCloud( + CreateProjectGCloud( + projectName: projectName, + billingAccount: billingAccount, + firebase: firebase)); + + _logAndStream( + GuiMessage.info("Creating project in Google Cloud"), outputStream); + + if (!await projectController.createProject()) { + throw CreateProjectException("Could not create project in Google Cloud"); + } + + String serviceKeyPath = "${projectDir.path}/key.json"; + + AccountControllerGCloud accountController = + setUpServiceAccount(projectName, serviceKeyPath); + + _logAndStream( + GuiMessage.info("Setting up principal account and verifying roles"), + outputStream); + + await _verifyServiceAccountRoles(accountController); + + String backendLocalDir = "${projectDir.path}/$backRepoName"; + String frontendLocalDir = "${projectDir.path}/$frontRepoName"; + + await _createRepositories(projectName, projectDir, outputStream, + backendLanguage: backendLanguage, + frontendLanguage: frontendLanguage, + frontendRepoName: frontRepoName, + backendRepoName: backRepoName, + backAction: backRepoAction, + frontAction: frontRepoAction, + frontImportUrl: frontImportUrl, + backImportUrl: backImportUrl, + frontSubpath: frontRepoSubpath, + backSubpath: backRepoSubpath); + + _logAndStream(GuiMessage.info("Setting up Sonarqube"), outputStream); + + SonarOutput sonarOutput = await _setUpSonarqube( + serviceKeyPath, + projectName, + projectDir, + ); + + if (wayat) { + await _setUpWayatRepos(projectDir, projectName, googleCloudRegion, + backendLocalDir, frontendLocalDir, inputStream, outputStream); + } + + PipelineControllerGCloud pipelineController = PipelineControllerGCloud(); + + if (backendLanguage != null) { + _logAndStream( + GuiMessage.info("Building BackEnd pipelines"), outputStream); + + await buildPipelines( + pipelineController: pipelineController, + projectName: projectName, + appEnd: ApplicationEnd.backend, + language: backendLanguage, + languageVersion: backendVersion, + localDir: backendLocalDir, + googleCloudRegion: googleCloudRegion, + deployServiceName: deployBackServiceName, + sonarOutput: sonarOutput); + } + + if (frontendLanguage != null) { + _logAndStream( + GuiMessage.info("Building FrontEnd pipelines"), outputStream); + + await buildPipelines( + pipelineController: pipelineController, + projectName: projectName, + appEnd: ApplicationEnd.frontend, + language: frontendLanguage, + languageVersion: frontendVersion, + localDir: frontendLocalDir, + googleCloudRegion: googleCloudRegion, + sonarOutput: sonarOutput, + deployServiceName: deployFrontServiceName, + registryLocation: googleCloudRegion, + androidFlutterPlatform: frontendLanguage == Language.flutter, + webFlutterPlatform: frontendLanguage == Language.flutter, + flutterWebRenderer: (frontendLanguage == Language.flutter) + ? FlutterWebRenderer.canvaskit + : null); + } + + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.saveGoogleProjectId(projectName); + + Log.success("Project $projectName succesfully created!"); + outputStream?.add(GuiMessage.success("Project created successfully", + "https://console.cloud.google.com/welcome?project=$projectName")); + Log.success( + "You can view the project by entering in: https://console.cloud.google.com/welcome?project=$projectName"); + + return true; + } + + @override + Future run(String projectId) async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + + await _checkAuthentication(); + + if (!(await cacheRepository.getGoogleProjectIds()).contains(projectId)) { + Log.error("The project $projectId does not exist in the TakeOff cache"); + return false; + } + + Directory projectFolder = Directory( + join(foldersService.getHostFolders()["workspace"]!, projectId)); + + if (!projectFolder.existsSync()) { + Log.error("The workspace folder of $projectId does not exist"); + return false; + } + + DockerController controller = GetIt.I.get(); + await controller.executeCommand([ + "-it", + "--workdir", + "/workspace/$projectId" + ], [ + "/bin/bash", + "-c", + "gcloud config set project $projectId && gcloud beta interactive && exit" + ], startMode: ProcessStartMode.detached, runInShell: true); + + return true; + } + + @override + Future init(String email, + {bool useStdin = false, + GCloudAuthController? controller, + Stream>? stdinStream}) async { + GCloudAuthController authController = controller ?? + GCloudAuthController(useStdin: useStdin, stdinStream: stdinStream); + return await authController.authenticate(email); + } + + @override + Future getAccount( + {GCloudAuthController? controller, + Stream>? stdinStream}) async { + GCloudAuthController authController = controller ?? GCloudAuthController(); + return await authController.getCurrentAccount(); + } + + @override + Future logOut( + {GCloudAuthController? controller, + Stream>? stdinStream}) async { + GCloudAuthController authController = controller ?? GCloudAuthController(); + return await authController.logOut(); + } + + @override + Future cleanProject(String projectId) async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.removeGoogleProject(projectId); + Directory projectWorkspace = Directory( + join(foldersService.getHostFolders()["workspace"]!, projectId)); + if (await projectWorkspace.exists()) { + try { + await projectWorkspace.delete(recursive: true); + } on FileSystemException catch (e) { + Log.error("Could not remove $projectId workspace folder: ${e.osError}"); + return false; + } + } + return true; + } + + @override + Future wayatQuickstart( + {required String billingAccount, + required String googleCloudRegion, + StreamController? inputStream, + StreamController? outputStream}) async { + DateTime now = DateTime.now(); + String projectName = + "wayat-takeoff-${now.hour}-${now.minute}-${now.day}-${now.month}-${now.year.toString().substring(2)}"; + FirebaseController firebaseController = FirebaseController(); + await firebaseController.authenticate(outputStream, inputStream); + + if (!await createProject( + projectName: projectName, + billingAccount: billingAccount, + backendLanguage: Language.python, + backendVersion: "3.10", + frontendLanguage: Language.flutter, + frontendVersion: "3.3.6", + googleCloudRegion: googleCloudRegion, + inputStream: inputStream, + outputStream: outputStream, + backRepoName: "wayat-python", + frontRepoName: "wayat-flutter", + deployFrontServiceName: "wayat-front", + deployBackServiceName: "wayat-back", + firebase: true, + wayat: true)) { + return false; + } + + if (outputStream != null) { + File finalStepsFile = File( + "${foldersService.getHostFolders()["workspace"]!}${Platform.pathSeparator}$projectName${Platform.pathSeparator}nextsteps.json"); + Map finalSteps = + jsonDecode(finalStepsFile.readAsStringSync()); + + QuickstartStepsOutput quickstartStepsOutput = + QuickstartStepsOutput.fromMap(finalSteps); + + for (QuickstartStep step in quickstartStepsOutput.steps) { + String browserMessage = (step.textToCopy == null) + ? "1.Use the button below to open a page in your browser\n2.${step.instructions}" + : "1.Copy the frontend URL: ${step.textToCopy}" + "\n2.Use the button below to open a page in your browser" + "\n2.${step.instructions}"; + + outputStream.add(GuiMessage.browser(browserMessage, step.goToUrl)); + await inputStream?.stream.take(1).last; + } + } else { + File finalStepsFile = File( + "${foldersService.getHostFolders()["workspace"]!}${Platform.pathSeparator}$projectName${Platform.pathSeparator}nextsteps.txt"); + String finalSteps = finalStepsFile.readAsStringSync(); + _logAndStream(GuiMessage.info(finalSteps), outputStream); + } + + _logAndStream( + GuiMessage.info("These manual steps should be done for wayat to work"), + outputStream); + + return true; + } + + /// Helper method that will set run the specific wayat scripts when executing [wayatQuickstart]. + Future _setUpWayatRepos( + Directory projectDir, + String projectName, + String googleCloudRegion, + String backendLocalDir, + String frontendLocalDir, + StreamController? inputStream, + StreamController? outputStream, + ) async { + _logAndStream(GuiMessage.info("Initializing Cloud Run"), outputStream); + + String frontUrlCloudRun = "${projectDir.path}/frontUrlCloudRun"; + String backUrlCloudRun = "${projectDir.path}/backUrlCloudRun"; + + await _initCloudRun(projectDir, projectName, googleCloudRegion, + frontUrlCloudRun, backUrlCloudRun); + + File frontendUrlFile = File( + "${foldersService.getHostFolders()["workspace"]!}${Platform.pathSeparator}$projectName${Platform.pathSeparator}frontUrlCloudRun"); + File backendUrlFile = File( + "${foldersService.getHostFolders()["workspace"]!}${Platform.pathSeparator}$projectName${Platform.pathSeparator}backUrlCloudRun"); + + String frontendUrl = frontendUrlFile.readAsStringSync().trim(); + String backendUrl = backendUrlFile.readAsStringSync().trim(); + + _logAndStream( + GuiMessage.info("Setting up Firebase & Firestore"), outputStream); + + await _setUpFirebase(projectName, projectDir.path, + enableMaps: true, + setUpAndroid: true, + setUpIos: true, + setUpWeb: true, + firestoreRegion: googleCloudRegion); + + String acceptConsentUrl = + "https://console.cloud.google.com/apis/credentials/consent?project=$projectName"; + + if (outputStream != null) { + outputStream.add(GuiMessage.browser( + "Accept Firebase's terms of service", acceptConsentUrl)); + await inputStream?.stream.take(1).last; + } else { + Log.info( + "\n\n===========================\n\nOpen $acceptConsentUrl and accept the terms of Firebase\n\n===========================\n\n"); + Log.info("Press enter to continue"); + stdin.readLineSync(); + } + + String mapsStaticSecretUrl = + "https://console.cloud.google.com/google/maps-apis/api-list?project=$projectName"; + String mapsStaticSecret = ""; + + if (outputStream != null) { + outputStream.add(GuiMessage.browser( + "Open the page and copy the Maps Secret", mapsStaticSecretUrl)); + await inputStream?.stream.take(1).last; + + outputStream + .add(GuiMessage.input("Introduce the Maps Secret", InputType.text)); + mapsStaticSecret = (await inputStream?.stream.take(1).last)!; + } else { + Log.info( + "\n\n===========================\n\nOpen $mapsStaticSecretUrl and copy the maps secret\n\n===========================\n\n"); + Log.info("Introduce the maps secret:"); + mapsStaticSecret = stdin.readLineSync() ?? ""; + while (mapsStaticSecret.isEmpty) { + Log.warning("Maps secret cannot be empty"); + mapsStaticSecret = stdin.readLineSync() ?? ""; + } + } + + _logAndStream(GuiMessage.info("Setting up Wayat"), outputStream); + + WayatController wayatController = WayatController(); + try { + await wayatController.setUpWayat(WayatBackend( + projectName: projectName, + workspace: projectDir.path, + backendRepoDir: backendLocalDir, + storageBucket: "$projectName.appspot.com")); + + await wayatController.setUpWayat(WayatFrontend( + projectName: projectName, + workspace: projectDir.path, + frontendRepoDir: frontendLocalDir, + keystoreFile: "${projectDir.path}/keystore.jks", + backendUrl: backendUrl, + frontendUrl: frontendUrl, + mapsStaticSecret: mapsStaticSecret)); + } on WayatException catch (e) { + throw CreateProjectException(e.message); + } + } + + /// Helper method to initialize Cloud Run in [wayatQuickstart] + Future _initCloudRun( + Directory projectDir, + String projectName, + String googleCloudRegion, + String frontUrlCloudRun, + String backUrlCloudRun) async { + CloudRunController cloudRunController = CloudRunController(); + try { + await cloudRunController.initCloudRun(InitCloudRun( + project: projectName, + name: "wayat-front", + region: googleCloudRegion, + urlOutputFile: frontUrlCloudRun)); + } on CloudRunException catch (e) { + throw CreateProjectException(e.message); + } + try { + await cloudRunController.initCloudRun(InitCloudRun( + project: projectName, + name: "wayat-back", + region: googleCloudRegion, + urlOutputFile: backUrlCloudRun)); + } on CloudRunException catch (e) { + throw CreateProjectException(e.message); + } + } + + /// Helper method to set up firebase in [wayatQuickstart] + Future _setUpFirebase(String projectName, String credentialsOutput, + {String? firestoreRegion, + bool? enableMaps, + bool? setUpAndroid, + bool? setUpIos, + bool? setUpWeb}) async { + FirebaseController controller = FirebaseController(); + try { + await controller.setUpFirebase(SetUpFirebase( + projectName: projectName, + credentialsOutputFolder: credentialsOutput, + setUpAndroid: setUpAndroid, + setUpIOS: setUpIos, + setUpWeb: setUpWeb, + firestoreRegion: firestoreRegion, + enableMaps: enableMaps)); + } on SetUpFirebaseException catch (e) { + throw CreateProjectException(e.message); + } + } + + /// Helper method to check that there is a logged user in Google Cloud in + /// [wayatQuickstart] and [createProject] + Future _checkAuthentication() async { + GCloudAuthController gCloudAuthController = GCloudAuthController(); + String currentAccount = await gCloudAuthController.getCurrentAccount(); + if (currentAccount.isEmpty) { + throw CreateProjectException( + "You need to be logged in Google Cloud. Execute the init command for Google Cloud."); + } + await gCloudAuthController.authenticate( + currentAccount, + ); + return currentAccount; + } + + /// Helper method to create the folder for [wayatQuickstart] and [createProject] + Future _createWorkspaceFolder(String projectName) async { + Directory projectDir = Directory( + "${FoldersService.containerFolders["workspace"]}/$projectName"); + + DockerController controller = GetIt.I.get(); + if (!await controller.executeCommand([], ["mkdir", projectDir.path])) { + throw CreateProjectException("Could not create project workspace"); + } + return projectDir; + } + + /// helper method to set up sonarqube for [wayatquickstart] and [createproject] + Future _setUpSonarqube( + String serviceKeyPath, String projectName, Directory projectDir) async { + SonarqubeController sonarqubeController = SonarqubeController(); + if (!await sonarqubeController.execute( + SetUpSonar( + serviceAccountFile: serviceKeyPath, + project: projectName, + stateFolder: "${projectDir.path}/sonarqube"), + "gcloud")) { + throw CreateProjectException("Could not set up SonarQube"); + } + + File sonarOutputFile = File( + "${foldersService.getHostFolders()["workspace"]!}${Platform.pathSeparator}$projectName${Platform.pathSeparator}sonarqube${Platform.pathSeparator}terraform.tfoutput.json"); + SonarOutput sonarOutput = + SonarOutput.fromMap(jsonDecode(sonarOutputFile.readAsStringSync())); + return sonarOutput; + } + + /// Helper method to create the service account for [wayatQuickstart] and [createProject] + AccountControllerGCloud setUpServiceAccount( + String projectName, String serviceKeyPath) { + AccountControllerGCloud accountController = AccountControllerGCloud( + SetUpPrincipalAccountGCloud( + googleAccount: "", + serviceAccount: "TakeOff", + projectId: projectName, + serviceKeyPath: serviceKeyPath), + VerifyRolesAndPermissionsGCloud( + googleAccount: "", + serviceAccount: "TakeOff", + projectId: projectName, + )); + return accountController; + } + + Future buildPipelines( + {required PipelineControllerGCloud pipelineController, + required String projectName, + required ApplicationEnd appEnd, + required Language language, + required String localDir, + required String googleCloudRegion, + required SonarOutput sonarOutput, + String? deployServiceName, + String? languageVersion, + String? registryLocation, + bool? androidFlutterPlatform, + bool? webFlutterPlatform, + FlutterWebRenderer? flutterWebRenderer}) async { + try { + await pipelineController.buildPipelines( + projectName: projectName, + appEnd: appEnd, + language: language, + languageVersion: languageVersion, + localDir: localDir, + googleCloudRegion: googleCloudRegion, + sonarUrl: sonarOutput.url, + sonarToken: sonarOutput.token, + registryLocation: registryLocation, + androidFlutterPlatform: androidFlutterPlatform, + webFlutterPlatform: webFlutterPlatform, + flutterWebRenderer: flutterWebRenderer, + deployServiceName: deployServiceName); + } on CreatePipelineException catch (e) { + throw CreateProjectException( + "Could not build the ${appEnd.name} pipelines: ${e.message}"); + } + } + + /// Helper method to create the repositories for [wayatQuickstart] and [createProject] + Future _createRepositories(String projectName, Directory projectDir, + StreamController? outputStream, + {required Language? backendLanguage, + required Language? frontendLanguage, + RepoAction frontAction = RepoAction.create, + RepoAction backAction = RepoAction.create, + String? frontImportUrl, + String? backImportUrl, + String? frontSubpath, + String? backSubpath, + String backendRepoName = "Backend", + String frontendRepoName = "Frontend"}) async { + RepositoryController repoController = RepositoryController(); + + if (backendLanguage != null) { + _logAndStream( + GuiMessage.info("Creating BackEnd repository"), outputStream); + + if (!await repoController.createRepository(CreateRepoGCloud( + name: backendRepoName, + project: projectName, + subpath: backSubpath, + setUpBranchStrategy: BranchStrategy.gitflow, + action: backAction, + sourceGitUrl: backImportUrl, + directory: projectDir.path))) { + throw CreateProjectException("Could not create BackEnd repository"); + } + } + if (frontendLanguage != null) { + _logAndStream( + GuiMessage.info("Creating FrontEnd Repository"), outputStream); + + if (!await repoController.createRepository(CreateRepoGCloud( + name: frontendRepoName, + project: projectName, + sourceGitUrl: frontImportUrl, + subpath: frontSubpath, + setUpBranchStrategy: BranchStrategy.gitflow, + action: frontAction, + directory: projectDir.path))) { + throw CreateProjectException("Could not create FrontEnd repository"); + } + } + } + + /// Helper method to verify the service accounts roles for [wayatQuickstart] and [createProject] + Future _verifyServiceAccountRoles( + AccountControllerGCloud accountController) async { + try { + await accountController.setUpAccountAndVerifyRoles(); + } on AccountException catch (e) { + throw CreateProjectException( + "Could not set up the service: ${e.message}"); + } + } + + /// Helper method to log into the console and the GUI stream for [wayatQuickstart] and [createProject] + void _logAndStream(GuiMessage message, StreamController? stream) { + Log.info(message.message); + stream?.add(message); + } + + @visibleForTesting + bool isQuickStartProject(String project) { + RegExp rule = RegExp( + r'wayat-takeoff-(\d{1,2})-(\d{1,2})-(\d{1,2})-(\d{1,2})-(\d{1,4})'); + return rule.hasMatch(project); + } + + @override + Uri getGCloudResourceUrl(String project, Resource resourceType) { + String url = ""; + switch (resourceType) { + case Resource.ide: + String url = + "${GCloudResourceUrl.baseConsolePath.rawValue}/cloudshelleditor?project=$project&cloudshell=true"; + return Uri.parse(url); + case Resource.pipeline: + String url = + "${GCloudResourceUrl.baseConsolePath.rawValue}/cloud-build/dashboard?project=$project"; + return Uri.parse(url); + case Resource.feRepo: + if (isQuickStartProject(project)) { + url = + "${GCloudResourceUrl.baseSourcePath.rawValue}/$project/wayat-flutter/"; + } else { + url = + "${GCloudResourceUrl.baseSourcePath.rawValue}/$project/Frontend/"; + } + return Uri.parse(url); + case Resource.beRepo: + if (isQuickStartProject(project)) { + url = + "${GCloudResourceUrl.baseSourcePath.rawValue}/$project/wayat-python/"; + } else { + url = + "${GCloudResourceUrl.baseSourcePath.rawValue}/$project/Backend/"; + } + return Uri.parse(url); + case Resource.none: + return Uri.parse(url); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/account/account_controller_gcloud.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/account/account_controller_gcloud.dart new file mode 100644 index 000000000..1dfbf0421 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/account/account_controller_gcloud.dart @@ -0,0 +1,50 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/account/account_exception.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/account/setup_principal_account.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions.dart'; +import 'package:takeoff_lib/src/utils/logger/log.dart'; + +class AccountControllerGCloud { + final VerifyRolesAndPermissionsGCloud verifyRolesScript; + final SetUpPrincipalAccountGCloud setUpAccountScript; + + AccountControllerGCloud(this.setUpAccountScript, this.verifyRolesScript); + + /// Sets up the service account for the project with the specified permissions and roles. + /// + /// If there is an error, a [AccountException] will be thrown with + /// a message indicating where the error was located. + /// + /// It does not return a boolean value. Instead, it throws a custom exception, + /// due to it having more than one point of failure. Exceptions provide a way + /// for the user to locate the error. + Future setUpAccountAndVerifyRoles() async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand([], setUpAccountScript.toCommand())) { + String errorMessage = "Could not set up the project account"; + Log.error(errorMessage); + throw AccountException(errorMessage); + } + + if (!await controller.executeCommand([], verifyRolesScript.toCommand())) { + String errorMessage = "Could not verify the roles of the account"; + Log.error(errorMessage); + throw AccountException(errorMessage); + } + + if (!await controller.executeCommand([], [ + "gcloud", + "auth", + "activate-service-account", + "${setUpAccountScript.serviceAccount}@${setUpAccountScript.projectId}.iam.gserviceaccount.com", + "--key-file", + setUpAccountScript.serviceKeyPath! + ])) { + String errorMessage = "Could not activate the service account"; + Log.error(errorMessage); + throw AccountException(errorMessage); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/pipeline/pipeline_controller_gcloud.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/pipeline/pipeline_controller_gcloud.dart new file mode 100644 index 000000000..62c8f296c --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/pipeline/pipeline_controller_gcloud.dart @@ -0,0 +1,126 @@ +import 'package:takeoff_lib/src/domain/application_end.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/pipeline/create_pipeline_exception.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/pipeline/pipeline_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_web_renderer.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/package_pipeline.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/quality_pipeline.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/test_pipeline.dart'; + +/// Controller for the pipelines in Google Cloud. +class PipelineControllerGCloud extends PipelineController { + /// Builds all the pipelines for a given [projectName] in Google Cloud + /// + /// It requires the [ApplicationEnd], the [Language], the repository [localDir] + /// and the [googleCloudRegion] to deploy. + Future buildPipelines( + {required String projectName, + required ApplicationEnd appEnd, + required Language language, + String? languageVersion, + required String localDir, + required String googleCloudRegion, + required String sonarUrl, + required String sonarToken, + String? registryLocation, + String? deployServiceName, + String targetBranch = "develop", + bool? androidFlutterPlatform, + bool? webFlutterPlatform, + FlutterWebRenderer? flutterWebRenderer}) async { + String buildPipelineName = "build-$projectName-${appEnd.name}"; + String qaPipelineName = "qa-$projectName-${appEnd.name}"; + String testPipelineName = "test-$projectName-${appEnd.name}"; + String packagePipelineName = "package-$projectName-${appEnd.name}"; + String deployPipelineName = "deploy-$projectName-${appEnd.name}"; + MachineType? machineType = + (language == Language.flutter) ? MachineType.E2_HIGHCPU_8 : null; + + if (!await execute(BuildPipelineGCloud( + configFile: + "/scripts/pipelines/gcloud/templates/build/build-pipeline.cfg", + pipelineName: buildPipelineName, + languageVersion: languageVersion, + registryLocation: registryLocation, + targetBranch: targetBranch, + language: language, + machineType: machineType, + localDirectory: localDir))) { + throw CreatePipelineException( + "Build pipeline could not be created for ${appEnd.name}"); + } + + if (!await execute(TestPipelineGCloud( + configFile: + "/scripts/pipelines/gcloud/templates/test/test-pipeline.cfg", + pipelineName: testPipelineName, + language: language, + languageVersion: languageVersion, + localDirectory: localDir, + targetBranch: targetBranch, + registryLocation: registryLocation, + machineType: machineType, + buildPipelineName: buildPipelineName))) { + throw CreatePipelineException( + "Test pipeline could not be created for ${appEnd.name}"); + } + + if (!await execute(QualityPipelineGCloud( + configFile: + "/scripts/pipelines/gcloud/templates/quality/quality-pipeline.cfg", + pipelineName: qaPipelineName, + language: language, + localDirectory: localDir, + buildPipelineName: buildPipelineName, + targetBranch: targetBranch, + languageVersion: languageVersion, + machineType: machineType, + testPipelineName: testPipelineName, + sonarUrl: sonarUrl, + sonarToken: sonarToken, + registryLocation: registryLocation))) { + throw CreatePipelineException( + "Quality pipeline could not be created for ${appEnd.name}"); + } + if (!await execute(PackagePipelineGCloud( + configFile: + "/scripts/pipelines/gcloud/templates/package/package-pipeline.cfg", + pipelineName: packagePipelineName, + language: language, + languageVersion: languageVersion, + localDirectory: localDir, + buildPipelineName: buildPipelineName, + qualityPipelineName: qaPipelineName, + targetBranch: targetBranch, + registryLocation: registryLocation, + machineType: machineType, + imageName: + "$googleCloudRegion-docker.pkg.dev/$projectName/${appEnd.name}/${appEnd.name}", + webFlutterPlatform: webFlutterPlatform, + androidFlutterPlatform: androidFlutterPlatform, + flutterWebRenderer: flutterWebRenderer))) { + throw CreatePipelineException( + "Package pipeline could not be created for ${appEnd.name}"); + } + + if (!await execute(DeployPipelineGCloud( + configFile: + "/scripts/pipelines/gcloud/templates/deploy-cloud-run/deploy-cloud-run-pipeline.cfg", + pipelineName: deployPipelineName, + language: language, + languageVersion: languageVersion, + targetBranch: targetBranch, + localDirectory: localDir, + machineType: machineType, + registryLocation: registryLocation, + gCloudRegion: googleCloudRegion, + serviceName: + deployServiceName ?? "$projectName-${appEnd.name}-service"))) { + throw CreatePipelineException( + "Deploy pipeline could not be created for ${appEnd.name}"); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/project/project_controller_gcloud.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/project/project_controller_gcloud.dart new file mode 100644 index 000000000..c86085aa4 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/project/project_controller_gcloud.dart @@ -0,0 +1,22 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/project/project_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/project/create_project.dart'; + +/// Project controller for Google Cloud. +class ProjectControllerGCloud implements ProjectController { + final CreateProjectGCloud createScript; + + ProjectControllerGCloud(this.createScript); + + @override + Future createProject() async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand([], createScript.toCommand())) { + return false; + } + + return true; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_controller.dart new file mode 100644 index 000000000..74c0ae877 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_controller.dart @@ -0,0 +1,14 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_exception.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/init_cloud_run.dart'; + +class CloudRunController { + Future initCloudRun(InitCloudRun script) async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand([], script.toCommand())) { + throw CloudRunException("Could not set up Cloud Run for ${script.name}"); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_exception.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_exception.dart new file mode 100644 index 000000000..4ed9ea6c4 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/cloud_run_exception.dart @@ -0,0 +1,8 @@ +class CloudRunException implements Exception { + final String message; + const CloudRunException(this.message); + @override + String toString() { + return "CloudRunException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/firebase_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/firebase_controller.dart new file mode 100644 index 000000000..66ce3b441 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/firebase_controller.dart @@ -0,0 +1,105 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/setup_firebase_exception.dart'; +import 'package:takeoff_lib/src/domain/gui_message/gui_message.dart'; +import 'package:takeoff_lib/src/domain/gui_message/input_type.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/setup_firebase.dart'; +import 'package:takeoff_lib/src/utils/logger/log.dart'; +import 'package:takeoff_lib/src/utils/url_launcher/url_launcher.dart'; + +class FirebaseController { + Future authenticate(StreamController? outputStream, + StreamController? inputStream) async { + DockerController controller = GetIt.I.get(); + + List volumeMappings = controller.getVolumeMappings(); + + List args = ["run", "--rm", "-i"] + + volumeMappings + + [DockerController.imageName] + + ["firebase", "login", "--interactive", "--no-localhost"]; + + Process dockerProc = + await Process.start(controller.command, args, runInShell: true); + + bool deniedDataCollection = false; + bool logged = false; + String url = ""; + String sessionId = ""; + + dockerProc.stdout.listen((event) async { + String output = String.fromCharCodes(event).trim(); + if (logged) return; + + if (!deniedDataCollection) { + dockerProc.stdin.writeln("n"); + deniedDataCollection = true; + return; + } + + for (String line in output.split("\n")) { + line = line.trim(); + if (sessionId.isEmpty && line.length == 5) { + sessionId = line; + continue; + } + + if (url.isEmpty && line.contains("https://")) { + url = line; + continue; + } + + if (line.contains("Enter authorization code:")) { + Log.info( + "Your session ID is: $sessionId\nFollow the instructions in your browser and " + "enter your authorization code:"); + await UrlLaucher().launch(url); + late String code; + if (outputStream == null) { + code = stdin.readLineSync() ?? ""; + while (code.isEmpty) { + Log.info("Enter the authorization code:"); + code = stdin.readLineSync() ?? ""; + } + } else { + outputStream.add(GuiMessage.input( + "Your session ID is: $sessionId. Follow " + "the instructions in:\n$url\nand enter your authorization code.", + InputType.text)); + code = await inputStream?.stream.take(1).last ?? ""; + while (code.isEmpty) { + outputStream.add(GuiMessage.input( + "Your session ID is: $sessionId. Follow " + "the instructions in:\n$url\nand enter your authorization code.", + InputType.text)); + code = await inputStream?.stream.take(1).last ?? ""; + } + } + dockerProc.stdin.writeln(code); + logged = true; + } + } + }); + stderr.addStream(dockerProc.stderr); + bool res = await dockerProc.exitCode == 0; + if (res) { + Log.success("Logged in with firebase"); + outputStream?.add(GuiMessage.info("Logged in with firebase")); + } else { + Log.error("Could not log in with firebase"); + outputStream?.add(GuiMessage.error("Cloud not log in with firebase")); + } + return res; + } + + Future setUpFirebase(SetUpFirebase script) async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand([], script.toCommand())) { + throw SetUpFirebaseException("Could not set up Firebase"); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/setup_firebase_exception.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/setup_firebase_exception.dart new file mode 100644 index 000000000..47b1e3593 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/setup_firebase_exception.dart @@ -0,0 +1,8 @@ +class SetUpFirebaseException implements Exception { + final String message; + const SetUpFirebaseException(this.message); + @override + String toString() { + return "SetUpFirebaseException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_controller.dart new file mode 100644 index 000000000..746ae01f1 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_controller.dart @@ -0,0 +1,14 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_exception.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/wayat_script.dart'; + +class WayatController { + Future setUpWayat(WayatScript script) async { + DockerController controller = GetIt.I.get(); + + if (!await controller.executeCommand([], script.toCommand())) { + throw WayatException("Could not set up wayat"); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_exception.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_exception.dart new file mode 100644 index 000000000..cf1669118 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/quickstart/wayat_exception.dart @@ -0,0 +1,8 @@ +class WayatException implements Exception { + final String message; + const WayatException(this.message); + @override + String toString() { + return "WayatException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/repository/repository_controller_gcloud.dart b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/repository/repository_controller_gcloud.dart new file mode 100644 index 000000000..d40fe6388 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/cloud/gcloud/hangar/repository/repository_controller_gcloud.dart @@ -0,0 +1,17 @@ +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/hangar/repository/repository_controller.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/repo/create_repo.dart'; + +class RepositoryControllerGCloud + implements RepositoryController { + @override + Future createRepository(CreateRepoGCloud script) async { + DockerController controller = GetIt.I.get(); + if (!await controller.executeCommand([], script.toCommand())) { + return false; + } + + return true; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/docker/docker_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/docker/docker_controller.dart new file mode 100644 index 000000000..b4ad2d51e --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/docker/docker_controller.dart @@ -0,0 +1,91 @@ +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:meta/meta.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/logger/log.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; + +/// Defines all Docker related operations, such as creating images, launching containers +/// and checking installation requirements. +abstract class DockerController { + final String command; + + /// Reference to the FolderService singleton. + /// + /// Is instanced in the main DockerController file because it's used + /// in all of the subclasses. + final FoldersService foldersService = GetIt.I.get(); + + /// Service to make system calls + final SystemService systemService; + + DockerController({required this.command, SystemService? systemService}) + : systemService = systemService ?? SystemService(); + + // DockerHub devonfwforge/hangar image + static String imageName = "devonfwforge/hangar:2022.51.1"; + + /// Launches a Hangar container with [dockerArgs] and mounted volumes executing + /// the orders passed in [commands]. + /// + /// For example, if we passed `["-d", "-p"]` as [dockerArgs] and `["ls"]` as [commands] + /// it would generate: + /// + /// `docker run --rm -d -p [-v hostFolder:containerFolder] hangar ls` + /// + /// Returns whether the execution was succesful. + Future executeCommand(List dockerArgs, List commands, + {ProcessStartMode startMode = ProcessStartMode.normal, + bool runInShell = false}) async { + List args = buildCommands(dockerArgs, commands); + + Process dockerProc = await Process.start(command, args, + mode: startMode, runInShell: runInShell); + if (startMode != ProcessStartMode.detached && + startMode != ProcessStartMode.detachedWithStdio) { + stdout.addStream(dockerProc.stdout); + stderr.addStream(dockerProc.stderr); + + Log.info("Executing ${Log.dockerProcessToString(args)}"); + + if (await dockerProc.exitCode != 0) { + Log.error("Exit code ${await dockerProc.exitCode}"); + Log.error("There was an unexpected error with the docker command"); + return false; + } + } + + return true; + } + + Future pullHangarImage() async { + ProcessResult pullResult = await Process.run(command, ["pull", imageName]); + return pullResult.exitCode == 0; + } + + /// Builds the list of arguments for the "docker" command in [executeCommand] + @visibleForTesting + List buildCommands(List dockerArgs, List commands) { + List volumeMappings = getVolumeMappings(); + + return ["run", "--rm"] + + dockerArgs + + volumeMappings + + [imageName] + + commands; + } + + /// Returns the list of the volume mappings for the Hangar container. + /// + /// Because the containers are stateless, we need to mount volumes in each run + /// to have persistence in the Cloud CLIs for things like authentication. + /// + /// It needs to be overwritten because of variances in folder paths of Docker + /// installations and platform. + /// + /// It will generate a list like the following: + /// + /// `["-v", "hostFolder:containerFolder", "-v", "hostFolder2:ContainerFolder2"]` + List getVolumeMappings(); +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/docker/docker_controller_factory.dart b/takeoff/takeoff_lib/lib/src/controllers/docker/docker_controller_factory.dart new file mode 100644 index 000000000..8a77cf457 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/docker/docker_controller_factory.dart @@ -0,0 +1,61 @@ +import 'package:get_it/get_it.dart'; +import 'package:meta/meta.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_installation.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/ddesktop_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/rancher_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/unix_controller.dart'; +import 'package:takeoff_lib/src/utils/logger/log.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; + +/// Factory for the Docker controller. +/// +/// Returns the appropiate instance of the Docker Controller depending on platform +/// and the current docker installation. +/// +/// The options are [RancherController], [DockerDesktopController] and [UnixController]. +class DockerControllerFactory { + final PlatformService platformService = GetIt.I.get(); + final SystemService systemService; + + DockerControllerFactory({SystemService? systemService}) + : systemService = systemService ?? SystemService(); + + /// Returns the appropiate [DockerController] instance. + Future create() async { + DockerType dockerType = await checkDockerInstallationType(); + switch (dockerType.installation) { + case DockerInstallation.rancherDesktop: + Log.info("Rancher desktop with ${dockerType.command.name}"); + return RancherController(command: dockerType.command.name); + case DockerInstallation.dockerDesktop: + Log.info("Docker desktop with ${dockerType.command.name}"); + return DockerDesktopController(); + case DockerInstallation.unix: + Log.info("Unix system with ${dockerType.command.name}"); + return UnixController(command: dockerType.command.name); + case DockerInstallation.unknown: + throw UnsupportedError( + "TakeOff could not determine the docker installation"); + } + } + + /// Checks which installation type is in the system. + /// + /// The argument [systemService] is only for testing purposes + @visibleForTesting + Future checkDockerInstallationType() async { + DockerType dockerType = GetIt.I.get(); + + if (platformService.isUnix) { + dockerType.installation = DockerInstallation.unix; + } else if (await systemService.isDockerDesktopInstalled()) { + dockerType.installation = DockerInstallation.dockerDesktop; + } else { + dockerType.installation = DockerInstallation.rancherDesktop; + } + + return dockerType; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/docker/docker_installation.dart b/takeoff/takeoff_lib/lib/src/controllers/docker/docker_installation.dart new file mode 100644 index 000000000..c8fb3b46d --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/docker/docker_installation.dart @@ -0,0 +1,15 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +/// Enum to distinguish between Docker installations +enum DockerInstallation { rancherDesktop, dockerDesktop, unix, unknown } + +enum DockerCommand { nerdctl, docker, none } + +class DockerType { + DockerInstallation installation; + DockerCommand command; + + DockerType({ + required this.installation, + required this.command, + }); +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/ddesktop_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/ddesktop_controller.dart new file mode 100644 index 000000000..b1f5adf31 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/ddesktop_controller.dart @@ -0,0 +1,25 @@ +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; + +/// [DockerController] implementation for Windows systems with Docker Desktop. +class DockerDesktopController extends DockerController { + DockerDesktopController({SystemService? systemService}) + : super(command: "docker", systemService: systemService); + + @override + List getVolumeMappings() { + Map hostFolders = foldersService.getHostFolders(); + Map containerFolders = FoldersService.containerFolders; + + List volumeMappings = hostFolders.entries + .map((hostFolder) => + "${hostFolder.value}:${containerFolders[hostFolder.key]}") + .toList(); + + for (int i = 0; i < volumeMappings.length; i += 2) { + volumeMappings.insert(i, "-v"); + } + return volumeMappings; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/rancher_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/rancher_controller.dart new file mode 100644 index 000000000..36e026b5b --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/rancher_controller.dart @@ -0,0 +1,30 @@ +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; + +/// [DockerController] implementation for Windows systems with Rancher Desktop. +class RancherController extends DockerController { + RancherController({required String command, SystemService? systemService}) + : super(command: command, systemService: systemService); + + @override + List getVolumeMappings() { + Map hostFolders = foldersService.getHostFolders(); + String preprend = (command == "docker") ? "/mnt/c" : ""; + + hostFolders = hostFolders.map((name, path) => + MapEntry(name, "$preprend${path.replaceAll("\\", "/").substring(2)}")); + + Map containerFolders = FoldersService.containerFolders; + + List volumeMappings = hostFolders.entries + .map((hostFolder) => + "${hostFolder.value}:${containerFolders[hostFolder.key]}") + .toList(); + + for (int i = 0; i < volumeMappings.length; i += 2) { + volumeMappings.insert(i, "-v"); + } + return volumeMappings; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/unix_controller.dart b/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/unix_controller.dart new file mode 100644 index 000000000..356cebc2c --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/docker/specific_controllers/unix_controller.dart @@ -0,0 +1,26 @@ +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; + +/// [DockerController] implementation for Unix systems, that do not need +/// Rancher Desktop nor Docker Desktop. +class UnixController extends DockerController { + UnixController({required String command, SystemService? systemService}) + : super(command: command, systemService: systemService); + + @override + List getVolumeMappings() { + Map hostFolders = foldersService.getHostFolders(); + Map containerFolders = FoldersService.containerFolders; + + List volumeMappings = hostFolders.entries + .map((hostFolder) => + "${hostFolder.value}:${containerFolders[hostFolder.key]}") + .toList(); + + for (int i = 0; i < volumeMappings.length; i += 2) { + volumeMappings.insert(i, "-v"); + } + return volumeMappings; + } +} diff --git a/takeoff/takeoff_lib/lib/src/controllers/persistence/cache_repository.dart b/takeoff/takeoff_lib/lib/src/controllers/persistence/cache_repository.dart new file mode 100644 index 000000000..72c135952 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/controllers/persistence/cache_repository.dart @@ -0,0 +1,19 @@ +abstract class CacheRepository { + /// Saves the Google Cloud email in the cache DB + Future saveGoogleEmail(String email); + + /// Retrieves the Google Cloud email from the cache DB + Future getGoogleEmail(); + + /// Stores a new Google Cloud Project ID + Future saveGoogleProjectId(String projectId); + + /// Returns all the Google Cloud Project IDs from the logged account + Future> getGoogleProjectIds(); + + /// Returns all the Google Cloud Project IDs from the logged account + Future removeGoogleProject(String projectId); + + /// Returns all the Google Cloud Project IDs from the logged account + Future removeGoogleEmail(); +} diff --git a/takeoff/takeoff_lib/lib/src/domain/application_end.dart b/takeoff/takeoff_lib/lib/src/domain/application_end.dart new file mode 100644 index 000000000..b6ed2c43c --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/application_end.dart @@ -0,0 +1,2 @@ +/// Defines the possible values for the application end in the pipeline controller. +enum ApplicationEnd { frontend, backend } diff --git a/takeoff/takeoff_lib/lib/src/domain/cloud_provider.dart b/takeoff/takeoff_lib/lib/src/domain/cloud_provider.dart new file mode 100644 index 000000000..7e6085b2e --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/cloud_provider.dart @@ -0,0 +1,21 @@ +import 'package:takeoff_lib/src/domain/cloud_provider_id.dart'; +import 'package:takeoff_lib/src/domain/gcloud.dart'; + +/// Defines all the necessary fields to identify a Cloud Provider +abstract class CloudProvider { + CloudProvider(); + + String get hostFolderName; + String get name; + + factory CloudProvider.fromId(CloudProviderId id) { + switch (id) { + case CloudProviderId.gcloud: + return GCloud(); + case CloudProviderId.aws: + throw UnsupportedError("Cloud provider AWS currently not supported"); + case CloudProviderId.azure: + throw UnsupportedError("Cloud provider Azure currently not supported"); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/cloud_provider_id.dart b/takeoff/takeoff_lib/lib/src/domain/cloud_provider_id.dart new file mode 100644 index 000000000..64595ee64 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/cloud_provider_id.dart @@ -0,0 +1,20 @@ +/// Enum to distinguish between cloud providers +enum CloudProviderId { + gcloud, + aws, + azure; + + factory CloudProviderId.fromString(String string) { + switch (string) { + case "gc": + return gcloud; + case "aws": + return aws; + case "azure": + return azure; + default: + throw UnsupportedError( + 'Values for cloud provider can be "gc", "aws" or "azure"'); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/gcloud.dart b/takeoff/takeoff_lib/lib/src/domain/gcloud.dart new file mode 100644 index 000000000..5b7b93673 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/gcloud.dart @@ -0,0 +1,11 @@ +import 'package:takeoff_lib/src/domain/cloud_provider.dart'; + +/// Specific implementation of Google Cloud as a Cloud Provider. +/// +/// This class exists currently to generify implementations, like `AuthController` +class GCloud extends CloudProvider { + @override + String get hostFolderName => "gcloud"; + @override + String get name => "Google Cloud"; +} diff --git a/takeoff/takeoff_lib/lib/src/domain/google_cloud_regions.dart b/takeoff/takeoff_lib/lib/src/domain/google_cloud_regions.dart new file mode 100644 index 000000000..0401ec3f4 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/google_cloud_regions.dart @@ -0,0 +1,43 @@ +List googleCloudRegions = [ + "asia-east1", + "asia-east2", + "asia-northeast1", + "asia-northeast2", + "asia-northeast3", + "asia-south1", + "asia-south2", + "asia-southeast1", + "asia-southeast2", + "australia-southeast1", + "australia-southeast2", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west2", + "europe-west3", + "europe-west4", + "europe-west6", + "europe-west8", + "europe-west9", + "me-west1", + "northamerica-northeast1", + "northamerica-northeast2", + "southamerica-east1", + "southamerica-west1", + "us-central1", + "us-east1", + "us-east4", + "us-east5", + "us-south1", + "us-west1", + "us-west2", + "us-west3", + "us-west4", +]; + +List firebaseRegions = [ + "us-central1", + "europe-west1", + "asia-southeast1", +]; diff --git a/takeoff/takeoff_lib/lib/src/domain/gui_message/gui_message.dart b/takeoff/takeoff_lib/lib/src/domain/gui_message/gui_message.dart new file mode 100644 index 000000000..de9d28953 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/gui_message/gui_message.dart @@ -0,0 +1,34 @@ +import 'package:takeoff_lib/src/domain/gui_message/message_type.dart'; + +import 'input_type.dart'; + +class GuiMessage { + MessageType type; + String message; + String? url; + InputType? inputType; + + GuiMessage( + {required this.type, required this.message, this.url, this.inputType}); + + factory GuiMessage.info(String message) { + return GuiMessage(type: MessageType.info, message: message); + } + + factory GuiMessage.input(String message, InputType inputType) { + return GuiMessage( + type: MessageType.input, message: message, inputType: inputType); + } + + factory GuiMessage.success(String message, String url) { + return GuiMessage(type: MessageType.success, message: message, url: url); + } + + factory GuiMessage.error(String message) { + return GuiMessage(type: MessageType.error, message: message); + } + + factory GuiMessage.browser(String message, String url) { + return GuiMessage(type: MessageType.browser, message: message, url: url); + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/gui_message/input_type.dart b/takeoff/takeoff_lib/lib/src/domain/gui_message/input_type.dart new file mode 100644 index 000000000..51a359b87 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/gui_message/input_type.dart @@ -0,0 +1 @@ +enum InputType { number, text } diff --git a/takeoff/takeoff_lib/lib/src/domain/gui_message/message_type.dart b/takeoff/takeoff_lib/lib/src/domain/gui_message/message_type.dart new file mode 100644 index 000000000..a43ebff99 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/gui_message/message_type.dart @@ -0,0 +1 @@ +enum MessageType { info, input, success, browser, error } diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart new file mode 100644 index 000000000..2adaba154 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart @@ -0,0 +1,62 @@ +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +/// Base class with common arguments for the generate pipeline scripts. +abstract class PipelineGenerator implements Script { + /// Configuration file containing pipeline definition. + String configFile; + + /// Name that will be set to the pipeline. + String pipelineName; + + /// Language or framework of the project. + Language language; + + /// Local directory of your project. + String localDirectory; + + /// Name of the branch to which the Pull Request will target. PR is not created if the flag is not provided. + String? targetBranch; + + /// [Required, if Flutter or Python] Language or framework version. + String? languageVersion; + + PipelineGenerator({ + required this.configFile, + required this.pipelineName, + required this.language, + required this.localDirectory, + this.targetBranch, + this.languageVersion, + }); + + @override + Map get errors => { + 2: "The arguments are not correct", + 127: + "Some necessary package is not installed:\nThe requisites are:\nGit, Github CLI, Azure CLI, GCloud CLI and Python" + }; + + @override + List toCommand() { + List args = [ + "-c", + configFile, + "-n", + pipelineName, + "--local-directory", + localDirectory + ]; + if (language != Language.none) { + args.addAll(["-l", language.name]); + } + if (targetBranch != null) { + args.addAll(["-b", targetBranch!]); + } + if (languageVersion != null) { + args.addAll(["--language-version", languageVersion!]); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/branch_strategy.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/branch_strategy.dart new file mode 100644 index 000000000..2da57182f --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/branch_strategy.dart @@ -0,0 +1,2 @@ +/// Possible values for the parameter [setUpBranchStrategy] of [CreateRepo] +enum BranchStrategy { gitflow } diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/create_repo.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/create_repo.dart new file mode 100644 index 000000000..b26cfc0f5 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/create_repo.dart @@ -0,0 +1,89 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/common/repo/branch_strategy.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/repo/repo_action.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +/// Creates or imports a repository on a provider +/// +/// It allows you to, based on the action, either: +/// +/// Create an empty repository with just a README file and clone it to your computer +/// into the directory you set. Useful when starting a project from scratch. +/// +/// Import an already existing directory or Git repository into your project giving +/// a path or an URL. Useful for taking to a provider the development of an existing project +abstract class CreateRepo implements Script { + /// Use case to fulfil: create, import. + RepoAction action; + + /// Path to the directory where your repository will be cloned or initialized. + String directory; + + /// Name for the repository. By default, the source repository or + /// directory name (either new or existing, depending on use case) is used. + String? name; + + /// Source URL of the Git repository to import. + String? sourceGitUrl; + + /// Source branch to be used as a basis to initialize the repository on import, as master branch. + String? sourceBranch; + + /// Removes branches other than the (possibly new) default one. + bool? removeOtherBranches; + + /// Creates branches and policies required for the desired workflow. Requires [sourceBranch] on import. + BranchStrategy? setUpBranchStrategy; + + /// Skips any user confirmation. + bool? force; + + /// When combined with [sourceGitURl] and [removeOtherBranches], imports only + /// the specified subpath of the source Git repository. + String? subpath; + + CreateRepo({ + required this.action, + required this.directory, + this.name, + this.sourceGitUrl, + this.sourceBranch, + this.removeOtherBranches, + this.setUpBranchStrategy, + this.force, + this.subpath, + }); + + @override + Map get errors => + {1: "Unexpected error. Check the arguments to avoid errors."}; + + @override + List toCommand() { + List args = []; + args.addAll(["-a", action.name, "-d", directory]); + + if (name != null) { + args.addAll(["-n", name!]); + } + if (sourceGitUrl != null) { + args.addAll(["-g", sourceGitUrl!]); + } + if (sourceBranch != null) { + args.addAll(["-b", sourceBranch!]); + } + if (removeOtherBranches != null) { + args.addAll(["-r", removeOtherBranches.toString()]); + } + if (force != null) { + args.addAll(["-f", force.toString()]); + } + if (subpath != null) { + args.addAll(["--subpath", subpath!]); + } + if (setUpBranchStrategy != null) { + args.addAll(["-s", "gitflow"]); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/repo_action.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/repo_action.dart new file mode 100644 index 000000000..d1eb84e67 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/repo/repo_action.dart @@ -0,0 +1,2 @@ +/// Possible values for the parameter [action] of [CreateRepo] +enum RepoAction { create, import } diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/sonarqube/setup_sonar.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/sonarqube/setup_sonar.dart new file mode 100644 index 000000000..90fb38ec3 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/common/sonarqube/setup_sonar.dart @@ -0,0 +1,33 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +class SetUpSonar implements Script { + String serviceAccountFile; + String project; + String stateFolder; + + SetUpSonar({ + required this.serviceAccountFile, + required this.project, + required this.stateFolder, + }); + + @override + Map get errors => {}; + + @override + List toCommand() { + List args = [ + "/scripts/sonarqube/gcloud/sonarqube.sh", + "apply", + "--state-folder", + stateFolder, + "--service_account_file", + serviceAccountFile, + "--project", + project, + ]; + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/account/setup_principal_account.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/account/setup_principal_account.dart new file mode 100644 index 000000000..bf00a78b3 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/account/setup_principal_account.dart @@ -0,0 +1,71 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +/// Enrolls a Principal (end user or service account) in a project with the provided roles attached. +/// +/// [serviceAcccount] & [googleAccount] are mutually exclusive. If one is passed, the other +/// should be an empty `String`. If both are passed, TakeOff will favour the Service Account. +class SetUpPrincipalAccountGCloud implements Script { + /// Google Account of an end user. Mutually exclusive with [serviceAccount]. + String googleAccount; + + /// Service Account Name. Mutually exclusive with [googleAccount]. + String serviceAccount; + + /// Short project name (ID) to which the principal will be enrolled. + String projectId; + + /// Roles (basic or predefined) to be attached to the principal in the project, splitted by comma. + String? roles; + + /// Path to a file containing the roles (basic or predefined) to be attached to the principal in the project. + String rolesFilePath; + + /// Path to a YAML file containing the custom role to be attached to the principal in the project. Requires [customRoleId]. + String? customRoleYamlPath; + + /// ID to be set to the custom role provided in [customRoleYamlPath]. + String? customRoleId; + + /// Path to store the generated service account key. + String? serviceKeyPath; + + SetUpPrincipalAccountGCloud( + {required this.googleAccount, + required this.serviceAccount, + required this.projectId, + this.roles, + this.rolesFilePath = "/scripts/accounts/gcloud/predefined-roles.txt", + this.customRoleYamlPath, + this.customRoleId, + this.serviceKeyPath}); + + @override + Map get errors => { + 2: "There was an error setting up the account.\nCheck the arguments to avoid errors.", + 127: "Google Cloud CLI is not installed", + }; + + @override + List toCommand() { + List args = ["/scripts/accounts/gcloud/setup-principal-account.sh"]; + if (serviceAccount.isNotEmpty) { + args.addAll(["-s", serviceAccount]); + } else { + args.addAll(["-g", googleAccount]); + } + args.addAll(["-p", projectId, "-f", rolesFilePath]); + + if (roles != null) { + args.addAll(["-r", roles!]); + } + if (customRoleYamlPath != null && customRoleId != null) { + args.addAll(["-c", customRoleYamlPath!, "-i", customRoleId!]); + } + if (serviceKeyPath != null) { + args.addAll(["-k", serviceKeyPath!]); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions.dart new file mode 100644 index 000000000..e8de87ba5 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions.dart @@ -0,0 +1,67 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +/// Checks if a Principal (end user or service account) has the specified roles and permissions in a given project. +class VerifyRolesAndPermissionsGCloud implements Script { + /// Google Account of an end user. Mutually exclusive with [serviceAccount]. + String googleAccount; + + /// Service Account Name. Mutually exclusive with [googleAccount]. + String serviceAccount; + + /// Short project name (ID) where the roles and permissions will be checked. + String projectId; + + /// Roles to be checked, splitted by comma. + String? roles; + + /// Path to a file containing the roles to be checked. + String rolesFilePath; + + /// Permissions to be checked, splitted by comma. + String? permissions; + + /// Path to a file containing the permissions to be checked. + String? permissionsFilePath; + + VerifyRolesAndPermissionsGCloud({ + required this.googleAccount, + required this.serviceAccount, + required this.projectId, + this.roles, + this.rolesFilePath = "/scripts/accounts/gcloud/predefined-roles.txt", + this.permissions, + this.permissionsFilePath, + }); + + @override + Map get errors => { + 2: "There was an error related to the account or parameters.\nCheck the arguments to avoid errors.", + 127: "Google Cloud CLI is not installed", + 3: "There was an error checking a role or permission", + }; + + @override + List toCommand() { + List args = [ + "/scripts/accounts/gcloud/verify-principal-roles-and-permissions.sh" + ]; + if (serviceAccount.isNotEmpty) { + args.addAll(["-s", serviceAccount]); + } else { + args.addAll(["-g", googleAccount]); + } + args.addAll(["-p", projectId, "-f", rolesFilePath]); + if (roles != null) { + args.addAll(["-r", roles!]); + } + if (permissions != null) { + args.addAll(["-e", permissions!]); + } + if (permissionsFilePath != null) { + args.addAll(["-i", permissionsFilePath!]); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart new file mode 100644 index 000000000..98a523f39 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart @@ -0,0 +1,11 @@ +/// Possible value for the parameter [machineType] in GCloud scripts +enum MachineType { + // ignore: constant_identifier_names + E2_HIGHCPU_8, + // ignore: constant_identifier_names + E2_HIGHCPU_32, + // ignore: constant_identifier_names + N1_HIGHCPU_8, + // ignore: constant_identifier_names + N1_HIGHCPU_32, +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline.dart new file mode 100644 index 000000000..535aad644 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline.dart @@ -0,0 +1,41 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; + +/// Script to create a Build Pipeline in Google Cloud +class BuildPipelineGCloud extends PipelineGenerator { + /// [Required, if Flutter] Artifact registry location. + String? registryLocation; + + /// Target directory of build process. Takes precedence over the language/framework default one. + String? targetDirectory; + + /// Machine type for pipeline runner. + MachineType? machineType; + + BuildPipelineGCloud( + {required super.configFile, + required super.pipelineName, + required super.language, + required super.localDirectory, + super.targetBranch, + super.languageVersion, + this.registryLocation, + this.targetDirectory, + this.machineType}); + + @override + List toCommand() { + List args = super.toCommand(); + args.insert(0, "/scripts/pipelines/gcloud/pipeline_generator.sh"); + if (registryLocation != null) { + args.addAll(["--registry-location", registryLocation!]); + } + if (targetDirectory != null) { + args.addAll(["-t", targetDirectory!]); + } + if (machineType != null) { + args.addAll(["-m", machineType!.name]); + } + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline.dart new file mode 100644 index 000000000..3b0dc6680 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline.dart @@ -0,0 +1,53 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; + +/// Script to create a deployment pipeline to Cloud Run in Google Cloud +class DeployPipelineGCloud extends PipelineGenerator { + /// Name for the cloud run service. + String serviceName; + + /// Region where the service will be deployed. + String gCloudRegion; + + /// Listening port of the service. If no value is passed is 8080. + int? port; + + /// Machine type for pipeline runner. + MachineType? machineType; + + String? registryLocation; + + DeployPipelineGCloud( + {required super.configFile, + required super.pipelineName, + required super.language, + required super.localDirectory, + required this.serviceName, + required this.gCloudRegion, + super.targetBranch, + super.languageVersion, + this.port, + this.registryLocation, + this.machineType}); + + @override + List toCommand() { + List args = super.toCommand(); + args.insert(0, "/scripts/pipelines/gcloud/pipeline_generator.sh"); + args.addAll( + ["--service-name", serviceName, "--gcloud-region", gCloudRegion]); + if (languageVersion != null) { + args.addAll(["--language-version", languageVersion!]); + } + if (registryLocation != null) { + args.addAll(["--registry-location", registryLocation!]); + } + if (port != null) { + args.addAll(["--port", port.toString()]); + } + if (machineType != null) { + args.addAll(["-m", machineType!.name]); + } + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_platform.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_platform.dart new file mode 100644 index 000000000..13e2176b9 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_platform.dart @@ -0,0 +1 @@ +enum FlutterPlatform { web, android } diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_web_renderer.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_web_renderer.dart new file mode 100644 index 000000000..467b87317 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_web_renderer.dart @@ -0,0 +1 @@ +enum FlutterWebRenderer { auto, canvaskit, html } diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/package_pipeline.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/package_pipeline.dart new file mode 100644 index 000000000..c40eabaf0 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/package_pipeline.dart @@ -0,0 +1,96 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/flutter_web_renderer.dart'; + +/// Script to create a Package Pipeline in Google Cloud +class PackagePipelineGCloud extends PipelineGenerator { + /// Build pipeline name. + String buildPipelineName; + + /// Quality pipeline name. + String qualityPipelineName; + + /// Name (excluding tag) for the generated container image. + String imageName; + + /// Open the Pull Request on the web browser if it cannot be automatically merged. Requires [targetBranch]. + bool openPRinBrowser; + + bool? androidFlutterPlatform; + bool? webFlutterPlatform; + + FlutterWebRenderer? flutterWebRenderer; + + /// [Required, if language not set] Path from the root of the project to its Dockerfile. + /// Takes precedence over the language/framework default one. + /// + /// If this is preferred over [language], put the [language] value to `None`. + String? dockerfile; + + String? registryLocation; + + MachineType? machineType; + + PackagePipelineGCloud( + {required super.configFile, + required super.pipelineName, + required super.language, + required super.localDirectory, + required this.buildPipelineName, + required this.qualityPipelineName, + required this.imageName, + super.languageVersion, + this.openPRinBrowser = false, + super.targetBranch, + this.registryLocation, + this.dockerfile, + this.androidFlutterPlatform, + this.webFlutterPlatform, + this.flutterWebRenderer, + this.machineType}); + + @override + List toCommand() { + List args = super.toCommand(); + args.insert(0, "/scripts/pipelines/gcloud/pipeline_generator.sh"); + args.addAll([ + "--build-pipeline-name", + buildPipelineName, + "--quality-pipeline-name", + qualityPipelineName, + "-i", + imageName, + //"-u", + //registryUser, + //"-p", + //registryPassword + ]); + if (languageVersion != null) { + args.addAll(["--language-version", languageVersion!]); + } + if (dockerfile != null && language == Language.none) { + args.addAll(["--dockerfile", dockerfile!]); + } + if (openPRinBrowser && targetBranch != null) { + args.add("-w"); + } + if (registryLocation != null) { + args.addAll(["--registry-location", registryLocation!]); + } + if (androidFlutterPlatform != null) { + args.add("--flutter-android-platform"); + } + if (machineType != null) { + args.addAll(["-m", machineType!.name]); + } + if (webFlutterPlatform != null) { + args.add("--flutter-web-platform"); + } + if (flutterWebRenderer != null) { + args.addAll(["--flutter-web-renderer", flutterWebRenderer!.name]); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/quality_pipeline.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/quality_pipeline.dart new file mode 100644 index 000000000..9032dc6a5 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/quality_pipeline.dart @@ -0,0 +1,59 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; + +/// Script to create a Quality Pipeline in Google Cloud +class QualityPipelineGCloud extends PipelineGenerator { + /// Build pipeline name. + String buildPipelineName; + + /// Build pipeline name. + String testPipelineName; + + /// SonarQube URL. + String sonarUrl; + + /// SonarQube token. + String sonarToken; + + /// Machine type for pipeline runner. + MachineType? machineType; + + String? registryLocation; + + QualityPipelineGCloud( + {required super.configFile, + required super.pipelineName, + required super.language, + required super.localDirectory, + required this.buildPipelineName, + required this.testPipelineName, + required this.sonarUrl, + required this.sonarToken, + super.targetBranch, + super.languageVersion, + this.machineType, + this.registryLocation}); + + @override + List toCommand() { + List args = super.toCommand(); + args.insert(0, "/scripts/pipelines/gcloud/pipeline_generator.sh"); + args.addAll([ + "--sonar-url", + sonarUrl, + "--sonar-token", + sonarToken, + "--build-pipeline-name", + buildPipelineName, + "--test-pipeline-name", + testPipelineName + ]); + if (machineType != null) { + args.addAll(["-m", machineType!.name]); + } + if (registryLocation != null) { + args.addAll(["--registry-location", registryLocation!]); + } + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/test_pipeline.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/test_pipeline.dart new file mode 100644 index 000000000..2b8847901 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/pipeline_generator/test_pipeline.dart @@ -0,0 +1,49 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/common/pipeline_generator/pipeline_generator.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; + +/// Script to create a Test Pipeline in Google Cloud +class TestPipelineGCloud extends PipelineGenerator { + /// Build pipeline name. + String buildPipelineName; + + /// Machine type for pipeline runner. + MachineType? machineType; + + /// Path to be persisted as an artifact after pipeline execution, + /// e.g. where the application stores logs or any other blob on runtime. + String? artifactPath; + + String? registryLocation; + + TestPipelineGCloud( + {required super.configFile, + required super.pipelineName, + required super.language, + required super.localDirectory, + required this.buildPipelineName, + super.targetBranch, + super.languageVersion, + this.machineType, + this.registryLocation, + this.artifactPath}); + + @override + List toCommand() { + List args = super.toCommand(); + args.insert(0, "/scripts/pipelines/gcloud/pipeline_generator.sh"); + args.addAll(["--build-pipeline-name", buildPipelineName]); + if (machineType != null) { + args.addAll(["-m", machineType!.name]); + } + if (artifactPath != null) { + args.addAll(["-a", artifactPath!]); + } + if (languageVersion != null) { + args.addAll(["--language-version", languageVersion!]); + } + if (registryLocation != null) { + args.addAll(["--registry-location", registryLocation!]); + } + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/project/create_project.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/project/create_project.dart new file mode 100644 index 000000000..8590c36c5 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/project/create_project.dart @@ -0,0 +1,66 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +/// Creates a new project and enables billing and required APIs. +class CreateProjectGCloud implements Script { + /// Name of the new project. + String projectName; + + /// Billing account. If not specified, won't be able to enable some services. + String billingAccount; + + /// Description for the new project. If not specified, name will be used as description. + String? description; + + /// ID of the folder for which the project will be configured. + String? folderId; + + /// ID of the organization for which the project will be configured. + String? organizationId; + + bool firebase; + + CreateProjectGCloud( + {required this.projectName, + required this.billingAccount, + this.description, + this.folderId, + this.organizationId, + this.firebase = false}); + + @override + Map get errors => { + 2: "Missing mandatory parameters: Project name & Billing account", + 127: "GCloug CLI is not installed", + 200: "Error while creating the project", + 210: "Unable to link project to billing account", + 220: "Cannot enable Cloud Source Repositories API", + 221: "Cannot enable Cloud Run API", + 222: "Cannot enable Artifact Registry API", + 223: "Cannot enable Cloud Build API", + 224: "Cannot enable Secret Manager API" + }; + + @override + List toCommand() { + List args = [ + "/scripts/accounts/gcloud/create-project.sh", + "-n", + projectName, + "-b", + billingAccount + ]; + if (description != null) { + args.addAll(["-d", description!]); + } + if (folderId != null) { + args.addAll(["-f", folderId!]); + } + if (organizationId != null) { + args.addAll(["-o", organizationId!]); + } + if (firebase) { + args.add("--firebase"); + } + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/repo/create_repo.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/repo/create_repo.dart new file mode 100644 index 000000000..67c7af352 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/gcloud/repo/create_repo.dart @@ -0,0 +1,27 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/common/repo/create_repo.dart'; + +class CreateRepoGCloud extends CreateRepo { + /// Short name (ID) of the Google Cloud project. + String project; + + CreateRepoGCloud({ + required this.project, + required super.action, + required super.directory, + super.name, + super.sourceGitUrl, + super.sourceBranch, + super.removeOtherBranches, + super.setUpBranchStrategy, + super.force, + super.subpath, + }); + + @override + List toCommand() { + List args = super.toCommand(); + args.insertAll( + 0, ["/scripts/repositories/gcloud/create-repo.sh", "-p", project]); + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/init_cloud_run.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/init_cloud_run.dart new file mode 100644 index 000000000..cf55256d9 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/init_cloud_run.dart @@ -0,0 +1,40 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +class InitCloudRun implements Script { + String project; + String name; + String? region; + String? urlOutputFile; + + InitCloudRun({ + required this.project, + required this.name, + this.region, + this.urlOutputFile, + }); + + @override + Map get errors => {}; + + @override + List toCommand() { + List args = [ + "/scripts/quickstart/gcloud/init-cloud-run.sh", + "-p", + project, + "-n", + name + ]; + + if (region != null) { + args.addAll(["-r", region!]); + } + + if (urlOutputFile != null) { + args.addAll(["-o", urlOutputFile!]); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/setup_firebase.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/setup_firebase.dart new file mode 100644 index 000000000..d8ce1a9a1 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/setup_firebase.dart @@ -0,0 +1,53 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +class SetUpFirebase implements Script { + String projectName; + String credentialsOutputFolder; + String? firestoreRegion; + bool? enableMaps; + bool? setUpIOS; + bool? setUpAndroid; + bool? setUpWeb; + + SetUpFirebase({ + required this.projectName, + required this.credentialsOutputFolder, + this.firestoreRegion, + this.enableMaps, + this.setUpIOS, + this.setUpAndroid, + this.setUpWeb, + }); + + @override + Map get errors => {}; + + @override + List toCommand() { + List args = [ + "/scripts/accounts/gcloud/setup-firebase.sh", + "-n", + projectName, + "-o", + credentialsOutputFolder + ]; + if (firestoreRegion != null) { + args.addAll(["-r", firestoreRegion!]); + } + if (enableMaps != null) { + args.add("--enable-maps"); + } + if (setUpIOS != null) { + args.add("--setup-ios"); + } + if (setUpAndroid != null) { + args.add("--setup-android"); + } + if (setUpWeb != null) { + args.add("--setup-web"); + } + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_step.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_step.dart new file mode 100644 index 000000000..312f5e325 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_step.dart @@ -0,0 +1,19 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +class QuickstartStep { + String goToUrl; + String? textToCopy; + String instructions; + + QuickstartStep({ + required this.goToUrl, + required this.instructions, + this.textToCopy, + }); + + factory QuickstartStep.fromMap(Map map) { + return QuickstartStep( + goToUrl: map["goto"]!, + instructions: map["instructions"]!, + textToCopy: map["copy"]); + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_steps_output.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_steps_output.dart new file mode 100644 index 000000000..91f3d4e10 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_steps_output.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/steps_output/quickstart_step.dart'; + +class QuickstartStepsOutput { + List steps; + + QuickstartStepsOutput({required this.steps}); + + factory QuickstartStepsOutput.fromMap(Map map) { + return QuickstartStepsOutput( + steps: (map["steps"]! as List) + .map((e) => QuickstartStep.fromMap(e)) + .toList()); + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_backend.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_backend.dart new file mode 100644 index 000000000..a155fcf78 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_backend.dart @@ -0,0 +1,36 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/wayat_script.dart'; + +class WayatBackend extends WayatScript { + String projectName; + String workspace; + String backendRepoDir; + String storageBucket; + + WayatBackend({ + required this.projectName, + required this.workspace, + required this.backendRepoDir, + required this.storageBucket, + }); + + @override + Map get errors => {}; + + @override + List toCommand() { + List args = [ + "/scripts/quickstart/gcloud/quickstart-wayat-backend.sh", + "-p", + projectName, + "-w", + workspace, + "-d", + backendRepoDir, + "--storage-bucket", + storageBucket, + ]; + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_frontend.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_frontend.dart new file mode 100644 index 000000000..00a9f8e69 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_frontend.dart @@ -0,0 +1,48 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:takeoff_lib/src/domain/hangar_scripts/quickstart/wayat_script.dart'; + +class WayatFrontend extends WayatScript { + String projectName; + String workspace; + String frontendRepoDir; + String keystoreFile; + String backendUrl; + String frontendUrl; + String mapsStaticSecret; + + WayatFrontend({ + required this.projectName, + required this.workspace, + required this.frontendRepoDir, + required this.keystoreFile, + required this.backendUrl, + required this.frontendUrl, + required this.mapsStaticSecret, + }); + + @override + Map get errors => {}; + + @override + List toCommand() { + List args = [ + "/scripts/quickstart/gcloud/quickstart-wayat-frontend.sh", + "-p", + projectName, + "-w", + workspace, + "-d", + frontendRepoDir, + "--keystore", + keystoreFile, + "--backend-url", + backendUrl, + "--frontend-url", + frontendUrl, + "--maps-static-secret", + mapsStaticSecret, + ]; + + return args; + } +} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_script.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_script.dart new file mode 100644 index 000000000..f6505de97 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/quickstart/wayat_script.dart @@ -0,0 +1,3 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/script.dart'; + +abstract class WayatScript implements Script {} diff --git a/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/script.dart b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/script.dart new file mode 100644 index 000000000..3cc753348 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/hangar_scripts/script.dart @@ -0,0 +1,5 @@ +/// Interface of the script classes +abstract class Script { + List toCommand(); + Map get errors; +} diff --git a/takeoff/takeoff_lib/lib/src/domain/language.dart b/takeoff/takeoff_lib/lib/src/domain/language.dart new file mode 100644 index 000000000..5eb499e51 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/language.dart @@ -0,0 +1,21 @@ +/// Possible values for the parameter [language] of [PipelineGenerator] +enum Language { + quarkus("quarkus"), + quarkusJVM("quarkus-jvm"), + node("node"), + angular("angular"), + python("python"), + flutter("flutter"), + none(""); + + final String name; + const Language(this.name); + + factory Language.fromString(String string) { + return Language.values.firstWhere((element) => element.name == string, + orElse: () => Language.none); + } + + @override + String toString() => name; +} diff --git a/takeoff/takeoff_lib/lib/src/domain/resource.dart b/takeoff/takeoff_lib/lib/src/domain/resource.dart new file mode 100644 index 000000000..2f6a745e9 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/resource.dart @@ -0,0 +1,18 @@ +enum Resource { + ide("ide"), + pipeline("pipeline"), + feRepo("fe-repo"), + beRepo("be-repo"), + none(""); + + final String name; + const Resource(this.name); + + factory Resource.fromString(String string) { + return Resource.values.firstWhere((element) => element.name == string, + orElse: () => Resource.none); + } + + @override + String toString() => name; +} diff --git a/takeoff/takeoff_lib/lib/src/domain/sonar_output.dart b/takeoff/takeoff_lib/lib/src/domain/sonar_output.dart new file mode 100644 index 000000000..2169bf319 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/domain/sonar_output.dart @@ -0,0 +1,16 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +class SonarOutput { + String token; + String url; + + SonarOutput({ + required this.token, + required this.url, + }); + + factory SonarOutput.fromMap(Map map) { + return SonarOutput( + token: map["sonarqube_token"]["value"], + url: map["sonarqube_url"]["value"]); + } +} diff --git a/takeoff/takeoff_lib/lib/src/persistence/cache_repository_impl.dart b/takeoff/takeoff_lib/lib/src/persistence/cache_repository_impl.dart new file mode 100644 index 000000000..c3ca01898 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/persistence/cache_repository_impl.dart @@ -0,0 +1,107 @@ +import 'package:get_it/get_it.dart'; +import 'package:sembast/sembast.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; + +class CacheRepositoryImpl extends CacheRepository { + /// Key to access to the Google Cloud email record + final String _googleCloudKey = 'gcloud_email'; + + final String _googleProjectIdsKey = 'gcloud_project_ids'; + + @override + Future saveGoogleEmail(String email) async { + Database db = GetIt.I.get(); + + StoreRef store = StoreRef.main(); + + await store.record(_googleCloudKey).put(db, email); + + return true; + } + + @override + Future getGoogleEmail() async { + Database db = GetIt.I.get(); + + StoreRef store = StoreRef.main(); + + String? email = await store.record(_googleCloudKey).get(db); + + return email ?? ""; + } + + @override + Future saveGoogleProjectId(String projectId) async { + Database db = GetIt.I.get(); + + StoreRef store = StoreRef.main(); + + String email = await getGoogleEmail(); + + // It's necessary that this list is List because sembast returns + //an ImmutableList?, which cannot be casted to List?. + List? ids = + await store.record("${email}_$_googleProjectIdsKey").get(db); + + ids ??= []; + + // Because ids is an immutable list of dynamic type, we have to transform + // each element to String and create a new list that is not read only. + // We first convert it to set to avoid duplicate project IDs + List newList = ids.map((e) => e.toString()).toSet().toList(); + newList.add(projectId); + + await store.record("${email}_$_googleProjectIdsKey").put(db, newList); + + return true; + } + + @override + Future> getGoogleProjectIds() async { + Database db = GetIt.I.get(); + + StoreRef store = StoreRef.main(); + + String email = await getGoogleEmail(); + List? ids = + await store.record("${email}_$_googleProjectIdsKey").get(db); + + ids ??= []; + + List res = ids.map((e) => e.toString()).toList(); + + return res; + } + + @override + Future removeGoogleProject(String projectId) async { + Database db = GetIt.I.get(); + + StoreRef store = StoreRef.main(); + + String email = await getGoogleEmail(); + List? ids = + await store.record("${email}_$_googleProjectIdsKey").get(db); + + ids ??= []; + if (ids.isEmpty) return true; + + List newList = ids.map((e) => e.toString()).toList(); + newList.remove(projectId); + + await store.record("${email}_$_googleProjectIdsKey").put(db, newList); + + return true; + } + + @override + Future removeGoogleEmail() async { + Database db = GetIt.I.get(); + + StoreRef store = StoreRef.main(); + + dynamic result = await store.record(_googleCloudKey).delete(db); + + return result != null; + } +} diff --git a/takeoff/takeoff_lib/lib/src/persistence/database/database_factory.dart b/takeoff/takeoff_lib/lib/src/persistence/database/database_factory.dart new file mode 100644 index 000000000..d2f405a47 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/persistence/database/database_factory.dart @@ -0,0 +1,21 @@ +import 'package:get_it/get_it.dart'; +import 'package:path/path.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; + +/// Creates the intance of the Database +class DbFactory { + DbFactory({String? dbPath}) + : _dbPath = dbPath ?? + join(GetIt.I.get().getCacheFolder().path, + "takeoff.db"); + + /// Path to the file of the database in the cache folder + final String _dbPath; + + /// Creates the [Database] instance + Future create() async { + return await databaseFactoryIo.openDatabase(_dbPath); + } +} diff --git a/takeoff/takeoff_lib/lib/src/takeoff_facade.dart b/takeoff/takeoff_lib/lib/src/takeoff_facade.dart new file mode 100644 index 000000000..431d0c94a --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/takeoff_facade.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:meta/meta.dart'; +import 'package:sembast/sembast.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller_factory.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_installation.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/persistence/database/database_factory.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +class TakeOffFacade { + @visibleForTesting + late GoogleCloudController googleController; + + /// Initializes all the singletons neeeded for the app to run and checks prerequisites. + /// + /// The singletons are the [DockerController] and the [Database] instances. + /// The [DockerController] is initialized as a singleton to avoid checking the + /// docker installation multiple times during the execution, consuming unnecessary resources. + Future initialize() async { + // This conditional is used to avoid exceptions using hot reload when + if (!GetIt.I.isRegistered()) { + GetIt.I.registerSingleton(PlatformService()); + GetIt.I.registerSingleton(FoldersService()); + GetIt.I.registerSingleton(DockerType( + installation: DockerInstallation.unknown, + command: DockerCommand.none)); + + if (!await SystemService().checkSystemPrerequisites()) { + return false; + } + + DockerController dockerController = + await DockerControllerFactory().create(); + GetIt.I.registerLazySingleton(() => dockerController); + GetIt.I.registerSingleton(await DbFactory().create()); + + googleController = GoogleCloudControllerImpl(); + } + + return true; + } + + /// Returns the currently stored email for each Cloud Provider. + /// + /// Currently only Google Cloud is supported. If you introduce an unsupported + /// provider or there is no currently logged account it will return an empty String. + Future getCurrentAccount(CloudProviderId cloudProvider) async { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return await googleController.getAccount(); + case CloudProviderId.aws: + case CloudProviderId.azure: + return ""; + } + } + + Future runProject(String project, CloudProviderId cloudProvider) async { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return googleController.run(project); + case CloudProviderId.aws: + case CloudProviderId.azure: + Log.warning("Currently not supported"); + return false; + } + } + + /// Logs into the [cloudProvider] with [email]. An optional [stdin] stream + /// is passed for the Google Cloud login. It will not have any effect + /// on any other provider. + /// + /// Returns whether the process is succesful. + Future init(String email, CloudProviderId cloudProvider, + {Stream>? stdinStream, bool useStdin = false}) async { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return await googleController.init(email, + useStdin: useStdin, stdinStream: stdinStream); + case CloudProviderId.aws: + case CloudProviderId.azure: + return false; + } + } + + Future logOut(CloudProviderId cloudProvider, + {Stream>? stdinStream}) async { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return await googleController.logOut(); + case CloudProviderId.aws: + case CloudProviderId.azure: + return false; + } + } + + /// Calls the method that will create a project in Google Cloud. + Future createProjectGCloud({ + required String projectName, + required String billingAccount, + Language? backendLanguage, + String? backendVersion, + Language? frontendLanguage, + String? frontendVersion, + required String googleCloudRegion, + StreamController? outputStream, + StreamController? inputStream, + }) async { + return await googleController.createProject( + projectName: projectName, + billingAccount: billingAccount, + backendLanguage: backendLanguage, + backendVersion: backendVersion, + frontendLanguage: frontendLanguage, + frontendVersion: frontendVersion, + googleCloudRegion: googleCloudRegion, + inputStream: inputStream, + outputStream: outputStream, + ); + } + + /// Creates Wayat in Google Cloud + Future quickstartWayat( + {required String billingAccount, + required String googleCloudRegion, + StreamController? outputStream, + StreamController? inputStream}) async { + return await googleController.wayatQuickstart( + billingAccount: billingAccount, + googleCloudRegion: googleCloudRegion, + outputStream: outputStream, + inputStream: inputStream); + } + + Future cleanProject( + CloudProviderId cloudProvider, String projectId) async { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return await googleController.cleanProject(projectId); + case CloudProviderId.aws: + case CloudProviderId.azure: + return false; + } + } + + Future> getProjects(CloudProviderId cloudProvider) async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + switch (cloudProvider) { + case CloudProviderId.gcloud: + return await cacheRepository.getGoogleProjectIds(); + case CloudProviderId.aws: + case CloudProviderId.azure: + return []; + } + } + + Uri getResource( + String project, CloudProviderId cloudProvider, Resource resource) { + switch (cloudProvider) { + case CloudProviderId.gcloud: + return googleController.getGCloudResourceUrl(project, resource); + case CloudProviderId.aws: + case CloudProviderId.azure: + return Uri.parse(""); + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/utils/folders/folders_service.dart b/takeoff/takeoff_lib/lib/src/utils/folders/folders_service.dart new file mode 100644 index 000000000..9d93af209 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/folders/folders_service.dart @@ -0,0 +1,149 @@ +import 'dart:io' show Directory, File, FileSystemException; +import 'package:meta/meta.dart'; +import 'package:get_it/get_it.dart'; +import 'package:path/path.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/src/utils/platform/unsupported_platform_exception.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; + +/// Service that provides all the necessary folders for the application +class FoldersService { + PlatformService platformService = GetIt.I.get(); + + /// Names of the folders that will be created in .takeoff/ + static Map windowsHostFolders = { + "gcloud": "AppData\\Roaming\\gcloud", + "aws": ".aws", + "azure": ".azure", + "kube": ".kube", + "github": "AppData\\Roaming\\GitHub CLI", + "ssh": ".ssh", + "workspace": "hangar_workspace", + "firebase": "AppData\\Roaming\\configstore", + "git": ".gitconfig", + }; + + static Map linuxHostFolders = { + "gcloud": ".config/gcloud", + "aws": ".aws", + "azure": ".azure", + "kube": ".kube", + "github": ".config/gh", + "ssh": ".ssh", + "workspace": "hangar_workspace", + "firebase": ".config/configstore", + "git": ".gitconfig", + }; + + /// Names of the folders that will be created in .takeoff/ + static Map containerFolders = { + "gcloud": "/root/.config/gcloud", + "aws": "/root/.aws", + "azure": "/root/.azure", + "kube": "/root/.kube", + "github": "/root/.config/gh", + "ssh": "/root/.ssh", + "workspace": "/workspace", + "firebase": "/root/.config/configstore", + "git": "/root/.gitconfig", + }; + + /// Returns the Cache folder as a file. It it does not exists, it's created. + /// + /// If executed in non-desktop platforms throws an [UnsupportedPlatformException] + Directory getCacheFolder() { + Directory cacheFolder = Directory(getCacheFolderPath()); + + if (!cacheFolder.existsSync()) { + try { + cacheFolder.createSync(recursive: false); + Log.info("Created folder $cacheFolder"); + } on FileSystemException catch (e) { + Log.error("Cannot create folder in ${cacheFolder.path}\nException: $e"); + rethrow; + } + } + + return cacheFolder; + } + + /// Returns the folder path where the database for cache will be located in + /// each platform. + /// + /// If executed in non-desktop platforms throws an [UnsupportedPlatformException] + @visibleForTesting + String getCacheFolderPath() { + Map env = platformService.env; + + if (platformService.isWindows) { + return "${env["UserProfile"]}\\AppData\\Roaming\\.takeoff\\"; + } else if (platformService.isUnix) { + return "${env["HOME"]}/.takeoff/"; + } + + throw UnsupportedPlatformException( + "Only Linux, Windows and MacOS are supported"); + } + + /// Creates all the folders necessary to mount the volumes for persistency of the Hangar containers + bool createHostFolders() { + Map env = platformService.env; + + late Map hostFolders; + late String baseFolder; + + if (platformService.isWindows) { + hostFolders = windowsHostFolders; + baseFolder = "${env["UserProfile"]}"; + } else if (platformService.isUnix) { + hostFolders = linuxHostFolders; + baseFolder = "${env["HOME"]}"; + } else { + throw UnsupportedPlatformException( + "Only Linux, Windows and MacOS are supported"); + } + + List values = hostFolders.values.toList(); + // .gitconfig is a file, not a directory, so it does not need to be created + values.remove(".gitconfig"); + File gitconfig = File(join(baseFolder, ".gitconfig")); + if (!gitconfig.existsSync()) { + try { + gitconfig.createSync(); + } on FileSystemException { + Log.error("Could not create .gitconfig file"); + return false; + } + } + + for (String folderName in values) { + Directory newFolder = Directory(join(baseFolder, folderName)); + if (!newFolder.existsSync()) { + try { + newFolder.createSync(recursive: true); + } on FileSystemException catch (e) { + Log.error("Could not create $folderName folder: ${e.osError}"); + return false; + } + } + } + + return true; + } + + /// Returns a Map where the key is the folder name and the values are the paths + Map getHostFolders() { + Map env = platformService.env; + + if (platformService.isWindows) { + return Map.fromEntries(windowsHostFolders.entries.map((entry) => + MapEntry(entry.key, "${env["UserProfile"]}\\${entry.value}"))); + } else if (platformService.isUnix) { + return Map.fromEntries(linuxHostFolders.entries.map( + (entry) => MapEntry(entry.key, "${env["HOME"]}/${entry.value}"))); + } + + throw UnsupportedPlatformException( + "Only Linux, Windows and MacOS are supported"); + } +} diff --git a/takeoff/takeoff_lib/lib/src/utils/logger/log.dart b/takeoff/takeoff_lib/lib/src/utils/logger/log.dart new file mode 100644 index 000000000..6dcedfe33 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/logger/log.dart @@ -0,0 +1,45 @@ +import 'package:logger/logger.dart'; + +class Log { + static final Logger _logger = Logger( + level: Level.verbose, + filter: ProductionFilter(), + printer: PrettyPrinter( + methodCount: 0, + noBoxingByDefault: true, + colors: true, + printEmojis: false), + output: null); + + static void info(String message, {showTimestamp = true}) { + String badge = (showTimestamp) ? "[INFO ${DateTime.now()}]" : "[INFO]"; + _logger.i("$badge $message"); + } + + static void debug(String message, {showTimestamp = true}) { + String badge = (showTimestamp) ? "[DEBUG ${DateTime.now()}]" : "[DEBUG]"; + _logger.d("$badge $message"); + } + + static void error(String message, {showTimestamp = true}) { + String badge = (showTimestamp) ? "[ERROR ${DateTime.now()}]" : "[ERROR]"; + _logger.e("$badge $message"); + } + + static void warning(String message, {showTimestamp = true}) { + String badge = + (showTimestamp) ? "[WARNING ${DateTime.now()}]" : "[WARNING]"; + _logger.w("$badge $message"); + } + + static void success(String message, {showTimestamp = true}) { + String badge = + (showTimestamp) ? "[SUCCESS ${DateTime.now()}]" : "[SUCCESS]"; + _logger.v("\x1B[32m$badge $message"); + } + + static String dockerProcessToString(List args) { + return args.fold( + "docker ", (previousValue, element) => "$previousValue $element"); + } +} diff --git a/takeoff/takeoff_lib/lib/src/utils/platform/platform_service.dart b/takeoff/takeoff_lib/lib/src/utils/platform/platform_service.dart new file mode 100644 index 000000000..192394f5e --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/platform/platform_service.dart @@ -0,0 +1,9 @@ +import 'dart:io' show Platform; + +class PlatformService { + Map get env => Platform.environment; + bool get isWindows => Platform.isWindows; + bool get isLinux => Platform.isLinux; + bool get isMacOS => Platform.isMacOS; + bool get isUnix => Platform.isLinux || Platform.isMacOS; +} diff --git a/takeoff/takeoff_lib/lib/src/utils/platform/unsupported_platform_exception.dart b/takeoff/takeoff_lib/lib/src/utils/platform/unsupported_platform_exception.dart new file mode 100644 index 000000000..3233fa102 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/platform/unsupported_platform_exception.dart @@ -0,0 +1,8 @@ +class UnsupportedPlatformException implements Exception { + final String message; + const UnsupportedPlatformException(this.message); + @override + String toString() { + return "UnsupportedException: $message"; + } +} diff --git a/takeoff/takeoff_lib/lib/src/utils/system/system_service.dart b/takeoff/takeoff_lib/lib/src/utils/system/system_service.dart new file mode 100644 index 000000000..482d82273 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/system/system_service.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_installation.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/logger/log.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; + +class SystemService { + FoldersService foldersService = GetIt.I.get(); + + /// Checks that all the necessary requirements for TakeOff to run are met. + /// + /// These are that there is a valid Docker installation and the cache folders are created. + Future checkSystemPrerequisites() async { + DockerCommand command = checkDockerCommand(); + switch (command) { + case DockerCommand.none: + Log.error("Neither docker nor nerdctl are running"); + return false; + default: + GetIt.I.get().command = command; + break; + } + if (!foldersService.createHostFolders()) { + Log.error("Could not create host folders"); + return false; + } + + return true; + } + + /// Whether Docker is installed and running. + /// + /// Both of this conditions are prerequisites for TakeOff to run. + DockerCommand checkDockerCommand() { + if (isNerdctlRunning()) { + return DockerCommand.nerdctl; + } + if (isDockerRunning()) { + return DockerCommand.docker; + } + + return DockerCommand.none; + } + + /// Whether Docker Desktop is installed + Future isDockerDesktopInstalled() async { + assert(GetIt.I.get().isWindows); + + ProcessResult taskChecker = await Process.run( + "tasklist", ["|", "find", "/i", "Docker Desktop.exe"], + stdoutEncoding: SystemEncoding(), runInShell: true); + + return (taskChecker.stdout as String).isNotEmpty; + } + + /// Whether the Docker daemon is running + bool isDockerRunning() { + try { + ProcessResult dockerProc = Process.runSync("docker", ["ps"]); + return dockerProc.exitCode == 0; + } on ProcessException { + return false; + } + } + + /// Whether the Docker daemon is running + bool isNerdctlRunning() { + try { + ProcessResult dockerProc = Process.runSync("nerdctl", ["ps"]); + return dockerProc.exitCode == 0; + } on ProcessException { + return false; + } + } +} diff --git a/takeoff/takeoff_lib/lib/src/utils/url_launcher/gcloud_url.dart b/takeoff/takeoff_lib/lib/src/utils/url_launcher/gcloud_url.dart new file mode 100644 index 000000000..96fe436cd --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/url_launcher/gcloud_url.dart @@ -0,0 +1,7 @@ +enum GCloudResourceUrl { + baseConsolePath("https://console.cloud.google.com"), + baseSourcePath("https://source.cloud.google.com"); + + const GCloudResourceUrl(this.rawValue); + final String rawValue; +} diff --git a/takeoff/takeoff_lib/lib/src/utils/url_launcher/url_launcher.dart b/takeoff/takeoff_lib/lib/src/utils/url_launcher/url_launcher.dart new file mode 100644 index 000000000..2233b7313 --- /dev/null +++ b/takeoff/takeoff_lib/lib/src/utils/url_launcher/url_launcher.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +class UrlLaucher { + /// Opens a given URL in the browser + Future launch(String url) { + if (Platform.isWindows) { + return Process.run("powershell", ["-command", 'Start-Process "$url"']); + } else if (Platform.isLinux) { + return Process.run("xdg-open", [url], runInShell: true); + } else if (Platform.isMacOS) { + return Process.run("open", [url], runInShell: true); + } else { + throw UnsupportedError('OS not supported'); + } + } +} diff --git a/takeoff/takeoff_lib/lib/takeoff_lib.dart b/takeoff/takeoff_lib/lib/takeoff_lib.dart new file mode 100644 index 000000000..aab94dbca --- /dev/null +++ b/takeoff/takeoff_lib/lib/takeoff_lib.dart @@ -0,0 +1,20 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library takeoff_lib; + +export 'src/takeoff_facade.dart'; +export 'src/utils/logger/log.dart'; +export 'src/domain/cloud_provider.dart'; +export 'src/domain/cloud_provider_id.dart'; +export 'src/domain/language.dart'; +export 'src/domain/resource.dart'; +export 'src/domain/gui_message/gui_message.dart'; +export 'src/domain/gui_message/input_type.dart'; +export 'src/domain/gui_message/message_type.dart'; +export 'src/controllers/cloud/common/hangar/project/create_project_exception.dart'; +export 'src/utils/url_launcher/url_launcher.dart'; +export 'src/domain/resource.dart'; +export 'src/domain/google_cloud_regions.dart'; + +// TODO: Export any libraries intended for clients of this package. diff --git a/takeoff/takeoff_lib/pubspec.yaml b/takeoff/takeoff_lib/pubspec.yaml new file mode 100644 index 000000000..7cb188717 --- /dev/null +++ b/takeoff/takeoff_lib/pubspec.yaml @@ -0,0 +1,20 @@ +name: takeoff_lib +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# homepage: https://www.example.com + +environment: + sdk: '>=2.18.2 <3.0.0' + +dependencies: + path: ^1.8.0 + logger: ^1.1.0 + get_it: ^7.2.0 + sembast: ^3.3.1 + meta: ^1.8.0 + +dev_dependencies: + lints: ^2.0.0 + test: ^1.16.0 + mockito: ^5.3.2 + build_runner: ^2.3.2 diff --git a/takeoff/takeoff_lib/test/src/controllers/cloud_providers/gcloud_controller_test.dart b/takeoff/takeoff_lib/test/src/controllers/cloud_providers/gcloud_controller_test.dart new file mode 100644 index 000000000..6057297e8 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/controllers/cloud_providers/gcloud_controller_test.dart @@ -0,0 +1,144 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:get_it/get_it.dart'; +import 'package:path/path.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/persistence/database/database_factory.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/takeoff_lib.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + late FoldersService foldersService; + + setUpAll(() { + GetIt.I.registerSingleton(PlatformService()); + foldersService = FoldersService(); + GetIt.I.registerSingleton(foldersService); + }); + + setUp(() async { + GetIt.I.registerSingleton( + await DbFactory(dbPath: "gcloud_controller_test.db").create()); + }); + + test("cleanProject removes the workspace folder and the project ID", + () async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String projectId = Random().nextInt(1000000000).toString(); + await cacheRepository.saveGoogleProjectId(projectId); + Directory directory = Directory( + join(foldersService.getHostFolders()["workspace"]!, projectId)); + if (directory.existsSync()) { + fail("Project directory already existed"); + } + directory.createSync(recursive: true); + + GoogleCloudControllerImpl googleCloudController = + GoogleCloudControllerImpl(); + await googleCloudController.cleanProject(projectId); + + expect(directory.existsSync(), false); + expect((await cacheRepository.getGoogleProjectIds()).contains(projectId), + false); + }); + + tearDown(() async { + await databaseFactoryIo.deleteDatabase("gcloud_controller_test.db"); + GetIt.I.unregister(); + }); + + test("RegExp match projectId", () { + GoogleCloudControllerImpl googleCloudController = + GoogleCloudControllerImpl(); + + String projectId_01 = "wayat-takeoff-1-1-1-2-2000"; + String projectId_02 = "wayat-takeoff-1-12-1-12-2000"; + String projectId_03 = "wayat-takeoff-11-12-11-22-2000"; + String projectId_04 = "wayat-takeoff"; + String projectId_05 = "test-wayat"; + + expect(googleCloudController.isQuickStartProject(projectId_01), true); + expect(googleCloudController.isQuickStartProject(projectId_02), true); + expect(googleCloudController.isQuickStartProject(projectId_03), true); + expect(googleCloudController.isQuickStartProject(projectId_04), false); + expect(googleCloudController.isQuickStartProject(projectId_05), false); + }); + + test("Get resource Url for quickstart projects", () { + GoogleCloudControllerImpl googleCloudController = + GoogleCloudControllerImpl(); + + String projectId = "wayat-takeoff-11-12-11-22-2000"; + + Uri ideUrl = + googleCloudController.getGCloudResourceUrl(projectId, Resource.ide); + Uri expectIdeUrl = Uri.parse( + "https://console.cloud.google.com/cloudshelleditor?project=$projectId&cloudshell=true"); + expect(ideUrl, expectIdeUrl); + + Uri pipelineUrl = googleCloudController.getGCloudResourceUrl( + projectId, Resource.pipeline); + Uri expectPipelineUrl = Uri.parse( + "https://console.cloud.google.com/cloud-build/dashboard?project=$projectId"); + expect(pipelineUrl, expectPipelineUrl); + + Uri feRepoUrl = googleCloudController.getGCloudResourceUrl( + projectId, Resource.feRepo); + Uri expectFeRepoUrl = Uri.parse( + "https://source.cloud.google.com/$projectId/wayat-flutter/"); + expect(feRepoUrl, expectFeRepoUrl); + + Uri beRepoUrl = googleCloudController.getGCloudResourceUrl( + projectId, Resource.beRepo); + Uri expectBeRepoUrl = Uri.parse( + "https://source.cloud.google.com/$projectId/wayat-python/"); + expect(beRepoUrl, expectBeRepoUrl); + + Uri resourceNone = + googleCloudController.getGCloudResourceUrl(projectId, Resource.none); + expect(resourceNone, Uri.parse("")); + }); + + test("Get resource Url for created projects", () { + GoogleCloudControllerImpl googleCloudController = + GoogleCloudControllerImpl(); + + String projectId = "wayat-takeoff"; + + Uri ideUrl = + googleCloudController.getGCloudResourceUrl(projectId, Resource.ide); + Uri expectIdeUrl = Uri.parse( + "https://console.cloud.google.com/cloudshelleditor?project=$projectId&cloudshell=true"); + expect(ideUrl, expectIdeUrl); + + Uri pipelineUrl = googleCloudController.getGCloudResourceUrl( + projectId, Resource.pipeline); + Uri expectPipelineUrl = Uri.parse( + "https://console.cloud.google.com/cloud-build/dashboard?project=$projectId"); + expect(pipelineUrl, expectPipelineUrl); + + Uri feRepoUrl = googleCloudController.getGCloudResourceUrl( + projectId, Resource.feRepo); + Uri expectFeRepoUrl = + Uri.parse("https://source.cloud.google.com/$projectId/Frontend/"); + expect(feRepoUrl, expectFeRepoUrl); + + Uri beRepoUrl = googleCloudController.getGCloudResourceUrl( + projectId, Resource.beRepo); + Uri expectBeRepoUrl = + Uri.parse("https://source.cloud.google.com/$projectId/Backend/"); + expect(beRepoUrl, expectBeRepoUrl); + + Uri resourceNone = + googleCloudController.getGCloudResourceUrl(projectId, Resource.none); + expect(resourceNone, Uri.parse("")); + }); +} diff --git a/takeoff/takeoff_lib/test/src/controllers/docker/docker_controller_factory_test.dart b/takeoff/takeoff_lib/test/src/controllers/docker/docker_controller_factory_test.dart new file mode 100644 index 000000000..f24dd2ebf --- /dev/null +++ b/takeoff/takeoff_lib/test/src/controllers/docker/docker_controller_factory_test.dart @@ -0,0 +1,113 @@ +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller_factory.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_installation.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/ddesktop_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/rancher_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/unix_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import 'docker_controller_factory_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec() +]) +void main() { + MockFoldersService mockFoldersService = MockFoldersService(); + + setUpAll(() { + GetIt.I.registerSingleton(mockFoldersService); + GetIt.I.registerSingleton(DockerType( + installation: DockerInstallation.unknown, + command: DockerCommand.docker)); + }); + + group("DockerController factory with Unix tests", () { + MockPlatformService mockPlatformService = MockPlatformService(); + + setUpAll(() { + when(mockPlatformService.isUnix).thenReturn(true); + GetIt.I.registerSingleton(mockPlatformService); + }); + + test("checkDockerInstallationType is correct", () async { + DockerControllerFactory factory = DockerControllerFactory(); + expect((await factory.checkDockerInstallationType()).installation, + DockerInstallation.unix); + }); + + test("create is correct", () async { + DockerControllerFactory factory = DockerControllerFactory(); + expect(await factory.create() is UnixController, true); + }); + + tearDownAll(() { + GetIt.I.unregister(); + }); + }); + + group("DockerController factory with Windows & Rancher Desktop tests", () { + MockPlatformService mockPlatformService = MockPlatformService(); + MockSystemService mockSystemService = MockSystemService(); + + setUpAll(() { + when(mockPlatformService.isWindows).thenReturn(true); + when(mockSystemService.isDockerDesktopInstalled()) + .thenAnswer((_) async => false); + GetIt.I.registerSingleton(mockPlatformService); + }); + + test("checkDockerInstallationType is correct", () async { + DockerControllerFactory factory = + DockerControllerFactory(systemService: mockSystemService); + expect((await factory.checkDockerInstallationType()).installation, + DockerInstallation.rancherDesktop); + }); + + test("create is correct", () async { + DockerControllerFactory factory = + DockerControllerFactory(systemService: mockSystemService); + expect(await factory.create() is RancherController, true); + }); + + tearDownAll(() { + GetIt.I.unregister(); + }); + }); + + group("DockerController factory with Windows & Docker Desktop tests", () { + MockPlatformService mockPlatformService = MockPlatformService(); + MockSystemService mockSystemService = MockSystemService(); + + setUpAll(() { + when(mockPlatformService.isWindows).thenReturn(true); + when(mockSystemService.isDockerDesktopInstalled()) + .thenAnswer((_) async => true); + GetIt.I.registerSingleton(mockPlatformService); + }); + + test("checkDockerInstallationType is correct", () async { + DockerControllerFactory factory = + DockerControllerFactory(systemService: mockSystemService); + expect((await factory.checkDockerInstallationType()).installation, + DockerInstallation.dockerDesktop); + }); + + test("create is correct", () async { + DockerControllerFactory factory = + DockerControllerFactory(systemService: mockSystemService); + expect(await factory.create() is DockerDesktopController, true); + }); + + tearDownAll(() { + GetIt.I.unregister(); + }); + }); +} diff --git a/takeoff/takeoff_lib/test/src/controllers/docker/docker_controller_test.dart b/takeoff/takeoff_lib/test/src/controllers/docker/docker_controller_test.dart new file mode 100644 index 000000000..5822bffa1 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/controllers/docker/docker_controller_test.dart @@ -0,0 +1,54 @@ +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/unix_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/system/system_service.dart'; +import 'package:test/test.dart'; + +import 'docker_controller_factory_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + MockFoldersService mockFoldersService = MockFoldersService(); + + setUpAll(() { + GetIt.I.registerSingleton(mockFoldersService); + }); + + test("Docker execute command is built correctly", () { + DockerController dockerController = UnixController(command: "docker"); + when(mockFoldersService.getHostFolders()).thenReturn( + { + "gcloud": "/folder/gcloud", + "aws": "/folder/aws", + "azure": "/folder/azure", + "kube": "/folder/kube", + "github": "/folder/github", + "ssh": "/folder/ssh", + }, + ); + + expect(dockerController.buildCommands(["-d", "-p"], ["bash"]), [ + "run", + "--rm", + "-d", + "-p", + "-v", + "/folder/gcloud:/root/.config/gcloud", + "-v", + "/folder/aws:/root/.aws", + "-v", + "/folder/azure:/root/.azure", + "-v", + "/folder/kube:/root/.kube", + "-v", + "/folder/github:/root/.config/gh", + "-v", + "/folder/ssh:/root/.ssh", + "devonfwforge/hangar:2022.51.1", + "bash" + ]); + }); +} diff --git a/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/ddesktop_controller_test.dart b/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/ddesktop_controller_test.dart new file mode 100644 index 000000000..fc1be0a0f --- /dev/null +++ b/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/ddesktop_controller_test.dart @@ -0,0 +1,49 @@ +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/ddesktop_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import 'ddesktop_controller_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + MockPlatformService mockPlatformService = MockPlatformService(); + setUpAll(() { + when(mockPlatformService.env) + .thenReturn({"UserProfile": "C:\\Users\\user"}); + when(mockPlatformService.isWindows).thenReturn(true); + + GetIt.I.registerSingleton(mockPlatformService); + GetIt.I.registerSingleton(FoldersService()); + }); + + test("Volume mappings are correct", () { + DockerDesktopController controller = DockerDesktopController(); + List expectedMappings = [ + '-v', + 'C:\\Users\\user\\AppData\\Roaming\\gcloud:/root/.config/gcloud', + '-v', + 'C:\\Users\\user\\.aws:/root/.aws', + '-v', + 'C:\\Users\\user\\.azure:/root/.azure', + '-v', + 'C:\\Users\\user\\.kube:/root/.kube', + '-v', + 'C:\\Users\\user\\AppData\\Roaming\\GitHub CLI:/root/.config/gh', + '-v', + 'C:\\Users\\user\\.ssh:/root/.ssh', + '-v', + 'C:\\Users\\user\\hangar_workspace:/workspace', + '-v', + 'C:\\Users\\user\\AppData\\Roaming\\configstore:/root/.config/configstore', + '-v', + 'C:\\Users\\user\\.gitconfig:/root/.gitconfig' + ]; + + expect(controller.getVolumeMappings(), expectedMappings); + }); +} diff --git a/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/rancher_controller_test.dart b/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/rancher_controller_test.dart new file mode 100644 index 000000000..214055ea1 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/rancher_controller_test.dart @@ -0,0 +1,49 @@ +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/rancher_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import 'rancher_controller_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + MockPlatformService mockPlatformService = MockPlatformService(); + setUpAll(() { + when(mockPlatformService.env) + .thenReturn({"UserProfile": "C:\\Users\\user"}); + when(mockPlatformService.isWindows).thenReturn(true); + + GetIt.I.registerSingleton(mockPlatformService); + GetIt.I.registerSingleton(FoldersService()); + }); + + test("Volume mappings are correct", () { + RancherController controller = RancherController(command: "docker"); + List expectedMappings = [ + '-v', + '/mnt/c/Users/user/AppData/Roaming/gcloud:/root/.config/gcloud', + '-v', + '/mnt/c/Users/user/.aws:/root/.aws', + '-v', + '/mnt/c/Users/user/.azure:/root/.azure', + '-v', + '/mnt/c/Users/user/.kube:/root/.kube', + '-v', + '/mnt/c/Users/user/AppData/Roaming/GitHub CLI:/root/.config/gh', + '-v', + '/mnt/c/Users/user/.ssh:/root/.ssh', + '-v', + '/mnt/c/Users/user/hangar_workspace:/workspace', + '-v', + '/mnt/c/Users/user/AppData/Roaming/configstore:/root/.config/configstore', + '-v', + '/mnt/c/Users/user/.gitconfig:/root/.gitconfig' + ]; + + expect(controller.getVolumeMappings(), expectedMappings); + }); +} diff --git a/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/unix_controller_test.dart b/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/unix_controller_test.dart new file mode 100644 index 000000000..9da48ff46 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/controllers/docker/specific_controllers/unix_controller_test.dart @@ -0,0 +1,48 @@ +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_lib/src/controllers/docker/specific_controllers/unix_controller.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import 'unix_controller_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + MockPlatformService mockPlatformService = MockPlatformService(); + setUpAll(() { + when(mockPlatformService.env).thenReturn({"HOME": "/home/user"}); + when(mockPlatformService.isUnix).thenReturn(true); + + GetIt.I.registerSingleton(mockPlatformService); + GetIt.I.registerSingleton(FoldersService()); + }); + + test("Volume mappings are correct", () { + UnixController controller = UnixController(command: "docker"); + List expectedMappings = [ + '-v', + '/home/user/.config/gcloud:/root/.config/gcloud', + '-v', + '/home/user/.aws:/root/.aws', + '-v', + '/home/user/.azure:/root/.azure', + '-v', + '/home/user/.kube:/root/.kube', + '-v', + '/home/user/.config/gh:/root/.config/gh', + '-v', + '/home/user/.ssh:/root/.ssh', + '-v', + '/home/user/hangar_workspace:/workspace', + '-v', + '/home/user/.config/configstore:/root/.config/configstore', + '-v', + '/home/user/.gitconfig:/root/.gitconfig' + ]; + + expect(controller.getVolumeMappings(), expectedMappings); + }); +} diff --git a/takeoff/takeoff_lib/test/src/domain/cloud_provider_id_test.dart b/takeoff/takeoff_lib/test/src/domain/cloud_provider_id_test.dart new file mode 100644 index 000000000..558c35426 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/cloud_provider_id_test.dart @@ -0,0 +1,25 @@ +import 'package:takeoff_lib/src/domain/cloud_provider_id.dart'; +import 'package:test/test.dart'; + +void main() { + test("CloudProviderId entity for 'gc' returns gcloud id", () { + CloudProviderId cloudProviderId = CloudProviderId.fromString('gc'); + expect(cloudProviderId, CloudProviderId.gcloud); + }); + + test("CloudProviderId entity for 'aws' returns aws id", () { + CloudProviderId cloudProviderId = CloudProviderId.fromString('aws'); + expect(cloudProviderId, CloudProviderId.aws); + }); + + test("CloudProviderId entity for 'azure' returns azure id", () { + CloudProviderId cloudProviderId = CloudProviderId.fromString('azure'); + expect(cloudProviderId, CloudProviderId.azure); + }); + + // test("CloudProviderId entity for '' returns unsuported error", () { + // CloudProviderId cloudProviderId = CloudProviderId.fromString(''); + // expect(cloudProviderId, isUnsupportedError); + // }); + +} diff --git a/takeoff/takeoff_lib/test/src/domain/cloud_provider_test.dart b/takeoff/takeoff_lib/test/src/domain/cloud_provider_test.dart new file mode 100644 index 000000000..5b359b454 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/cloud_provider_test.dart @@ -0,0 +1,21 @@ +import 'package:takeoff_lib/src/domain/cloud_provider.dart'; +import 'package:takeoff_lib/src/domain/cloud_provider_id.dart'; +import 'package:takeoff_lib/src/domain/gcloud.dart'; +import 'package:test/test.dart'; + +void main() { + test("CloudProvider entity for gcloud returns a gcloud instance", () { + CloudProvider cloudProvider = CloudProvider.fromId(CloudProviderId.gcloud); + expect(cloudProvider, isA()); + }); + + // test("CloudProvider entity for aws returns not supported error", () { + // CloudProvider cloudProvider = CloudProvider.fromId(CloudProviderId.aws); + // expect(cloudProvider, isUnsupportedError); + // }); + + // test("CloudProvider entity for azure returns not supported error", () { + // expect(CloudProvider.fromId(CloudProviderId.azure), isUnsupportedError); + // }); + +} diff --git a/takeoff/takeoff_lib/test/src/domain/gcloud_test.dart b/takeoff/takeoff_lib/test/src/domain/gcloud_test.dart new file mode 100644 index 000000000..f1f0a0efd --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/gcloud_test.dart @@ -0,0 +1,10 @@ +import 'package:takeoff_lib/src/domain/gcloud.dart'; +import 'package:test/test.dart'; + +void main() { + test("GCloud entity getters are correct", () { + GCloud gCloud = GCloud(); + expect(gCloud.hostFolderName, "gcloud"); + expect(gCloud.name, "Google Cloud"); + }); +} diff --git a/takeoff/takeoff_lib/test/src/domain/gui_message/gui_message_test.dart b/takeoff/takeoff_lib/test/src/domain/gui_message/gui_message_test.dart new file mode 100644 index 000000000..ff56bbd8a --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/gui_message/gui_message_test.dart @@ -0,0 +1,33 @@ +import 'package:takeoff_lib/src/domain/gui_message/gui_message.dart'; +import 'package:takeoff_lib/src/domain/gui_message/input_type.dart'; +import 'package:takeoff_lib/src/domain/gui_message/message_type.dart'; +import 'package:test/test.dart'; + +void main() { + test("GuiMessage returns a correct GuiMessage type", () { + GuiMessage guiMessage = GuiMessage(type: MessageType.info, message: 'hello world'); + expect(guiMessage, isA()); + }); + + test("GuiMessage.info returns a correct GuiMessage type", () { + expect(GuiMessage.info('Hello World'), isA()); + }); + + test("GuiMessage.input returns a correct GuiMessage type", () { + GuiMessage guiMessage = GuiMessage(type: MessageType.info, message: 'hello world'); + expect(GuiMessage.input('Hello World', InputType.text), isA()); + }); + + test("GuiMessage.success returns a correct GuiMessage type", () { + expect(GuiMessage.success('Hello World', 'http://www.google.com'), isA()); + }); + + test("GuiMessage.error returns a correct GuiMessage type", () { + expect(GuiMessage.error('Hello World'), isA()); + }); + + test("GuiMessage.browser returns a correct GuiMessage type", () { + expect(GuiMessage.browser('Hello World', 'http://www.google.com'), isA()); + }); + +} diff --git a/takeoff/takeoff_lib/test/src/domain/hangar_scripts/common/language/language_test.dart b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/common/language/language_test.dart new file mode 100644 index 000000000..f15f5af54 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/common/language/language_test.dart @@ -0,0 +1,44 @@ +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:test/test.dart'; + +void main() { + test("Language quarkus returns the correct language type", () { + Language language = Language.fromString('quarkus'); + expect(language, Language.quarkus); + }); + + test("Language quarkus-jvm returns the correct language type", () { + Language language = Language.fromString('quarkus-jvm'); + expect(language, Language.quarkusJVM); + }); + + test("Language node returns the correct language type", () { + Language language = Language.fromString('node'); + expect(language, Language.node); + }); + + test("Language angular returns the correct language type", () { + Language language = Language.fromString('angular'); + expect(language, Language.angular); + }); + + test("Language python returns the correct language type", () { + Language language = Language.fromString('python'); + expect(language, Language.python); + }); + + test("Language flutter returns the correct language type", () { + Language language = Language.fromString('flutter'); + expect(language, Language.flutter); + }); + + test("Language empty returns the correct language type", () { + Language language = Language.fromString(''); + expect(language, Language.none); + }); + + test("Language unknown returns the correct language type", () { + Language language = Language.fromString('unknown'); + expect(language, Language.none); + }); +} diff --git a/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/account/setup_principal_account_test.dart b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/account/setup_principal_account_test.dart new file mode 100644 index 000000000..7c416cde7 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/account/setup_principal_account_test.dart @@ -0,0 +1,15 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/account/setup_principal_account.dart'; +import 'package:test/test.dart'; + +void main() { + test("SetUpPrincipalAccountGCloud to command generates the arguments correctly", () { + SetUpPrincipalAccountGCloud setUpPrincipalAccountGCloud = SetUpPrincipalAccountGCloud(googleAccount: "", serviceAccount: "TakeOff", projectId: "test1"); + expect(setUpPrincipalAccountGCloud.toCommand(), ['/scripts/accounts/gcloud/setup-principal-account.sh','-s','TakeOff','-p','test1','-f','/scripts/accounts/gcloud/predefined-roles.txt']); + }); + + test("SetUpPrincipalAccountGCloud to command with empty service account and all the parameters generates the arguments correctly", () { + SetUpPrincipalAccountGCloud setUpPrincipalAccountGCloud = SetUpPrincipalAccountGCloud(googleAccount: "", serviceAccount: "", projectId: "", roles: "roles", customRoleYamlPath: "customRoleYamlPath", customRoleId: "customRoleId", serviceKeyPath: "serviceKeyPath"); + expect(setUpPrincipalAccountGCloud.toCommand(), ['/scripts/accounts/gcloud/setup-principal-account.sh','-g','','-p','','-f','/scripts/accounts/gcloud/predefined-roles.txt','-r','roles','-c','customRoleYamlPath','-i','customRoleId','-k','serviceKeyPath']); + }); + +} diff --git a/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions_test.dart b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions_test.dart new file mode 100644 index 000000000..075d21f1e --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions_test.dart @@ -0,0 +1,15 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/account/verify_roles_and_permissions.dart'; +import 'package:test/test.dart'; + +void main() { + test("VerifyRolesAndPermissionsGCloud to command generates the arguments correctly", () { + VerifyRolesAndPermissionsGCloud verifyRolesAndPermissionsGCloud = VerifyRolesAndPermissionsGCloud(googleAccount: "", serviceAccount: "serviceAccount", projectId: "projectId"); + expect(verifyRolesAndPermissionsGCloud.toCommand(), ['/scripts/accounts/gcloud/verify-principal-roles-and-permissions.sh','-s','serviceAccount','-p','projectId','-f','/scripts/accounts/gcloud/predefined-roles.txt']); + }); + + test("VerifyRolesAndPermissionsGCloud to command with empty service account and all the parameters generates the arguments correctly", () { + VerifyRolesAndPermissionsGCloud verifyRolesAndPermissionsGCloud = VerifyRolesAndPermissionsGCloud(googleAccount: "", serviceAccount: "", projectId: "",roles: "roles", permissions: "permissions", permissionsFilePath: "permissionsFilePath"); + expect(verifyRolesAndPermissionsGCloud.toCommand(), ['/scripts/accounts/gcloud/verify-principal-roles-and-permissions.sh','-g','','-p','','-f','/scripts/accounts/gcloud/predefined-roles.txt','-r','roles','-e','permissions','-i','permissionsFilePath']); + }); + +} diff --git a/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline_test.dart b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline_test.dart new file mode 100644 index 000000000..076e69108 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline_test.dart @@ -0,0 +1,55 @@ +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/common/machine_type.dart'; +import 'package:takeoff_lib/src/domain/hangar_scripts/gcloud/pipeline_generator/build_pipeline.dart'; +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:test/test.dart'; + +void main() { + test("BuildPipelineGCloud to command generates the arguments correctly", () { + BuildPipelineGCloud buildPipelineGCloud = BuildPipelineGCloud( + configFile: "configFile", + pipelineName: "pipelineName", + language: Language.flutter, + localDirectory: "localDirectory"); + expect(buildPipelineGCloud.toCommand(), [ + '/scripts/pipelines/gcloud/pipeline_generator.sh', + '-c', + 'configFile', + '-n', + 'pipelineName', + '--local-directory', + 'localDirectory', + '-l', + 'flutter' + ]); + }); + + test( + "BuildPipelineGCloud to command with all the parameters generates the arguments correctly", + () { + BuildPipelineGCloud buildPipelineGCloud = BuildPipelineGCloud( + configFile: "configFile", + pipelineName: "pipelineName", + language: Language.flutter, + localDirectory: "localDirectory", + registryLocation: "registryLocation", + targetDirectory: "targetDirectory", + machineType: MachineType.E2_HIGHCPU_32); + expect(buildPipelineGCloud.toCommand(), [ + '/scripts/pipelines/gcloud/pipeline_generator.sh', + '-c', + 'configFile', + '-n', + 'pipelineName', + '--local-directory', + 'localDirectory', + '-l', + 'flutter', + '--registry-location', + 'registryLocation', + '-t', + 'targetDirectory', + '-m', + 'E2_HIGHCPU_32' + ]); + }); +} diff --git a/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline_test.dart b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline_test.dart new file mode 100644 index 000000000..e66c471a0 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/hangar_scripts/gcloud/pipeline_generator/deploy_pipeline_test.dart @@ -0,0 +1,7 @@ +import 'package:test/test.dart'; + +void main() { + // test("BuildPipelineGCloud to command generates the arguments correctly", () { + // }); + +} diff --git a/takeoff/takeoff_lib/test/src/domain/sonar_output_test.dart b/takeoff/takeoff_lib/test/src/domain/sonar_output_test.dart new file mode 100644 index 000000000..ce6a6fd62 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/domain/sonar_output_test.dart @@ -0,0 +1,19 @@ +import 'package:takeoff_lib/src/domain/sonar_output.dart'; +import 'package:test/test.dart'; + +void main() { + test("Sonar output entity getters are correct", () { + SonarOutput sonarOutput = SonarOutput(token: 'token', url: 'url'); + expect(sonarOutput.token, "token"); + expect(sonarOutput.url, "url"); + }); + + test('Sonar output from map get the correct values', () { + final Map mapSonarOutput = {'sonarqube_token':{'value':'token'},'sonarqube_url':{'value':'url'}}; + + SonarOutput sonarOutput = SonarOutput.fromMap(mapSonarOutput); + expect(sonarOutput.token, "token"); + expect(sonarOutput.url, "url"); + }); + +} diff --git a/takeoff/takeoff_lib/test/src/persistence/cache_repository_test.dart b/takeoff/takeoff_lib/test/src/persistence/cache_repository_test.dart new file mode 100644 index 000000000..f11fac7d1 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/persistence/cache_repository_test.dart @@ -0,0 +1,59 @@ +import 'dart:math'; + +import 'package:get_it/get_it.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/persistence/database/database_factory.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + setUp(() async { + GetIt.I.registerSingleton( + await DbFactory(dbPath: "cache_repo_test.db").create()); + }); + + test("saveGoogleEmail & getGoogleEmail are correct", () async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.saveGoogleEmail("google_email@gmail.com"); + expect(await cacheRepository.getGoogleEmail(), "google_email@gmail.com"); + await cacheRepository.saveGoogleEmail("another@gmail.com"); + expect(await cacheRepository.getGoogleEmail() == "google_email@gmail.com", + false); + }); + + test("saveGoogleProjectId & getGoogleProjectIds are correct", () async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String email = "mail@mail.com"; + await cacheRepository.saveGoogleEmail(email); + + List projects = List.generate( + Random().nextInt(15) + 5, (_) => Random().nextInt(1000000).toString()); + for (String elem in projects) { + await cacheRepository.saveGoogleProjectId(elem); + } + + expect(await cacheRepository.getGoogleProjectIds(), projects); + }); + + test("removeGoogleProject is correct", () async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String projectId = Random().nextInt(1000000000).toString(); + await cacheRepository.saveGoogleProjectId(projectId); + + expect((await cacheRepository.getGoogleProjectIds()).contains(projectId), + true); + + await cacheRepository.removeGoogleProject(projectId); + + expect((await cacheRepository.getGoogleProjectIds()).contains(projectId), + false); + }); + + tearDown(() async { + await databaseFactoryIo.deleteDatabase("cache_repo_test.db"); + GetIt.I.unregister(); + }); +} diff --git a/takeoff/takeoff_lib/test/src/takeoff_facade_test.dart b/takeoff/takeoff_lib/test/src/takeoff_facade_test.dart new file mode 100644 index 000000000..0a32f0f07 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/takeoff_facade_test.dart @@ -0,0 +1,273 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:path/path.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:takeoff_lib/src/controllers/cloud/common/auth/auth_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller.dart'; +import 'package:takeoff_lib/src/controllers/cloud/gcloud/gcloud_controller_impl.dart'; +import 'package:takeoff_lib/src/controllers/docker/docker_controller.dart'; +import 'package:takeoff_lib/src/controllers/persistence/cache_repository.dart'; +import 'package:takeoff_lib/src/domain/cloud_provider.dart'; +import 'package:takeoff_lib/src/domain/cloud_provider_id.dart'; +import 'package:takeoff_lib/src/domain/gui_message/gui_message.dart'; +import 'package:takeoff_lib/src/domain/language.dart'; +import 'package:takeoff_lib/src/domain/resource.dart'; +import 'package:takeoff_lib/src/persistence/cache_repository_impl.dart'; +import 'package:takeoff_lib/src/persistence/database/database_factory.dart'; +import 'package:takeoff_lib/src/takeoff_facade.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:test/test.dart'; + +import 'takeoff_facade_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec>(), + MockSpec() +]) +void main() { + MockDockerController mockDockerController = MockDockerController(); + MockGoogleCloudController mockGoogleCloudController = + MockGoogleCloudController(); + // MockAuthController mockAuthController = MockAuthController(); + late FoldersService foldersService; + late TakeOffFacade facade; + late GoogleCloudController googleCloudController; + + setUpAll(() { + // GetIt.I.registerSingleton>(mockAuthController); + GetIt.I.registerSingleton(mockDockerController); + GetIt.I.registerSingleton(PlatformService()); + foldersService = FoldersService(); + GetIt.I.registerSingleton(foldersService); + facade = TakeOffFacade(); + googleCloudController = GoogleCloudControllerImpl(); + }); + + setUp(() async { + GetIt.I.registerSingleton( + await DbFactory(dbPath: "facade_test.db").create()); + facade.googleController = googleCloudController; + }); + + test( + "getCurrentAccount returns empty for Google Cloud if there is no logged account", + () async { + expect(await facade.getCurrentAccount(CloudProviderId.gcloud), ""); + }); + + test("getCurrentAccount returns the current stored Google Cloud email", + () async { + String email = "${Random().nextInt(100000000)}@mail.com"; + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.saveGoogleEmail(email); + + expect(await facade.getCurrentAccount(CloudProviderId.gcloud), email); + }); + + test("getCurrentAccount returns empty string for unsupported clouds", + () async { + //Not supported + expect(await facade.getCurrentAccount(CloudProviderId.aws), ""); + expect(await facade.getCurrentAccount(CloudProviderId.azure), ""); + }); + + test( + "getProjects returns empty for Google Cloud if there is no created projects", + () async { + facade.googleController = mockGoogleCloudController; + + expect(await facade.getProjects(CloudProviderId.gcloud), []); + }); + + test("getProjects returns the correct Google Cloud projects", () async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + + String email = "${Random().nextInt(100000000)}@mail.com"; + await cacheRepository.saveGoogleEmail(email); + + List projects = List.generate( + Random().nextInt(15) + 5, (_) => Random().nextInt(1000000).toString()); + for (String elem in projects) { + await cacheRepository.saveGoogleProjectId(elem); + } + + expect(projects, await facade.getProjects(CloudProviderId.gcloud)); + + //Not supported + expect(await facade.getProjects(CloudProviderId.aws), []); + expect(await facade.getProjects(CloudProviderId.azure), []); + }); + + test("Clean project in Google Cloud is correct", () async { + CacheRepository cacheRepository = CacheRepositoryImpl(); + String projectId = Random().nextInt(1000000000).toString(); + + await cacheRepository.saveGoogleProjectId(projectId); + + expect( + (await facade.getProjects(CloudProviderId.gcloud)).contains(projectId), + true); + + Directory directory = Directory( + join(foldersService.getHostFolders()["workspace"]!, projectId)); + if (directory.existsSync()) { + fail("Project directory already existed"); + } + directory.createSync(recursive: true); + + await facade.cleanProject(CloudProviderId.gcloud, projectId); + + expect(directory.existsSync(), false); + expect( + (await facade.getProjects(CloudProviderId.gcloud)).contains(projectId), + false); + + //Not supported + expect(await facade.cleanProject(CloudProviderId.aws, "project"), false); + expect(await facade.cleanProject(CloudProviderId.azure, "project"), false); + }); + + test("LogOut the user", () async { + String email = "${Random().nextInt(100000000)}@mail.com"; + CacheRepository cacheRepository = CacheRepositoryImpl(); + await cacheRepository.saveGoogleEmail(email); + + // Check that the user is saved + expect(await facade.getCurrentAccount(CloudProviderId.gcloud), email); + // If user exists logOut returns true and getting the account returns empty + expect(await facade.logOut(CloudProviderId.gcloud), true); + expect(await facade.getCurrentAccount(CloudProviderId.gcloud), ""); + + // If user doesn't exist returns false + expect(await facade.logOut(CloudProviderId.gcloud), false); + + //Currently not supported + expect(await facade.logOut(CloudProviderId.aws), false); + expect(await facade.logOut(CloudProviderId.azure), false); + }); + + test("runProject calls the correct methods", () async { + facade.googleController = mockGoogleCloudController; + when(mockGoogleCloudController.run(any)).thenAnswer((_) async => true); + + expect(await facade.runProject("id", CloudProviderId.gcloud), true); + + verify(mockGoogleCloudController.run("id")).called(1); + + expect(await facade.runProject("id", CloudProviderId.aws), false); + expect(await facade.runProject("id", CloudProviderId.azure), false); + }); + + test("init calls the correct methods", () async { + facade.googleController = mockGoogleCloudController; + when(mockGoogleCloudController.init(any)).thenAnswer((_) async => true); + + expect(await facade.init("email", CloudProviderId.gcloud), true); + + verify(mockGoogleCloudController.init("email")).called(1); + + expect(await facade.init("email", CloudProviderId.aws), false); + expect(await facade.init("email", CloudProviderId.azure), false); + }); + + test("createProjectGCloud calls the correct method", () async { + facade.googleController = mockGoogleCloudController; + when(mockGoogleCloudController.createProject( + projectName: anyNamed("projectName"), + billingAccount: anyNamed("billingAccount"), + googleCloudRegion: anyNamed("googleCloudRegion"), + backendLanguage: anyNamed("backendLanguage"), + frontendLanguage: anyNamed("frontendLanguage"), + backendVersion: anyNamed("backendVersion"), + frontendVersion: anyNamed("frontendVersion"), + outputStream: anyNamed("outputStream"), + inputStream: anyNamed("inputStream"))) + .thenAnswer((_) async => true); + + StreamController outputStream = StreamController(); + StreamController inputStream = StreamController(); + + expect( + await facade.createProjectGCloud( + projectName: "name", + billingAccount: "billing", + googleCloudRegion: "region", + backendLanguage: Language.node, + backendVersion: "1", + frontendLanguage: Language.flutter, + frontendVersion: "1", + outputStream: outputStream, + inputStream: inputStream), + true); + + verify(mockGoogleCloudController.createProject( + projectName: "name", + billingAccount: "billing", + googleCloudRegion: "region", + backendLanguage: Language.node, + backendVersion: "1", + frontendLanguage: Language.flutter, + frontendVersion: "1", + outputStream: outputStream, + inputStream: inputStream)) + .called(1); + }); + + test("quickstartWayat calls the correct method", () async { + facade.googleController = mockGoogleCloudController; + when(mockGoogleCloudController.wayatQuickstart( + billingAccount: anyNamed("billingAccount"), + googleCloudRegion: anyNamed("googleCloudRegion"), + outputStream: anyNamed("outputStream"), + inputStream: anyNamed("inputStream"))) + .thenAnswer((_) async => true); + + StreamController outputStream = StreamController(); + StreamController inputStream = StreamController(); + + expect( + await facade.quickstartWayat( + billingAccount: "billing", + googleCloudRegion: "region", + inputStream: inputStream, + outputStream: outputStream), + true); + + verify(mockGoogleCloudController.wayatQuickstart( + billingAccount: "billing", + googleCloudRegion: "region", + inputStream: inputStream, + outputStream: outputStream)) + .called(1); + }); + + test("getResource calls the right method", () async { + facade.googleController = mockGoogleCloudController; + Uri gcloudResUrl = Uri.parse("https://gcloudresourceurl.com"); + Uri emptyUri = Uri.parse(""); + when(mockGoogleCloudController.getGCloudResourceUrl(any, any)) + .thenReturn(gcloudResUrl); + + expect(facade.getResource("project", CloudProviderId.gcloud, Resource.ide), + gcloudResUrl); + + //Not supported + expect(facade.getResource("project", CloudProviderId.azure, Resource.ide), + emptyUri); + expect(facade.getResource("project", CloudProviderId.aws, Resource.ide), + emptyUri); + }); + + tearDown(() async { + GetIt.I.unregister(); + await databaseFactoryIo.deleteDatabase("facade_test.db"); + }); +} diff --git a/takeoff/takeoff_lib/test/src/utils/folders/folder_service_test.dart b/takeoff/takeoff_lib/test/src/utils/folders/folder_service_test.dart new file mode 100644 index 000000000..4e5724a32 --- /dev/null +++ b/takeoff/takeoff_lib/test/src/utils/folders/folder_service_test.dart @@ -0,0 +1,110 @@ +import 'dart:io'; + +import 'package:get_it/get_it.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:takeoff_lib/src/utils/folders/folders_service.dart'; +import 'package:takeoff_lib/src/utils/platform/platform_service.dart'; +import 'package:takeoff_lib/src/utils/platform/unsupported_platform_exception.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +import 'folder_service_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + MockPlatformService mockPlatformService = MockPlatformService(); + setUpAll(() { + when(mockPlatformService.env) + .thenReturn({"UserProfile": "C:\\Users\\user", "HOME": "/home/user"}); + + GetIt.I.registerSingleton(mockPlatformService); + }); + + group("Unix FoldersService tests", () { + setUpAll(() { + when(mockPlatformService.isUnix).thenReturn(true); + }); + + test("Get cache folder path is correct", () { + Directory cacheFolder = Directory("/home/user/.takeoff/"); + + FoldersService foldersService = FoldersService(); + expect(foldersService.getCacheFolderPath(), cacheFolder.path); + }); + + test("Host folders are generated correctly", () { + Map linuxResult = { + "gcloud": "/home/user/.config/gcloud", + "aws": "/home/user/.aws", + "azure": "/home/user/.azure", + "kube": "/home/user/.kube", + "github": "/home/user/.config/gh", + "ssh": "/home/user/.ssh", + "workspace": "/home/user/hangar_workspace", + "firebase": "/home/user/.config/configstore", + "git": "/home/user/.gitconfig" + }; + + FoldersService foldersService = FoldersService(); + expect(foldersService.getHostFolders(), linuxResult); + }); + }); + + group("Windows FoldersService tests", () { + setUpAll(() { + when(mockPlatformService.isWindows).thenReturn(true); + }); + + test("Get cache folder path is correct", () { + Directory cacheFolder = + Directory("C:\\Users\\user\\AppData\\Roaming\\.takeoff\\"); + + FoldersService foldersService = FoldersService(); + expect(foldersService.getCacheFolderPath(), cacheFolder.path); + }); + + test("Host folders are generated correctly", () { + Map windowsResult = { + "gcloud": "C:\\Users\\user\\AppData\\Roaming\\gcloud", + "aws": "C:\\Users\\user\\.aws", + "azure": "C:\\Users\\user\\.azure", + "kube": "C:\\Users\\user\\.kube", + "github": "C:\\Users\\user\\AppData\\Roaming\\GitHub CLI", + "ssh": "C:\\Users\\user\\.ssh", + "workspace": "C:\\Users\\user\\hangar_workspace", + "firebase": "C:\\Users\\user\\AppData\\Roaming\\configstore", + "git": "C:\\Users\\user\\.gitconfig" + }; + + FoldersService foldersService = FoldersService(); + expect(foldersService.getHostFolders(), windowsResult); + }); + }); + + group("Unsupported platform test", () { + setUpAll(() { + when(mockPlatformService.isWindows).thenReturn(false); + when(mockPlatformService.isUnix).thenReturn(false); + }); + test("Get cache folder is correct", () { + try { + FoldersService foldersService = FoldersService(); + foldersService.getCacheFolderPath(); + fail("Unsupported Platform Exception should have been thrown"); + } on UnsupportedPlatformException { + expect(true, true); + } + }); + + test("Host folders are generated correctly", () { + try { + FoldersService foldersService = FoldersService(); + foldersService.getHostFolders(); + fail("Unsupported Platform Exception should have been thrown"); + } on UnsupportedPlatformException { + expect(true, true); + } + }); + }); +} diff --git a/takeoff/takeoff_lib/test/takeoff_lib_test.dart b/takeoff/takeoff_lib/test/takeoff_lib_test.dart new file mode 100644 index 000000000..7de0162a4 --- /dev/null +++ b/takeoff/takeoff_lib/test/takeoff_lib_test.dart @@ -0,0 +1,11 @@ +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () {}); + }); +}