diff --git a/backend/src/zango/api/platform/tenancy/v1/serializers.py b/backend/src/zango/api/platform/tenancy/v1/serializers.py index 4fc4d50bb..3d94bca5b 100644 --- a/backend/src/zango/api/platform/tenancy/v1/serializers.py +++ b/backend/src/zango/api/platform/tenancy/v1/serializers.py @@ -39,6 +39,18 @@ 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") 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..6cba8be01 --- /dev/null +++ b/backend/src/zango/api/platform/tenancy/v1/utils.py @@ -0,0 +1,64 @@ +import json +import os +import zipfile + + +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 diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 88d9bd1ef..60302d46e 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -1,4 +1,5 @@ import json +import os import traceback 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, @@ -25,6 +27,7 @@ ThemeModelSerializer, UserRoleSerializerModel, ) +from .utils import extract_app_details_from_zip class AppViewAPIV1(ZangoGenericPlatformAPIView): @@ -92,11 +95,25 @@ def get(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): data = request.data + app_template = data.get("app_template", 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, + } + ) 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", ) @@ -149,9 +166,16 @@ def get(self, request, *args, **kwargs): return get_api_response(success, response, status) + def get_branch(self, config, key, default=None): + 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, @@ -162,6 +186,34 @@ 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", {} + ) + + 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, + "--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), @@ -180,6 +232,9 @@ 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/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/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..17f56e059 100644 --- a/backend/src/zango/apps/shared/tenancy/models.py +++ b/backend/src/zango/apps/shared/tenancy/models.py @@ -1,4 +1,7 @@ +import os import re +import requests +import tempfile import uuid from collections import namedtuple @@ -10,6 +13,7 @@ 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 @@ -115,6 +119,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 + ) auto_create_schema = False @@ -127,14 +134,33 @@ 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: + 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(downloaded_file_path), app_template_name + ) # initialize tenant's workspace - init_task = initialize_workspace.apply_async((str(obj.uuid),), countdown=1) + 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): diff --git a/backend/src/zango/apps/shared/tenancy/tasks.py b/backend/src/zango/apps/shared/tenancy/tasks.py index 80f2d4be7..9eae1b95c 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 @@ -12,7 +14,7 @@ @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 +22,6 @@ def initialize_workspace(tenant_uuid): # Creating schema tenant.create_schema(check_if_exists=True) - # migrating schema call_command( "migrate_schemas", @@ -28,7 +29,6 @@ def initialize_workspace(tenant_uuid): schema_name=tenant.schema_name, interactive=False, ) - # Create workspace Folder project_base_dir = settings.BASE_DIR @@ -36,27 +36,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/git_setup.py b/backend/src/zango/cli/git_setup.py index e568525bb..c01413c22 100644 --- a/backend/src/zango/cli/git_setup.py +++ b/backend/src/zango/cli/git_setup.py @@ -1,9 +1,14 @@ import json import os +import sys import click import git +import django + +from .update_apps import find_project_name + def is_valid_app_directory(directory): # Define your validation criteria @@ -20,7 +25,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. @@ -32,32 +37,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 - git_config = settings.get("git_config", {}) - git_config["repo_url"] = git_repo_url + app_name = settings.get("app_name", "") + tenant_obj = TenantModel.objects.get(name=app_name) + 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 - 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) + 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 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 @@ -115,6 +132,8 @@ def git_setup( try: if initialize: + os.system(f"rm -rf {app_directory}/.git") + os.system("rm -rf .gitignore") # Initialize git repository repo = git.Repo.init(app_directory) @@ -140,8 +159,9 @@ def git_setup( # Check if the branch exists locally if dev_branch in remote_branches: - # Checkout the existing branch - repo.git.checkout(dev_branch) + 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) diff --git a/backend/src/zango/cli/update_apps.py b/backend/src/zango/cli/update_apps.py index f3e154be1..7ab4cff19 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,7 +115,16 @@ def setup_and_pull(path, repo_url, branch="main"): return success, message -def run_migrations(tenant, app_directory): +def run_app_migrations(tenant, app_directory): + print("Running app migrations...") + try: + subprocess.run(["python", "manage.py", "ws_migrate", tenant], check=True) + print("Migrations ran successfully.") + except subprocess.CalledProcessError: + raise Exception("Migrations failed.") + + +def run_package_migrations(tenant, app_directory): # Run package migrations print("Running package migrations...") @@ -141,22 +150,13 @@ def run_migrations(tenant, app_directory): except subprocess.CalledProcessError: print("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 - def sync_static(tenant): try: 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 +245,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 @@ -292,6 +293,38 @@ 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 @@ -302,6 +335,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: @@ -333,8 +367,15 @@ 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) + 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) # Sync Static sync_static(tenant_name) @@ -368,7 +409,13 @@ 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() + import traceback + + print(traceback.format_exc()) + 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 +456,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) @@ -436,8 +485,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") @@ -472,10 +522,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["repo_url"] # Split the repo URL into parts parts = repo_url.split("://") @@ -483,7 +534,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["branch"].get(settings.ENV, "main") update_allowed, message = is_update_allowed( tenant, app_settings, git_mode, repo_url, branch @@ -527,5 +578,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, + ) diff --git a/backend/src/zango/core/package_utils.py b/backend/src/zango/core/package_utils.py index 1e11822bb..a14df2ec6 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: 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/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 3e2735233..d17fa1840 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'; +import { getRepoName } from '../../../../../utils/helper'; function DetailsTable() { const appConfigurationData = useSelector(selectAppConfigurationData); @@ -100,6 +102,72 @@ function DetailsTable() { } /> + + + + ):( + + No Template found + + ) + } + /> + + + {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/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx b/frontend/src/pages/appConfiguration/components/Modals/UpdateAppDetailsModal/UpdateAppDetailsForm.jsx index 68afe91ba..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'; @@ -31,12 +32,26 @@ 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?.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 ?? '', + sync_packages: appConfigurationData?.app?.extra_config?.sync_packages ?? true }; 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(), + sync_packages: Yup.boolean() }); let onSubmit = (values) => { @@ -48,7 +63,26 @@ 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 + }, + 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); + let dynamicFormData = transformToFormData(tempValues); const makeApiCall = async () => { @@ -168,6 +202,57 @@ const UpdateAppDetailsForm = ({ closeModal }) => { } formik={formik} /> + + + + +
{ 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

+
+
+
+
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