From c43834b21f15545a25c61289db6a6b6dfbd0a8d6 Mon Sep 17 00:00:00 2001 From: Jurica Grgicevic Date: Mon, 9 Nov 2020 12:32:07 +0100 Subject: [PATCH 1/4] feat: fetch gitlab envs --- test/repos/tasks/test_sync_repos.py | 39 ++- test/services/test_forms.py | 2 + zoo/base/assets/js/components/RepoInput.vue | 8 + zoo/factories.py | 11 +- zoo/repos/admin.py | 5 + zoo/repos/forms.py | 2 + zoo/repos/gitlab.py | 8 + .../migrations/0008_repositoryenvironment.py | 43 ++++ zoo/repos/models.py | 16 ++ zoo/repos/tasks.py | 37 ++- .../templates/repos/fields/repo_input.html | 7 + zoo/repos/urls.py | 1 + zoo/repos/views.py | 23 ++ zoo/services/assets/img/gitlab-icon-rgb.svg | 69 +++++ zoo/services/assets/js/gitlab_envs.js | 80 ++++++ zoo/services/assets/js/service_form.js | 6 +- zoo/services/constants.py | 5 + zoo/services/forms.py | 9 +- .../migrations/0026_environment_type.py | 22 ++ zoo/services/models.py | 7 +- .../templates/services/service_form.html | 238 ++++++++++-------- zoo/services/views.py | 7 +- 22 files changed, 524 insertions(+), 121 deletions(-) create mode 100644 zoo/repos/migrations/0008_repositoryenvironment.py create mode 100644 zoo/services/assets/img/gitlab-icon-rgb.svg create mode 100644 zoo/services/assets/js/gitlab_envs.js create mode 100644 zoo/services/migrations/0026_environment_type.py diff --git a/test/repos/tasks/test_sync_repos.py b/test/repos/tasks/test_sync_repos.py index a7b304b2..3a4a7f5c 100644 --- a/test/repos/tasks/test_sync_repos.py +++ b/test/repos/tasks/test_sync_repos.py @@ -3,12 +3,21 @@ import pytest from faker import Faker -from zoo.repos.models import Provider, Repository +from zoo.repos.models import Provider, Repository, RepositoryEnvironment from zoo.repos.tasks import sync_repos pytestmark = pytest.mark.django_db +class FakeGitlabEnviroment: + def __init__(self, id=None, name=None, external_url=None, slug=None): + self.fake = Faker() + self.id = self.fake.pyint() if id is None else id + self.name = self.fake.word() if name is None else name + self.slug = self.fake.word() if slug is None else slug + self.external_url = self.fake.url() if external_url is None else external_url + + class FakeGitProject: def __init__(self, pid): self.fake = Faker() @@ -53,26 +62,42 @@ def generate_project_list(pid=None, owner=None, name=None, url=None, **kwargs): } +def generate_proj_envs_list(): + return [FakeGitlabEnviroment()] + + def test_sync_untouched_repo(repository): project_list = generate_project_list( repository.remote_id, repository.owner, repository.name, repository.url ) + gitlab_envs = generate_proj_envs_list() with patch( "gitlab.v4.objects.ProjectManager.list", return_value=project_list["gitlab"] ), patch( "github.AuthenticatedUser.AuthenticatedUser.get_repos", return_value=project_list["github"], + ), patch( + "zoo.repos.tasks.get_project_enviroments", + return_value=gitlab_envs, ): sync_repos() gitlab_project = project_list["gitlab"][0] - assert gitlab_project.id == repository.remote_id + repository = Repository.objects.get( + remote_id=gitlab_project.id, provider=Provider.GITLAB.value + ) assert gitlab_project.namespace["full_path"] == repository.owner assert gitlab_project.path == repository.name assert gitlab_project.web_url == repository.url + repo_env = repository.repository_environments.first() + assert gitlab_envs[0].name == repo_env.name + assert gitlab_envs[0].external_url == repo_env.external_url + github_project = project_list["github"][0] - assert github_project.id == repository.remote_id + 1 + repository = Repository.objects.get( + remote_id=github_project.id, provider=Provider.GITHUB.value + ) assert github_project.owner.login == repository.owner assert github_project.name == repository.name assert github_project.svn_url == repository.url @@ -80,11 +105,15 @@ def test_sync_untouched_repo(repository): def test_sync_moved_repo(repository): project_list = generate_project_list(repository.remote_id) + gitlab_envs = generate_proj_envs_list() with patch( "gitlab.v4.objects.ProjectManager.list", return_value=project_list["gitlab"] ), patch( "github.AuthenticatedUser.AuthenticatedUser.get_repos", return_value=project_list["github"], + ), patch( + "zoo.repos.tasks.get_project_enviroments", + return_value=gitlab_envs, ): sync_repos() @@ -96,6 +125,10 @@ def test_sync_moved_repo(repository): assert gitlab_project.path == repository.name assert gitlab_project.web_url == repository.url + repo_env = repository.repository_environments.first() + assert gitlab_envs[0].name == repo_env.name + assert gitlab_envs[0].external_url == repo_env.external_url + github_project = project_list["github"][0] repository = Repository.objects.get( remote_id=github_project.id, provider=Provider.GITHUB.value diff --git a/test/services/test_forms.py b/test/services/test_forms.py index e0334e90..92b07c7d 100644 --- a/test/services/test_forms.py +++ b/test/services/test_forms.py @@ -116,11 +116,13 @@ def test_service_form__complete__incorrect_status(repository): "environments-0-service_urls_0": fake.url(), "environments-0-service_urls_1": fake.url(), "environments-0-DELETE": False, + "environments-0-type": "zoo", "environments-1-name": fake.word(), "environments-1-dashboard_url": fake.url(), "environments-1-service_urls_0": fake.url(), "environments-1-service_urls_1": fake.url(), "environments-1-DELETE": False, + "environments-1-type": "zoo", } diff --git a/zoo/base/assets/js/components/RepoInput.vue b/zoo/base/assets/js/components/RepoInput.vue index 7543e1fe..75d7ad21 100644 --- a/zoo/base/assets/js/components/RepoInput.vue +++ b/zoo/base/assets/js/components/RepoInput.vue @@ -36,6 +36,8 @@ import { match } from "ramda" import RepoInputSuggestions from "./RepoInputSuggestions" import $ from 'jquery/src/jquery' +import { gitlabEnvs } from "../../../../services/assets/js/gitlab_envs.js" + function createPopup () { $('i.magic.icon').popup({ @@ -51,6 +53,11 @@ function popupAction (action) { $('i.magic.icon').popup(action) } +function loadGitlabEnvs(repoId) { + gitlabEnvsInfo["repoId"] = repoId + gitlabEnvs.load(gitlabEnvsInfo) +} + export default { template: "#repo-input-text-field-markup", data () { @@ -153,6 +160,7 @@ export default { this.$refs.suggestions.selectSuggestion(); this.isOnEditMode = false this.$store.commit("refreshEnteredText") + loadGitlabEnvs(parseInt(this.inputValue)) }, }, components: { diff --git a/zoo/factories.py b/zoo/factories.py index 103e8ac5..34843dbc 100644 --- a/zoo/factories.py +++ b/zoo/factories.py @@ -18,7 +18,7 @@ from zoo.auditing.check_discovery import Kind from zoo.auditing.models import Issue from zoo.datacenters.models import InfraNode -from zoo.repos.models import Repository +from zoo.repos.models import Repository, RepositoryEnvironment from zoo.services.models import Environment, Impact, Link, Service, Status, Tier @@ -33,6 +33,15 @@ class Meta: url = LazyAttribute(lambda o: f"https://gitlab.com/{o.owner}/{o.name}") +class RepositoryEnvironmentFactory(DjangoModelFactory): + class Meta: + model = RepositoryEnvironment + + name = Faker("domain_word") + repository = SubFactory(RepositoryFactory) + external_url = LazyAttribute(lambda o: f"https://gitlab.com/{o.name}/{o.name}") + + class IssueFactory(DjangoModelFactory): class Meta: model = Issue diff --git a/zoo/repos/admin.py b/zoo/repos/admin.py index 3e9786d8..5b4ebd95 100644 --- a/zoo/repos/admin.py +++ b/zoo/repos/admin.py @@ -6,3 +6,8 @@ @admin.register(models.Repository) class RepoAdmin(admin.ModelAdmin): search_fields = ("remote_id", "owner", "name", "provider") + + +@admin.register(models.RepositoryEnvironment) +class RepoEnvAdmin(admin.ModelAdmin): + search_fields = ("name", "external_url") diff --git a/zoo/repos/forms.py b/zoo/repos/forms.py index 9897372a..17ea0643 100644 --- a/zoo/repos/forms.py +++ b/zoo/repos/forms.py @@ -1,5 +1,6 @@ from django.forms import widgets +from ..services.constants import EnviromentType from .models import Repository @@ -19,4 +20,5 @@ def get_context(self, name, value, attrs): ) context["widget"]["value"] = value + context["env_type_gitlab"] = EnviromentType.GITLAB.value return context diff --git a/zoo/repos/gitlab.py b/zoo/repos/gitlab.py index 47556d45..c3c006d7 100644 --- a/zoo/repos/gitlab.py +++ b/zoo/repos/gitlab.py @@ -13,6 +13,14 @@ log = structlog.get_logger() +def get_project_enviroments(remote_id): + try: + project = gitlab.projects.get(remote_id) + return project.environments.list(as_list=False) + except GitlabGetError: + return [] + + def get_project(remote_id): try: project = gitlab.projects.get(remote_id) diff --git a/zoo/repos/migrations/0008_repositoryenvironment.py b/zoo/repos/migrations/0008_repositoryenvironment.py new file mode 100644 index 00000000..6765c155 --- /dev/null +++ b/zoo/repos/migrations/0008_repositoryenvironment.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.19 on 2021-03-28 20:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("repos", "0007_endpoint"), + ] + + operations = [ + migrations.CreateModel( + name="RepositoryEnvironment", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("external_url", models.CharField(max_length=100, null=True)), + ( + "repository", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="repository_environments", + related_query_name="repository_environment", + to="repos.Repository", + ), + ), + ], + options={ + "ordering": ["name"], + "unique_together": {("repository", "name")}, + }, + ), + ] diff --git a/zoo/repos/models.py b/zoo/repos/models.py index d9fcc210..54d54f61 100644 --- a/zoo/repos/models.py +++ b/zoo/repos/models.py @@ -86,3 +86,19 @@ class Meta: ) summary = models.CharField(max_length=500, null=True, blank=True) operation = models.CharField(max_length=200, null=True, blank=True) + + +class RepositoryEnvironment(models.Model): + class Meta: + unique_together = ("repository", "name") + ordering = ["name"] + + repository = models.ForeignKey( + Repository, + on_delete=models.CASCADE, + related_name="repository_environments", + related_query_name="repository_environment", + ) + + name = models.CharField(max_length=100) + external_url = models.CharField(max_length=100, null=True) diff --git a/zoo/repos/tasks.py b/zoo/repos/tasks.py index d3cd4250..bc9157ab 100644 --- a/zoo/repos/tasks.py +++ b/zoo/repos/tasks.py @@ -12,11 +12,13 @@ from ..auditing import runner from ..auditing.check_discovery import CHECKS as AUDITING_CHECKS from ..repos.models import Endpoint +from ..services.constants import EnviromentType from ..services.models import Environment, Service from .exceptions import MissingFilesError, RepositoryNotFoundError from .github import get_repositories as get_github_repositories +from .gitlab import get_project_enviroments from .gitlab import get_repositories as get_gitlab_repositories -from .models import Repository +from .models import Repository, RepositoryEnvironment from .utils import download_repository, get_scm_module, openapi_definition from .zoo_yml import parse, validate @@ -68,6 +70,9 @@ def sync_repos(): repo.full_clean() repo.save() + if project["provider"] == "gitlab": + sync_enviroments_from_gitlab(repo) + @shared_task def schedule_pulls(): @@ -208,3 +213,33 @@ def get_zoo_file_content(proj: Dict) -> str: return provider.get_file_content( proj["id"], settings.ZOO_YAML_FILE, settings.ZOO_YAML_DEFAULT_REF ) + + +def sync_enviroments_from_gitlab(repo: Repository): + gl_envs = get_project_enviroments(repo.remote_id) + envs = [] + for gl_env in gl_envs: + env, _ = RepositoryEnvironment.objects.get_or_create( + repository_id=repo.id, + name=gl_env.name, + external_url=gl_env.external_url, + ) + envs.append(env) + + RepositoryEnvironment.objects.filter(repository_id=repo.id).exclude( + id__in=[env.id for env in envs] + ).delete() + + # update gitlab envs on every service + services = Service.objects.filter(repository_id=repo.id) + for service in services: + Environment.objects.filter( + service_id=service.id, type=EnviromentType.GITLAB.value + ).exclude(name__in=[env.name for env in envs]).delete() + for env in envs: + Environment.objects.update_or_create( + service_id=service.id, + name=env.name, + external_url=env.external_url, + type=EnviromentType.GITLAB.value, + ) diff --git a/zoo/repos/templates/repos/fields/repo_input.html b/zoo/repos/templates/repos/fields/repo_input.html index 56b5a5c3..63db47f1 100644 --- a/zoo/repos/templates/repos/fields/repo_input.html +++ b/zoo/repos/templates/repos/fields/repo_input.html @@ -1,3 +1,5 @@ +{% load static %} +
@@ -20,4 +22,9 @@ name: '{{ widget.name }}', initialValue: '{{ widget.value|default:"" }}' }; + const gitlabEnvsInfo = { + envTypeGitlab: '{{ env_type_gitlab }}', + imageUrl: '{% static 'img/gitlab-icon-rgb.svg' %}', + envsUrl: '{% url "get_gitlab_envs" %}' + } diff --git a/zoo/repos/urls.py b/zoo/repos/urls.py index eb8ceba1..9f0cdc5b 100644 --- a/zoo/repos/urls.py +++ b/zoo/repos/urls.py @@ -6,4 +6,5 @@ urlpatterns = [ path("", views.RepoList.as_view(), name="repo_list"), path("//", views.repo_details, name="repo_details"), + path("api/get-gitlab-envs/", views.get_gitlab_envs, name="get_gitlab_envs"), ] diff --git a/zoo/repos/views.py b/zoo/repos/views.py index f1d0be3a..e6126cd9 100644 --- a/zoo/repos/views.py +++ b/zoo/repos/views.py @@ -1,9 +1,11 @@ import structlog from django.http import Http404, JsonResponse +from django.views.decorators.http import require_GET from django.views.generic import ListView from . import models from .exceptions import RepositoryNotFoundError +from .models import Provider, Repository from .utils import get_scm_module log = structlog.get_logger() @@ -22,3 +24,24 @@ def repo_details(request, provider, repo_id): raise Http404(f"Project {repo_id} not found") return JsonResponse(details) + + +@require_GET +def get_gitlab_envs(request): + project_id = request.GET.get("project_id") + + if not project_id: + return JsonResponse({"message": "Missing project_id"}, safe=False) + + try: + repo = Repository.objects.get(id=project_id, provider=Provider.GITLAB.value) + except RepositoryNotFoundError: + return JsonResponse({"message": "Wrong project_id"}, safe=False) + + return JsonResponse( + [ + {"name": gl_env.name, "dashboardUrl": gl_env.external_url} + for gl_env in repo.repository_environments.all() + ], + safe=False, + ) diff --git a/zoo/services/assets/img/gitlab-icon-rgb.svg b/zoo/services/assets/img/gitlab-icon-rgb.svg new file mode 100644 index 00000000..edb193bd --- /dev/null +++ b/zoo/services/assets/img/gitlab-icon-rgb.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + 1x + + + 1x + + + 1x + + + + 1x + + + diff --git a/zoo/services/assets/js/gitlab_envs.js b/zoo/services/assets/js/gitlab_envs.js new file mode 100644 index 00000000..e9973223 --- /dev/null +++ b/zoo/services/assets/js/gitlab_envs.js @@ -0,0 +1,80 @@ +const gitlabEnvs = { + _resetGilabEnvs: (envTypeGitlab) => { + const gitlab_envs = $("input[value='" + envTypeGitlab + "']").closest(".ui.segment"); + if (gitlab_envs.length > 0) { + $.each(gitlab_envs, function(index, element){ + const uiSegment = $(element) + + // set values to default + uiSegment.find(".field.name input").val(""); + uiSegment.find('input[name$="type"]').val(""); + uiSegment.find('input[name$="dashboard_url"]').val(""); + uiSegment.find('input[name$="logs_url"]').val(""); + uiSegment.find('input[name$="open_api_url"]').val(""); + + // remove gitlab icon + uiSegment.find(".gitlab-img").hide() + + uiSegment.hide(); + uiSegment.find(".field.name input").prop("required", false); + + //if all segments are hidden, + const hidden = $(".ui.segment.environment:hidden"); + if (hidden.length == 5) { + let firstSegment = hidden.eq(0); + firstSegment.show() + } + + if (hidden.length > 0) { + $("button.add-environment").prop("disabled", false); + } + + }) + } + }, + _getFirstAvailableSegment: () => { + const all_envs = $(".ui.segment.environment"); + if (all_envs.length == 0) { return false } + + for (let i = 0; i < all_envs.length; i++) { + let firstSegment = all_envs.eq(i) + if (firstSegment.find(".field.name input").val() == "") { + return firstSegment + } + } + + return false; + }, + _fillData: (envTypeGitlab, firstSegment, response) => { + firstSegment.find(":not(.no-reset) input").val(""); + firstSegment.find('input[name$="type"]').val(envTypeGitlab); + firstSegment.find(".field.name input").val(response["name"]).prop('readonly', true); + firstSegment.find('input[name$="dashboard_url"]').val(response["dashboardUrl"]).prop('readonly', true); + firstSegment.find(".gitlab-img").show() + firstSegment.show(); + }, + load: (gitlabEnvsInfo) => { + //delete previous gitlab envs + gitlabEnvs._resetGilabEnvs(gitlabEnvsInfo.envTypeGitlab) + + //fetch gitlab envs + $.get(gitlabEnvsInfo.envsUrl + "?project_id=" + gitlabEnvsInfo.repoId, function (response) { + if (response.length == 0) { return console.log('Fetching Gitlab environments completed! No envs.') } + + $.each(response, function(index, element) { + let firstSegment = gitlabEnvs._getFirstAvailableSegment(); + if (!firstSegment) { return;} + + gitlabEnvs._fillData(gitlabEnvsInfo.envTypeGitlab, firstSegment, element) + if ($(".ui.segment.environment:hidden").length == 0) { + $("button.add-environment").prop("disabled", true); + } + }) + console.log('Fetching Gitlab environments completed!') + }).fail(function() { + console.log( "Error fetching envs" ); + }) + } +} + +export {gitlabEnvs}; diff --git a/zoo/services/assets/js/service_form.js b/zoo/services/assets/js/service_form.js index 9e7b1d38..5692cdbf 100644 --- a/zoo/services/assets/js/service_form.js +++ b/zoo/services/assets/js/service_form.js @@ -35,6 +35,7 @@ $("button.remove-environment").click(function () { var uiSegment = checkbox.closest(".ui.segment"); uiSegment.hide(); uiSegment.find(".field.name input").prop("required", false); + uiSegment.find(".field.type input").val(""); var hidden = $(".ui.segment.environment:hidden"); if (hidden.length > 0) { @@ -58,15 +59,16 @@ $("button.remove-link").click(function () { $("button.add-environment").click(function () { $(this).blur(); - var hidden = $(".ui.segment.environment:hidden"); + let hidden = $(".ui.segment.environment:hidden"); if (hidden.length > 0) { if (hidden.length == 1) { $(this).prop("disabled", true); } - var firstSegment = hidden.eq(0); + let firstSegment = hidden.eq(0); firstSegment.find(":not(.no-reset) input").val(""); firstSegment.find(".field.name input").prop("required", true); + firstSegment.find('input[name$="type"]').val(envTypeZoo); firstSegment.show(); } }); diff --git a/zoo/services/constants.py b/zoo/services/constants.py index cc60458d..3c346bc5 100644 --- a/zoo/services/constants.py +++ b/zoo/services/constants.py @@ -19,3 +19,8 @@ class SentryIssueCategory(Enum): DECAYING = "decaying" SPOILED = "spoiled" FRESH = "fresh" + + +class EnviromentType(Enum): + GITLAB = "gitlab" + ZOO = "zoo" diff --git a/zoo/services/forms.py b/zoo/services/forms.py index 90716050..bcbd2a7d 100644 --- a/zoo/services/forms.py +++ b/zoo/services/forms.py @@ -52,7 +52,14 @@ class EnvironmentForm(forms.ModelForm): class Meta: model = models.Environment - fields = ["name", "dashboard_url", "logs_url", "service_urls", "open_api_url"] + fields = [ + "name", + "dashboard_url", + "logs_url", + "service_urls", + "open_api_url", + "type", + ] labels = { "dashboard_url": "Dashboard URL", "logs_url": "Logs URL", diff --git a/zoo/services/migrations/0026_environment_type.py b/zoo/services/migrations/0026_environment_type.py new file mode 100644 index 00000000..8ee23bca --- /dev/null +++ b/zoo/services/migrations/0026_environment_type.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.19 on 2021-03-23 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("services", "0025_remove_general_tag"), + ] + + operations = [ + migrations.AddField( + model_name="environment", + name="type", + field=models.CharField( + choices=[("gitlab", "gitlab"), ("zoo", "zoo")], + max_length=100, + null=True, + ), + ), + ] diff --git a/zoo/services/models.py b/zoo/services/models.py index cf743902..8ff73e2e 100644 --- a/zoo/services/models.py +++ b/zoo/services/models.py @@ -10,7 +10,7 @@ from djangoql.schema import DjangoQLSchema from . import ratings -from .constants import Impact, SentryIssueCategory, Status +from .constants import EnviromentType, Impact, SentryIssueCategory, Status from .managers import SentryIssueManager @@ -191,6 +191,11 @@ class Meta: dashboard_url = models.URLField(max_length=500, null=True, blank=True) logs_url = models.URLField(max_length=500, null=True, blank=True) open_api_url = models.URLField(max_length=500, null=True, blank=True) + type = models.CharField( + choices=((item.value, item.value) for item in EnviromentType), + null=True, + max_length=100, + ) def slugify_attribute(attribute): diff --git a/zoo/services/templates/services/service_form.html b/zoo/services/templates/services/service_form.html index f786193d..91f451ea 100644 --- a/zoo/services/templates/services/service_form.html +++ b/zoo/services/templates/services/service_form.html @@ -14,10 +14,10 @@ {% endblock %} {% block disclaimer %} -{% singleton 'instance.Hints' as hints %} -{% if hints.service_form_explanation %} -

{{ hints.service_form_explanation|safe }}

-{% endif %} + {% singleton 'instance.Hints' as hints %} + {% if hints.service_form_explanation %} +

{{ hints.service_form_explanation|safe }}

+ {% endif %} {% endblock %} {% block content %} @@ -27,134 +27,152 @@ {% endblock %} {% block extrafields %} -
-
+
+

Environments

- +
+ +
+
{{ envs_formset.management_form }} {% for form in envs_formset %} -
1 %}style="display: none"{% endif %}> - {% if forloop.counter > 1 %} -
- -
- {% endif %} - {% if form.errors %} -
-
Please correct the following errors
-
    - {% for field, messages in form.errors.items %} -
  • {{ field }} - {{ messages|join:" " }}
  • - {% endfor %} -
+
1 %}style="display: none"{% endif %}> + - {% endif %} -
- {% for field in form %} - {% with field.field.widget.input_type as type %} - {% if type == "hidden" %} - {{ field }} + {% if forloop.counter > 1 and form.type != env_type_gitlab %} +
+ +
+ {% endif %} + {% if form.errors %} +
+
Please correct the following errors
+
    + {% for field, messages in form.errors.items %} +
  • {{ field }} - {{ messages|join:" " }}
  • + {% endfor %} +
+
+ {% endif %} +
+ {% for field in form %} + {% with field.field.widget.input_type as type %} + {% if type == "hidden" %} + {{ field }} + {% endif %} + {% endwith %} + {% endfor %} +
+ {{ form.DELETE.as_hidden }} + +
+
+ + {% if form.type == env_type_gitlab %} + + {% else %} + {{ form.name }} {% endif %} - {% endwith %} - {% endfor %} -
- {{ form.DELETE.as_hidden }} -
-
- - {{ form.name }} +
+
+ + {{ form.dashboard_url }} +
-
- - {{ form.dashboard_url }} +
+ + {{ form.logs_url }} +
+
+ + {% if form.type == env_type_gitlab %} + + {% else %} + {{ form.service_urls }} + {% endif %} +
+
+ + {{ form.open_api_url }}
-
- - {{ form.logs_url }} -
-
- - {{ form.service_urls }} -
-
- - {{ form.open_api_url }} -
-
{% endfor %} -
-