From 4ce753c9793f1a6092d3648a705d8aae28641d9c Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:06:49 -0300 Subject: [PATCH] Download CLI with SuperAdmin + Github release (#290) * feat(app) add download cli * feat(ci) add release to actions * fix(app) cli binary names --- .github/actions/build/API/action.yaml | 47 +++++ .github/actions/build/CLI/action.yaml | 11 +- .../build/flutter-app/linux/action.yaml | 6 + .../actions/build/multi-docker/action.yaml | 6 + .github/actions/build/windows/action.yaml | 6 + .github/workflows/build-docker-deploy.yaml | 19 ++ APP/lib/common/snackbar.dart | 8 + APP/lib/l10n/app_en.arb | 7 + APP/lib/l10n/app_fr.arb | 7 + APP/lib/models/netbox.dart | 2 +- APP/lib/pages/projects_page.dart | 10 +- APP/lib/pages/results_page.dart | 2 +- .../widgets/tenants/popups/backup_popup.dart | 6 +- APP/lib/widgets/tools/download_cli_popup.dart | 177 ++++++++++++++++++ 14 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 .github/actions/build/API/action.yaml create mode 100644 APP/lib/widgets/tools/download_cli_popup.dart diff --git a/.github/actions/build/API/action.yaml b/.github/actions/build/API/action.yaml new file mode 100644 index 000000000..2d079738f --- /dev/null +++ b/.github/actions/build/API/action.yaml @@ -0,0 +1,47 @@ +name: API Publish +description: Publish API into Nextcloud + +inputs: + VERSION: + description: "Version of the API" + required: true + NEXT_CREDENTIALS: + description: "NEXT_CREDENTIALS" + required: true + +runs: + using: "composite" + steps: + - uses: actions/checkout@v3.2.0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + shell: bash + run: | + cd API + make allos + + - name: Upload result for job + uses: actions/upload-artifact@v3 + with: + name: api-build + path: | + API/OGrEE_API_Linux_x64 + API/OGrEE_API_OSX_x64 + API/OGrEE_API_Win_x64 + + - name: Send to Nextcloud + shell: bash + env: + NEXT_CREDENTIALS: ${{ inputs.NEXT_CREDENTIALS }} + NEXT_ADDR: https://nextcloud.ditrit.io/remote.php/dav/files/github.actions/Ogree/1_Core/1_API/bin/${{ inputs.VERSION }} + run: | + cd API + curl -u $NEXT_CREDENTIALS -X MKCOL $NEXT_ADDR + curl -u $NEXT_CREDENTIALS -T OGrEE_API_Linux_x64 $NEXT_ADDR/OGrEE_API_Linux_${{ inputs.VERSION }} + curl -u $NEXT_CREDENTIALS -T OGrEE_API_OSX_x64 $NEXT_ADDR/OGrEE_API_OSX_${{ inputs.VERSION }} + curl -u $NEXT_CREDENTIALS -T OGrEE_API_Win_x64 $NEXT_ADDR/OGrEE_API_Win_${{ inputs.VERSION }}.exe \ No newline at end of file diff --git a/.github/actions/build/CLI/action.yaml b/.github/actions/build/CLI/action.yaml index 8f1a1d2f3..6a679cb72 100644 --- a/.github/actions/build/CLI/action.yaml +++ b/.github/actions/build/CLI/action.yaml @@ -1,5 +1,5 @@ name: CLI Publish -description: PUblish CLI into Nextcloud +description: Publish CLI into Nextcloud inputs: VERSION: @@ -25,6 +25,15 @@ runs: cd CLI make main && make mac && make win + - name: Upload result for job + uses: actions/upload-artifact@v3 + with: + name: cli-build + path: | + CLI/cli + CLI/cli.mac + CLI/cli.exe + - name: Send to Nextcloud shell: bash env: diff --git a/.github/actions/build/flutter-app/linux/action.yaml b/.github/actions/build/flutter-app/linux/action.yaml index 24a73e74b..2edf9e346 100644 --- a/.github/actions/build/flutter-app/linux/action.yaml +++ b/.github/actions/build/flutter-app/linux/action.yaml @@ -41,4 +41,10 @@ runs: zip -r ./OGrEE_APP_Web.zip build/web/* curl -u $NEXT_CREDENTIALS -T OGrEE_APP_Web.zip $NEXT_ADDR/OGrEE_APP_Web_${{ inputs.VERSION }}.zip + + - name: Upload result for job + uses: actions/upload-artifact@v3 + with: + name: app-web-build + path: APP/OGrEE_APP_Web.zip \ No newline at end of file diff --git a/.github/actions/build/multi-docker/action.yaml b/.github/actions/build/multi-docker/action.yaml index 829166571..a305d2bb8 100644 --- a/.github/actions/build/multi-docker/action.yaml +++ b/.github/actions/build/multi-docker/action.yaml @@ -86,6 +86,12 @@ runs: TEAM_USERNAME: ${{ inputs.TEAM_USERNAME }} TEAM_PASSWORD: ${{ inputs.TEAM_PASSWORD }} + - name: Publish API on Nexcloud + uses: ./.github/actions/build/API + with: + VERSION: ${{ env.VERSION }} + NEXT_CREDENTIALS: ${{ inputs.NEXT_CREDENTIALS }} + - name: Publish CLI on Nexcloud uses: ./.github/actions/build/CLI with: diff --git a/.github/actions/build/windows/action.yaml b/.github/actions/build/windows/action.yaml index 690ef7f51..5c5aebbea 100644 --- a/.github/actions/build/windows/action.yaml +++ b/.github/actions/build/windows/action.yaml @@ -53,6 +53,12 @@ runs: run: | "%programfiles(x86)%\Inno Setup 6\iscc.exe" "inno-wininstaller.iss" shell: cmd + + - name: Upload result for job + uses: actions/upload-artifact@v3 + with: + name: app-win-build + path: Output/ogree-app-installer.exe - name: Send to Nextcloud shell: pwsh diff --git a/.github/workflows/build-docker-deploy.yaml b/.github/workflows/build-docker-deploy.yaml index 13d09a416..fa39ef652 100644 --- a/.github/workflows/build-docker-deploy.yaml +++ b/.github/workflows/build-docker-deploy.yaml @@ -41,6 +41,7 @@ jobs: build-app-windows: name: 🏗️📤 Build APP and Installer for Windows + permissions: write-all needs: build-docker runs-on: windows-latest steps: @@ -63,4 +64,22 @@ jobs: VERSION: ${{ env.VERSION }} NEXT_CREDENTIALS: ${{ secrets.NEXT_CREDENTIALS }} + - name: Download all artifacts + uses: actions/download-artifact@v3 + + - name: 🏷️ Create release if release + if: ${{ !contains(env.ENVIRONMENT, 'Candidate') }} + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ env.VERSION }} + files: | + cli-build/cli.exe + cli-build/cli + cli-build/cli.mac + api-build/OGrEE_API_Linux_x64 + api-build/OGrEE_API_OSX_x64 + api-build/OGrEE_API_Win_x64 + app-web-build/OGrEE_APP_Web.zip + app-win-build/ogree-app-installer.exe + diff --git a/APP/lib/common/snackbar.dart b/APP/lib/common/snackbar.dart index a9af57146..55a31a753 100644 --- a/APP/lib/common/snackbar.dart +++ b/APP/lib/common/snackbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; void showSnackBar( ScaffoldMessengerState messenger, @@ -6,6 +7,7 @@ void showSnackBar( Duration duration = const Duration(seconds: 6), bool isError = false, bool isSuccess = false, + String copyText = "", }) { var color = Colors.blueGrey.shade900; if (isError) color = Colors.red.shade900; @@ -18,6 +20,12 @@ void showSnackBar( backgroundColor: color, content: Text(message), duration: duration, + action: copyText == "" + ? null + : SnackBarAction( + label: "COPY", + onPressed: () => + Clipboard.setData(ClipboardData(text: copyText))), showCloseIcon: duration.inSeconds > 5, ), ); diff --git a/APP/lib/l10n/app_en.arb b/APP/lib/l10n/app_en.arb index ca35907a7..1711ec6bf 100644 --- a/APP/lib/l10n/app_en.arb +++ b/APP/lib/l10n/app_en.arb @@ -16,6 +16,7 @@ "reset": "Reset", "close": "Close", "unavailable": "Unavailable", + "download": "Download", "welcome": "Welcome to OGrEE", "welcomeConnect": "Login to your personal space:", @@ -133,6 +134,12 @@ "downloadBackup": "Download backup archive", "backupInfoMessage": "A backup archive file will be created in the host machine of the database container.\nIf this option is selected, this file will also be downloaded in your current machine.", + "tools": "Tools", + "downloadCli": "Download latest CLI", + "downloadCliTitle": "Download latest CLI", + "selectOS": "Select OS:", + "fileSavedTo": "File succesfully saved to:", + "unableDownload": "Unable to download file", "onlyOneTool": "Only one {tool} is allowed", "selectSQL": "Select SQL file", "importNetbox": "Import Netbox Dump", diff --git a/APP/lib/l10n/app_fr.arb b/APP/lib/l10n/app_fr.arb index cb3a0b91b..3b8420678 100644 --- a/APP/lib/l10n/app_fr.arb +++ b/APP/lib/l10n/app_fr.arb @@ -16,6 +16,7 @@ "reset": "Réinitialiser", "close": "Fermer", "unavailable": "Indisponible", + "download": "Télécharger", "@welcome": { "description": "Login Page" }, "welcome": "Bienvenue sur OGrEE", @@ -163,6 +164,12 @@ "downloadBackup": "Télécharger archive de backup", "backupInfoMessage": "Un fichier de backup sera créé dans la machine hôte de ce déploiement Docker.\nSi sélectionné, le fichier sera aussi télécharger dans votre machine.", + "tools": "Outils", + "downloadCli": "Télécharger la CLI", + "downloadCliTitle": "Télécharger la CLI la plus récente", + "selectOS": "Choisir SO :" , + "fileSavedTo": "Téléchargé dans :", + "unableDownload": "Impossible de télécharger le fichier", "onlyOneTool": "Seul un {tool} est possible", "@onlyOneTool": { "placeholders": { diff --git a/APP/lib/models/netbox.dart b/APP/lib/models/netbox.dart index 9d41aa0fa..f809a2a52 100644 --- a/APP/lib/models/netbox.dart +++ b/APP/lib/models/netbox.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -enum Tools { netbox, opendcim } +enum Tools { netbox, opendcim, cli } class Netbox { String userName; diff --git a/APP/lib/pages/projects_page.dart b/APP/lib/pages/projects_page.dart index 34eaac141..38931e1ed 100644 --- a/APP/lib/pages/projects_page.dart +++ b/APP/lib/pages/projects_page.dart @@ -15,6 +15,7 @@ import 'package:ogree_app/widgets/projects/project_card.dart'; import 'package:ogree_app/widgets/tenants/tenant_card.dart'; import 'package:ogree_app/widgets/tools/create_netbox_popup.dart'; import 'package:ogree_app/widgets/tools/create_opendcim_popup.dart'; +import 'package:ogree_app/widgets/tools/download_cli_popup.dart'; import 'package:ogree_app/widgets/tools/tool_card.dart'; class ProjectsPage extends StatefulWidget { @@ -226,6 +227,10 @@ class _ProjectsPageState extends State { value: Tools.opendcim, child: Text("${localeMsg.create} OpenDCIM"), ), + PopupMenuItem( + value: Tools.cli, + child: Text(localeMsg.downloadCli), + ), ]; return ElevatedButton( @@ -256,6 +261,9 @@ class _ProjectsPageState extends State { CreateOpenDcimPopup(parentCallback: refreshFromChildren)); } break; + case Tools.cli: + showCustomPopup(context, DownloadCliPopup()); + break; } }, itemBuilder: (_) => entries, @@ -267,7 +275,7 @@ class _ProjectsPageState extends State { top: 8, bottom: 8, right: _isSmallDisplay ? 0 : 10), child: const Icon(Icons.timeline), ), - _isSmallDisplay ? Container() : Text("${localeMsg.create} tools"), + _isSmallDisplay ? Container() : Text(localeMsg.tools), ], ), ), diff --git a/APP/lib/pages/results_page.dart b/APP/lib/pages/results_page.dart index 7c49facdd..2943516a9 100644 --- a/APP/lib/pages/results_page.dart +++ b/APP/lib/pages/results_page.dart @@ -364,7 +364,7 @@ class _ResultsPageState extends State { } file.writeAsBytes(bytes, flush: true).then((value) => showSnackBar( ScaffoldMessenger.of(context), - "File succesfully saved to: $fileName")); + "${AppLocalizations.of(context)!.fileSavedTo} $fileName")); } } } diff --git a/APP/lib/widgets/tenants/popups/backup_popup.dart b/APP/lib/widgets/tenants/popups/backup_popup.dart index 5c6e6f3e2..6a8fd0eb3 100644 --- a/APP/lib/widgets/tenants/popups/backup_popup.dart +++ b/APP/lib/widgets/tenants/popups/backup_popup.dart @@ -209,9 +209,9 @@ class _BackupPopupState extends State { var path = (await getApplicationDocumentsDirectory()).path; var fileName = '$path/$filename'; var file = File(fileName); - file.writeAsBytes(value, flush: true).then((value) => - showSnackBar( - messenger, "File succesfully saved to: $fileName")); + file.writeAsBytes(value, flush: true).then((value) => showSnackBar( + messenger, + "${AppLocalizations.of(context)!.fileSavedTo} $fileName")); } } else { showSnackBar(messenger, value, isSuccess: true); diff --git a/APP/lib/widgets/tools/download_cli_popup.dart b/APP/lib/widgets/tools/download_cli_popup.dart new file mode 100644 index 000000000..6b4e9dedc --- /dev/null +++ b/APP/lib/widgets/tools/download_cli_popup.dart @@ -0,0 +1,177 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:ogree_app/common/snackbar.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:ogree_app/common/theme.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:ogree_app/widgets/select_objects/settings_view/tree_filter.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:http/http.dart' as http; + +enum CliOS { windows, linux, macOS } + +class DownloadCliPopup extends StatefulWidget { + const DownloadCliPopup({super.key}); + + @override + State createState() => _DownloadCliPopupState(); +} + +class _DownloadCliPopupState extends State { + CliOS _selectedOS = CliOS.windows; + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + final localeMsg = AppLocalizations.of(context)!; + final isSmallDisplay = IsSmallDisplay(MediaQuery.of(context).size.width); + return Center( + child: Container( + width: 400, + constraints: const BoxConstraints(maxHeight: 190), + margin: const EdgeInsets.symmetric(horizontal: 20), + decoration: PopupDecoration, + child: Padding( + padding: EdgeInsets.fromLTRB( + isSmallDisplay ? 30 : 40, 20, isSmallDisplay ? 30 : 40, 15), + child: ScaffoldMessenger( + child: Builder( + builder: (context) => Scaffold( + backgroundColor: Colors.white, + body: ListView( + padding: EdgeInsets.zero, + children: [ + Center( + child: Text( + localeMsg.downloadCliTitle, + style: Theme.of(context).textTheme.headlineMedium, + )), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(localeMsg.selectOS), + const SizedBox(width: 20), + SizedBox( + height: 35, + width: 165, + child: DropdownButtonFormField( + decoration: GetFormInputDecoration( + false, + null, + icon: Icons.desktop_windows, + ), + value: _selectedOS, + items: CliOS.values + .map>( + (CliOS value) { + return DropdownMenuItem( + value: value, + child: Text( + value == CliOS.macOS + ? value.name + : value.name.capitalize(), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: (CliOS? value) { + setState(() { + _selectedOS = value!; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.blue.shade900), + onPressed: () => Navigator.pop(context), + label: Text(localeMsg.cancel), + icon: const Icon( + Icons.cancel_outlined, + size: 16, + ), + ), + const SizedBox(width: 15), + ElevatedButton.icon( + onPressed: () => + submitCreateOpenDcim(localeMsg), + label: Text(localeMsg.download), + icon: _isLoading + ? Container( + width: 24, + height: 24, + padding: const EdgeInsets.all(2.0), + child: + const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3, + ), + ) + : const Icon(Icons.download, size: 16)) + ], + ) + ], + ), + ))), + ), + ), + ); + } + + submitCreateOpenDcim(AppLocalizations localeMsg) async { + const urlPath = + 'https://github.com/ditrit/OGrEE-Core/releases/latest/download/'; + String cliName = "cli"; + switch (_selectedOS) { + case CliOS.windows: + cliName = "$cliName.exe"; + break; + case CliOS.linux: + break; + case CliOS.macOS: + cliName = "$cliName.mac"; + break; + } + if (kIsWeb) { + html.AnchorElement(href: urlPath + cliName) + ..setAttribute("download", cliName) + ..click(); + } else { + // Save to local filesystem + setState(() { + _isLoading = true; + }); + final messenger = ScaffoldMessenger.of(context); + final navigator = Navigator.of(context); + final response = await http.get(Uri.parse(urlPath + cliName)); + navigator.pop(); + if (response.statusCode >= 200 && response.statusCode < 300) { + var path = (await getApplicationDocumentsDirectory()).path; + var fileName = '$path/$cliName'; + var file = File(fileName); + for (var i = 1; await file.exists(); i++) { + fileName = '$path/$cliName ($i)'; + file = File(fileName); + } + file.writeAsBytes(response.bodyBytes, flush: true).then((value) => + showSnackBar(ScaffoldMessenger.of(context), + "${localeMsg.fileSavedTo} $fileName", + copyText: fileName)); + } else { + showSnackBar(messenger, localeMsg.unableDownload); + } + setState(() { + _isLoading = false; + }); + } + } +}