From aa505b7eb29695514de730d14e572ec671d96943 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Mon, 16 Sep 2024 15:34:48 +0530 Subject: [PATCH 01/21] Added package install and launch from template functionality. --- .../zango/api/platform/tenancy/v1/utils.py | 58 +++++++++++++++ .../zango/api/platform/tenancy/v1/views.py | 14 ++++ .../0005_tenantmodel_app_template.py | 19 +++++ .../src/zango/apps/shared/tenancy/models.py | 14 +++- .../src/zango/apps/shared/tenancy/tasks.py | 55 ++++++++++----- backend/src/zango/cli/update_apps.py | 70 +++++++++++-------- backend/src/zango/core/package_utils.py | 30 ++++---- 7 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 backend/src/zango/api/platform/tenancy/v1/utils.py create mode 100644 backend/src/zango/apps/shared/tenancy/migrations/0005_tenantmodel_app_template.py diff --git a/backend/src/zango/api/platform/tenancy/v1/utils.py b/backend/src/zango/api/platform/tenancy/v1/utils.py new file mode 100644 index 000000000..a280e54b8 --- /dev/null +++ b/backend/src/zango/api/platform/tenancy/v1/utils.py @@ -0,0 +1,58 @@ +import os +import json +import zipfile +import shutil +import tempfile + +def extract_app_details_from_zip(template_zip): + settings_filename='settings.json' + try: + with zipfile.ZipFile(template_zip, 'r') as zip_file: + # Check if the settings file exists in the zip + zip_name = str(template_zip).split(".")[0] + + # List all files in the zip + all_files = zip_file.namelist() + + # Find the settings file path + settings_path = None + for file_path in all_files: + if file_path == os.path.join(zip_name, settings_filename): + settings_path = file_path + break + + if not settings_path: + raise FileNotFoundError(f"{settings_filename} not found in the zip file.") + + # Read the contents of the settings file + with zip_file.open(settings_path) as settings_file: + settings_content = settings_file.read() + + # Parse the JSON content + settings = json.loads(settings_content) + return settings.get("version", None), settings.get("app_name", None), "nice app" + + except zipfile.BadZipFile: + print(f"Error: {template_zip} is not a valid zip file.") + except json.JSONDecodeError: + print(f"Error: {settings_filename} is not a valid JSON file.") + except Exception as e: + print(f"An error occurred: {str(e)}") + + return None, None, None + +def extract_zip_to_temp_dir(app_template): + # Create a temporary directory + temp_dir = os.getcwd() + + try: + # Extract the zip file to the temporary directory + with zipfile.ZipFile(app_template, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # Return the path of the temporary directory + return temp_dir + except Exception as e: + # If an error occurs, remove the temporary directory and re-raise the exception + os.rmdir(temp_dir) + raise e \ No newline at end of file diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 88d9bd1ef..66e562217 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -1,3 +1,4 @@ +import os import json import traceback @@ -25,6 +26,7 @@ ThemeModelSerializer, UserRoleSerializerModel, ) +from .utils import extract_app_details_from_zip, extract_zip_to_temp_dir class AppViewAPIV1(ZangoGenericPlatformAPIView): @@ -92,11 +94,23 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): data = request.data + app_template = data.get("app_template", False) + app_template_name=None + if app_template: + app_template_name = str(app_template).split(".")[0] + _, app_name, description = extract_app_details_from_zip(app_template) + data.update({ + "name":app_name, + "description":description, + "app_template":app_template, + }) try: app, task_id = TenantModel.create( name=data["name"], schema_name=data["name"], description=data["description"], + app_template_name=app_template_name, + app_template=app_template, tenant_type="app", status="staged", ) diff --git a/backend/src/zango/apps/shared/tenancy/migrations/0005_tenantmodel_app_template.py b/backend/src/zango/apps/shared/tenancy/migrations/0005_tenantmodel_app_template.py new file mode 100644 index 000000000..cc8ac254a --- /dev/null +++ b/backend/src/zango/apps/shared/tenancy/migrations/0005_tenantmodel_app_template.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.11 on 2024-09-13 08:18 + +from django.db import migrations +import zango.core.storage_utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0004_tenantmodel_fav_icon_alter_tenantmodel_logo'), + ] + + operations = [ + migrations.AddField( + model_name='tenantmodel', + name='app_template', + field=zango.core.storage_utils.ZFileField(blank=True, null=True, upload_to=zango.core.storage_utils.RandomUniqueFileName, validators=[zango.core.storage_utils.validate_file_extension], verbose_name='template used for app'), + ), + ] diff --git a/backend/src/zango/apps/shared/tenancy/models.py b/backend/src/zango/apps/shared/tenancy/models.py index 221110739..632e156cb 100644 --- a/backend/src/zango/apps/shared/tenancy/models.py +++ b/backend/src/zango/apps/shared/tenancy/models.py @@ -1,3 +1,4 @@ +import os import re import uuid @@ -12,6 +13,8 @@ from zango.core.model_mixins import FullAuditMixin from zango.core.storage_utils import ZFileField +from zango.api.platform.tenancy.v1.utils import extract_zip_to_temp_dir + from .tasks import initialize_workspace from .utils import ( @@ -115,6 +118,7 @@ class TenantModel(TenantMixin, FullAuditMixin): logo = ZFileField(verbose_name="Logo", null=True, blank=True) fav_icon = ZFileField(verbose_name="Fav Icon", null=True, blank=True) extra_config = models.JSONField(null=True, blank=True) + app_template = ZFileField(verbose_name="template used for app", null=True, blank=True) auto_create_schema = False @@ -127,13 +131,21 @@ def suspend(self): self.save() @classmethod - def create(cls, name, schema_name, description, **other_params): + def create(cls, name, schema_name, description, app_template_name, **other_params): _check_tenant_name(name) obj = cls.objects.create( name=name, schema_name=schema_name, description=description, **other_params ) + app_template_path = None + if obj.app_template: + + app_template_path=os.path.join(extract_zip_to_temp_dir(obj.app_template), app_template_name) # initialize tenant's workspace +<<<<<<< Updated upstream init_task = initialize_workspace.apply_async((str(obj.uuid),), countdown=1) +======= + init_task = initialize_workspace.delay(str(obj.uuid), app_template_path) +>>>>>>> Stashed changes return obj, init_task.id diff --git a/backend/src/zango/apps/shared/tenancy/tasks.py b/backend/src/zango/apps/shared/tenancy/tasks.py index 80f2d4be7..debd197c2 100644 --- a/backend/src/zango/apps/shared/tenancy/tasks.py +++ b/backend/src/zango/apps/shared/tenancy/tasks.py @@ -1,4 +1,6 @@ import os +import shutil +import subprocess import cookiecutter.main @@ -10,9 +12,8 @@ from .utils import DEFAULT_THEME_CONFIG, assign_policies_to_anonymous_user - @shared_task -def initialize_workspace(tenant_uuid): +def initialize_workspace(tenant_uuid, app_template_path=None): try: from zango.apps.shared.tenancy.models import TenantModel, ThemesModel @@ -20,7 +21,6 @@ def initialize_workspace(tenant_uuid): # Creating schema tenant.create_schema(check_if_exists=True) - # migrating schema call_command( "migrate_schemas", @@ -28,7 +28,6 @@ def initialize_workspace(tenant_uuid): schema_name=tenant.schema_name, interactive=False, ) - # Create workspace Folder project_base_dir = settings.BASE_DIR @@ -36,27 +35,45 @@ def initialize_workspace(tenant_uuid): if not os.path.exists(workspace_dir): os.makedirs(workspace_dir) - # Creating app folder with the initial files - template_directory = os.path.join( - os.path.dirname(__file__), "workspace_folder_template" - ) - cookiecutter_context = {"app_name": tenant.name} + if app_template_path is not None: + app_dir = os.path.join(workspace_dir, tenant.name) + if not os.path.exists(app_dir): + os.makedirs(app_dir) - cookiecutter.main.cookiecutter( - template_directory, - extra_context=cookiecutter_context, - output_dir=workspace_dir, - no_input=True, - ) + shutil.copytree(app_template_path, app_dir, dirs_exist_ok=True) + + shutil.rmtree(app_template_path) + shutil.rmtree(os.path.join(app_dir, "packages")) + else: + # Creating app folder with the initial files + template_directory = os.path.join( + os.path.dirname(__file__), "workspace_folder_template" + ) + cookiecutter_context = {"app_name": tenant.name} + + cookiecutter.main.cookiecutter( + template_directory, + extra_context=cookiecutter_context, + output_dir=workspace_dir, + no_input=True, + ) tenant.status = "deployed" tenant.deployed_on = timezone.now() tenant.save(update_fields=["status", "deployed_on"]) - assign_policies_to_anonymous_user(tenant.schema_name) - theme = ThemesModel.objects.create( - name="Default", tenant=tenant, config=DEFAULT_THEME_CONFIG - ) + if not app_template_path: + assign_policies_to_anonymous_user(tenant.schema_name) + theme = ThemesModel.objects.create( + name="Default", tenant=tenant, config=DEFAULT_THEME_CONFIG + ) + else: + try: + subprocess.run( + ["zango", "update-apps", "--app_name", tenant.name], check=True + ) + except subprocess.CalledProcessError as e: + print(f"Failed to update app: {e}") if tenant.status == "deployed": return {"result": "success"} else: diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index f3e154be1..aa72f9352 100644 --- a/backend/src/zango/cli/update_apps.py +++ b/backend/src/zango/cli/update_apps.py @@ -67,8 +67,8 @@ def setup_and_pull(path, repo_url, branch="main"): raise Exception("Repository is bare") print(f"Repository found at {path}") - # Clean the working directory (remove untracked files and reset to HEAD) - repo.git.clean("-fd") + # Clean the working directory (remove untracked files and reset to HEAD) exlcuding packages + repo.git.clean("-fd", "--exclude=packages") repo.git.reset("--hard", "HEAD") # Check if the branch is the same, if not, checkout the correct branch @@ -115,32 +115,7 @@ def setup_and_pull(path, repo_url, branch="main"): return success, message -def run_migrations(tenant, app_directory): - # Run package migrations - print("Running package migrations...") - - updated_app_manifest = json.loads( - open(os.path.join(app_directory, "manifest.json")).read() - ) - - installed_packages = updated_app_manifest["packages"] - - for package in installed_packages: - try: - subprocess.run( - [ - "python", - "manage.py", - "ws_migrate", - tenant, - "--package", - package["name"], - ], - check=True, - ) - except subprocess.CalledProcessError: - print("Migrations failed for package: ", package) - +def run_app_migrations(tenant, app_directory): print("Running app migrations...") try: subprocess.run(["python", "manage.py", "ws_migrate", tenant], check=True) @@ -291,6 +266,33 @@ def extract_release_notes(file_path, version): return None +def same_package_version_exists(package, app_directory): + existing_package_path = os.path.join(app_directory, "packages", package['name']) + if not os.path.exists(existing_package_path): + return False + existing_package = json.loads( + open(os.path.join(existing_package_path, "manifest.json")).read() + ) + return package["version"] == existing_package["version"] + +def install_packages(tenant, app_directory): + from zango.core.package_utils import install_package + # Run package migrations + + updated_app_manifest = json.loads( + open(os.path.join(app_directory, "manifest.json")).read() + ) + + installed_packages = updated_app_manifest["packages"] + + for package in installed_packages: + try: + if not same_package_version_exists(package, app_directory): + print(f"Installing package: {package['name']}") + res = install_package(package["name"], package["version"], tenant.name, True) + print(res) + except subprocess.CalledProcessError: + print("Failed to install package: ", package) def create_release(tenant_name, app_settings, app_directory, git_mode): from django.db import connection @@ -333,8 +335,11 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): release.status = "in_progress" release.save(update_fields=["status"]) - # Run migrations - run_migrations(tenant_name, app_directory) + # install packages + install_packages(tenant, app_directory) + + # Run app migrations + run_app_migrations(tenant_name, app_directory) # Sync Static sync_static(tenant_name) @@ -368,6 +373,8 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): print("No version change detected for") except Exception as e: + import traceback + print(traceback.format_exc()) print(f"An error occurred while creating/updating release: {e}") @@ -436,8 +443,9 @@ def update_apps(app_name): project_name = find_project_name() project_root = os.getcwd() click.echo("Initializing project setup") - os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings") sys.path.insert(0, project_root) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings") + django.setup() click.echo("Project setup initialized") diff --git a/backend/src/zango/core/package_utils.py b/backend/src/zango/core/package_utils.py index 1e11822bb..43ac6d6aa 100644 --- a/backend/src/zango/core/package_utils.py +++ b/backend/src/zango/core/package_utils.py @@ -143,7 +143,7 @@ def get_package_configuration_url(request, tenant, package_name): return "" -def install_package(package_name, version, tenant): +def install_package(package_name, version, tenant, release=False): if package_installed(package_name, tenant): return "Package already installed" try: @@ -173,8 +173,9 @@ def install_package(package_name, version, tenant): # f"tmp/{package_name}/{version}/", # f"workspaces/{tenant}/packages/{package_name}", # ) - update_manifest_json(tenant, package_name, version) - update_settings_json(tenant, package_name, version) + if not release: + update_manifest_json(tenant, package_name, version) + update_settings_json(tenant, package_name, version) subprocess.run(f"python manage.py sync_static {tenant}", shell=True) subprocess.run("python manage.py collectstatic --noinput", shell=True) @@ -183,17 +184,18 @@ def install_package(package_name, version, tenant): f"python manage.py ws_migrate {tenant} --package {package_name}", shell=True, ) - - from zango.apps.dynamic_models.workspace.base import Workspace - from zango.apps.shared.tenancy.models import TenantModel - - tenant_obj = TenantModel.objects.get(name=tenant) - connection.set_tenant(tenant_obj) - with connection.cursor() as c: - ws = Workspace(connection.tenant, request=None, as_systemuser=True) - ws.ready() - ws.sync_tasks(tenant) - ws.sync_policies() + if not release: + from zango.apps.dynamic_models.workspace.base import Workspace + from zango.apps.shared.tenancy.models import TenantModel + + tenant_obj = TenantModel.objects.get(name=tenant) + connection.set_tenant(tenant_obj) + + with connection.cursor() as c: + ws = Workspace(connection.tenant, request=None, as_systemuser=True) + ws.ready() + ws.sync_policies() + ws.sync_tasks(tenant) return "Package Installed" except Exception: From aed1be55298936be7e6f68100a367f27641aee28 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Mon, 16 Sep 2024 15:44:26 +0530 Subject: [PATCH 02/21] formatting and linting fixes --- .../zango/api/platform/tenancy/v1/utils.py | 38 +++++++++++-------- .../zango/api/platform/tenancy/v1/views.py | 17 +++++---- .../src/zango/apps/shared/tenancy/models.py | 16 ++++---- .../src/zango/apps/shared/tenancy/tasks.py | 1 + backend/src/zango/cli/update_apps.py | 12 ++++-- backend/src/zango/core/package_utils.py | 2 +- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/utils.py b/backend/src/zango/api/platform/tenancy/v1/utils.py index a280e54b8..6cba8be01 100644 --- a/backend/src/zango/api/platform/tenancy/v1/utils.py +++ b/backend/src/zango/api/platform/tenancy/v1/utils.py @@ -1,36 +1,41 @@ -import os import json +import os import zipfile -import shutil -import tempfile + def extract_app_details_from_zip(template_zip): - settings_filename='settings.json' + settings_filename = "settings.json" try: - with zipfile.ZipFile(template_zip, 'r') as zip_file: + with zipfile.ZipFile(template_zip, "r") as zip_file: # Check if the settings file exists in the zip zip_name = str(template_zip).split(".")[0] # List all files in the zip all_files = zip_file.namelist() - + # Find the settings file path settings_path = None for file_path in all_files: if file_path == os.path.join(zip_name, settings_filename): settings_path = file_path break - + if not settings_path: - raise FileNotFoundError(f"{settings_filename} not found in the zip file.") - + raise FileNotFoundError( + f"{settings_filename} not found in the zip file." + ) + # Read the contents of the settings file with zip_file.open(settings_path) as settings_file: settings_content = settings_file.read() - + # Parse the JSON content settings = json.loads(settings_content) - return settings.get("version", None), settings.get("app_name", None), "nice app" + return ( + settings.get("version", None), + settings.get("app_name", None), + "nice app", + ) except zipfile.BadZipFile: print(f"Error: {template_zip} is not a valid zip file.") @@ -38,21 +43,22 @@ def extract_app_details_from_zip(template_zip): print(f"Error: {settings_filename} is not a valid JSON file.") except Exception as e: print(f"An error occurred: {str(e)}") - + return None, None, None + def extract_zip_to_temp_dir(app_template): # Create a temporary directory temp_dir = os.getcwd() - + try: # Extract the zip file to the temporary directory - with zipfile.ZipFile(app_template, 'r') as zip_ref: + with zipfile.ZipFile(app_template, "r") as zip_ref: zip_ref.extractall(temp_dir) - + # Return the path of the temporary directory return temp_dir except Exception as e: # If an error occurs, remove the temporary directory and re-raise the exception os.rmdir(temp_dir) - raise e \ No newline at end of file + raise e diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 66e562217..2cb7f6298 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -1,4 +1,3 @@ -import os import json import traceback @@ -26,7 +25,7 @@ ThemeModelSerializer, UserRoleSerializerModel, ) -from .utils import extract_app_details_from_zip, extract_zip_to_temp_dir +from .utils import extract_app_details_from_zip class AppViewAPIV1(ZangoGenericPlatformAPIView): @@ -95,15 +94,17 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): data = request.data app_template = data.get("app_template", False) - app_template_name=None + app_template_name = None if app_template: app_template_name = str(app_template).split(".")[0] _, app_name, description = extract_app_details_from_zip(app_template) - data.update({ - "name":app_name, - "description":description, - "app_template":app_template, - }) + data.update( + { + "name": app_name, + "description": description, + "app_template": app_template, + } + ) try: app, task_id = TenantModel.create( name=data["name"], diff --git a/backend/src/zango/apps/shared/tenancy/models.py b/backend/src/zango/apps/shared/tenancy/models.py index 632e156cb..260e276d6 100644 --- a/backend/src/zango/apps/shared/tenancy/models.py +++ b/backend/src/zango/apps/shared/tenancy/models.py @@ -11,10 +11,9 @@ from django.db import models from django.utils import timezone +from zango.api.platform.tenancy.v1.utils import extract_zip_to_temp_dir from zango.core.model_mixins import FullAuditMixin from zango.core.storage_utils import ZFileField -from zango.api.platform.tenancy.v1.utils import extract_zip_to_temp_dir - from .tasks import initialize_workspace from .utils import ( @@ -118,7 +117,9 @@ class TenantModel(TenantMixin, FullAuditMixin): logo = ZFileField(verbose_name="Logo", null=True, blank=True) fav_icon = ZFileField(verbose_name="Fav Icon", null=True, blank=True) extra_config = models.JSONField(null=True, blank=True) - app_template = ZFileField(verbose_name="template used for app", null=True, blank=True) + app_template = ZFileField( + verbose_name="template used for app", null=True, blank=True + ) auto_create_schema = False @@ -138,14 +139,11 @@ def create(cls, name, schema_name, description, app_template_name, **other_param ) app_template_path = None if obj.app_template: - - app_template_path=os.path.join(extract_zip_to_temp_dir(obj.app_template), app_template_name) + app_template_path = os.path.join( + extract_zip_to_temp_dir(obj.app_template), app_template_name + ) # initialize tenant's workspace -<<<<<<< Updated upstream - init_task = initialize_workspace.apply_async((str(obj.uuid),), countdown=1) -======= init_task = initialize_workspace.delay(str(obj.uuid), app_template_path) ->>>>>>> Stashed changes return obj, init_task.id diff --git a/backend/src/zango/apps/shared/tenancy/tasks.py b/backend/src/zango/apps/shared/tenancy/tasks.py index debd197c2..9eae1b95c 100644 --- a/backend/src/zango/apps/shared/tenancy/tasks.py +++ b/backend/src/zango/apps/shared/tenancy/tasks.py @@ -12,6 +12,7 @@ from .utils import DEFAULT_THEME_CONFIG, assign_policies_to_anonymous_user + @shared_task def initialize_workspace(tenant_uuid, app_template_path=None): try: diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index aa72f9352..2658e8257 100644 --- a/backend/src/zango/cli/update_apps.py +++ b/backend/src/zango/cli/update_apps.py @@ -266,8 +266,9 @@ def extract_release_notes(file_path, version): return None + def same_package_version_exists(package, app_directory): - existing_package_path = os.path.join(app_directory, "packages", package['name']) + existing_package_path = os.path.join(app_directory, "packages", package["name"]) if not os.path.exists(existing_package_path): return False existing_package = json.loads( @@ -275,6 +276,7 @@ def same_package_version_exists(package, app_directory): ) return package["version"] == existing_package["version"] + def install_packages(tenant, app_directory): from zango.core.package_utils import install_package # Run package migrations @@ -289,11 +291,14 @@ def install_packages(tenant, app_directory): try: if not same_package_version_exists(package, app_directory): print(f"Installing package: {package['name']}") - res = install_package(package["name"], package["version"], tenant.name, True) + res = install_package( + package["name"], package["version"], tenant.name, True + ) print(res) except subprocess.CalledProcessError: print("Failed to install package: ", package) + def create_release(tenant_name, app_settings, app_directory, git_mode): from django.db import connection @@ -374,6 +379,7 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): except Exception as e: import traceback + print(traceback.format_exc()) print(f"An error occurred while creating/updating release: {e}") @@ -445,7 +451,7 @@ def update_apps(app_name): click.echo("Initializing project setup") sys.path.insert(0, project_root) os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings") - + django.setup() click.echo("Project setup initialized") diff --git a/backend/src/zango/core/package_utils.py b/backend/src/zango/core/package_utils.py index 43ac6d6aa..a14df2ec6 100644 --- a/backend/src/zango/core/package_utils.py +++ b/backend/src/zango/core/package_utils.py @@ -190,7 +190,7 @@ def install_package(package_name, version, tenant, release=False): tenant_obj = TenantModel.objects.get(name=tenant) connection.set_tenant(tenant_obj) - + with connection.cursor() as c: ws = Workspace(connection.tenant, request=None, as_systemuser=True) ws.ready() From 58ac45ca23d451998ed8431f3ea875e81d03752c Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Mon, 16 Sep 2024 16:55:37 +0530 Subject: [PATCH 03/21] added template downloading. --- .../src/zango/apps/shared/tenancy/models.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/zango/apps/shared/tenancy/models.py b/backend/src/zango/apps/shared/tenancy/models.py index 260e276d6..17f56e059 100644 --- a/backend/src/zango/apps/shared/tenancy/models.py +++ b/backend/src/zango/apps/shared/tenancy/models.py @@ -1,5 +1,7 @@ import os import re +import requests +import tempfile import uuid from collections import namedtuple @@ -139,12 +141,26 @@ def create(cls, name, schema_name, description, app_template_name, **other_param ) app_template_path = None if obj.app_template: + if obj.app_template.url.startswith("https://"): + downloaded_file_path = cls.download_file(obj.app_template.url) + else: + downloaded_file_path = obj.app_template app_template_path = os.path.join( - extract_zip_to_temp_dir(obj.app_template), app_template_name + extract_zip_to_temp_dir(downloaded_file_path), app_template_name ) # initialize tenant's workspace init_task = initialize_workspace.delay(str(obj.uuid), app_template_path) return obj, init_task.id + + @staticmethod + def download_file(url): + response = requests.get(url) + response.raise_for_status() # Raise an exception for bad status codes + + # Create a temporary file + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(response.content) + return temp_file.name class Domain(DomainMixin, FullAuditMixin): From c9938d0e3cae5210b80027aae8aa10585f7b157b Mon Sep 17 00:00:00 2001 From: adhiraj23zelthy Date: Tue, 24 Sep 2024 18:30:46 +0530 Subject: [PATCH 04/21] template upload --- .../src/assets/images/svg/single-file.svg | 6 +++ .../AppConfiguration/DetailsTable/index.jsx | 18 +++++++ .../LaunchNewAppModal/LaunchNewAppForm.jsx | 51 +++++++++++++++++-- 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 frontend/src/assets/images/svg/single-file.svg diff --git a/frontend/src/assets/images/svg/single-file.svg b/frontend/src/assets/images/svg/single-file.svg new file mode 100644 index 000000000..290cda0fa --- /dev/null +++ b/frontend/src/assets/images/svg/single-file.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx b/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx index 3e2735233..0d59410ba 100644 --- a/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx +++ b/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx @@ -2,6 +2,8 @@ import { useSelector } from 'react-redux'; import { ReactComponent as EachAppIcon } from '../../../../../assets/images/svg/each-app-icon.svg'; import { selectAppConfigurationData } from '../../../slice'; import EachDescriptionRow from './EachDescriptionRow'; +import { ReactComponent as SingleFileIcon } from '../../../../../assets/images/svg/single-file.svg'; + function DetailsTable() { const appConfigurationData = useSelector(selectAppConfigurationData); @@ -100,6 +102,22 @@ function DetailsTable() { } /> + + + + ):( + + No Template found + + ) + } + /> ); diff --git a/frontend/src/pages/platform/components/Modals/LaunchNewAppModal/LaunchNewAppForm.jsx b/frontend/src/pages/platform/components/Modals/LaunchNewAppModal/LaunchNewAppForm.jsx index ec931f593..dfa2b2f4a 100644 --- a/frontend/src/pages/platform/components/Modals/LaunchNewAppModal/LaunchNewAppForm.jsx +++ b/frontend/src/pages/platform/components/Modals/LaunchNewAppModal/LaunchNewAppForm.jsx @@ -5,6 +5,7 @@ import * as Yup from 'yup'; import InputField from '../../../../../components/Form/InputField'; import SubmitButton from '../../../../../components/Form/SubmitButton'; import TextareaField from '../../../../../components/Form/TextareaField'; +import FileUpload from '../../../../../components/Form/FileUpload'; import useApi from '../../../../../hooks/useApi'; import { transformToFormDataOrder } from '../../../../../utils/form'; import { setPollingTastIds, toggleRerenderPage } from '../../../slice'; @@ -16,12 +17,41 @@ const LaunchNewAppForm = ({ closeModal }) => { let initialValues = { name: '', description: '', + app_template: null }; - let validationSchema = Yup.object({ - name: Yup.string().required('Required'), - description: Yup.string().required('Required'), - }); + let validationSchema = Yup.object().shape({ + name: Yup.string(), + description: Yup.string(), + app_template: Yup.mixed(), + }).test('custom', null, function(value) { + if (value.app_template) { + return true; + } + + if (!value.name && !value.description) { + return this.createError({ + path: 'app_template', + message: 'Required', + }); + } + + if (value.name && !value.description) { + return this.createError({ + path: 'description', + message: 'Required', + }); + } + + if (!value.name && value.description) { + return this.createError({ + path: 'name', + message: 'Required', + }); + } + + return true; + }); const makeApiCall = async (dynamicFormData) => { const { response, success } = await triggerApi({ @@ -79,6 +109,19 @@ const LaunchNewAppForm = ({ closeModal }) => { onChange={formik.handleChange} formik={formik} /> +
+
+
+

OR

+
+
+
+
From 7243af36b9486854dbc66684ab98cfe2b58f9012 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Thu, 26 Sep 2024 12:28:48 +0530 Subject: [PATCH 05/21] moved git config to tenant model --- backend/src/zango/cli/git_setup.py | 18 ++++++++++-------- backend/src/zango/cli/update_apps.py | 7 ++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/src/zango/cli/git_setup.py b/backend/src/zango/cli/git_setup.py index e568525bb..147e47442 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -4,6 +4,7 @@ import click import git +from zango.apps.shared.tenancy.models import TenantModel def is_valid_app_directory(directory): # Define your validation criteria @@ -20,7 +21,7 @@ def update_settings_with_git_repo_url( app_directory, git_repo_url, dev_branch, staging_branch, prod_branch ): """ - Update the 'git_repo_url' in the 'settings.json' file located in the app directory. + Update the 'git_repo_url' in the TenantMode.extra_config field. Args: - app_directory (str): Full path of the app directory. @@ -37,21 +38,22 @@ def update_settings_with_git_repo_url( settings = json.load(settings_file) # Update git_repo_url in settings - git_config = settings.get("git_config", {}) + app_name = settings.get("app_name", {}) + tenant_obj = TenantModel.objects.get(name=app_name) + + git_config = tenant_obj.extra_config.get("git_config",{}) git_config["repo_url"] = git_repo_url git_branch = git_config.get("branch", {}) git_branch.update( {"dev": dev_branch, "staging": staging_branch, "prod": prod_branch} ) git_config["branch"] = git_branch - settings["git_config"] = git_config - - # Write updated settings back to settings.json - with open(settings_file_path, "w") as settings_file: - json.dump(settings, settings_file, indent=4) + tenant_obj.extra_config.update(git_config) + tenant_obj.save() return True - + except TenantModel.DoesNotExist: + click.echo(f"App not found: {e}") except FileNotFoundError: click.echo(f"Error: settings.json not found in {app_directory}.") except json.JSONDecodeError: diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index f3e154be1..4d92cafa7 100644 --- a/backend/src/zango/cli/update_apps.py +++ b/backend/src/zango/cli/update_apps.py @@ -472,10 +472,11 @@ def update_apps(app_name): repo_url = None branch = None git_mode = False - if app_settings.get("git_config"): + git_settings = tenant_obj.extra_config.get("git_config") + if git_settings: git_mode = True # Initialize git repository - repo_url = app_settings["git_config"]["repo_url"] + repo_url = git_settings["git_config"]["repo_url"] # Split the repo URL into parts parts = repo_url.split("://") @@ -483,7 +484,7 @@ def update_apps(app_name): # Add username and password to the URL repo_url = f"{parts[0]}://{settings.GIT_USERNAME}:{settings.GIT_PASSWORD}@{parts[1]}" - branch = app_settings["git_config"]["branch"].get(settings.ENV, "main") + branch = git_settings["git_config"]["branch"].get(settings.ENV, "main") update_allowed, message = is_update_allowed( tenant, app_settings, git_mode, repo_url, branch From 8636329aeb65c975df85094386f5ab2eabff6a73 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Thu, 26 Sep 2024 15:01:02 +0530 Subject: [PATCH 06/21] added git config in tenant model --- backend/src/zango/cli/git_setup.py | 31 +++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/backend/src/zango/cli/git_setup.py b/backend/src/zango/cli/git_setup.py index 147e47442..620785f13 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -1,10 +1,12 @@ import json import os +import sys import click import git +from .update_apps import find_project_name -from zango.apps.shared.tenancy.models import TenantModel +import django def is_valid_app_directory(directory): # Define your validation criteria @@ -33,33 +35,44 @@ def update_settings_with_git_repo_url( settings_file_path = os.path.join(app_directory, "settings.json") try: + project_name = find_project_name() + project_root = os.getcwd() + os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"{project_name}.settings") + sys.path.insert(0, project_root) + django.setup() + + from zango.apps.shared.tenancy.models import TenantModel + # Load current settings from settings.json with open(settings_file_path, "r") as settings_file: settings = json.load(settings_file) # Update git_repo_url in settings - app_name = settings.get("app_name", {}) + app_name = settings.get("app_name","") tenant_obj = TenantModel.objects.get(name=app_name) - - git_config = tenant_obj.extra_config.get("git_config",{}) - git_config["repo_url"] = git_repo_url + git_config={} + if tenant_obj.extra_config: + git_config = tenant_obj.extra_config.get("git_config",{}) + else: + git_config["repo_url"] = git_repo_url git_branch = git_config.get("branch", {}) git_branch.update( {"dev": dev_branch, "staging": staging_branch, "prod": prod_branch} ) git_config["branch"] = git_branch - tenant_obj.extra_config.update(git_config) + if tenant_obj.extra_config: + tenant_obj.extra_config.update({"git_config":git_config}) + else: + tenant_obj.extra_config={"git_config":git_config} tenant_obj.save() return True - except TenantModel.DoesNotExist: - click.echo(f"App not found: {e}") except FileNotFoundError: click.echo(f"Error: settings.json not found in {app_directory}.") except json.JSONDecodeError: click.echo(f"Error: settings.json is not valid JSON in {app_directory}.") except Exception as e: - click.echo(f"Error occurred while updating settings.json: {e}") + click.echo(f"Error occurred while updating tenant extra config: {e}") return False From 38bde6a47661a5c6f3d1f2308543c4331db3a3cb Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Thu, 26 Sep 2024 15:30:40 +0530 Subject: [PATCH 07/21] modified app update api to handle extra_config. --- .../src/zango/api/platform/tenancy/v1/serializers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/src/zango/api/platform/tenancy/v1/serializers.py b/backend/src/zango/api/platform/tenancy/v1/serializers.py index 4fc4d50bb..c6a93ea11 100644 --- a/backend/src/zango/api/platform/tenancy/v1/serializers.py +++ b/backend/src/zango/api/platform/tenancy/v1/serializers.py @@ -39,6 +39,17 @@ def get_date_format_display(self, obj): return obj.get_date_format_display() def update(self, instance, validated_data): + request = self.context["request"] + extra_config_str = request.data.get("extra_config") + + # Convert extra_config from string to JSON if it exists + if extra_config_str: + try: + extra_config_json = json.loads(extra_config_str) + validated_data["extra_config"] = extra_config_json + except json.JSONDecodeError: + raise serializers.ValidationError({"extra_config": "Invalid JSON format"}) + instance = super(TenantSerializerModel, self).update(instance, validated_data) request = self.context["request"] domains = request.data.getlist("domains") From f48a964c398de970ec64b7a2ca202326aaa7435d Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Thu, 26 Sep 2024 16:33:04 +0530 Subject: [PATCH 08/21] run git setup --- .../api/platform/tenancy/v1/serializers.py | 2 +- .../zango/api/platform/tenancy/v1/views.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/serializers.py b/backend/src/zango/api/platform/tenancy/v1/serializers.py index c6a93ea11..7d8449d3c 100644 --- a/backend/src/zango/api/platform/tenancy/v1/serializers.py +++ b/backend/src/zango/api/platform/tenancy/v1/serializers.py @@ -66,7 +66,7 @@ def update(self, instance, validated_data): if created: domain_obj.is_primary = False domain_obj.save() - + return instance diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 88d9bd1ef..0a4261bf7 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -1,5 +1,6 @@ import json import traceback +import os from django_celery_results.models import TaskResult @@ -10,6 +11,7 @@ from zango.apps.permissions.models import PolicyModel from zango.apps.shared.tenancy.models import TenantModel, ThemesModel from zango.apps.shared.tenancy.utils import DATEFORMAT, DATETIMEFORMAT, TIMEZONES +from zango.cli.git_setup import git_setup from zango.core.api import ( ZangoGenericPlatformAPIView, get_api_response, @@ -19,6 +21,7 @@ from zango.core.permissions import IsPlatformUserAllowedApp from zango.core.utils import get_search_columns + from .serializers import ( AppUserModelSerializerModel, TenantSerializerModel, @@ -148,6 +151,9 @@ def get(self, request, *args, **kwargs): status = 500 return get_api_response(success, response, status) + + def get_branch(config, key, default): + return config.get("branch", {}).get(key, default) def put(self, request, *args, **kwargs): try: @@ -162,6 +168,24 @@ def put(self, request, *args, **kwargs): serializer.save() success = True status_code = 200 + if serializer.data.get("extra_config", None): + app_directory = os.path.join(os.getcwd(), "workspaces", obj.name) + new_git_config = serializer.data["extra_config"].get("git_config", {}) + old_git_config = obj.extra_config.get("git_config", {}) if obj.extra_config else {} + + new_repo_url = new_git_config.get("repo_url") + old_repo_url = old_git_config.get("repo_url") + + if new_repo_url and (not old_git_config or new_repo_url != old_repo_url): + git_setup( + app_directory, + new_repo_url, + self.get_branch(new_git_config, "dev", "development"), + self.get_branch(new_git_config, "staging", "staging"), + self.get_branch(new_git_config, "prod", "main"), + True + ) + result = { "message": "App Settings Updated Successfully", "app_uuid": str(obj.uuid), From 5d56d31612eec9224ae4af9e9ed4299258394794 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Thu, 26 Sep 2024 17:56:20 +0530 Subject: [PATCH 09/21] minor --- backend/src/zango/api/platform/tenancy/v1/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 0a4261bf7..e126ce54a 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -153,7 +153,8 @@ def get(self, request, *args, **kwargs): return get_api_response(success, response, status) def get_branch(config, key, default): - return config.get("branch", {}).get(key, default) + branch = config.get('branch', {}).get(key, default) + return branch if branch else default def put(self, request, *args, **kwargs): try: From cf6e13ae22fda4783f5201d55c0874460b764f01 Mon Sep 17 00:00:00 2001 From: Adhiraj Kar Date: Thu, 26 Sep 2024 18:24:45 +0530 Subject: [PATCH 10/21] Githup repo config frontend (#390) * feat: github repo config * feat: repo url config --- .../UpdateAppDetailsForm.jsx | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx index 68afe91ba..e4f14d3a5 100644 --- a/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx +++ b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx @@ -31,12 +31,24 @@ const UpdateAppDetailsForm = ({ closeModal }) => { timezone: appConfigurationData?.app?.timezone ?? '', date_format: appConfigurationData?.app?.date_format ?? '', datetime_format: appConfigurationData?.app?.datetime_format ?? '', + repo_url: appConfigurationData?.app?.extra_config?.repo_url ?? '', + dev: appConfigurationData?.app?.extra_config?.git_config?.dev ?? '', + prod: appConfigurationData?.app?.extra_config?.git_config?.prod ?? '', + staging: appConfigurationData?.app?.extra_config?.git_config?.staging ?? '' }; let validationSchema = Yup.object({ name: Yup.string().required('Required'), description: Yup.string().required('Required'), domains: Yup.array().of(Yup.string().required('Required')), + repo_url: Yup.string().url('Must be a valid URL').when(['dev', 'prod', 'staging'], { + is: (dev, prod, staging) => dev || prod || staging, + then: Yup.string().required('Required'), + otherwise: Yup.string().notRequired(), + }), + dev: Yup.string(), + prod: Yup.string(), + staging: Yup.string(), }); let onSubmit = (values) => { @@ -48,7 +60,24 @@ const UpdateAppDetailsForm = ({ closeModal }) => { if (!tempValues['fav_icon']) { delete tempValues['fav_icon']; } - + const extra_config = { + git_config: { + branch: { + dev: tempValues.dev==''?null:tempValues.dev, + prod: tempValues.prod==''?null:tempValues.prod, + staging: tempValues.staging==''?null:tempValues.staging + }, + repo_url: tempValues.repo_url==''?null:tempValues.repo_url + } + }; + + delete tempValues.dev; + delete tempValues.prod; + delete tempValues.staging; + delete tempValues.repo_url; + + tempValues.extra_config = JSON.stringify(extra_config); + let dynamicFormData = transformToFormData(tempValues); const makeApiCall = async () => { @@ -168,6 +197,46 @@ const UpdateAppDetailsForm = ({ closeModal }) => { } formik={formik} /> + + + +
Date: Thu, 26 Sep 2024 19:09:08 +0530 Subject: [PATCH 11/21] fix: edit data --- .../Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx index e4f14d3a5..0a7be87fa 100644 --- a/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx +++ b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx @@ -31,10 +31,10 @@ const UpdateAppDetailsForm = ({ closeModal }) => { timezone: appConfigurationData?.app?.timezone ?? '', date_format: appConfigurationData?.app?.date_format ?? '', datetime_format: appConfigurationData?.app?.datetime_format ?? '', - repo_url: appConfigurationData?.app?.extra_config?.repo_url ?? '', - dev: appConfigurationData?.app?.extra_config?.git_config?.dev ?? '', - prod: appConfigurationData?.app?.extra_config?.git_config?.prod ?? '', - staging: appConfigurationData?.app?.extra_config?.git_config?.staging ?? '' + repo_url: appConfigurationData?.app?.extra_config?.git_config?.repo_url ?? '', + dev: appConfigurationData?.app?.extra_config?.git_config?.branch?.dev ?? '', + prod: appConfigurationData?.app?.extra_config?.git_config?.branch?.prod ?? '', + staging: appConfigurationData?.app?.extra_config?.git_config?.branch?.staging ?? '' }; let validationSchema = Yup.object({ From d0e1aa3d63e12fbee449baa5f7356f0c3e895841 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Fri, 27 Sep 2024 15:29:11 +0530 Subject: [PATCH 12/21] initialie repo with url --- .../zango/api/platform/tenancy/v1/serializers.py | 1 - .../src/zango/api/platform/tenancy/v1/views.py | 16 ++++++++++++---- backend/src/zango/cli/git_setup.py | 1 + 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/serializers.py b/backend/src/zango/api/platform/tenancy/v1/serializers.py index 7d8449d3c..dcae71ee5 100644 --- a/backend/src/zango/api/platform/tenancy/v1/serializers.py +++ b/backend/src/zango/api/platform/tenancy/v1/serializers.py @@ -41,7 +41,6 @@ def get_date_format_display(self, obj): def update(self, instance, validated_data): request = self.context["request"] extra_config_str = request.data.get("extra_config") - # Convert extra_config from string to JSON if it exists if extra_config_str: try: diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index e126ce54a..4160c7cc8 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -152,13 +152,14 @@ def get(self, request, *args, **kwargs): return get_api_response(success, response, status) - def get_branch(config, key, default): + def get_branch(self, config, key, default): branch = config.get('branch', {}).get(key, default) return branch if branch else default def put(self, request, *args, **kwargs): try: obj = self.get_obj(**kwargs) + old_git_config = obj.extra_config.get("git_config", {}) if obj.extra_config else {} serializer = TenantSerializerModel( instance=obj, data=request.data, @@ -172,19 +173,26 @@ def put(self, request, *args, **kwargs): if serializer.data.get("extra_config", None): app_directory = os.path.join(os.getcwd(), "workspaces", obj.name) new_git_config = serializer.data["extra_config"].get("git_config", {}) - old_git_config = obj.extra_config.get("git_config", {}) if obj.extra_config else {} new_repo_url = new_git_config.get("repo_url") old_repo_url = old_git_config.get("repo_url") + # if repo_url is null, clean the repository + if not new_repo_url: + os.system(f"rm -rf {app_directory}/.git") if new_repo_url and (not old_git_config or new_repo_url != old_repo_url): git_setup( - app_directory, + [app_directory, + "--git_repo_url", new_repo_url, + "--dev_branch", self.get_branch(new_git_config, "dev", "development"), + "--staging_branch", self.get_branch(new_git_config, "staging", "staging"), + "--prod_branch", self.get_branch(new_git_config, "prod", "main"), - True + "--initialize"], + standalone_mode=False ) result = { diff --git a/backend/src/zango/cli/git_setup.py b/backend/src/zango/cli/git_setup.py index 620785f13..c2a82a55c 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -130,6 +130,7 @@ def git_setup( try: if initialize: + os.system(f"rm -rf {app_directory}/.git") # Initialize git repository repo = git.Repo.init(app_directory) From 365c92eaa216d6768d2c22814c7a8b62623dff13 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Tue, 1 Oct 2024 15:08:10 +0530 Subject: [PATCH 13/21] minor --- backend/src/zango/api/platform/tenancy/v1/views.py | 8 ++++---- backend/src/zango/cli/git_setup.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 4160c7cc8..7cbf2ce81 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -1,6 +1,7 @@ import json import traceback import os +import git from django_celery_results.models import TaskResult @@ -152,7 +153,7 @@ def get(self, request, *args, **kwargs): return get_api_response(success, response, status) - def get_branch(self, config, key, default): + def get_branch(self, config, key, default=None): branch = config.get('branch', {}).get(key, default) return branch if branch else default @@ -177,9 +178,6 @@ def put(self, request, *args, **kwargs): new_repo_url = new_git_config.get("repo_url") old_repo_url = old_git_config.get("repo_url") - # if repo_url is null, clean the repository - if not new_repo_url: - os.system(f"rm -rf {app_directory}/.git") if new_repo_url and (not old_git_config or new_repo_url != old_repo_url): git_setup( [app_directory, @@ -213,6 +211,8 @@ def put(self, request, *args, **kwargs): result = {"message": error_message} except Exception as e: + import traceback + print(traceback.format_exc()) success = False result = {"message": str(e)} status_code = 500 diff --git a/backend/src/zango/cli/git_setup.py b/backend/src/zango/cli/git_setup.py index c2a82a55c..1a439bec0 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -131,6 +131,7 @@ def git_setup( try: if initialize: os.system(f"rm -rf {app_directory}/.git") + os.system(f"rm -rf .gitignore") # Initialize git repository repo = git.Repo.init(app_directory) @@ -155,9 +156,8 @@ def git_setup( remote_branches = [ref.name.split("/")[-1] for ref in origin.refs] # Check if the branch exists locally - if dev_branch in remote_branches: - # Checkout the existing branch - repo.git.checkout(dev_branch) + if dev_branch in remote_branches or staging_branch in remote_branches or prod_branch in remote_branches: + raise Exception("Can't initialize repository with existing remote branches with same name.") else: # Create a new branch and checkout repo.git.checkout("-b", dev_branch) From 8f5e2dce71cfedfba1de367a2e707045d219782a Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Tue, 1 Oct 2024 15:11:16 +0530 Subject: [PATCH 14/21] ruff fix --- .../api/platform/tenancy/v1/serializers.py | 6 ++- .../zango/api/platform/tenancy/v1/views.py | 49 +++++++++++-------- backend/src/zango/cli/git_setup.py | 26 ++++++---- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/serializers.py b/backend/src/zango/api/platform/tenancy/v1/serializers.py index dcae71ee5..3d94bca5b 100644 --- a/backend/src/zango/api/platform/tenancy/v1/serializers.py +++ b/backend/src/zango/api/platform/tenancy/v1/serializers.py @@ -47,7 +47,9 @@ def update(self, instance, validated_data): extra_config_json = json.loads(extra_config_str) validated_data["extra_config"] = extra_config_json except json.JSONDecodeError: - raise serializers.ValidationError({"extra_config": "Invalid JSON format"}) + raise serializers.ValidationError( + {"extra_config": "Invalid JSON format"} + ) instance = super(TenantSerializerModel, self).update(instance, validated_data) request = self.context["request"] @@ -65,7 +67,7 @@ def update(self, instance, validated_data): if created: domain_obj.is_primary = False domain_obj.save() - + return instance diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 7cbf2ce81..82d2691c2 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -1,7 +1,6 @@ import json -import traceback import os -import git +import traceback from django_celery_results.models import TaskResult @@ -22,7 +21,6 @@ from zango.core.permissions import IsPlatformUserAllowedApp from zango.core.utils import get_search_columns - from .serializers import ( AppUserModelSerializerModel, TenantSerializerModel, @@ -152,15 +150,17 @@ def get(self, request, *args, **kwargs): status = 500 return get_api_response(success, response, status) - + def get_branch(self, config, key, default=None): - branch = config.get('branch', {}).get(key, default) + branch = config.get("branch", {}).get(key, default) return branch if branch else default def put(self, request, *args, **kwargs): try: obj = self.get_obj(**kwargs) - old_git_config = obj.extra_config.get("git_config", {}) if obj.extra_config else {} + old_git_config = ( + obj.extra_config.get("git_config", {}) if obj.extra_config else {} + ) serializer = TenantSerializerModel( instance=obj, data=request.data, @@ -173,26 +173,32 @@ def put(self, request, *args, **kwargs): status_code = 200 if serializer.data.get("extra_config", None): app_directory = os.path.join(os.getcwd(), "workspaces", obj.name) - new_git_config = serializer.data["extra_config"].get("git_config", {}) - + new_git_config = serializer.data["extra_config"].get( + "git_config", {} + ) + new_repo_url = new_git_config.get("repo_url") old_repo_url = old_git_config.get("repo_url") - if new_repo_url and (not old_git_config or new_repo_url != old_repo_url): + if new_repo_url and ( + not old_git_config or new_repo_url != old_repo_url + ): git_setup( - [app_directory, - "--git_repo_url", - new_repo_url, - "--dev_branch", - self.get_branch(new_git_config, "dev", "development"), - "--staging_branch", - self.get_branch(new_git_config, "staging", "staging"), - "--prod_branch", - self.get_branch(new_git_config, "prod", "main"), - "--initialize"], - standalone_mode=False + [ + app_directory, + "--git_repo_url", + new_repo_url, + "--dev_branch", + self.get_branch(new_git_config, "dev", "development"), + "--staging_branch", + self.get_branch(new_git_config, "staging", "staging"), + "--prod_branch", + self.get_branch(new_git_config, "prod", "main"), + "--initialize", + ], + standalone_mode=False, ) - + result = { "message": "App Settings Updated Successfully", "app_uuid": str(obj.uuid), @@ -212,6 +218,7 @@ def put(self, request, *args, **kwargs): result = {"message": error_message} except Exception as e: import traceback + print(traceback.format_exc()) success = False result = {"message": str(e)} diff --git a/backend/src/zango/cli/git_setup.py b/backend/src/zango/cli/git_setup.py index 1a439bec0..c63f41b48 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -4,10 +4,12 @@ import click import git -from .update_apps import find_project_name import django +from .update_apps import find_project_name + + def is_valid_app_directory(directory): # Define your validation criteria required_files = ["settings.json", "manifest.json"] @@ -48,11 +50,11 @@ def update_settings_with_git_repo_url( settings = json.load(settings_file) # Update git_repo_url in settings - app_name = settings.get("app_name","") + app_name = settings.get("app_name", "") tenant_obj = TenantModel.objects.get(name=app_name) - git_config={} + git_config = {} if tenant_obj.extra_config: - git_config = tenant_obj.extra_config.get("git_config",{}) + git_config = tenant_obj.extra_config.get("git_config", {}) else: git_config["repo_url"] = git_repo_url git_branch = git_config.get("branch", {}) @@ -61,9 +63,9 @@ def update_settings_with_git_repo_url( ) git_config["branch"] = git_branch if tenant_obj.extra_config: - tenant_obj.extra_config.update({"git_config":git_config}) + tenant_obj.extra_config.update({"git_config": git_config}) else: - tenant_obj.extra_config={"git_config":git_config} + tenant_obj.extra_config = {"git_config": git_config} tenant_obj.save() return True @@ -131,7 +133,7 @@ def git_setup( try: if initialize: os.system(f"rm -rf {app_directory}/.git") - os.system(f"rm -rf .gitignore") + os.system("rm -rf .gitignore") # Initialize git repository repo = git.Repo.init(app_directory) @@ -156,8 +158,14 @@ def git_setup( remote_branches = [ref.name.split("/")[-1] for ref in origin.refs] # Check if the branch exists locally - if dev_branch in remote_branches or staging_branch in remote_branches or prod_branch in remote_branches: - raise Exception("Can't initialize repository with existing remote branches with same name.") + if ( + dev_branch in remote_branches + or staging_branch in remote_branches + or prod_branch in remote_branches + ): + raise Exception( + "Can't initialize repository with existing remote branches with same name." + ) else: # Create a new branch and checkout repo.git.checkout("-b", dev_branch) From 4b6febba7276e0365a49c1d3325115666f5e4883 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Tue, 1 Oct 2024 16:14:56 +0530 Subject: [PATCH 15/21] minor --- backend/src/zango/cli/git_setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/zango/cli/git_setup.py b/backend/src/zango/cli/git_setup.py index c63f41b48..c01413c22 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -158,11 +158,7 @@ def git_setup( remote_branches = [ref.name.split("/")[-1] for ref in origin.refs] # Check if the branch exists locally - if ( - dev_branch in remote_branches - or staging_branch in remote_branches - or prod_branch in remote_branches - ): + if dev_branch in remote_branches: raise Exception( "Can't initialize repository with existing remote branches with same name." ) From 9c76ac02b6d711bf09c5fc2e2885376944631f83 Mon Sep 17 00:00:00 2001 From: deepakdinesh1123 Date: Fri, 18 Oct 2024 10:06:00 +0530 Subject: [PATCH 16/21] release fixes --- .../tenancy/management/commands/ws_migrate.py | 18 +++++++++-- backend/src/zango/cli/update_apps.py | 30 ++++++++++++------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/backend/src/zango/apps/shared/tenancy/management/commands/ws_migrate.py b/backend/src/zango/apps/shared/tenancy/management/commands/ws_migrate.py index 71414fa37..d223743b5 100644 --- a/backend/src/zango/apps/shared/tenancy/management/commands/ws_migrate.py +++ b/backend/src/zango/apps/shared/tenancy/management/commands/ws_migrate.py @@ -1,3 +1,5 @@ +import os + from django_tenants.management.commands.migrate_schemas import MigrateSchemasCommand from django.conf import settings @@ -45,8 +47,18 @@ def handle(self, *args, **options): "dynamic_models": f"workspaces.{ options['workspace']}.migrations" } else: - settings.MIGRATION_MODULES = { - "dynamic_models": f"workspaces.{ options['workspace']}.packages.{options['package']}.migrations" - } + if os.path.exists( + f"workspaces/{options['workspace']}/packages/{options['package']}/migrations" + ): + settings.MIGRATION_MODULES = { + "dynamic_models": f"workspaces.{ options['workspace']}.packages.{options['package']}.migrations" + } + else: + self.stdout.write( + self.style.NOTICE( + f"\n\nThe package '{options['package']}' does not have any migrations. Please ensure that you have entered the correct package name and try again." + ) + ) + exit(0) options["schema_name"] = tenant_obj.schema_name super().handle(*args, **options) diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index 4d92cafa7..619a51fa5 100644 --- a/backend/src/zango/cli/update_apps.py +++ b/backend/src/zango/cli/update_apps.py @@ -139,15 +139,14 @@ def run_migrations(tenant, app_directory): check=True, ) except subprocess.CalledProcessError: - print("Migrations failed for package: ", package) + raise Exception(f"Migrations failed for package: {package}") print("Running app migrations...") try: subprocess.run(["python", "manage.py", "ws_migrate", tenant], check=True) print("Migrations ran successfully.") except subprocess.CalledProcessError: - print("Migrations failed.") - raise + raise Exception("Migrations failed.") def sync_static(tenant): @@ -155,8 +154,7 @@ def sync_static(tenant): subprocess.run(["python", "manage.py", "sync_static", tenant], check=True) print("Static files collected successfully.") except subprocess.CalledProcessError: - print("Collecting static files failed.") - raise + raise Exception("Collecting static files failed.") def collect_static(): @@ -245,6 +243,7 @@ def execute_fixtures(tenant_name, last_version, current_version, app_directory): bold=True, ) click.echo(error_message, err=True) + raise Exception(message) return failed_fixture_dict @@ -302,6 +301,7 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): tenant = TenantModel.objects.get(name=tenant_name) connection.set_tenant(tenant) with connection.cursor() as c: + release = None try: current_version = app_settings.get("version") if not current_version: @@ -368,7 +368,10 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): print("No version change detected for") except Exception as e: - print(f"An error occurred while creating/updating release: {e}") + if release: + release.status = "failed" + release.save() + raise Exception(f"An error occurred while creating/updating release: {e}") def is_update_allowed(tenant, app_settings, git_mode=False, repo_url=None, branch=None): @@ -409,8 +412,10 @@ def is_update_allowed(tenant, app_settings, git_mode=False, repo_url=None, branc return False, "Invalid Zango version specifier in settings.json" if git_mode: + if is_version_greater(remote_version, local_version): + return False, "Remote version is greater than local version" if not ( - is_version_greater(remote_version, local_version) + is_version_greater(local_version, remote_version) or ( last_release and is_version_greater(remote_version, last_release.version) @@ -476,7 +481,7 @@ def update_apps(app_name): if git_settings: git_mode = True # Initialize git repository - repo_url = git_settings["git_config"]["repo_url"] + repo_url = git_settings["repo_url"] # Split the repo URL into parts parts = repo_url.split("://") @@ -484,7 +489,7 @@ def update_apps(app_name): # Add username and password to the URL repo_url = f"{parts[0]}://{settings.GIT_USERNAME}:{settings.GIT_PASSWORD}@{parts[1]}" - branch = git_settings["git_config"]["branch"].get(settings.ENV, "main") + branch = git_settings["branch"].get(settings.ENV, "main") update_allowed, message = is_update_allowed( tenant, app_settings, git_mode, repo_url, branch @@ -528,5 +533,8 @@ def update_apps(app_name): except Exception as e: import traceback - print(traceback.format_exc()) - click.echo(f"An error occurred: {e}") + error_message = click.style( + f"An error occurred while updating app {tenant}: {traceback.format_exc()}", + fg="red", + bold=True, + ) From c8230fb54daa67728299de95e466b18eda3a10c9 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Tue, 22 Oct 2024 15:28:14 +0530 Subject: [PATCH 17/21] added sync packages setting --- backend/src/zango/cli/update_apps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index a6882057a..2a6ca8d97 100644 --- a/backend/src/zango/cli/update_apps.py +++ b/backend/src/zango/cli/update_apps.py @@ -340,8 +340,9 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): release.status = "in_progress" release.save(update_fields=["status"]) - # install packages - install_packages(tenant, app_directory) + if tenant.extra_config.get("sync_packages", True): + # install packages + install_packages(tenant, app_directory) # Run app migrations run_app_migrations(tenant_name, app_directory) From e08687349f362b0e4465939cfd46ba5b3d27a082 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Tue, 22 Oct 2024 16:36:09 +0530 Subject: [PATCH 18/21] apply package migrations --- backend/src/zango/cli/update_apps.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index 2a6ca8d97..7ab4cff19 100644 --- a/backend/src/zango/cli/update_apps.py +++ b/backend/src/zango/cli/update_apps.py @@ -124,6 +124,33 @@ def run_app_migrations(tenant, app_directory): raise Exception("Migrations failed.") +def run_package_migrations(tenant, app_directory): + # Run package migrations + print("Running package migrations...") + + updated_app_manifest = json.loads( + open(os.path.join(app_directory, "manifest.json")).read() + ) + + installed_packages = updated_app_manifest["packages"] + + for package in installed_packages: + try: + subprocess.run( + [ + "python", + "manage.py", + "ws_migrate", + tenant, + "--package", + package["name"], + ], + check=True, + ) + except subprocess.CalledProcessError: + print("Migrations failed for package: ", package) + + def sync_static(tenant): try: subprocess.run(["python", "manage.py", "sync_static", tenant], check=True) @@ -343,6 +370,9 @@ def create_release(tenant_name, app_settings, app_directory, git_mode): if tenant.extra_config.get("sync_packages", True): # install packages install_packages(tenant, app_directory) + else: + # simply apply package migrations + run_package_migrations(tenant_name, app_directory) # Run app migrations run_app_migrations(tenant_name, app_directory) From 93ae372136f256c2bf236a74a95e606c526a0e99 Mon Sep 17 00:00:00 2001 From: devilsautumn Date: Fri, 25 Oct 2024 15:02:47 +0530 Subject: [PATCH 19/21] fixed issue with no app template --- backend/src/zango/api/platform/tenancy/v1/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 439e1cc61..60302d46e 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -95,7 +95,7 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): data = request.data - app_template = data.get("app_template", False) + app_template = data.get("app_template", None) app_template_name = None if app_template: app_template_name = str(app_template).split(".")[0] From c6fc81d82ebd65cb4a3fc29baa16bb8fcc788d8b Mon Sep 17 00:00:00 2001 From: adhiraj23zelthy Date: Thu, 24 Oct 2024 17:11:07 +0530 Subject: [PATCH 20/21] feat: fe sync packages --- .../UpdateAppDetailsForm.jsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx index 0a7be87fa..ae3e46c17 100644 --- a/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx +++ b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx @@ -9,6 +9,7 @@ import InputFieldArray from '../../../../../components/Form/InputFieldArray'; import SelectField from '../../../../../components/Form/SelectField'; import SubmitButton from '../../../../../components/Form/SubmitButton'; import TextareaField from '../../../../../components/Form/TextareaField'; +import CheckboxField from '../../../../../components/Form/CheckboxField'; import useApi from '../../../../../hooks/useApi'; import { transformToFormData } from '../../../../../utils/form'; @@ -34,7 +35,8 @@ const UpdateAppDetailsForm = ({ closeModal }) => { repo_url: appConfigurationData?.app?.extra_config?.git_config?.repo_url ?? '', dev: appConfigurationData?.app?.extra_config?.git_config?.branch?.dev ?? '', prod: appConfigurationData?.app?.extra_config?.git_config?.branch?.prod ?? '', - staging: appConfigurationData?.app?.extra_config?.git_config?.branch?.staging ?? '' + staging: appConfigurationData?.app?.extra_config?.git_config?.branch?.staging ?? '', + sync_packages: appConfigurationData?.app?.extra_config?.sync_packages ?? true }; let validationSchema = Yup.object({ @@ -49,6 +51,7 @@ const UpdateAppDetailsForm = ({ closeModal }) => { dev: Yup.string(), prod: Yup.string(), staging: Yup.string(), + sync_packages: Yup.boolean() }); let onSubmit = (values) => { @@ -68,13 +71,15 @@ const UpdateAppDetailsForm = ({ closeModal }) => { staging: tempValues.staging==''?null:tempValues.staging }, repo_url: tempValues.repo_url==''?null:tempValues.repo_url - } + }, + sync_packages: tempValues.sync_packages }; delete tempValues.dev; delete tempValues.prod; delete tempValues.staging; delete tempValues.repo_url; + delete tempValues.sync_packages; tempValues.extra_config = JSON.stringify(extra_config); @@ -237,6 +242,17 @@ const UpdateAppDetailsForm = ({ closeModal }) => { onChange={formik.handleChange} formik={formik} /> +
Date: Fri, 25 Oct 2024 12:58:43 +0530 Subject: [PATCH 21/21] detail about github url & branches --- .../src/mocks/appConfigurationHandlers.js | 2 +- .../AppConfiguration/DetailsTable/index.jsx | 52 ++++++++++++++++++- frontend/src/utils/helper.js | 6 +++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/frontend/src/mocks/appConfigurationHandlers.js b/frontend/src/mocks/appConfigurationHandlers.js index 0cf605a27..d49f0809b 100644 --- a/frontend/src/mocks/appConfigurationHandlers.js +++ b/frontend/src/mocks/appConfigurationHandlers.js @@ -30,7 +30,7 @@ export const appConfigurationHandlers = [ datetime_format_display: 'August 05 2006, 3:05 PM', logo: '', fav_icon: '', - extra_config: null, + extra_config: {"git_config":{"branch":{"dev":"dev","prod":"main","staging":"staging"},"repo_url":"https://github.com/Healthlane-Technologies/Zango"},"sync_packages":true}, domains: [ { domain: 'zel3-neapp.zelthy.in', is_primary: true }, { domain: 'domainame2.com', is_primary: false }, diff --git a/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx b/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx index 0d59410ba..d17fa1840 100644 --- a/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx +++ b/frontend/src/pages/appConfiguration/components/AppConfiguration/DetailsTable/index.jsx @@ -3,7 +3,7 @@ import { ReactComponent as EachAppIcon } from '../../../../../assets/images/svg/ import { selectAppConfigurationData } from '../../../slice'; import EachDescriptionRow from './EachDescriptionRow'; import { ReactComponent as SingleFileIcon } from '../../../../../assets/images/svg/single-file.svg'; - +import { getRepoName } from '../../../../../utils/helper'; function DetailsTable() { const appConfigurationData = useSelector(selectAppConfigurationData); @@ -118,6 +118,56 @@ function DetailsTable() { ) } /> + + + {appConfigurationData?.app?.extra_config?.git_config?.repo_url ? getRepoName(appConfigurationData?.app?.extra_config?.git_config?.repo_url) : null} + + + ) : ( + + Not configured + + ) + } + /> + { + appConfigurationData?.app?.extra_config?.git_config?.repo_url ? (<> + + {appConfigurationData?.app?.extra_config?.git_config?.branch?.dev} + + } + /> + + {appConfigurationData?.app?.extra_config?.git_config?.branch?.staging} + + } + /> + + {appConfigurationData?.app?.extra_config?.git_config?.branch?.prod} + + } + /> + ) : null + } + ); diff --git a/frontend/src/utils/helper.js b/frontend/src/utils/helper.js index 6b2f2c30c..98291cb1c 100644 --- a/frontend/src/utils/helper.js +++ b/frontend/src/utils/helper.js @@ -35,3 +35,9 @@ export const getPlatformVersion = () => { return platformVersion; }; + +export function getRepoName(githubUrl) { + const regex = /github\.com\/([^/]+\/[^/]+)/; + const match = githubUrl.match(regex); + return match ? match[1] : null; +} \ No newline at end of file